Merge pull request #179 from unix/button

feat(button): support to display icons in button
This commit is contained in:
witt
2020-05-08 00:09:08 +08:00
committed by GitHub
11 changed files with 432 additions and 30 deletions

View File

@@ -0,0 +1,189 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonIcon should render correctly 1`] = `
"<button class=\\"btn \\"><span class=\\"icon \\"><svg></svg><style>
.icon {
position: absolute;
left: var(--zeit-ui-button-padding);
right: auto;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
color: var(--zeit-ui-button-color);
z-index: 1;
}
.right {
right: var(--zeit-ui-button-padding);
left: auto;
}
.icon :global(svg) {
background: transparent;
height: calc(var(--zeit-ui-button-height) / 2.35);
width: calc(var(--zeit-ui-button-height) / 2.35);
}
</style></span><div class=\\"text left\\">action<style>
.left {
padding-left: 0;
}
.right {
padding-right: 0;
}
</style></div><style>
.btn {
box-sizing: border-box;
display: inline-block;
padding: 0 1.375rem;
height: 2.5rem;
line-height: 2.5rem;
min-width: 12.5rem;
width: auto;
border-radius: 5px;
font-weight: 400;
font-size: .875rem;
user-select: none;
outline: none;
text-transform: capitalize;
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease 0s;
position: relative;
overflow: hidden;
color: #666;
background-color: #fff;
border: 1px solid #eaeaea;
cursor: pointer;
pointer-events: auto;
box-shadow: none;
--zeit-ui-button-padding: 1.375rem;
--zeit-ui-button-height: 2.5rem;
--zeit-ui-button-color: #666;
}
.btn:hover {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
border-color: #000;
cursor: pointer;
pointer-events: auto;
box-shadow: none;
transform: translate3d(0px, 0px, 0px);
}
.btn :global(.text) {
position: relative;
z-index: 1;
display: inline-flex;
justify-content: center;
align-items: center;
text-align: center;
line-height: inherit;
top: -1px;
}
.btn :global(.text p),
.btn :global(.text pre),
.btn :global(.text div) {
margin: 0;
}
</style></button>"
`;
exports[`ButtonIcon should work with right 1`] = `
"<button class=\\"btn \\"><span class=\\"icon right \\"><svg></svg><style>
.icon {
position: absolute;
left: var(--zeit-ui-button-padding);
right: auto;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
color: var(--zeit-ui-button-color);
z-index: 1;
}
.right {
right: var(--zeit-ui-button-padding);
left: auto;
}
.icon :global(svg) {
background: transparent;
height: calc(var(--zeit-ui-button-height) / 2.35);
width: calc(var(--zeit-ui-button-height) / 2.35);
}
</style></span><div class=\\"text right\\">action<style>
.left {
padding-left: 0;
}
.right {
padding-right: 0;
}
</style></div><style>
.btn {
box-sizing: border-box;
display: inline-block;
padding: 0 1.375rem;
height: 2.5rem;
line-height: 2.5rem;
min-width: 12.5rem;
width: auto;
border-radius: 5px;
font-weight: 400;
font-size: .875rem;
user-select: none;
outline: none;
text-transform: capitalize;
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease 0s;
position: relative;
overflow: hidden;
color: #666;
background-color: #fff;
border: 1px solid #eaeaea;
cursor: pointer;
pointer-events: auto;
box-shadow: none;
--zeit-ui-button-padding: 1.375rem;
--zeit-ui-button-height: 2.5rem;
--zeit-ui-button-color: #666;
}
.btn:hover {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
border-color: #000;
cursor: pointer;
pointer-events: auto;
box-shadow: none;
transform: translate3d(0px, 0px, 0px);
}
.btn :global(.text) {
position: relative;
z-index: 1;
display: inline-flex;
justify-content: center;
align-items: center;
text-align: center;
line-height: inherit;
top: -1px;
}
.btn :global(.text p),
.btn :global(.text pre),
.btn :global(.text div) {
margin: 0;
}
</style></button>"
`;

View File

@@ -0,0 +1,41 @@
import React from 'react'
import { mount } from 'enzyme'
import { Button } from 'components'
const Icon: React.FC<any> = () => <svg />
describe('ButtonIcon', () => {
it('should render correctly', () => {
const wrapper = mount(<Button icon={<Icon />}>action</Button>)
expect(wrapper.html()).toMatchSnapshot()
expect(() => wrapper.unmount()).not.toThrow()
})
it('should work with right', () => {
const wrapper = mount(<Button iconRight={<Icon />}>action</Button>)
expect(wrapper.html()).toMatchSnapshot()
expect(() => wrapper.unmount()).not.toThrow()
})
it('the width of the text should be filled', () => {
const autoWrapper = mount(
<Button auto icon={<Icon />}>
action
</Button>,
)
const wrapper = mount(<Button icon={<Icon />}>action</Button>)
const autoHtml = autoWrapper.find('.text').html()
const html = wrapper.find('.text').html()
expect(html).not.toEqual(autoHtml)
const mini = mount(<Button size="mini">action</Button>)
const miniIcon = mount(
<Button size="mini" icon={<Icon />}>
action
</Button>,
)
const miniHtml = mini.find('.text').html()
const miniIconHtml = miniIcon.find('.text').html()
expect(miniHtml).not.toEqual(miniIconHtml)
})
})

View File

@@ -1,6 +1,6 @@
import React from 'react'
import { mount } from 'enzyme'
import { Button } from '../../'
import { Button } from 'components'
import { sleep } from 'tests/utils'
describe('Button', () => {

View File

@@ -0,0 +1,57 @@
import React from 'react'
import withDefaults from '../utils/with-defaults'
interface Props {
isRight?: boolean
className?: string
}
const defaultProps = {
isRight: false,
className: '',
}
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type ButtonIconProps = Props & typeof defaultProps & NativeAttrs
const ButtonIcon: React.FC<React.PropsWithChildren<ButtonIconProps>> = ({
isRight,
children,
className,
...props
}) => {
return (
<span className={`icon ${isRight ? 'right' : ''} ${className}`} {...props}>
{children}
<style jsx>{`
.icon {
position: absolute;
left: var(--zeit-ui-button-padding);
right: auto;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
color: var(--zeit-ui-button-color);
z-index: 1;
}
.right {
right: var(--zeit-ui-button-padding);
left: auto;
}
.icon :global(svg) {
background: transparent;
height: calc(var(--zeit-ui-button-height) / 2.35);
width: calc(var(--zeit-ui-button-height) / 2.35);
}
`}</style>
</span>
)
}
const MemoButtonIcon = React.memo(ButtonIcon)
export default withDefaults(MemoButtonIcon, defaultProps)

View File

@@ -4,6 +4,7 @@ import useTheme from '../styles/use-theme'
import { ButtonTypes, NormalSizes } from '../utils/prop-types'
import ButtonDrip from './button.drip'
import ButtonLoading from '../loading'
import ButtonIcon from './button-icon'
import { getButtonColors, getButtonCursor, getButtonHoverColors, getButtonSize } from './styles'
interface Props {
@@ -15,6 +16,8 @@ interface Props {
auto?: boolean
effect?: boolean
disabled?: boolean
icon?: React.ReactNode
iconRight?: React.ReactNode
onClick?: React.MouseEventHandler<HTMLButtonElement>
className?: string
}
@@ -45,6 +48,8 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
onClick,
auto,
size,
icon,
iconRight,
className,
...props
}) => {
@@ -94,6 +99,32 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
onClick && onClick(event)
}
const childrenWithIcon = useMemo(() => {
const hasIcon = icon || iconRight
const isRight = Boolean(iconRight)
const paddingForAutoMode =
auto || size === 'mini'
? `calc(var(--zeit-ui-button-height) / 2 + var(--zeit-ui-button-padding) * .5)`
: 0
if (!hasIcon) return <div className="text">{children}</div>
return (
<>
<ButtonIcon isRight={isRight}>{hasIcon}</ButtonIcon>
<div className={`text ${isRight ? 'right' : 'left'}`}>
{children}
<style jsx>{`
.left {
padding-left: ${paddingForAutoMode};
}
.right {
padding-right: ${paddingForAutoMode};
}
`}</style>
</div>
</>
)
}, [children, icon, auto, size])
return (
<button
ref={buttonRef}
@@ -101,7 +132,7 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
disabled={disabled}
onClick={clickHandler}
{...props}>
{loading ? <ButtonLoading /> : <div className="text">{children}</div>}
{loading ? <ButtonLoading /> : childrenWithIcon}
{dripShow && (
<ButtonDrip
x={dripX}
@@ -128,7 +159,7 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease;
transition: all 0.2s ease 0s;
position: relative;
overflow: hidden;
color: ${color};
@@ -137,10 +168,14 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
cursor: ${cursor};
pointer-events: ${events};
box-shadow: ${shadow ? theme.expressiveness.shadowSmall : 'none'};
--zeit-ui-button-padding: ${padding};
--zeit-ui-button-height: ${height};
--zeit-ui-button-color: ${color};
}
.btn:hover {
color: ${hover.color};
--zeit-ui-button-color: ${hover.color};
background-color: ${hover.bg};
border-color: ${hover.border};
cursor: ${cursor};
@@ -149,7 +184,7 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
transform: translate3d(0px, ${shadow ? '-1px' : '0px'}, 0px);
}
.text {
.btn :global(.text) {
position: relative;
z-index: 1;
display: inline-flex;
@@ -160,9 +195,9 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
top: -1px;
}
.text :global(p),
.text :global(pre),
.text :global(div) {
.btn :global(.text p),
.btn :global(.text pre),
.btn :global(.text div) {
margin: 0;
}
`}</style>

View File

@@ -1,5 +1,3 @@
import Button from './button'
import { ButtonProps } from './button'
export type Props = ButtonProps
export default Button

View File

@@ -225,9 +225,9 @@ export const getButtonSize = (size: NormalSizes = 'medium', auto: boolean): Butt
mini: {
height: '1.5rem',
width: 'initial',
padding: '1.875rem',
padding: '1.375rem',
fontSize: '.75rem',
minWidth: '6.25rem',
minWidth: '5.25rem',
},
small: {
height: '2rem',

View File

@@ -19,7 +19,7 @@ exports[`UseToast should render different actions 1`] = `
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease;
transition: all 0.2s ease 0s;
position: relative;
overflow: hidden;
color: #fff;
@@ -28,10 +28,14 @@ exports[`UseToast should render different actions 1`] = `
cursor: pointer;
pointer-events: auto;
box-shadow: none;
--zeit-ui-button-padding: 0.625rem;
--zeit-ui-button-height: 1.5rem;
--zeit-ui-button-color: #fff;
}
.btn:hover {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
border-color: #000;
cursor: pointer;
@@ -40,7 +44,7 @@ exports[`UseToast should render different actions 1`] = `
transform: translate3d(0px, 0px, 0px);
}
.text {
.btn :global(.text) {
position: relative;
z-index: 1;
display: inline-flex;
@@ -51,9 +55,9 @@ exports[`UseToast should render different actions 1`] = `
top: -1px;
}
.text :global(p),
.text :global(pre),
.text :global(div) {
.btn :global(.text p),
.btn :global(.text pre),
.btn :global(.text div) {
margin: 0;
}
</style></button><button class=\\"btn \\"><div class=\\"text\\">remove</div><style>
@@ -74,7 +78,7 @@ exports[`UseToast should render different actions 1`] = `
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease;
transition: all 0.2s ease 0s;
position: relative;
overflow: hidden;
color: #666;
@@ -83,10 +87,14 @@ exports[`UseToast should render different actions 1`] = `
cursor: pointer;
pointer-events: auto;
box-shadow: none;
--zeit-ui-button-padding: 0.625rem;
--zeit-ui-button-height: 1.5rem;
--zeit-ui-button-color: #666;
}
.btn:hover {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
border-color: #000;
cursor: pointer;
@@ -95,7 +103,7 @@ exports[`UseToast should render different actions 1`] = `
transform: translate3d(0px, 0px, 0px);
}
.text {
.btn :global(.text) {
position: relative;
z-index: 1;
display: inline-flex;
@@ -106,9 +114,9 @@ exports[`UseToast should render different actions 1`] = `
top: -1px;
}
.text :global(p),
.text :global(pre),
.text :global(div) {
.btn :global(.text p),
.btn :global(.text pre),
.btn :global(.text div) {
margin: 0;
}
</style></button></div><style>

View File

@@ -15,9 +15,9 @@ const MockToast: React.FC<{}> = () => {
setToast(params)
}
return (
<button id="btn" onClick={clickHandler}>
<div id="btn" onClick={clickHandler}>
btn
</button>
</div>
)
}

View File

@@ -1,6 +1,10 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Button, Spacer } from 'components'
import Router from 'next/router'
import Settings from '@zeit-ui/react-icons/settings'
import UserX from '@zeit-ui/react-icons/userX'
import Power from '@zeit-ui/react-icons/power'
export const meta = {
title: 'button',
@@ -11,7 +15,6 @@ export const meta = {
Used to trigger an operation.
<Playground
title="Basic"
desc="The basic `Button` contains an animation effect."
@@ -86,21 +89,55 @@ Used to trigger an operation.
</>
`} />
<Playground
title="With Icons"
desc="The `color` and `size` of the icon will be set automatically."
scope={{ Button, Spacer, Settings, UserX, Power }}
code={`
<>
<Button iconRight={<Power />} auto size="small">shut off</Button>
<Spacer y={.5} />
<Button icon={<Settings />} auto>Action</Button>
<Spacer y={.5} />
<Button icon={<Settings />} type="success" size="small">Success</Button>
<Spacer y={.5} />
<Button icon={<Settings />} type="secondary">Success</Button>
<Spacer y={.5} />
<Button icon={<UserX />} type="error" ghost>Remove User</Button>
<Spacer y={.5} />
<Button icon={<UserX />} disabled>Remove User</Button>
</>
`} />
<Attributes edit="/pages/en-us/components/button.mdx">
<Attributes.Title>Button.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **type** | button type | `ButtonTypes` | `'default', 'secondary', 'success', 'warning', 'error', 'abort'` | `default` |
| **size** | button size | `NormalSizes` | `'mini', 'small', 'medium', 'large'` | `medium` |
| **type** | button type | `ButtonTypes` | [ButtonTypes](#buttontypes) | `default` |
| **size** | button size | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **ghost** | the opposite color | `boolean` | - | `false` |
| **loading** | display loading indicator | `boolean` | - | `false` |
| **shadow** | display shadow | `boolean` | - | `false` |
| **auto** | autoscale width | `boolean` | - | `false` |
| **effect** | display animation | `boolean` | - | `true` |
| **disabled** | disable button | `boolean` | - | `false` |
| **icon** | show icon in button | `ReactNode` | - | - |
| **iconRight** | show icon on the other side of the button | `ReactNode` | - | - |
| **onClick** | click handler | `MouseEventHandler` | - | - |
| ... | native props | `ButtonHTMLAttributes` | `'autoFocus', 'name', 'className', ...` | - |
| ... | native props | `ButtonHTMLAttributes` | `'id', 'className', ...` | - |
<Attributes.Title>ButtonTypes</Attributes.Title>
```ts
type ButtonTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error' | 'abort'
```
<Attributes.Title>NormalSizes</Attributes.Title>
```ts
type NormalSizes = 'mini' | 'small' | 'medium' | 'large'
```
</Attributes>

View File

@@ -1,6 +1,9 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Button, Spacer } from 'components'
import Router from 'next/router'
import Settings from '@zeit-ui/react-icons/settings'
import UserX from '@zeit-ui/react-icons/userX'
import Power from '@zeit-ui/react-icons/power'
export const meta = {
title: '按钮 Button',
@@ -85,13 +88,33 @@ export const meta = {
</>
`} />
<Playground
title="图标"
desc="按钮内的图标色彩和大小将会被自动设置。"
scope={{ Button, Spacer, Settings, UserX, Power }}
code={`
<>
<Button iconRight={<Power />} auto size="small">关闭</Button>
<Spacer y={.5} />
<Button icon={<Settings />} auto>按钮</Button>
<Spacer y={.5} />
<Button icon={<Settings />} type="success" size="small">成功</Button>
<Spacer y={.5} />
<Button icon={<Settings />} type="secondary">成功</Button>
<Spacer y={.5} />
<Button icon={<UserX />} type="error" ghost>移除用户</Button>
<Spacer y={.5} />
<Button icon={<UserX />} disabled>移除用户</Button>
</>
`} />
<Attributes edit="/pages/zh-cn/components/button.mdx">
<Attributes.Title>Button.Props</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认
| ---------- | ---------- | ---- | -------------- | ------ |
| **type** | 按钮的类型 | `ButtonTypes` | `'default', 'secondary', 'success', 'warning', 'error', 'abort'` | `default` |
| **size** | 按钮的大小 | `NormalSizes` | `'mini', 'small', 'medium', 'large'` | `medium` |
| **type** | 按钮的类型 | `ButtonTypes` | [ButtonTypes](#buttontypes) | `default` |
| **size** | 按钮的大小 | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **ghost** | 是否为反色按钮 | `boolean` | - | `false` |
| **loading** | 是否显示加载中的指示器 | `boolean` | - | `false` |
| **shadow** | 是否显示阴影 | `boolean` | - | `false` |
@@ -99,7 +122,21 @@ export const meta = {
| **effect** | 是否显示动画效果 | `boolean` | - | `true` |
| **disabled** | 是否禁用按钮 | `boolean` | - | `false` |
| **onClick** | 点击事件 | `MouseEventHandler` | - | - |
| ... | 原生属性 | `ButtonHTMLAttributes` | `'autoFocus', 'name', 'className', ...` | - |
| **icon** | 在按钮内显示图标 | `ReactNode` | - | - |
| **iconRight** | 在按钮的另一侧显示图标 | `ReactNode` | - | - |
| ... | 原生属性 | `ButtonHTMLAttributes` | `'id', 'className', ...` | - |
<Attributes.Title>ButtonTypes</Attributes.Title>
```ts
type ButtonTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error' | 'abort'
```
<Attributes.Title>NormalSizes</Attributes.Title>
```ts
type NormalSizes = 'mini' | 'small' | 'medium' | 'large'
```
</Attributes>