Merge pull request #197 from unix/group

feat(button-group): add component
This commit is contained in:
witt
2020-05-11 20:40:17 +08:00
committed by GitHub
18 changed files with 1072 additions and 109 deletions

View File

@@ -0,0 +1,493 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonGroup buttons should be displayed vertically 1`] = `
"<div class=\\"btn-group vertical \\"><button type=\\"button\\" class=\\"btn \\"><div class=\\"text\\">action1</div><style>
.btn {
box-sizing: border-box;
display: inline-block;
padding: 0 1.25rem;
height: 2.5rem;
line-height: 2.5rem;
min-width: min-content;
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: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
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.25rem;
--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><button type=\\"button\\" class=\\"btn \\"><div class=\\"text\\">action2</div><style>
.btn {
box-sizing: border-box;
display: inline-block;
padding: 0 1.25rem;
height: 2.5rem;
line-height: 2.5rem;
min-width: min-content;
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: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
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.25rem;
--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><style>
.btn-group {
display: inline-flex;
border-radius: 5px;
margin: 4pt;
border: 1px solid #eaeaea;
background-color: transparent;
overflow: hidden;
height: min-content;
}
.vertical {
flex-direction: column;
}
.btn-group :global(.btn) {
border: none;
}
.btn-group :global(.btn .text) {
top: 0;
}
.horizontal :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid #eaeaea;
}
.horizontal :global(.btn:not(:last-child)) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.vertical :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 1px solid #eaeaea;
}
.vertical :global(.btn:not(:last-child)) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style></div>"
`;
exports[`ButtonGroup props should be passed to each button 1`] = `
"<div class=\\"btn-group horizontal \\"><button type=\\"button\\" class=\\"btn \\"><div class=\\"text\\">action</div><style>
.btn {
box-sizing: border-box;
display: inline-block;
padding: 0 0.625rem;
height: 1.5rem;
line-height: 1.5rem;
min-width: min-content;
width: auto;
border-radius: 5px;
font-weight: 400;
font-size: .75rem;
user-select: none;
outline: none;
text-transform: capitalize;
justify-content: center;
text-align: center;
white-space: nowrap;
transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
position: relative;
overflow: hidden;
color: #fff;
background-color: #0070f3;
border: 1px solid #0070f3;
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: #0070f3;
--zeit-ui-button-color: #0070f3;
background-color: #fff;
border-color: #0070f3;
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><style>
.btn-group {
display: inline-flex;
border-radius: 5px;
margin: 4pt;
border: 1px solid #fff;
background-color: transparent;
overflow: hidden;
height: min-content;
}
.vertical {
flex-direction: column;
}
.btn-group :global(.btn) {
border: none;
}
.btn-group :global(.btn .text) {
top: 0;
}
.horizontal :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid #fff;
}
.horizontal :global(.btn:not(:last-child)) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.vertical :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 1px solid #fff;
}
.vertical :global(.btn:not(:last-child)) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style></div>"
`;
exports[`ButtonGroup props should be passed to each button 2`] = `
"<div class=\\"btn-group horizontal \\"><button type=\\"button\\" class=\\"btn \\"><div class=\\"text\\">action</div><style>
.btn {
box-sizing: border-box;
display: inline-block;
padding: 0 0.625rem;
height: 1.5rem;
line-height: 1.5rem;
min-width: min-content;
width: auto;
border-radius: 5px;
font-weight: 400;
font-size: .75rem;
user-select: none;
outline: none;
text-transform: capitalize;
justify-content: center;
text-align: center;
white-space: nowrap;
transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
position: relative;
overflow: hidden;
color: #fff;
background-color: #0070f3;
border: 1px solid #0070f3;
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: #0070f3;
--zeit-ui-button-color: #0070f3;
background-color: #fff;
border-color: #0070f3;
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><style>
.btn-group {
display: inline-flex;
border-radius: 5px;
margin: 4pt;
border: 1px solid #0070f3;
background-color: transparent;
overflow: hidden;
height: min-content;
}
.vertical {
flex-direction: column;
}
.btn-group :global(.btn) {
border: none;
}
.btn-group :global(.btn .text) {
top: 0;
}
.horizontal :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid #0070f3;
}
.horizontal :global(.btn:not(:last-child)) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.vertical :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 1px solid #0070f3;
}
.vertical :global(.btn:not(:last-child)) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style></div>"
`;
exports[`ButtonGroup should render correctly 1`] = `
"<div class=\\"btn-group horizontal \\"><button type=\\"button\\" class=\\"btn \\"><div class=\\"text\\">action</div><style>
.btn {
box-sizing: border-box;
display: inline-block;
padding: 0 1.25rem;
height: 2.5rem;
line-height: 2.5rem;
min-width: min-content;
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: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
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.25rem;
--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><style>
.btn-group {
display: inline-flex;
border-radius: 5px;
margin: 4pt;
border: 1px solid #eaeaea;
background-color: transparent;
overflow: hidden;
height: min-content;
}
.vertical {
flex-direction: column;
}
.btn-group :global(.btn) {
border: none;
}
.btn-group :global(.btn .text) {
top: 0;
}
.horizontal :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid #eaeaea;
}
.horizontal :global(.btn:not(:last-child)) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.vertical :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 1px solid #eaeaea;
}
.vertical :global(.btn:not(:last-child)) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style></div>"
`;

View File

@@ -0,0 +1,53 @@
import React from 'react'
import { mount } from 'enzyme'
import { ButtonGroup, Button } from 'components'
import { nativeEvent } from 'tests/utils'
describe('ButtonGroup', () => {
it('should render correctly', () => {
const wrapper = mount(
<ButtonGroup>
<Button>action</Button>
</ButtonGroup>,
)
expect(wrapper.html()).toMatchSnapshot()
expect(() => wrapper.unmount()).not.toThrow()
})
it('props should be passed to each button', () => {
const wrapper = mount(
<ButtonGroup size="mini" type="success">
<Button>action</Button>
</ButtonGroup>,
)
expect(wrapper.html()).toMatchSnapshot()
wrapper.setProps({ ghost: true })
expect(wrapper.html()).toMatchSnapshot()
expect(() => wrapper.unmount()).not.toThrow()
})
it('should ignore events when group disabled', () => {
const handler = jest.fn()
const wrapper = mount(
<ButtonGroup>
<Button onClick={handler}>action</Button>
</ButtonGroup>,
)
wrapper.find('button').simulate('click', nativeEvent)
expect(handler).toHaveBeenCalledTimes(1)
wrapper.setProps({ disabled: true })
wrapper.find('button').simulate('click', nativeEvent)
expect(handler).toHaveBeenCalledTimes(1)
})
it('buttons should be displayed vertically', () => {
const wrapper = mount(
<ButtonGroup vertical>
<Button>action1</Button>
<Button>action2</Button>
</ButtonGroup>,
)
expect(wrapper.html()).toMatchSnapshot()
expect(() => wrapper.unmount()).not.toThrow()
})
})

View File

@@ -0,0 +1,20 @@
import React from 'react'
import { NormalSizes, ButtonTypes } from '../utils/prop-types'
export interface ButtonGroupConfig {
size?: NormalSizes
type?: ButtonTypes
ghost?: boolean
disabled?: boolean
isButtonGroup: boolean
}
const defaultContext = {
isButtonGroup: false,
disabled: false,
}
export const ButtonGroupContext = React.createContext<ButtonGroupConfig>(defaultContext)
export const useButtonGroupContext = (): ButtonGroupConfig =>
React.useContext<ButtonGroupConfig>(ButtonGroupContext)

View File

@@ -0,0 +1,116 @@
import React, { useMemo } from 'react'
import useTheme from '../styles/use-theme'
import withDefaults from '../utils/with-defaults'
import { NormalSizes, ButtonTypes } from '../utils/prop-types'
import { ButtonGroupContext, ButtonGroupConfig } from './button-group-context'
import { getButtonColors } from '../button/styles'
interface Props {
disabled?: boolean
vertical?: boolean
ghost?: boolean
size?: NormalSizes
type?: ButtonTypes
className?: string
}
const defaultProps = {
disabled: false,
vertical: false,
ghost: false,
size: 'medium' as NormalSizes,
type: 'default' as ButtonTypes,
className: '',
}
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type ButtonGroupProps = Props & typeof defaultProps & NativeAttrs
const ButtonGroup: React.FC<React.PropsWithChildren<ButtonGroupProps>> = ({
disabled,
size,
type,
ghost,
vertical,
children,
className,
}) => {
const theme = useTheme()
const initialValue = useMemo<ButtonGroupConfig>(
() => ({
disabled,
size,
type,
ghost,
isButtonGroup: true,
}),
[disabled, size, type],
)
const { border } = useMemo(() => {
const results = getButtonColors(theme, type, disabled, ghost)
if (!ghost && type !== 'default')
return {
...results,
border: theme.palette.background,
}
return results
}, [theme, type, disabled, ghost])
return (
<ButtonGroupContext.Provider value={initialValue}>
<div className={`btn-group ${vertical ? 'vertical' : 'horizontal'} ${className}`}>
{children}
<style jsx>{`
.btn-group {
display: inline-flex;
border-radius: ${theme.layout.radius};
margin: ${theme.layout.gapQuarter};
border: 1px solid ${border};
background-color: transparent;
overflow: hidden;
height: min-content;
}
.vertical {
flex-direction: column;
}
.btn-group :global(.btn) {
border: none;
}
.btn-group :global(.btn .text) {
top: 0;
}
.horizontal :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid ${border};
}
.horizontal :global(.btn:not(:last-child)) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.vertical :global(.btn:not(:first-child)) {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 1px solid ${border};
}
.vertical :global(.btn:not(:last-child)) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
`}</style>
</div>
</ButtonGroupContext.Provider>
)
}
const MemoButtonGroup = React.memo(ButtonGroup)
export default withDefaults(MemoButtonGroup, defaultProps)

View File

@@ -0,0 +1,3 @@
import ButtonGroup from './button-group'
export default ButtonGroup

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonIcon should render correctly 1`] = `
"<button class=\\"btn \\"><span class=\\"icon \\"><svg></svg><style>
"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon \\"><svg></svg><style>
.icon {
position: absolute;
left: var(--zeit-ui-button-padding);
@@ -26,13 +26,13 @@ exports[`ButtonIcon should render correctly 1`] = `
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>
.left {
padding-left: 0;
}
.right {
padding-right: 0;
}
</style></div><style>
.btn {
box-sizing: border-box;
display: inline-block;
@@ -50,7 +50,8 @@ exports[`ButtonIcon should render correctly 1`] = `
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease 0s;
transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
position: relative;
overflow: hidden;
color: #666;
@@ -95,7 +96,7 @@ exports[`ButtonIcon should render correctly 1`] = `
`;
exports[`ButtonIcon should work with right 1`] = `
"<button class=\\"btn \\"><span class=\\"icon right \\"><svg></svg><style>
"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon right \\"><svg></svg><style>
.icon {
position: absolute;
left: var(--zeit-ui-button-padding);
@@ -120,13 +121,13 @@ exports[`ButtonIcon should work with right 1`] = `
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>
.left {
padding-left: 0;
}
.right {
padding-right: 0;
}
</style></div><style>
.btn {
box-sizing: border-box;
display: inline-block;
@@ -144,7 +145,8 @@ exports[`ButtonIcon should work with right 1`] = `
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease 0s;
transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
position: relative;
overflow: hidden;
color: #666;

View File

@@ -1,10 +1,11 @@
import React, { useRef, useState, MouseEvent, useMemo } from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import { ButtonTypes, NormalSizes } from '../utils/prop-types'
import withDefaults from '../utils/with-defaults'
import ButtonDrip from './button.drip'
import ButtonLoading from '../loading'
import ButtonIcon from './button-icon'
import { ButtonTypes, NormalSizes } from '../utils/prop-types'
import { filterPropsWithGroup, getButtonChildrenWithIcon } from './utils'
import { useButtonGroupContext } from '../button-group/button-group-context'
import { getButtonColors, getButtonCursor, getButtonHoverColors, getButtonSize } from './styles'
interface Props {
@@ -37,27 +38,30 @@ const defaultProps = {
type NativeAttrs = Omit<React.ButtonHTMLAttributes<any>, keyof Props>
export type ButtonProps = Props & typeof defaultProps & NativeAttrs
const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
children,
disabled,
type,
loading,
shadow,
ghost,
effect,
onClick,
auto,
size,
icon,
iconRight,
className,
...props
}) => {
const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({ ...btnProps }) => {
const theme = useTheme()
const buttonRef = useRef<HTMLButtonElement>(null)
const [dripShow, setDripShow] = useState<boolean>(false)
const [dripX, setDripX] = useState<number>(0)
const [dripY, setDripY] = useState<number>(0)
const groupConfig = useButtonGroupContext()
const {
children,
disabled,
type,
loading,
shadow,
ghost,
effect,
onClick,
auto,
size,
icon,
iconRight,
className,
...props
} = filterPropsWithGroup(btnProps, groupConfig)
const { bg, border, color } = useMemo(() => getButtonColors(theme, type, disabled, ghost), [
theme,
type,
@@ -99,35 +103,19 @@ 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])
const childrenWithIcon = useMemo(
() =>
getButtonChildrenWithIcon(auto, size, children, {
icon,
iconRight,
}),
[auto, size, children, icon, iconRight],
)
return (
<button
ref={buttonRef}
type="button"
className={`btn ${className}`}
disabled={disabled}
onClick={clickHandler}
@@ -159,7 +147,8 @@ const Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease 0s;
transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
position: relative;
overflow: hidden;
color: ${color};

View File

@@ -0,0 +1,56 @@
import React, { ReactNode } from 'react'
import { NormalSizes } from '../utils/prop-types'
import ButtonIcon from './button-icon'
import { ButtonProps } from 'components/button/button'
import { ButtonGroupConfig } from 'components/button-group/button-group-context'
export const getButtonChildrenWithIcon = (
auto: boolean,
size: NormalSizes,
children: ReactNode,
icons: {
icon?: React.ReactNode
iconRight?: React.ReactNode
},
) => {
const { icon, iconRight } = icons
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>
</>
)
}
export const filterPropsWithGroup = (
props: React.PropsWithChildren<ButtonProps>,
config: ButtonGroupConfig,
): ButtonProps => {
if (!config.isButtonGroup) return props
return {
...props,
auto: true,
shadow: false,
ghost: config.ghost || props.ghost,
size: config.size || props.size,
type: config.type || props.type,
disabled: config.disabled || props.disabled,
}
}

View File

@@ -53,3 +53,4 @@ export { default as Divider } from './divider'
export { default as User } from './user'
export { default as Page } from './page'
export { default as Grid } from './grid'
export { default as ButtonGroup } from './button-group'

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UseToast should render different actions 1`] = `
"<div class=\\"toast-container \\"><div class=\\"toast \\"><div class=\\"message\\">hello</div><div class=\\"action\\"><button class=\\"btn \\"><div class=\\"text\\">remove</div><style>
"<div class=\\"toast-container \\"><div class=\\"toast \\"><div class=\\"message\\">hello</div><div class=\\"action\\"><button type=\\"button\\" class=\\"btn \\"><div class=\\"text\\">remove</div><style>
.btn {
box-sizing: border-box;
display: inline-block;
@@ -19,7 +19,8 @@ exports[`UseToast should render different actions 1`] = `
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease 0s;
transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
position: relative;
overflow: hidden;
color: #fff;
@@ -60,7 +61,7 @@ exports[`UseToast should render different actions 1`] = `
.btn :global(.text div) {
margin: 0;
}
</style></button><button class=\\"btn \\"><div class=\\"text\\">remove</div><style>
</style></button><button type=\\"button\\" class=\\"btn \\"><div class=\\"text\\">remove</div><style>
.btn {
box-sizing: border-box;
display: inline-block;
@@ -78,7 +79,8 @@ exports[`UseToast should render different actions 1`] = `
justify-content: center;
text-align: center;
white-space: nowrap;
transition: all 0.2s ease 0s;
transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms;
position: relative;
overflow: hidden;
color: #666;
@@ -133,13 +135,13 @@ exports[`UseToast should render different actions 1`] = `
border: 0;
border-radius: 5px;
padding: 16pt;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
opacity: 1;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
transform: translate3d(0, 100%, 0px) scale(1);
transition: all 400ms ease;
transition: transform 400ms ease 0ms, visibility 200ms ease 0ms, opacity 200ms ease 0ms;
}
.toast.visible {
@@ -191,7 +193,7 @@ exports[`UseToast should render different actions 1`] = `
`;
exports[`UseToast should work with different types 1`] = `
"<div class=\\"toast-container \\"><div class=\\"toast \\"><div class=\\"message\\">hello</div><div class=\\"action\\"></div><style>
"<div class=\\"toast-container \\"><div class=\\"toast visible \\"><div class=\\"message\\">hello</div><div class=\\"action\\"></div><style>
.toast {
width: 420px;
max-width: 90vw;
@@ -205,13 +207,13 @@ exports[`UseToast should work with different types 1`] = `
border: 0;
border-radius: 5px;
padding: 16pt;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
opacity: 1;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
transform: translate3d(0, 100%, 0px) scale(1);
transition: all 400ms ease;
transition: transform 400ms ease 0ms, visibility 200ms ease 0ms, opacity 200ms ease 0ms;
}
.toast.visible {

View File

@@ -61,7 +61,7 @@ describe('UseToast', () => {
expectToastIsHidden(wrapper)
triggerToast(wrapper, { type: 'success', text: 'hello' })
await updateWrapper(wrapper)
await updateWrapper(wrapper, 100)
expectToastIsShow(wrapper)
expect(wrapper.find('.toast-container').html()).toMatchSnapshot()
})

View File

@@ -21,13 +21,7 @@ const ToastContainer: React.FC<React.PropsWithChildren<{}>> = () => {
const toastElements = useMemo(
() =>
toasts.map((t, i) => (
<ToastItem
index={i}
total={toasts.length}
toast={t}
onHover={hover}
key={`toast-${t.id}-${i}`}
/>
<ToastItem index={i} total={toasts.length} toast={t} onHover={hover} key={`toast-${i}`} />
)),
[toasts, hover],
)

View File

@@ -61,21 +61,23 @@ const getColors = (palette: ZeitUIThemesPalette, type?: NormalTypes) => {
const ToastItem: React.FC<ToatItemProps> = React.memo(({ index, total, toast, onHover }) => {
const theme = useTheme()
const { color, bgColor } = getColors(theme.palette, toast.type)
const { color, bgColor } = useMemo(() => getColors(theme.palette, toast.type), [
theme.palette,
toast.type,
])
const [visible, setVisible] = useState<boolean>(false)
const [hide, setHide] = useState<boolean>(false)
const reverseIndex = useMemo(() => total - (index + 1), [total, index])
const translate = useMemo(() => {
const calc = `100% + -75px + -${20 * reverseIndex}px`
if (reverseIndex > 5) return `translate3d(0, -75px, -${reverseIndex}px) scale(.01)`
if (reverseIndex >= 4) return `translate3d(0, -75px, -${reverseIndex}px) scale(.7)`
if (onHover) {
return `translate3d(0, ${reverseIndex * -75}px, -${reverseIndex}px) scale(${
total === 1 ? 1 : 0.98205
})`
}
return `translate3d(0, calc(${calc}), -${reverseIndex}px) scale(${1 - 0.05 * reverseIndex})`
}, [onHover, index, total])
}, [onHover, index, total, reverseIndex])
useEffect(() => {
const timer = setTimeout(() => {
@@ -98,6 +100,8 @@ const ToastItem: React.FC<ToatItemProps> = React.memo(({ index, total, toast, on
clearTimeout(timer)
}
}, [reverseIndex, toast.willBeDestroy])
/* istanbul ignore next */
if (reverseIndex > 10) return null
return (
<div
@@ -119,13 +123,13 @@ const ToastItem: React.FC<ToatItemProps> = React.memo(({ index, total, toast, on
border: 0;
border-radius: ${theme.layout.radius};
padding: ${theme.layout.gap};
box-shadow: ${theme.expressiveness.shadowSmall};
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
opacity: ${reverseIndex > 4 ? 0 : 1};
box-shadow: ${reverseIndex > 4 ? 'none' : theme.expressiveness.shadowSmall};
transform: translate3d(0, 100%, 0px) scale(1);
transition: all 400ms ease;
transition: transform 400ms ease 0ms, visibility 200ms ease 0ms, opacity 200ms ease 0ms;
}
.toast.visible {

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'
import React, { useEffect } from 'react'
import { NormalTypes } from '../utils/prop-types'
import useCurrentState from '../utils/use-current-state'
import { useZEITUIContext } from '../utils/use-zeit-ui-context'
@@ -22,35 +22,33 @@ const defaultToast = {
delay: 2000,
}
let destoryStack: Array<string> = []
let maxDestoryTime: number = 0
let destoryTimer: number | undefined
const useToasts = (): [Array<Toast>, (t: Toast) => void] => {
const { updateToasts, toastHovering, toasts } = useZEITUIContext()
const destoryStack = useRef<Array<string>>([])
const destoryTimer = useRef<number | undefined>()
const maxDestoryTime = useRef<number>(0)
const [, setHovering, hoveringRef] = useCurrentState<boolean>(toastHovering)
useEffect(() => setHovering(toastHovering), [toastHovering])
const destoryAll = (delay: number) => {
// Wait for all components to display before destroying
// The destory means direct remove all element, whether in animation or not.
const nextDestoryTime = delay + 500
const destoryAll = (delay: number, time: number) => {
/* istanbul ignore next */
if (nextDestoryTime < maxDestoryTime.current) return
clearTimeout(destoryTimer.current)
maxDestoryTime.current = nextDestoryTime
if (time <= maxDestoryTime) return
clearTimeout(destoryTimer)
maxDestoryTime = time
destoryTimer.current = window.setTimeout(() => {
destoryTimer = window.setTimeout(() => {
/* istanbul ignore next */
updateToasts((currentToasts: Array<ToastWithID>) => {
if (destoryStack.current.length < currentToasts.length) {
if (destoryStack.length < currentToasts.length) {
return currentToasts
}
destoryStack.current = []
destoryStack = []
return []
})
clearTimeout(destoryTimer.current)
}, maxDestoryTime.current)
clearTimeout(destoryTimer)
}, delay + 350)
}
const setToast = (toast: Toast): void => {
@@ -64,8 +62,8 @@ const useToasts = (): [Array<Toast>, (t: Toast) => void] => {
return { ...item, willBeDestroy: true }
})
})
destoryStack.current.push(id)
destoryAll(delay)
destoryStack.push(id)
destoryAll(delay, performance.now())
}
updateToasts((currentToasts: Array<ToastWithID>) => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,116 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Button, Spacer, ButtonGroup } from 'components'
export const meta = {
title: 'Button-Group',
group: 'Data Entry',
}
## Button Group
A set of related buttons.
<Playground
title="Basic"
scope={{ Button, ButtonGroup }}
code={`
<ButtonGroup>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
`} />
<Playground
title="Variant"
desc="set the type or styles of all buttons in the group."
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup type="success">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
<ButtonGroup type="abort">
<Button>Action1</Button>
<Button>Action2</Button>
</ButtonGroup>
<ButtonGroup type="warning" ghost>
<Button>Action1</Button>
<Button>Action2</Button>
</ButtonGroup>
</>
`} />
<Playground
title="Sizes"
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup size="small">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
<ButtonGroup size="mini">
<Button>Action1</Button>
<Button>Action2</Button>
</ButtonGroup>
</>
`} />
<Playground
title="Vertical"
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup size="small" vertical>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
<Button>Four</Button>
</ButtonGroup>
</>
`} />
<Playground
title="Disabled"
desc="disable all buttons in the group."
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup size="small" disabled>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
</>
`} />
<Attributes edit="/pages/en-us/components/button-group.mdx">
<Attributes.Title>ButtonGroup.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **type** | button type | `ButtonTypes` | [ButtonTypes](#buttontypes) | `default` |
| **size** | button size | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **ghost** | the opposite color | `boolean` | - | `false` |
| **vertical** | show all buttons vertically | `boolean` | - | `false` |
| **disabled** | disable all buttons | `boolean` | - | `false` |
| ... | 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>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>

View File

@@ -0,0 +1,116 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Button, Spacer, ButtonGroup } from 'components'
export const meta = {
title: '按钮组 Button-Group',
group: '数据录入',
}
## Button Group / 按钮组
展示一组具备相关性的按钮。
<Playground
title="基础"
scope={{ Button, ButtonGroup }}
code={`
<ButtonGroup>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
`} />
<Playground
title="变体"
desc="设置组内所有按钮的类型或样式。"
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup type="success">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
<ButtonGroup type="abort">
<Button>Action1</Button>
<Button>Action2</Button>
</ButtonGroup>
<ButtonGroup type="warning" ghost>
<Button>Action1</Button>
<Button>Action2</Button>
</ButtonGroup>
</>
`} />
<Playground
title="大小"
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup size="small">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
<ButtonGroup size="mini">
<Button>Action1</Button>
<Button>Action2</Button>
</ButtonGroup>
</>
`} />
<Playground
title="垂直的"
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup size="small" vertical>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
<Button>Four</Button>
</ButtonGroup>
</>
`} />
<Playground
title="禁用"
desc="禁用组内所有的按钮。"
scope={{ Button, ButtonGroup }}
code={`
<>
<ButtonGroup size="small" disabled>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
</>
`} />
<Attributes edit="/pages/zh-cn/components/button-group.mdx">
<Attributes.Title>ButtonGroup.Props</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认
| ---------- | ---------- | ---- | -------------- | ------ |
| **type** | 按钮类型 | `ButtonTypes` | [ButtonTypes](#buttontypes) | `default` |
| **size** | 按钮大小 | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **ghost** | 相反色彩模式的按钮 | `boolean` | - | `false` |
| **vertical** | 以垂直方式显示所有按钮 | `boolean` | - | `false` |
| **disabled** | 是否禁用所有按钮 | `boolean` | - | `false` |
| ... | 原生属性 | `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>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>