
大家好,今天我将告诉您如何为XMars UI项目开发按钮。 哦,是的,这是一件小事,但是有话要说。 我将省略与向开源项目中添加新组件相关的详细信息。 我将在另一篇文章中更详细地讨论该项目。
引言
XMars UI是我的新开源项目之一。 一个简单的UI组件库,用于HTML / CSS和React。 将来,我计划支持Vue。 到目前为止,它只有一个按钮和图标:)
该项目是在Telegram Contest的框架中诞生的,其实质是开发客户端的Web版本。 我们决定与同事一起,为什么不参加呢。 共享角色是为了让我有一个布局,并且当一位同事理解授权后,我将连接到编写组件。 一切都会好起来的,但是登录电报并不是那么简单。 结果,我们没有发送任何东西,但是我赶上了一堆东西,结果却是徒劳的。 但是正如Varlamov所说,您的项目已经值得了,因为您花了很多时间在上面。 很难不同意这一点,因为如果您花费大量时间和金钱,那么仅在项目开始时就设置Webpack不再是免费的。 考虑到所有这些耻辱,我决定必须以某种方式将其投入开源。 使用哪个单一的引导程序? 我想要自己的其他项目的UI框架。
界面中的按钮也许是用户与应用程序交互的主要元素。 因此,这是任何UI框架/库的第一个组件。
在Telegram的设计中,按钮没有太多变化:

我突出显示了3个主要(默认,重音,主要),带有一个图标和绿色的圆形。 仍然是半透明的,但请放下它。 在大多数情况下,我尝试从需求着手开发XMars UI ,但我还没有弄清楚在何处需要透明按钮。
库用户应该习惯使用CSS类。 我不喜欢BEM这样的命名系统。 我喜欢Bootstrap命名类的方式。 但我会简化一点。 代替.btn .btn-primary
,而只是.btn .primary
。 对于React组件,它看起来像这样:
<Button primary>Hey</Button>
相同的按钮但是有波纹效果:
<Button primary ripple>Hey</Button>
HTML / CSS
UI库不应绑定到任何UI框架。 将来,我计划在Vue
组件上扩展布局。 让我们开始使用简单的HTML / CSS。
在Tailwindcss项目的内部,这是一个实用程序优先的CSS框架,即为您提供实用程序而不是成熟组件的框架。

除Tailwindcss外,PostCSS还用于mixins , 变量和嵌套样式。
关于使用这种框架以及如何配置项目的更多详细信息,我将在另一篇文章中讲述。 在这个阶段,拥有如此强大的工具包并充分使用它所使用的组件就足够了。
<button>
具有许多我们需要删除或重新定义的默认样式。

对于Tailwindcss,button标记具有以下样式:

默认情况下将所有不必要的内容删除。 您可以雕刻自己想要的东西,而不必担心默认边框会在某些状态下消失。 但请注意,默认outline
仍需添加:

XMars UI中的按钮具有.btn
类:
<button class="btn">Button</button>
将此类添加到我们的样式中:
.btn { @apply text-black text-base leading-snug; @apply py-3 px-4 border-none rounded-lg; @apply inline-block cursor-pointer no-underline; &:focus { @apply outline-none; } }
除了Tailwindcss提供了可以使用的类之外,它还提供了一种mixins
。 @apply
不是SCSS或PostCSS的某种插件。 这是Tailwindcss本身的语法。 从名称上通常可以清楚地应用所应用的样式。 py-3
和px-4
可能会引起问题。 第一个是沿y填充,即垂直padding-top: 0.75rem;
,即padding-top: 0.75rem;
padding-bottom: 0.75rem;
。 因此, px-4
水平为padding-right: 1rem;
, padding-left: 1rem;
。
Telegram提供的将其轻描淡写的设计记录不佳, border-radius
按钮之类的东西必须直接用标尺从图像中获取。 有没有想过border-radius
值的确切含义是什么?

这实际上是角落中所得圆的半径。 如果是集体农场,则可以如上图所示更改标尺。 因此,我确实在Gimp中使用了矩形选择。

设计中按钮border-radius
为10 8px
,但是不幸的是,在Tailwindcss框中没有此类,但是从视觉8px
,默认字体大小(rem)为8 8px
rounded-lg
。
这是目前发生的情况,我将按钮涂成灰色,以便可以看到边缘:

接下来,我们需要对以下内容产生影响:hover
。 然后,Telegram的设计师决定对一些颜色进行照明,并从#707579
颜色指示为0.08%。 我看到两个选择,只需用吸管上色或按照记录进行即可。 第一种选择较为简单,但将来并非最佳选择。 事实是,如果背景不同于白色,那么在上:hover
我们将获得特定的颜色,失去按钮的“亮度”和透明度。 为此,最好遵循文档并进行Alpha测试 男 频道。 这可以以无数种方式完成,例如使用SCSS颜色功能。 但是该项目中没有SCSS,而且由于颜色相同,我不想将任何插件连接到PostCSS,我们将使一切变得非常简单。 在Chrome中,有一个colopaker可让您将颜色转换成不同的系统,在其中驱动十六进制颜色#707579
,将其转换为rgba
并设置alpha通道-0.08%。

瞧! 东西突然闪过我的照片:

我们rgba(112, 117, 121, 0.08)
。

(:悬停)
进一步的无聊和不费力气,我添加了其余状态:
&:hover { background-color: var(--grey04); } &.accent { color: var(--blue01); } &.primary { @apply text-white; background-color: var(--blue01); &:hover { background-color: var(--blue02); } }
反应组件
最初,该按钮是为Telegram竞赛准备的,无法使用任何框架。 我必须在纯JS上实现波纹效果。 我真的很希望这样,但是当您独自完成项目时,就必须牺牲一些东西。
需要某种逻辑(例如纹波效应)的组件将被实现,并且只能作为React组件使用。
在React组件中包装按钮并不困难:
import React, { FunctionComponent } from 'react'; export interface ButtonProps { } const Button: FunctionComponent<ButtonProps> = (props) => { return ( <button className="btn">props.children</button> ); } export default Button;
该按钮将以指定的样式显示,但实际上没有什么用。 我们需要使用户能够自定义按钮,添加自定义样式,挂起事件处理程序等。
为了使用户能够传输所有必要的内容,首先您需要克服Typescript,否则,即使onClick
也不允许您正常传输。 ButtonProps
稍微编辑ButtonProps
界面,我们解决了这个问题:
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
之后,我们可以安全地破坏props
:
<button className="btn" {...props}>props.children</button>
该按钮的类似用法将达到预期的效果:
<Button onClick={() => alert()}>Hey</Button>

接下来,我们添加按钮样式和注册自定义(突然有人需要)类的功能。 npm classnames软件包非常适合这些目的。
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, additionalClass?: string, } ... const classNames = classnames( 'btn', { primary }, { accent }, additionalClass ); ... <button className={classNames} {...props}>props.children</button>
始终设置btn
类,但仅在true
设置primary
和accent
。 如果类名具有逻辑true
值,则添加类,使用缩写ES6,我们得到一个简单的条目{ primary }
,而不是{ primary: true }
。
additionalClass
是一个字符串,如果为空或未定义,则它对我们没有特殊作用,只会向元素添加任何内容。
首先,我分配了以下props
:
{...omit(props, ['additionalClass', 'primary'])}
省略所有不适用于button元素的props
的内容,但这不是必需的,因为React不会渲染太多。
涟漪效应

实际上这是它的外观,但是希望“波”与点击位置发散。
制作动画的方法有无数种,就像关于蓝色方块的笑话一样。
但是google通过查看codepen上的示例,可以清楚地看到,在大多数情况下,“波”是通过子元素实现的,该子元素会扩展和消失。
根据点击的坐标,它位于按钮内。 在XMars UI中 ,目前我决定不像Material UI那样在onPress
上实现此效果,但我计划在将来进行onPress
。 到目前为止,只有onClick
。

上图是所有魔术。 点击会创建一个子按钮元素,该元素绝对位于该点击的中心并展开。 overflow: hidden
属性overflow: hidden
wave超出按钮。 必须在动画末尾删除该项目。
首先,我们将尽可能地使用Tailwindcss定义样式:
.with-ripple { @apply relative overflow-hidden; @keyframes ripple { to { @apply opacity-0; transform: scale(2.5); } } .ripple { @apply absolute; z-index: 1; border-radius: 50%; background-color: var(--grey04); transform: scale(0); animation: ripple 0.6s linear; } &.primary { .ripple { background-color: var(--black02); } } }
负责效果的元素将分配为.ripple
类。 border-radius: 50%;
等于一个圆(角为50%的圆角* 2),该按钮具有相对位置, .ripple
按钮具有绝对位置。 动画非常简单,增加的波在0.6秒内变得透明。 背景颜色与相同:hover
和折叠,两种透明的“波浪”颜色和按钮可为我们提供所需的结果。 在蓝色的.primary
按钮上,这并不是很重要,您可以在其中使用非透明的颜色。
点击时,您需要创建“ wave”元素。 因此,我们为此业务创建了一个状态,并向按钮添加了适当的单击处理程序,但这种方式不会干扰自定义onClick。
... const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); ... function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> );
rippleElements
元素-一组JSX元素,此处的render函数可能看起来是多余的,但这更多的是样式和将来的合并问题。
function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); }
创建“波浪”的onRippleClick
处理程序。 通过单击按钮,我们可以找到用于正确定位圆的按钮的大小,然后将所有必需的内容传递给newRippleElement
函数newRippleElement
该函数依次简单地创建带有ripple
类的div
元素,从而创建必要的定位样式。
其中值得重点onAnimationEnd
的主要内容onAnimationEnd
。 我们需要此事件以从已使用的元素中清除DOM。
function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); }
重要的是不要忘记,将当前的rippleElements
传递给参数,否则您将获得具有旧值的数组,并且所有操作均无法按预期进行。
全按钮代码:
import React, { FunctionComponent, ButtonHTMLAttributes, useState } from 'react'; import uuid from 'uuid/v4'; import classnames from 'classnames'; export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, circle?: boolean, ripple?: boolean, additionalClass?: string, } const Button: FunctionComponent<ButtonProps> = (props) => { const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); const {primary, accent, circle, ripple, additionalClass, children} = props; const classNames = classnames( 'btn', { primary }, { 'with-ripple': ripple }, { circle }, { accent }, additionalClass ); function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); } function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); } function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> ); } export default Button;
最终结果可以在这里报告。
结论
省略了很多内容,例如,如何设置项目,如何编写文档,测试项目中的新组件。 我将尝试用单独的出版物介绍这些主题。
XMars UI Github存储库