chore: release v1.8.0 (#367)

* feat(snippet): add custom symbol and toast

* docs(snippet): add docs for custom symbol and toast

* fix(snippet): default toast type as normal type

* test(snippet): add custom symbol and toast

* docs(snippet): add enum type of APIs

* docs(toast): add normal-types

* chore: release v1.8.0-canary.1

* feat(modal): optimize the animation of layer

* test: update snapshots

* chore: release v1.8.0-canary.2

* docs: format import statement

* feat(modal): optimize the max width, height and alignment of layer

* test: update snapshots

* chore: release v1.8.0-canary.3

* Button (#319)

* feat(button): center the icon if the button is empty

* test(button): add testcase for icon only

* docs(button): add example for icon only

* chore: release v1.8.0-canary.4

* fix(snippet): remove space when the symbol is empty (#325)

* fix(snippet): remove space when the symbol is empty

style(snippet): remove unnecessary escape symbols

* fix(snippet): ignore spaces in symbol

style: fix lint warning

* chore: release v1.8.0-canary.5

* feat(tabs): sync the label and set value to required (#334)

* feat(tabs): sync the label and set value to required

* test(tabs): add testcase for label sync

* docs(tabs): update value to required

* feat(modal): use Button to reconstrust Modal.Action (#332)

* feat(modal): use Button to reconstrust Modal.Action

* docs(modal): add example for action loading

* test: update snapshots

* chore: release v1.8.0-canary.6

* fix(input): always synchronize external value and ignore changes (#336)

* fix(input): always synchronize external value and ignore changes

* feat(input): support imperative API to update

* fix(textarea): imperative api same as input (#341)

* feat(dropdown): allow dropdown to set the specified container (#344)

* feat(dropdown): allow dropdown to set the specified container

* test(modal): update snapshots

* docs(select): add example for custom popup container

* fix(dropdown): fix type of getPopupContainer

* test(dropdown): add testcase for specified container rendering

* chore: release v1.8.0-canary.7

* fix(link): fix hard-coded background color (#347)

* style(description): fix title font-weight (#348)

* docs(link): fix duplicate examples (#346)

* style(popover-item): the whole item should be clickable when using with link (#345)

* fix(modal): fix type of Modal.Action (#351)

* chore: release v1.8.0-canary.8

* feat(modal): lock tab action inside modal (#354)

* feat(button): add style to focus buttons

* feat(collections): add util function

* feat(modal): lock tab action inside modal

* test(modal): add tests for modal focus

* test: update style of button

* fix(table): fix column's props are not tracked (#362)

* chore: release v1.8.0-canary.9

* fix(table): children of column should be kept in sync (#364)

* chore: release v1.8.0-canary.10

Co-authored-by: Augusto <augustoconti10@gmail.com>
Co-authored-by: yqrashawn <namy.19@gmail.com>
Co-authored-by: Zhao Lei <firede@firede.us>
This commit is contained in:
witt
2020-08-14 16:59:49 +08:00
committed by GitHub
parent 3c0fa081aa
commit bb575498fe
60 changed files with 1507 additions and 596 deletions

View File

@@ -35,7 +35,8 @@ exports[`ButtonGroup buttons should be displayed vertically 1`] = `
--zeit-ui-button-bg: #fff;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
@@ -96,7 +97,8 @@ exports[`ButtonGroup buttons should be displayed vertically 1`] = `
--zeit-ui-button-bg: #fff;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
@@ -205,7 +207,8 @@ exports[`ButtonGroup props should be passed to each button 1`] = `
--zeit-ui-button-bg: #0070f3;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #0070f3;
--zeit-ui-button-color: #0070f3;
background-color: #fff;
@@ -314,7 +317,8 @@ exports[`ButtonGroup props should be passed to each button 2`] = `
--zeit-ui-button-bg: #0070f3;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #0070f3;
--zeit-ui-button-color: #0070f3;
background-color: #fff;
@@ -423,7 +427,8 @@ exports[`ButtonGroup should render correctly 1`] = `
--zeit-ui-button-bg: #fff;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonIcon should render correctly 1`] = `
"<button type=\\"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);
@@ -25,6 +25,11 @@ exports[`ButtonIcon should render correctly 1`] = `
height: calc(var(--zeit-ui-button-height) / 2.35);
width: calc(var(--zeit-ui-button-height) / 2.35);
}
.single {
position: static;
transform: none;
}
</style></span><div class=\\"text left\\">action<style>
.left {
padding-left: 0;
@@ -66,7 +71,8 @@ exports[`ButtonIcon should render correctly 1`] = `
--zeit-ui-button-bg: #fff;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
@@ -97,7 +103,7 @@ exports[`ButtonIcon should render correctly 1`] = `
`;
exports[`ButtonIcon should work with right 1`] = `
"<button type=\\"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);
@@ -121,6 +127,11 @@ exports[`ButtonIcon should work with right 1`] = `
height: calc(var(--zeit-ui-button-height) / 2.35);
width: calc(var(--zeit-ui-button-height) / 2.35);
}
.single {
position: static;
transform: none;
}
</style></span><div class=\\"text right\\">action<style>
.left {
padding-left: 0;
@@ -162,7 +173,103 @@ exports[`ButtonIcon should work with right 1`] = `
--zeit-ui-button-bg: #fff;
}
.btn:hover {
.btn:hover,
.btn:focus {
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 without text 1`] = `
"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon right single \\"><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);
}
.single {
position: static;
transform: none;
}
</style></span><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: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms, color 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.375rem;
--zeit-ui-button-height: 2.5rem;
--zeit-ui-button-color: #666;
--zeit-ui-button-bg: #fff;
}
.btn:hover,
.btn:focus {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;

View File

@@ -16,6 +16,13 @@ describe('ButtonIcon', () => {
expect(() => wrapper.unmount()).not.toThrow()
})
it('should work without text', () => {
const wrapper = mount(<Button iconRight={<Icon />} />)
const text = wrapper.find('.text')
expect(wrapper.html()).toMatchSnapshot()
expect(text.length).toBe(0)
})
it('the width of the text should be filled', () => {
const autoWrapper = mount(
<Button auto icon={<Icon />}>

View File

@@ -3,6 +3,7 @@ import withDefaults from '../utils/with-defaults'
interface Props {
isRight?: boolean
isSingle?: boolean
className?: string
}
@@ -16,12 +17,15 @@ export type ButtonIconProps = Props & typeof defaultProps & NativeAttrs
const ButtonIcon: React.FC<React.PropsWithChildren<ButtonIconProps>> = ({
isRight,
isSingle,
children,
className,
...props
}) => {
return (
<span className={`icon ${isRight ? 'right' : ''} ${className}`} {...props}>
<span
className={`icon ${isRight ? 'right' : ''} ${isSingle ? 'single' : ''} ${className}`}
{...props}>
{children}
<style jsx>{`
.icon {
@@ -47,6 +51,11 @@ const ButtonIcon: React.FC<React.PropsWithChildren<ButtonIconProps>> = ({
height: calc(var(--zeit-ui-button-height) / 2.35);
width: calc(var(--zeit-ui-button-height) / 2.35);
}
.single {
position: static;
transform: none;
}
`}</style>
</span>
)

View File

@@ -64,6 +64,7 @@ const Button = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<Butto
const [dripY, setDripY] = useState<number>(0)
const groupConfig = useButtonGroupContext()
const filteredProps = filterPropsWithGroup(btnProps, groupConfig)
/* eslint-disable @typescript-eslint/no-unused-vars */
const {
children,
disabled,
@@ -81,6 +82,7 @@ const Button = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<Butto
className,
...props
} = filteredProps
/* eslint-enable @typescript-eslint/no-unused-vars */
const { bg, border, color } = useMemo(() => getButtonColors(theme.palette, filteredProps), [
theme.palette,
@@ -180,7 +182,8 @@ const Button = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<Butto
--zeit-ui-button-bg: ${bg};
}
.btn:hover {
.btn:hover,
.btn:focus {
color: ${hover.color};
--zeit-ui-button-color: ${hover.color};
background-color: ${hover.bg};

View File

@@ -21,6 +21,13 @@ export const getButtonChildrenWithIcon = (
? `calc(var(--zeit-ui-button-height) / 2 + var(--zeit-ui-button-padding) * .5)`
: 0
if (!hasIcon) return <div className="text">{children}</div>
if (React.Children.count(children) === 0) {
return (
<ButtonIcon isRight={isRight} isSingle>
{hasIcon}
</ButtonIcon>
)
}
return (
<>
<ButtonIcon isRight={isRight}>{hasIcon}</ButtonIcon>

View File

@@ -37,6 +37,7 @@ const Description: React.FC<DescriptionProps> = ({ title, content, className, ..
text-transform: uppercase;
white-space: nowrap;
color: ${theme.palette.accents_5};
font-weight: 500;
display: flex;
}

View File

@@ -68,6 +68,6 @@ describe('Image Browser', () => {
</Image.Browser>,
)
const rel = wrapper.find('a').getDOMNode().getAttribute('rel')
expect(anchorRel).toEqual(anchorRel)
expect(rel).toEqual(anchorRel)
})
})

View File

@@ -67,7 +67,7 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
const [selfValue, setSelfValue] = useState<string>(initialValue)
const [hover, setHover] = useState<boolean>(false)
const { heightRatio, fontSize } = useMemo(() => getSizes(size), [size])
const showClearIcon = useMemo(() => clearable && selfValue !== '', [selfValue, clearable])
const isControlledComponent = useMemo(() => value !== undefined, [value])
const labelClasses = useMemo(() => (labelRight ? 'right-label' : label ? 'left-label' : ''), [
label,
labelRight,
@@ -80,12 +80,12 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
theme.palette,
status,
])
const changeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
if (disabled || readOnly) return
setSelfValue(event.target.value)
onChange && onChange(event)
}
const clearHandler = (event: React.MouseEvent<HTMLDivElement>) => {
setSelfValue('')
onClearClick && onClearClick(event)
@@ -121,9 +121,18 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
)
useEffect(() => {
if (value === undefined) return
setSelfValue(value)
}, [value])
if (isControlledComponent) {
setSelfValue(value as string)
}
})
const controlledValue = isControlledComponent
? { value: selfValue }
: { defaultValue: initialValue }
const inputProps = {
...props,
...controlledValue,
}
return (
<div className="with-label">
@@ -139,7 +148,6 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
type="text"
ref={inputRef}
className={`${disabled ? 'disabled' : ''} ${iconClasses}`}
value={selfValue}
placeholder={placeholder}
disabled={disabled}
readOnly={readOnly}
@@ -147,11 +155,11 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
onBlur={blurHandler}
onChange={changeHandler}
autoComplete={autoComplete}
{...props}
{...inputProps}
/>
{clearable && (
<InputClearIcon
visible={showClearIcon}
visible={Boolean(inputRef.current && inputRef.current.value !== '')}
heightRatio={heightRatio}
disabled={disabled || readOnly}
onClick={clearHandler}

View File

@@ -120,7 +120,7 @@ exports[`Link should render correctly 1`] = `
}
.link:hover {
background-color: #0076ff1a;
background-color: rgba(0, 112, 243, 0.1);
color: #3291ff;
}
</style></a></div>"

View File

@@ -3,6 +3,7 @@ import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import useWarning from '../utils/use-warning'
import LinkIcon from './icon'
import { addColorAlpha } from '../utils/color'
export interface Props {
href?: string
@@ -65,7 +66,7 @@ const Link = React.forwardRef<HTMLAnchorElement, React.PropsWithChildren<LinkPro
}
.link:hover {
background-color: ${block ? '#0076ff1a' : 'unset'};
background-color: ${block ? addColorAlpha(theme.palette.link, 0.1) : 'unset'};
color: ${hoverColor};
}
`}</style>

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Modal customization should be supported 1`] = `
"<div class=\\"wrapper test-class wrapper-enter\\"><h2 class=\\"\\">Modal</h2><style>
"<div class=\\"wrapper test-class wrapper-enter\\" role=\\"dialog\\" tabindex=\\"-1\\"><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><h2 class=\\"\\">Modal</h2><style>
h2 {
font-size: 1.5rem;
line-height: 1.6;
@@ -16,10 +16,10 @@ exports[`Modal customization should be supported 1`] = `
text-transform: capitalize;
color: #000;
}
</style><style>
</style><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><style>
.wrapper {
max-width: 85vw;
max-height: 75vh;
max-width: 90vw;
max-height: 90vh;
width: 100px;
overflow: hidden;
display: flex;
@@ -32,14 +32,15 @@ exports[`Modal customization should be supported 1`] = `
padding: 16pt;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.12);
opacity: 0;
transform: translate3d(0px, -40px, 0px);
outline: none;
transform: translate3d(0px, -30px, 0px);
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s,
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
}
.wrapper-enter {
opacity: 0;
transform: translate3d(0px, -40px, 0px);
transform: translate3d(0px, -30px, 0px);
}
.wrapper-enter-active {
@@ -54,13 +55,21 @@ exports[`Modal customization should be supported 1`] = `
.wrapper-leave-active {
opacity: 0;
transform: translate3d(0px, -50px, 0px);
transform: translate3d(0px, -30px, 0px);
}
.hide-tab {
outline: none;
overflow: hidden;
width: 0;
height: 0;
opacity: 0;
}
</style></div>"
`;
exports[`Modal should render correctly 1`] = `
"<div class=\\"backdrop transition-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><div class=\\"wrapper wrapper-enter\\"><h2 class=\\"\\">Modal</h2><style>
"<div class=\\"backdrop backdrop-wrapper-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><div class=\\"wrapper wrapper-enter\\" role=\\"dialog\\" tabindex=\\"-1\\"><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><h2 class=\\"\\">Modal</h2><style>
h2 {
font-size: 1.5rem;
line-height: 1.6;
@@ -94,6 +103,7 @@ exports[`Modal should render correctly 1`] = `
margin: 0 -16pt;
padding: 16pt 16pt 8pt;
overflow-y: auto;
position: relative;
}
.content > :global(*:first-child) {
@@ -103,53 +113,131 @@ exports[`Modal should render correctly 1`] = `
.content > :global(*:last-child) {
margin-bottom: 0;
}
</style><div></div><footer><button class=\\"\\">Cancel</button><style>
button {
font-size: 0.75rem;
text-transform: uppercase;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
outline: none;
text-decoration: none;
transition: all 200ms ease-in-out 0s;
border: none;
color: #666;
background-color: #fff;
cursor: pointer;
flex: 1;
}
</style><div></div><footer><button type=\\"button\\" class=\\"btn mock \\"><div class=\\"text\\">Cancel</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: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms, color 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.375rem;
--zeit-ui-button-height: 2.5rem;
--zeit-ui-button-color: #666;
--zeit-ui-button-bg: #fff;
}
button:hover {
color: #000;
background-color: #fafafa;
}
</style><button class=\\"\\">Submit</button><style>
button {
font-size: 0.75rem;
text-transform: uppercase;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
outline: none;
text-decoration: none;
transition: all 200ms ease-in-out 0s;
border: none;
color: #000;
background-color: #fff;
cursor: pointer;
flex: 1;
}
.btn:hover,
.btn:focus {
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);
}
button:hover {
color: #000;
background-color: #fafafa;
}
</style></footer><style>
.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 mock \\"><div class=\\"text\\">Submit</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: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
border 200ms ease 0ms, color 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.375rem;
--zeit-ui-button-height: 2.5rem;
--zeit-ui-button-color: #666;
--zeit-ui-button-bg: #fff;
}
.btn:hover,
.btn:focus {
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></footer><style>
footer {
display: flex;
overflow: hidden;
@@ -164,7 +252,7 @@ exports[`Modal should render correctly 1`] = `
border-bottom-right-radius: 5px;
}
footer > :global(button + button) {
footer > :global(button.btn + button.btn) {
border-left: 1px solid #eaeaea;
}
@@ -172,10 +260,10 @@ exports[`Modal should render correctly 1`] = `
height: 3.625rem;
flex-shrink: 0;
}
</style><style>
</style><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><style>
.wrapper {
max-width: 85vw;
max-height: 75vh;
max-width: 90vw;
max-height: 90vh;
width: 26rem;
overflow: hidden;
display: flex;
@@ -188,14 +276,15 @@ exports[`Modal should render correctly 1`] = `
padding: 16pt;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.12);
opacity: 0;
transform: translate3d(0px, -40px, 0px);
outline: none;
transform: translate3d(0px, -30px, 0px);
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s,
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
}
.wrapper-enter {
opacity: 0;
transform: translate3d(0px, -40px, 0px);
transform: translate3d(0px, -30px, 0px);
}
.wrapper-enter-active {
@@ -210,7 +299,15 @@ exports[`Modal should render correctly 1`] = `
.wrapper-leave-active {
opacity: 0;
transform: translate3d(0px, -50px, 0px);
transform: translate3d(0px, -30px, 0px);
}
.hide-tab {
outline: none;
overflow: hidden;
width: 0;
height: 0;
opacity: 0;
}
</style></div></div><div class=\\"offset\\"></div><style>
.backdrop {
@@ -221,7 +318,7 @@ exports[`Modal should render correctly 1`] = `
align-content: center;
align-items: center;
flex-direction: column;
justify-content: space-around;
justify-content: center;
height: 100vh;
width: 100vw;
overflow: auto;
@@ -258,5 +355,21 @@ exports[`Modal should render correctly 1`] = `
pointer-events: none;
z-index: 1000;
}
.backdrop-wrapper-enter .layer {
opacity: 0;
}
.backdrop-wrapper-enter-active .layer {
opacity: 0.25;
}
.backdrop-wrapper-leave .layer {
opacity: 0.25;
}
.backdrop-wrapper-leave-active .layer {
opacity: 0;
}
</style></div>"
`;

View File

@@ -3,6 +3,13 @@ import { mount } from 'enzyme'
import { Modal } from 'components'
import { nativeEvent, updateWrapper } from 'tests/utils'
import { expectModalIsClosed, expectModalIsOpened } from './use-modal.test'
import { act } from 'react-dom/test-utils'
const TabEvent = {
key: 'TAB',
keyCode: 9,
which: 9,
}
describe('Modal', () => {
it('should render correctly', () => {
@@ -117,4 +124,32 @@ describe('Modal', () => {
expect(html).toContain('test-class')
expect(() => wrapper.unmount()).not.toThrow()
})
it('focus should only be switched within modal', () => {
const wrapper = mount(
<Modal open={true} width="100px" wrapClassName="test-class">
<Modal.Title>Modal</Modal.Title>
</Modal>,
)
const tabStart = wrapper.find('.hide-tab').at(0).getDOMNode()
const tabEnd = wrapper.find('.hide-tab').at(1).getDOMNode()
const eventElement = wrapper.find('.wrapper').at(0)
expect(document.activeElement).toBe(tabStart)
act(() => {
eventElement.simulate('keydown', {
...TabEvent,
shiftKey: true,
})
})
expect(document.activeElement).toBe(tabEnd)
act(() => {
eventElement.simulate('keydown', {
...TabEvent,
shiftKey: false,
})
})
expect(document.activeElement).toBe(tabStart)
})
})

View File

@@ -1,7 +1,15 @@
import React, { MouseEvent, useMemo } from 'react'
import withDefaults from '../utils/with-defaults'
import React, {
MouseEvent,
PropsWithoutRef,
RefAttributes,
useImperativeHandle,
useMemo,
useRef,
} from 'react'
import css from 'styled-jsx/css'
import useTheme from '../styles/use-theme'
import { useModalContext } from './modal-context'
import Button, { ButtonProps } from '../button/button'
type ModalActionEvent = MouseEvent<HTMLButtonElement> & {
close: () => void
@@ -20,66 +28,83 @@ const defaultProps = {
disabled: false,
}
type NativeAttrs = Omit<React.ButtonHTMLAttributes<any>, keyof Props>
export type ModalActionProps = Props & typeof defaultProps & NativeAttrs
export type ModalActionProps = Props & typeof defaultProps & Omit<ButtonProps, keyof Props>
const ModalAction: React.FC<ModalActionProps> = ({
className,
children,
onClick,
passive,
disabled,
...props
}) => {
const theme = useTheme()
const { close } = useModalContext()
const clickHandler = (event: MouseEvent<HTMLButtonElement>) => {
if (disabled) return
const actionEvent = Object.assign({}, event, {
close: () => close && close(),
})
onClick && onClick(actionEvent)
}
const ModalAction = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<ModalActionProps>>(
(
{ className, children, onClick, passive, disabled, ...props },
ref: React.Ref<HTMLButtonElement | null>,
) => {
const theme = useTheme()
const btnRef = useRef<HTMLButtonElement>(null)
const { close } = useModalContext()
useImperativeHandle(ref, () => btnRef.current)
const color = useMemo(() => {
return passive || disabled ? theme.palette.accents_5 : theme.palette.foreground
}, [theme.palette, passive, disabled])
const clickHandler = (event: MouseEvent<HTMLButtonElement>) => {
if (disabled) return
const actionEvent = Object.assign({}, event, {
close: () => close && close(),
})
onClick && onClick(actionEvent)
}
const bgColor = useMemo(() => {
return disabled ? theme.palette.accents_1 : theme.palette.background
}, [theme.palette, disabled])
const color = useMemo(() => {
return passive ? theme.palette.accents_5 : theme.palette.foreground
}, [theme.palette, passive, disabled])
return (
<>
<button className={className} onClick={clickHandler} {...props}>
const bgColor = useMemo(() => {
return disabled ? theme.palette.accents_1 : theme.palette.background
}, [theme.palette, disabled])
const { className: resolveClassName, styles } = css.resolve`
button.btn {
font-size: 0.75rem;
border: none;
color: ${color};
background-color: ${theme.palette.background};
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
flex: 1;
height: 100%;
border-radius: 0;
}
button.btn:hover,
button.btn:focus {
color: ${disabled ? color : theme.palette.foreground};
background-color: ${disabled ? bgColor : theme.palette.accents_1};
}
`
const overrideProps = {
...props,
effect: false,
ref: btnRef,
}
return (
<Button
className={`${resolveClassName} ${className}`}
onClick={clickHandler}
disabled={disabled}
{...overrideProps}>
{children}
</button>
<style jsx>{`
button {
font-size: 0.75rem;
text-transform: uppercase;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
outline: none;
text-decoration: none;
transition: all 200ms ease-in-out 0s;
border: none;
color: ${color};
background-color: ${bgColor};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
flex: 1;
}
{styles}
</Button>
)
},
)
button:hover {
color: ${disabled ? color : theme.palette.foreground};
background-color: ${disabled ? bgColor : theme.palette.accents_1};
}
`}</style>
</>
)
}
type ModalActionComponent<T, P = {}> = React.ForwardRefExoticComponent<
PropsWithoutRef<P> & RefAttributes<T>
>
export default withDefaults(ModalAction, defaultProps)
type ComponentProps = Partial<typeof defaultProps> &
Omit<Props, keyof typeof defaultProps> &
Partial<Omit<ButtonProps, keyof Props>>
ModalAction.defaultProps = defaultProps
export default ModalAction as ModalActionComponent<HTMLButtonElement, ComponentProps>

View File

@@ -22,7 +22,7 @@ const ModalActions: React.FC<React.PropsWithChildren<{}>> = ({ children, ...prop
border-bottom-right-radius: ${theme.layout.radius};
}
footer > :global(button + button) {
footer > :global(button.btn + button.btn) {
border-left: 1px solid ${theme.palette.border};
}

View File

@@ -26,6 +26,7 @@ const ModalContent: React.FC<ModalContentProps> = ({ className, children, ...pro
margin: 0 -${theme.layout.gap};
padding: ${theme.layout.gap} ${theme.layout.gap} ${theme.layout.gapHalf};
overflow-y: auto;
position: relative;
}
.content > :global(*:first-child) {

View File

@@ -1,7 +1,8 @@
import React from 'react'
import React, { useEffect, useRef } from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import CSSTransition from '../shared/css-transition'
import { isChildElement } from '../utils/collections'
interface Props {
className?: string
@@ -24,15 +25,49 @@ const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = ({
...props
}) => {
const theme = useTheme()
const modalContent = useRef<HTMLDivElement>(null)
const tabStart = useRef<HTMLDivElement>(null)
const tabEnd = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!visible) return
const activeElement = document.activeElement
const isChild = isChildElement(modalContent.current, activeElement)
if (isChild) return
tabStart.current && tabStart.current.focus()
}, [visible])
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
const isTabDown = event.keyCode === 9
if (!visible || !isTabDown) return
const activeElement = document.activeElement
if (event.shiftKey) {
if (activeElement === tabStart.current) {
tabEnd.current && tabEnd.current.focus()
}
} else {
if (activeElement === tabEnd.current) {
tabStart.current && tabStart.current.focus()
}
}
}
return (
<CSSTransition name="wrapper" visible={visible} clearTime={300}>
<div className={`wrapper ${className}`} {...props}>
<div
className={`wrapper ${className}`}
role="dialog"
tabIndex={-1}
onKeyDown={onKeyDown}
ref={modalContent}
{...props}>
<div tabIndex={0} className="hide-tab" aria-hidden="true" ref={tabStart} />
{children}
<div tabIndex={0} className="hide-tab" aria-hidden="true" ref={tabEnd} />
<style jsx>{`
.wrapper {
max-width: 85vw;
max-height: 75vh;
max-width: 90vw;
max-height: 90vh;
width: ${width};
overflow: hidden;
display: flex;
@@ -45,14 +80,15 @@ const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = ({
padding: ${theme.layout.gap};
box-shadow: ${theme.expressiveness.shadowLarge};
opacity: 0;
transform: translate3d(0px, -40px, 0px);
outline: none;
transform: translate3d(0px, -30px, 0px);
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s,
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
}
.wrapper-enter {
opacity: 0;
transform: translate3d(0px, -40px, 0px);
transform: translate3d(0px, -30px, 0px);
}
.wrapper-enter-active {
@@ -67,7 +103,15 @@ const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = ({
.wrapper-leave-active {
opacity: 0;
transform: translate3d(0px, -50px, 0px);
transform: translate3d(0px, -30px, 0px);
}
.hide-tab {
outline: none;
overflow: hidden;
width: 0;
height: 0;
opacity: 0;
}
`}</style>
</div>

View File

@@ -39,7 +39,6 @@ const PopoverItem: React.FC<React.PropsWithChildren<PopoverItemProps>> = ({
line-height: 1.25rem;
text-align: left;
transition: color 0.1s ease 0s, background-color 0.1s ease 0s;
width: max-content;
}
.item:hover {
@@ -50,6 +49,12 @@ const PopoverItem: React.FC<React.PropsWithChildren<PopoverItemProps>> = ({
margin: 0;
}
.item > :global(.link) {
width: 100%;
padding: 0.5rem ${theme.layout.gap};
margin: -0.5rem -${theme.layout.gap};
}
.item.line {
line-height: 0;
height: 0;

View File

@@ -9,6 +9,7 @@ interface Props {
className?: string
dropdownStyle?: object
disableMatchWidth?: boolean
getPopupContainer?: () => HTMLElement | null
}
const defaultProps = {
@@ -25,12 +26,17 @@ const SelectDropdown: React.FC<React.PropsWithChildren<SelectDropdownProps>> = (
className,
dropdownStyle,
disableMatchWidth,
getPopupContainer,
}) => {
const theme = useTheme()
const { ref } = useSelectContext()
return (
<Dropdown parent={ref} visible={visible} disableMatchWidth={disableMatchWidth}>
<Dropdown
parent={ref}
visible={visible}
disableMatchWidth={disableMatchWidth}
getPopupContainer={getPopupContainer}>
<div className={`select-dropdown ${className}`} style={dropdownStyle}>
{children}
<style jsx>{`

View File

@@ -28,6 +28,7 @@ interface Props {
dropdownClassName?: string
dropdownStyle?: object
disableMatchWidth?: boolean
getPopupContainer?: () => HTMLElement | null
}
const defaultProps = {
@@ -60,6 +61,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
dropdownClassName,
dropdownStyle,
disableMatchWidth,
getPopupContainer,
...props
}) => {
const theme = useTheme()
@@ -148,7 +150,8 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
visible={visible}
className={dropdownClassName}
dropdownStyle={dropdownStyle}
disableMatchWidth={disableMatchWidth}>
disableMatchWidth={disableMatchWidth}
getPopupContainer={getPopupContainer}>
{children}
</SelectDropdown>
{!pure && (

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Backdrop should render correctly 1`] = `
"<div class=\\"backdrop transition-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><span>test-value</span></div><div class=\\"offset\\"></div><style>
"<div class=\\"backdrop backdrop-wrapper-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><span>test-value</span></div><div class=\\"offset\\"></div><style>
.backdrop {
position: fixed;
top: 0;
@@ -10,7 +10,7 @@ exports[`Backdrop should render correctly 1`] = `
align-content: center;
align-items: center;
flex-direction: column;
justify-content: space-around;
justify-content: center;
height: 100vh;
width: 100vw;
overflow: auto;
@@ -47,5 +47,21 @@ exports[`Backdrop should render correctly 1`] = `
pointer-events: none;
z-index: 1000;
}
.backdrop-wrapper-enter .layer {
opacity: 0;
}
.backdrop-wrapper-enter-active .layer {
opacity: 0.25;
}
.backdrop-wrapper-leave .layer {
opacity: 0.25;
}
.backdrop-wrapper-leave-active .layer {
opacity: 0;
}
</style></div>"
`;

View File

@@ -164,4 +164,24 @@ describe('Dropdown', () => {
expect(() => wrapper.unmount()).not.toThrow()
})
it('should render to specified container', () => {
const Mock: React.FC<{}> = () => {
const ref = useRef<HTMLDivElement>(null)
const customContainer = useRef<HTMLDivElement>(null)
return (
<div>
<div ref={customContainer} id="custom" />
<div ref={ref}>
<Dropdown parent={ref} visible getPopupContainer={() => customContainer.current}>
<span>test-value</span>
</Dropdown>
</div>
</div>
)
}
const wrapper = mount(<Mock />)
const customContainer = wrapper.find('#custom')
expect(customContainer.html()).toContain('dropdown')
})
})

View File

@@ -12,13 +12,13 @@ interface Props {
const defaultProps = {
onClick: () => {},
visible: false,
offsetY: 0,
}
export type BackdropProps = Props & typeof defaultProps
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type BackdropProps = Props & typeof defaultProps & NativeAttrs
const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
({ children, onClick, visible }) => {
({ children, onClick, visible, ...props }) => {
const theme = useTheme()
const [, setIsContentMouseDown, IsContentMouseDownRef] = useCurrentState(false)
const clickHandler = (event: MouseEvent<HTMLElement>) => {
@@ -37,8 +37,8 @@ const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
}
return (
<CSSTransition visible={visible} clearTime={300}>
<div className="backdrop" onClick={clickHandler} onMouseUp={mouseUpHandler}>
<CSSTransition name="backdrop-wrapper" visible={visible} clearTime={300}>
<div className="backdrop" onClick={clickHandler} onMouseUp={mouseUpHandler} {...props}>
<div className="layer" />
<div
onClick={childrenClickHandler}
@@ -56,7 +56,7 @@ const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
align-content: center;
align-items: center;
flex-direction: column;
justify-content: space-around;
justify-content: center;
height: 100vh;
width: 100vw;
overflow: auto;
@@ -93,6 +93,22 @@ const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
pointer-events: none;
z-index: 1000;
}
.backdrop-wrapper-enter .layer {
opacity: 0;
}
.backdrop-wrapper-enter-active .layer {
opacity: ${theme.expressiveness.portalOpacity};
}
.backdrop-wrapper-leave .layer {
opacity: ${theme.expressiveness.portalOpacity};
}
.backdrop-wrapper-leave-active .layer {
opacity: 0;
}
`}</style>
</div>
</CSSTransition>

View File

@@ -10,6 +10,7 @@ interface Props {
parent?: MutableRefObject<HTMLElement | null> | undefined
visible: boolean
disableMatchWidth?: boolean
getPopupContainer?: () => HTMLElement | null
}
interface ReactiveDomReact {
@@ -26,31 +27,48 @@ const defaultRect: ReactiveDomReact = {
width: 0,
}
const getRect = (ref: MutableRefObject<HTMLElement | null>): ReactiveDomReact => {
const getOffset = (el?: HTMLElement | null | undefined) => {
if (!el)
return {
top: 0,
left: 0,
}
const { top, left } = el.getBoundingClientRect()
return { top, left }
}
const getRect = (
ref: MutableRefObject<HTMLElement | null>,
getContainer?: () => HTMLElement | null,
): ReactiveDomReact => {
if (!ref || !ref.current) return defaultRect
const rect = ref.current.getBoundingClientRect()
const container = getContainer ? getContainer() : null
const scrollElement = container || document.documentElement
const { top: offsetTop, left: offsetLeft } = getOffset(container)
return {
...rect,
width: rect.width || rect.right - rect.left,
top: rect.bottom + document.documentElement.scrollTop,
left: rect.left + document.documentElement.scrollLeft,
top: rect.bottom + scrollElement.scrollTop - offsetTop,
left: rect.left + scrollElement.scrollLeft - offsetLeft,
}
}
const Dropdown: React.FC<React.PropsWithChildren<Props>> = React.memo(
({ children, parent, visible, disableMatchWidth }) => {
const el = usePortal('dropdown')
({ children, parent, visible, disableMatchWidth, getPopupContainer }) => {
const el = usePortal('dropdown', getPopupContainer)
const [rect, setRect] = useState<ReactiveDomReact>(defaultRect)
if (!parent) return null
const updateRect = () => {
const { top, left, right, width: nativeWidth } = getRect(parent)
const { top, left, right, width: nativeWidth } = getRect(parent, getPopupContainer)
setRect({ top, left, right, width: nativeWidth })
}
useResize(updateRect)
useClickAnyWhere(() => {
const { top, left } = getRect(parent)
const { top, left } = getRect(parent, getPopupContainer)
const shouldUpdatePosition = top !== rect.top || left !== rect.left
if (!shouldUpdatePosition) return
updateRect()

View File

@@ -58,6 +58,122 @@ exports[`Snippet should render correctly 1`] = `
</style></div>"
`;
exports[`Snippet should work with custom symbol 1`] = `
"<div class=\\"snippet \\"><pre>yarn add @zeit-ui/react</pre><div class=\\"copy\\"><svg viewBox=\\"0 0 24 24\\" width=\\"22\\" height=\\"22\\" stroke=\\"currentColor\\" stroke-width=\\"1.5\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\" style=\\"color: currentcolor;\\"><path d=\\"M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z\\"></path></svg></div><style>
.snippet {
position: relative;
width: initial;
max-width: 100%;
padding: 8pt;
padding-right: calc(2 * 16pt);
color: #000;
background-color: #fff;
border: 1px solid #eaeaea;
border-radius: 5px;
}
pre {
margin: 0;
padding: 0;
border: none;
background-color: transparent;
color: #000;
font-size: 0.8125rem;
}
pre::before {
content: '> ';
user-select: none;
}
pre :global(*) {
margin: 0;
padding: 0;
font-size: inherit;
color: inherit;
}
.copy {
position: absolute;
right: 0;
top: -2px;
transform: translateY(50%);
background-color: #fff;
display: inline-flex;
justify-content: center;
align-items: center;
width: calc(2 * 16pt);
color: inherit;
transition: opacity 0.2s ease 0s;
border-radius: 5px;
cursor: pointer;
user-select: none;
}
.copy:hover {
opacity: 0.7;
}
</style></div>"
`;
exports[`Snippet should work with custom symbol 2`] = `
"<div class=\\"snippet \\"><pre>yarn add @zeit-ui/react</pre><div class=\\"copy\\"><svg viewBox=\\"0 0 24 24\\" width=\\"22\\" height=\\"22\\" stroke=\\"currentColor\\" stroke-width=\\"1.5\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\" style=\\"color: currentcolor;\\"><path d=\\"M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z\\"></path></svg></div><style>
.snippet {
position: relative;
width: initial;
max-width: 100%;
padding: 8pt;
padding-right: calc(2 * 16pt);
color: #000;
background-color: #fff;
border: 1px solid #eaeaea;
border-radius: 5px;
}
pre {
margin: 0;
padding: 0;
border: none;
background-color: transparent;
color: #000;
font-size: 0.8125rem;
}
pre::before {
content: '';
user-select: none;
}
pre :global(*) {
margin: 0;
padding: 0;
font-size: inherit;
color: inherit;
}
.copy {
position: absolute;
right: 0;
top: -2px;
transform: translateY(50%);
background-color: #fff;
display: inline-flex;
justify-content: center;
align-items: center;
width: calc(2 * 16pt);
color: inherit;
transition: opacity 0.2s ease 0s;
border-radius: 5px;
cursor: pointer;
user-select: none;
}
.copy:hover {
opacity: 0.7;
}
</style></div>"
`;
exports[`Snippet should work with different styles 1`] = `
"<div><div class=\\"snippet \\"><pre>yarn add @zeit-ui/react</pre><div class=\\"copy\\"><svg viewBox=\\"0 0 24 24\\" width=\\"22\\" height=\\"22\\" stroke=\\"currentColor\\" stroke-width=\\"1.5\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\" style=\\"color: currentcolor;\\"><path d=\\"M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z\\"></path></svg></div><style>
.snippet {

View File

@@ -70,6 +70,29 @@ describe('Snippet', () => {
expect(wrapper.find('.copy').length).toBe(0)
})
it('should work with custom symbol', () => {
const wrapper = mount(<Snippet text={command} symbol={'>'} />)
expect(wrapper.html()).toMatchSnapshot()
expect(() => wrapper.unmount()).not.toThrow()
const emptySymbolWrapper = mount(<Snippet text={command} symbol=" " />)
expect(emptySymbolWrapper.html()).toMatchSnapshot()
const emptySymbolWrapper2 = mount(<Snippet text={command} symbol="" />)
expect(emptySymbolWrapper2.html()).toEqual(emptySymbolWrapper.html())
expect(() => emptySymbolWrapper.unmount()).not.toThrow()
expect(() => emptySymbolWrapper2.unmount()).not.toThrow()
})
it('should work with custom toast', () => {
document.execCommand = jest.fn()
const wrapper = mount(<Snippet text={command} toastText="Code copied!" toastType="secondary" />)
wrapper.find('.copy').simulate('click')
expect(document.execCommand).toHaveBeenCalled()
;(document.execCommand as jest.Mock).mockRestore()
})
afterAll(() => {
;(window.getSelection as jest.Mock).mockRestore()
;(document.createRange as jest.Mock).mockRestore()

View File

@@ -1,7 +1,7 @@
import React, { useMemo, useRef } from 'react'
import useTheme from '../styles/use-theme'
import withDefaults from '../utils/with-defaults'
import { SnippetTypes, CopyTypes } from '../utils/prop-types'
import { SnippetTypes, CopyTypes, NormalTypes } from '../utils/prop-types'
import { getStyles } from './styles'
import SnippetIcon from './snippet-icon'
import useClipboard from '../utils/use-clipboard'
@@ -9,6 +9,9 @@ import useToasts from '../use-toasts'
interface Props {
text?: string | string[]
symbol?: string
toastText?: string
toastType?: NormalTypes
filled?: boolean
width?: string
copy?: CopyTypes
@@ -18,6 +21,9 @@ interface Props {
const defaultProps = {
filled: false,
symbol: '$',
toastText: 'Copied to clipboard!',
toastType: 'success' as NormalTypes,
width: 'initial',
copy: 'default' as CopyTypes,
type: 'default' as SnippetTypes,
@@ -38,6 +44,9 @@ const Snippet: React.FC<React.PropsWithChildren<SnippetProps>> = ({
type,
filled,
children,
symbol,
toastText,
toastType,
text,
width,
copy: copyType,
@@ -58,12 +67,16 @@ const Snippet: React.FC<React.PropsWithChildren<SnippetProps>> = ({
if (!ref.current) return ''
return ref.current.textContent
}, [ref.current, children, text])
const symbolBefore = useMemo(() => {
const str = symbol.trim()
return str ? `${str} ` : ''
}, [symbol])
const clickHandler = () => {
if (!childText || !showCopyIcon) return
copy(childText)
if (copyType === 'slient') return
setToast({ text: 'Copied to clipboard!', type: 'success' })
setToast({ text: toastText, type: toastType })
}
return (
@@ -101,7 +114,7 @@ const Snippet: React.FC<React.PropsWithChildren<SnippetProps>> = ({
}
pre::before {
content: '$ ';
content: '${symbolBefore}';
user-select: none;
}

View File

@@ -3,6 +3,7 @@ import { mount } from 'enzyme'
import { Table, Code } from 'components'
import { cellActions } from 'components/table/table-cell'
import { nativeEvent, updateWrapper } from 'tests/utils'
import { act } from 'react-dom/test-utils'
const data = [
{ property: 'type', description: 'Content type', default: '-' },
@@ -166,4 +167,40 @@ describe('Table', () => {
expect(wrapper.find('thead').find('code').length).not.toBe(0)
expect(wrapper.html()).toMatchSnapshot()
})
it('the changes of column should be tracked', () => {
const Mock = ({ label }: { label: string }) => {
return (
<Table data={data}>
<Table.Column prop="description" label={label} />
</Table>
)
}
const wrapper = mount(<Mock label="test1" />)
expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test1')
act(() => {
wrapper.setProps({ label: 'test2' })
})
expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test2')
expect(() => wrapper.unmount()).not.toThrow()
})
it('the changes of children should be tracked', () => {
const Mock = ({ label }: { label: string }) => {
return (
<Table data={data}>
<Table.Column prop="description">{label}</Table.Column>
</Table>
)
}
const wrapper = mount(<Mock label="test1" />)
expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test1')
act(() => {
wrapper.setProps({ label: 'test2' })
})
expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test2')
expect(() => wrapper.unmount()).not.toThrow()
})
})

View File

@@ -16,19 +16,19 @@ const TableColumn: React.FC<React.PropsWithChildren<TableColumnProps>> = ({
label,
width,
}) => {
const { appendColumn } = useTableContext()
const { updateColumn } = useTableContext()
if (!prop || prop.trim() === '') {
useWarning('The props "prop" is required.', 'Table.Column')
}
useEffect(() => {
appendColumn &&
appendColumn({
updateColumn &&
updateColumn({
label: children || label,
value: `${prop}`.trim(),
width,
})
}, [])
}, [children, label, prop, width])
return null
}

View File

@@ -8,7 +8,7 @@ export type TableColumnItem = {
export interface TableConfig {
columns: Array<TableColumnItem>
appendColumn?: (column: TableColumnItem) => void
updateColumn?: (column: TableColumnItem) => void
removeRow?: (rowIndex: number) => void
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import TableColumn from './table-column'
import TableHead from './table-head'
import TableBody from './table-body'
@@ -46,11 +46,17 @@ const Table: React.FC<React.PropsWithChildren<TableProps>> = ({
}) => {
const ref = useRef<HTMLTableElement>(null)
const [{ width }, updateShape] = useRealShape<HTMLTableElement>(ref)
const [columns, setColumns, columnsRef] = useCurrentState<Array<TableColumnItem>>([])
const [columns, setColumns] = useState<Array<TableColumnItem>>([])
const [selfData, setSelfData, dataRef] = useCurrentState<Array<TableColumnItem>>([])
const appendColumn = (column: TableColumnItem) => {
const pureCurrent = columnsRef.current.filter(item => item.value !== column.value)
setColumns([...pureCurrent, column])
const updateColumn = (column: TableColumnItem) => {
setColumns(last => {
const hasColumn = last.find(item => item.value === column.value)
if (!hasColumn) return [...last, column]
return last.map(item => {
if (item.value !== column.value) return item
return column
})
})
}
const removeRow = (rowIndex: number) => {
const next = dataRef.current.filter((_, index) => index !== rowIndex)
@@ -61,7 +67,7 @@ const Table: React.FC<React.PropsWithChildren<TableProps>> = ({
const initialValue = useMemo<TableConfig>(
() => ({
columns,
appendColumn,
updateColumn,
removeRow,
}),
[columns],

View File

@@ -87,36 +87,26 @@ describe('Tabs', () => {
expect(active.text()).toContain('label2')
})
it('should warning when label duplicated', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
mount(
<Tabs>
<Tabs.Item label="label1" value="1">
test-1
</Tabs.Item>
<Tabs.Item label="label2" value="1">
test-2
</Tabs.Item>
</Tabs>,
)
expect(errorSpy).toHaveBeenCalled()
errorSpy.mockRestore()
})
it('should re-render when items updated', () => {
const Mock = ({ label = 'label1' }) => {
return (
<Tabs value="1">
<Tabs.Item label={label} value="1">
test-1
</Tabs.Item>
<Tabs.Item label="label-fixed" value="2">
test-label-fixed
</Tabs.Item>
</Tabs>
)
}
const wrapper = mount(<Mock />)
let active = wrapper.find('header').find('.active')
expect(active.text()).toContain('label1')
it('should use label as key when value is missing', async () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
const wrapper = mount(
<Tabs>
<Tabs.Item label="label1">test-1</Tabs.Item>
<Tabs.Item label="label2">test-2</Tabs.Item>
</Tabs>,
)
expect(errorSpy).not.toHaveBeenCalled()
wrapper.setProps({ value: 'label2' })
await updateWrapper(wrapper, 350)
const active = wrapper.find('header').find('.active')
wrapper.setProps({ label: 'label2' })
active = wrapper.find('header').find('.active')
expect(active.text()).toContain('label2')
errorSpy.mockRestore()
expect(() => wrapper.unmount()).not.toThrow()
})
})

View File

@@ -8,7 +8,6 @@ export interface TabsLabelItem {
export interface TabsConfig {
register?: (item: TabsLabelItem) => void
unregister?: (item: TabsLabelItem) => void
currentValue?: string
inGroup: boolean
}

View File

@@ -4,7 +4,7 @@ import { useTabsContext } from './tabs-context'
interface Props {
label: string | React.ReactNode
value?: string
value: string
disabled?: boolean
}
@@ -16,20 +16,16 @@ export type TabsItemProps = Props & typeof defaultProps
const TabsItem: React.FC<React.PropsWithChildren<TabsItemProps>> = ({
children,
value: userCustomValue,
value,
label,
disabled,
}) => {
const value = useMemo(() => userCustomValue || `${label}`, [userCustomValue, label])
const { register, unregister, currentValue } = useTabsContext()
const { register, currentValue } = useTabsContext()
const isActive = useMemo(() => currentValue === value, [currentValue, value])
useEffect(() => {
register && register({ value, label, disabled })
return () => {
unregister && unregister({ value, label, disabled })
}
}, [])
}, [value, label, disabled])
/* eslint-disable react/jsx-no-useless-fragment */
return isActive ? <>{children}</> : null

View File

@@ -2,8 +2,6 @@ import React, { useEffect, useMemo, useState } from 'react'
import TabsItem from './tabs-item'
import useTheme from '../styles/use-theme'
import { TabsLabelItem, TabsConfig, TabsContext } from './tabs-context'
import useCurrentState from '../utils/use-current-state'
import useWarning from '../utils/use-warning'
interface Props {
initialValue?: string
@@ -32,24 +30,26 @@ const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
}) => {
const theme = useTheme()
const [selfValue, setSelfValue] = useState<string | undefined>(userCustomInitialValue)
const [tabs, setTabs, tabsRef] = useCurrentState<Array<TabsLabelItem>>([])
const [tabs, setTabs] = useState<Array<TabsLabelItem>>([])
const register = (next: TabsLabelItem) => {
const hasItem = tabsRef.current.find(item => item.value === next.value)
if (hasItem) {
useWarning('The "value" of each "Tabs.Item" must be unique.', 'Tabs')
}
setTabs([...tabsRef.current, next])
}
const unregister = (next: TabsLabelItem) => {
const nextTabs = tabsRef.current.filter(item => item.value !== next.value)
setTabs([...nextTabs])
setTabs(last => {
const hasItem = last.find(item => item.value === next.value)
if (!hasItem) return [...last, next]
return last.map(item => {
if (item.value !== next.value) return item
return {
...item,
label: next.label,
disabled: next.disabled,
}
})
})
}
const initialValue = useMemo<TabsConfig>(
() => ({
register,
unregister,
currentValue: selfValue,
inGroup: true,
}),
@@ -71,13 +71,13 @@ const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
<TabsContext.Provider value={initialValue}>
<div className={`tabs ${className}`} {...props}>
<header className={hideDivider ? 'hide-divider' : ''}>
{tabs.map((item, index) => (
{tabs.map(item => (
<div
className={`tab ${selfValue === item.value ? 'active' : ''} ${
item.disabled ? 'disabled' : ''
}`}
role="button"
key={item.value + index}
key={item.value}
onClick={() => clickHandler(item)}>
{item.label}
</div>

View File

@@ -2,208 +2,208 @@
exports[`Textarea should render correctly 1`] = `
"<div class=\\"wrapper \\"><textarea placeholder=\\"placeholder\\"></textarea><style>
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: initial;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #eaeaea;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: initial;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #eaeaea;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 6.25rem;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 6.25rem;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
.disabled > textarea {
cursor: not-allowed;
}
.disabled > textarea {
cursor: not-allowed;
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div>"
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div>"
`;
exports[`Textarea should work with different styles 1`] = `
"<div><div class=\\"wrapper \\"><textarea></textarea><style>
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: initial;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #666;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: initial;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #666;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 6.25rem;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 6.25rem;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
.disabled > textarea {
cursor: not-allowed;
}
.disabled > textarea {
cursor: not-allowed;
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div><div class=\\"wrapper \\"><textarea></textarea><style>
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: 20%;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #eaeaea;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div><div class=\\"wrapper \\"><textarea></textarea><style>
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: 20%;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #eaeaea;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 6.25rem;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 6.25rem;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
.disabled > textarea {
cursor: not-allowed;
}
.disabled > textarea {
cursor: not-allowed;
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div><div class=\\"wrapper \\"><textarea></textarea><style>
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: initial;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #eaeaea;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div><div class=\\"wrapper \\"><textarea></textarea><style>
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: initial;
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: 5px;
border: 1px solid #eaeaea;
color: #000;
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.hover {
border-color: #666;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
.wrapper.disabled {
background-color: #fafafa;
border-color: #eaeaea;
cursor: not-allowed;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 100px;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: 100px;
resize: none;
border: none;
outline: none;
padding: 8pt;
}
.disabled > textarea {
cursor: not-allowed;
}
.disabled > textarea {
cursor: not-allowed;
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div></div>"
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px #fff inset !important;
}
</style></div></div>"
`;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react'
import React, { useRef, useImperativeHandle, useEffect, useMemo, useState } from 'react'
import useTheme from '../styles/use-theme'
import withDefaults from '../utils/with-defaults'
import { NormalTypes } from '../utils/prop-types'
@@ -32,113 +32,130 @@ const defaultProps = {
type NativeAttrs = Omit<React.TextareaHTMLAttributes<any>, keyof Props>
export type TextareaProps = Props & typeof defaultProps & NativeAttrs
const Textarea: React.FC<React.PropsWithChildren<TextareaProps>> = ({
width,
status,
minHeight,
disabled,
readOnly,
onFocus,
onBlur,
className,
initialValue,
onChange,
value,
placeholder,
...props
}) => {
const theme = useTheme()
const [selfValue, setSelfValue] = useState<string>(initialValue)
const [hover, setHover] = useState<boolean>(false)
const { color, borderColor, hoverBorder } = useMemo(() => getColors(theme.palette, status), [
theme.palette,
status,
])
const Textarea = React.forwardRef<HTMLTextAreaElement, React.PropsWithChildren<TextareaProps>>(
(
{
width,
status,
minHeight,
disabled,
readOnly,
onFocus,
onBlur,
className,
initialValue,
onChange,
value,
placeholder,
...props
},
ref: React.Ref<HTMLTextAreaElement | null>,
) => {
const theme = useTheme()
const textareaRef = useRef<HTMLTextAreaElement>(null)
useImperativeHandle(ref, () => textareaRef.current)
const isControlledComponent = useMemo(() => value !== undefined, [value])
const [selfValue, setSelfValue] = useState<string>(initialValue)
const [hover, setHover] = useState<boolean>(false)
const { color, borderColor, hoverBorder } = useMemo(() => getColors(theme.palette, status), [
theme.palette,
status,
])
const changeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
if (disabled || readOnly) return
setSelfValue(event.target.value)
onChange && onChange(event)
}
const focusHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
setHover(true)
onFocus && onFocus(e)
}
const blurHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
setHover(false)
onBlur && onBlur(e)
}
const changeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
if (disabled || readOnly) return
setSelfValue(event.target.value)
onChange && onChange(event)
}
const focusHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
setHover(true)
onFocus && onFocus(e)
}
const blurHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
setHover(false)
onBlur && onBlur(e)
}
useEffect(() => {
if (value === undefined) return
setSelfValue(value)
}, [value])
useEffect(() => {
if (isControlledComponent) {
setSelfValue(value as string)
}
})
return (
<div className={`wrapper ${hover ? 'hover' : ''} ${disabled ? 'disabled' : ''} ${className}`}>
<textarea
disabled={disabled}
value={selfValue}
placeholder={placeholder}
readOnly={readOnly}
onFocus={focusHandler}
onBlur={blurHandler}
onChange={changeHandler}
{...props}
/>
<style jsx>{`
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: ${width};
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: ${theme.layout.radius};
border: 1px solid ${borderColor};
color: ${color};
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
const controlledValue = isControlledComponent
? { value: selfValue }
: { defaultValue: initialValue }
const textareaProps = {
...props,
...controlledValue,
}
.wrapper.hover {
border-color: ${hoverBorder};
}
return (
<div className={`wrapper ${hover ? 'hover' : ''} ${disabled ? 'disabled' : ''} ${className}`}>
<textarea
ref={textareaRef}
disabled={disabled}
placeholder={placeholder}
readOnly={readOnly}
onFocus={focusHandler}
onBlur={blurHandler}
onChange={changeHandler}
{...textareaProps}
/>
<style jsx>{`
.wrapper {
display: inline-flex;
box-sizing: border-box;
user-select: none;
width: ${width};
min-width: 12.5rem;
max-width: 95vw;
height: auto;
border-radius: ${theme.layout.radius};
border: 1px solid ${borderColor};
color: ${color};
transition: border 0.2s ease 0s, color 0.2s ease 0s;
}
.wrapper.disabled {
background-color: ${theme.palette.accents_1};
border-color: ${theme.palette.accents_2};
cursor: not-allowed;
}
.wrapper.hover {
border-color: ${hoverBorder};
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: ${theme.font.sans};
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: ${minHeight};
resize: none;
border: none;
outline: none;
padding: ${theme.layout.gapHalf};
}
.wrapper.disabled {
background-color: ${theme.palette.accents_1};
border-color: ${theme.palette.accents_2};
cursor: not-allowed;
}
.disabled > textarea {
cursor: not-allowed;
}
textarea {
background-color: transparent;
box-shadow: none;
display: block;
font-family: ${theme.font.sans};
font-size: 0.875rem;
width: 100%;
height: 100%;
min-height: ${minHeight};
resize: none;
border: none;
outline: none;
padding: ${theme.layout.gapHalf};
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px ${theme.palette.background} inset !important;
}
`}</style>
</div>
)
}
.disabled > textarea {
cursor: not-allowed;
}
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:active,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px ${theme.palette.background} inset !important;
}
`}</style>
</div>
)
},
)
export default withDefaults(Textarea, defaultProps)

View File

@@ -35,7 +35,8 @@ exports[`UseToast should render different actions 1`] = `
--zeit-ui-button-bg: #000;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;
@@ -96,7 +97,8 @@ exports[`UseToast should render different actions 1`] = `
--zeit-ui-button-bg: #fff;
}
.btn:hover {
.btn:hover,
.btn:focus {
color: #000;
--zeit-ui-button-color: #000;
background-color: #fff;

View File

@@ -111,3 +111,16 @@ export const getReactNode = (node?: React.ReactNode | (() => React.ReactNode)):
if (typeof node !== 'function') return node
return (node as () => React.ReactNode)()
}
export const isChildElement = (
parent: Element | null | undefined,
child: Element | null | undefined,
): boolean => {
if (!parent || !child) return false
let node: (Node & ParentNode) | null = child
while (node) {
if (node === parent) return true
node = node.parentNode
}
return false
}

View File

@@ -8,7 +8,10 @@ const createElement = (id: string): HTMLElement => {
return el
}
const usePortal = (selectId: string = getId()): HTMLElement | null => {
const usePortal = (
selectId: string = getId(),
getContainer?: () => HTMLElement | null,
): HTMLElement | null => {
const id = `zeit-ui-${selectId}`
const { isBrowser } = useSSR()
const [elSnapshot, setElSnapshot] = useState<HTMLElement | null>(
@@ -16,11 +19,13 @@ const usePortal = (selectId: string = getId()): HTMLElement | null => {
)
useEffect(() => {
const hasElement = document.querySelector<HTMLElement>(`#${id}`)
const customContainer = getContainer ? getContainer() : null
const parentElement = customContainer || document.body
const hasElement = parentElement.querySelector<HTMLElement>(`#${id}`)
const el = hasElement || createElement(id)
if (!hasElement) {
document.body.appendChild(el)
parentElement.appendChild(el)
}
setElSnapshot(el)
}, [])

View File

@@ -3,7 +3,7 @@ import Router from 'next/router'
import { useTheme } from 'components'
import Controls from 'lib/components/controls'
import LogoIcon from 'lib/components/icons/logo'
import { useConfigs } from '../../config-context'
import { useConfigs } from 'lib/config-context'
const MenuLinks = () => {
const theme = useTheme()

View File

@@ -1,6 +1,6 @@
{
"name": "@zeit-ui/react",
"version": "1.7.7",
"version": "1.8.0-canary.10",
"main": "dist/index.js",
"module": "esm/index.js",
"types": "dist/index.d.ts",

View File

@@ -110,7 +110,7 @@ Used to trigger an operation.
scope={{ Button, Spacer, Settings, UserX, Power }}
code={`
<>
<Button iconRight={<Power />} auto size="small">shut off</Button>
<Button iconRight={<Power />} auto size="small" />
<Spacer y={.5} />
<Button icon={<Settings />} auto>Action</Button>
<Spacer y={.5} />

View File

@@ -185,6 +185,28 @@ Retrieve text input from a user.
`}
/>
<Playground
title="Imperative API"
desc="Update component in an uncontrolled way."
scope={{ Input, Spacer, Button }}
code={`
() => {
const ref = React.useRef(null)
const setChange = () => {
ref && (ref.current.value = Math.random().toString(32))
}
return (
<>
<Input initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
<Spacer y={.5} />
<Button auto type="secondary" size="small"
onClick={setChange}>set value</Button>
</>
)
}
`}
/>
<Attributes edit="/pages/en-us/components/input.mdx">
<Attributes.Title>Input.Props</Attributes.Title>

View File

@@ -49,7 +49,7 @@ Hyperlinks between pages.
<Text><Link href="#">The Evil Rabbit Jumped over the Fence.</Link></Text>
<Text><Link href="#" color>The Evil Rabbit Jumped over the Fence.</Link></Text>
<Text><Link href="#" underline>The Evil Rabbit Jumped over the Fence.</Link></Text>
<Text><Link href="#" underline>The Evil Rabbit Jumped over the Fence.</Link></Text>
<Text><Link href="#" color underline>The Evil Rabbit Jumped over the Fence.</Link></Text>
</>
`}
/>

View File

@@ -159,6 +159,30 @@ Display popup content that requires attention or provides additional information
`}
/>
<Playground
title="Loading"
scope={{ Modal, Button, useModal }}
code={`
() => {
const { visible, setVisible, bindings } = useModal()
return (
<>
<Button auto onClick={() => setVisible(true)}>Show Modal</Button>
<Modal {...bindings}>
<Modal.Title>Modal</Modal.Title>
<Modal.Subtitle>This is a modal</Modal.Subtitle>
<Modal.Content>
<p>Some content contained within the modal.</p>
</Modal.Content>
<Modal.Action passive onClick={() => setVisible(false)}>Cancel</Modal.Action>
<Modal.Action loading>Submit</Modal.Action>
</Modal>
</>
)
}
`}
/>
<Attributes edit="/pages/en-us/components/modal.mdx">
<Attributes.Title>Modal.Props</Attributes.Title>
@@ -190,12 +214,6 @@ Display popup content that requires attention or provides additional information
| --------- | ------------ | ---------------- | ------------------------ | ------- |
| ... | native props | `HTMLAttributes` | `'id', 'className', ...` | - |
<Attributes.Title>Modal.Actions.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default |
| --------- | ----------- | ---- | --------------- | ------- |
| - | - | - | - | - |
<Attributes.Title>Modal.Action.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default |
@@ -203,6 +221,7 @@ Display popup content that requires attention or provides additional information
| **passive** | display passive mode | `boolean` | - | `false` |
| **disabled** | disable current action | `boolean` | - | `false` |
| **onClick** | click handler | [(event: ModalActionEvent) => void](#modalactionevent) | - | - |
| **loading** | show loading | `boolean` | - | `false` |
| ... | native props | `ButtonHTMLAttributes` | `'id', 'className', ...` | - |
<Attributes.Title>useModal</Attributes.Title>

View File

@@ -1,5 +1,5 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Select, Spacer, Code } from 'components'
import { Select, Spacer, Code, Modal, useModal, Button } from 'components'
export const meta = {
title: 'select',
@@ -142,25 +142,56 @@ Display a dropdown list of items.
`}
/>
<Playground
title="Set parent element"
desc="you can specify the container for drop-down box rendering."
scope={{ Select, Spacer, useModal, Modal, Button, Code }}
code={`
() => {
const { visible, setVisible, bindings } = useModal()
return (
<>
<Button auto onClick={() => setVisible(true)}>Show Select</Button>
<Modal {...bindings}>
<Modal.Title>Modal</Modal.Title>
<Modal.Content id="customModalSelect">
<Select placeholder="Choose one" initialValue="1"
getPopupContainer={() => document.getElementById('customModalSelect')}>
<Select.Option value="1"><Code>TypeScript</Code></Select.Option>
<Select.Option value="2"><Code>JavaScript</Code></Select.Option>
</Select>
<p>Scroll through the content to see the changes.</p>
<div style={{ height: '1200px' }}></div>
<p>Scroll through the content to see the changes.</p>
</Modal.Content>
<Modal.Action passive onClick={() => setVisible(false)}>Cancel</Modal.Action>
</Modal>
</>
)
}
`}
/>
<Attributes edit="/pages/en-us/components/select.mdx">
<Attributes.Title>Select.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default |
| --------------------- | --------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
| **value** | selected value | `string`, `string[]` | - | - |
| **initialValue** | initial value | `string`, `string[]` | - | - |
| **placeholder** | placeholder string | `string` | - | - |
| **width** | css width value of select | `string` | - | `initial` |
| **size** | select component size | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **icon** | icon component | `ComponentType` | - | `SVG Component` |
| **pure** | remove icon component | `boolean` | - | `false` |
| **multiple** | support multiple selection | `boolean` | - | `false` |
| **disabled** | disable current radio | `boolean` | - | `false` |
| **onChange** | selected value | <Code>(val: string &#124; string[]) => void </Code> | - | - |
| **dropdownClassName** | className of dropdown menu | `string` | - | - |
| **dropdownStyle** | style of dropdown menu | `object` | - | - |
| **disableMatchWidth** | disable Option from follow Select width | `boolean` | - | `false` |
| ... | native props | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
| Attribute | Description | Type | Accepted values | Default |
| --------------------- | ----------------------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
| **value** | selected value | `string`, `string[]` | - | - |
| **initialValue** | initial value | `string`, `string[]` | - | - |
| **placeholder** | placeholder string | `string` | - | - |
| **width** | css width value of select | `string` | - | `initial` |
| **size** | select component size | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **icon** | icon component | `ComponentType` | - | `SVG Component` |
| **pure** | remove icon component | `boolean` | - | `false` |
| **multiple** | support multiple selection | `boolean` | - | `false` |
| **disabled** | disable current radio | `boolean` | - | `false` |
| **onChange** | selected value | <Code>(val: string &#124; string[]) => void </Code> | - | - |
| **dropdownClassName** | className of dropdown menu | `string` | - | - |
| **dropdownStyle** | style of dropdown menu | `object` | - | - |
| **disableMatchWidth** | disable Option from follow Select width | `boolean` | - | `false` |
| **getPopupContainer** | dropdown render parent element, the default is `body` | `() => HTMLElement` | - | - |
| ... | native props | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
<Attributes.Title>Select.Option.Props</Attributes.Title>

View File

@@ -65,6 +65,22 @@ Display a snippet of copyable code for the command line.
`}
/>
<Playground
title="custom symbol"
scope={{ Snippet }}
code={`
<Snippet symbol=">" text="yarn add @zeit-ui/react" width="300px" />
`}
/>
<Playground
title="custom toast"
scope={{ Snippet }}
code={`
<Snippet toastText="Code copied!" toastType="secondary" text="yarn add @zeit-ui/react" width="300px" />
`}
/>
<Playground
title="filled"
scope={{ Snippet, Spacer }}
@@ -84,14 +100,17 @@ Display a snippet of copyable code for the command line.
<Attributes edit="/pages/en-us/components/snippet.mdx">
<Attributes.Title>Snippet.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default |
| ---------- | ----------------------- | ----------------------------- | -------------------------------- | --------- |
| **text** | code snippet | `string` `Array<string>` | - | - |
| **type** | snippet types | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes) | `default` |
| **filled** | filled style | `boolean` | - | `false` |
| **width** | set CSS string | `string` | - | `initial` |
| **copy** | function of copy button | `CopyTypes` | `'default', 'slient', 'prevent'` | `default` |
| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
| Attribute | Description | Type | Accepted values | Default |
| ------------- | ----------------------- | ----------------------------- | -------------------------------- | ---------------------- |
| **text** | code snippet | `string` `Array<string>` | - | - |
| **type** | snippet types | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes) | `default` |
| **filled** | filled style | `boolean` | - | `false` |
| **width** | set CSS string | `string` | - | `initial` |
| **copy** | function of copy button | `CopyTypes` | `'default', 'slient', 'prevent'` | `default` |
| **symbol** | symbol snippet | `string` | - | `$` |
| **toastText** | toast text | `string` | - | `Copied to clipboard!` |
| **toastType** | toast type | `NormalTypes` | [NormalTypes](#normaltypes) | `success` |
| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
<Attributes.Title>SnippetTypes</Attributes.Title>
@@ -99,6 +118,12 @@ Display a snippet of copyable code for the command line.
type SnippetTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error' | 'dark' | 'lite'
```
<Attributes.Title>NormalTypes</Attributes.Title>
```ts
type NormalTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error'
```
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>

View File

@@ -134,7 +134,7 @@ Display tab content.
| Attribute | Description | Type | Accepted values | Default |
| ------------------- | ------------------- | --------- | --------------- | ------- |
| **label**(required) | display tab's label | `string` | - | - |
| **value** | unique ident value | `string` | - | - |
| **value**(required) | unique ident value | `string` | - | - |
| **disabled** | disable current tab | `boolean` | - | `false` |
<Attributes.Title>useTabs</Attributes.Title>

View File

@@ -99,6 +99,28 @@ Retrieve multi-line user input.
`}
/>
<Playground
title="Imperative API"
desc="Update component in an uncontrolled way."
scope={{ Textarea, Spacer, Button }}
code={`
() => {
const ref = React.useRef(null)
const setChange = () => {
ref && (ref.current.value = Math.random().toString(32))
}
return (
<>
<Textarea initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
<Spacer y={.5} />
<Button auto type="secondary" size="small"
onClick={setChange}>set value</Button>
</>
)
}
`}
/>
<Attributes edit="/pages/en-us/components/textarea.mdx">
<Attributes.Title alias="Input.Textarea">Textarea.Props</Attributes.Title>

View File

@@ -125,6 +125,16 @@ interface ToastAction {
}
```
<Attributes.Title>NormalTypes</Attributes.Title>
```ts
type NormalTypes = 'default'
| 'secondary'
| 'success'
| 'warning'
| 'error'
```
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>

View File

@@ -109,7 +109,7 @@ export const meta = {
scope={{ Button, Spacer, Settings, UserX, Power }}
code={`
<>
<Button iconRight={<Power />} auto size="small">关闭</Button>
<Button iconRight={<Power />} auto size="small" />
<Spacer y={.5} />
<Button icon={<Settings />} auto>按钮</Button>
<Spacer y={.5} />

View File

@@ -186,6 +186,28 @@ export const meta = {
`}
/>
<Playground
title="命令式 API"
desc="使用非受控方式更新组件。"
scope={{ Input, Spacer, Button }}
code={`
() => {
const ref = React.useRef(null)
const setChange = () => {
ref && (ref.current.value = Math.random().toString(32))
}
return (
<>
<Input initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
<Spacer y={.5} />
<Button auto type="secondary" size="small"
onClick={setChange}>设置值</Button>
</>
)
}
`}
/>
<Attributes edit="/pages/zh-cn/components/input.mdx">
<Attributes.Title>Input.Props</Attributes.Title>

View File

@@ -37,7 +37,7 @@ export const meta = {
<Text><Link href="#">严格模式通过抛出错误来消除了一些原有静默错误</Link></Text>
<Text><Link href="#" color>严格模式通过抛出错误来消除了一些原有静默错误</Link></Text>
<Text><Link href="#" underline>通过使用指南和教程来学习如何用 React</Link></Text>
<Text><Link href="#" underline>通过使用指南和教程来学习如何用 React</Link></Text>
<Text><Link href="#" color underline>通过使用指南和教程来学习如何用 React</Link></Text>
</>
`}
/>

View File

@@ -40,8 +40,8 @@ export const meta = {
<Modal.Content>
<p> <Code>yield</Code> 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。</p>
</Modal.Content>
<Modal.Action passive>放弃使用</Modal.Action>
<Modal.Action>明白了</Modal.Action>
<Modal.Action passive onClick={({ close }) => close()}>放弃使用</Modal.Action>
<Modal.Action onClick={({ close }) => close()}>明白了</Modal.Action>
</Modal>
</div>
)
@@ -69,8 +69,8 @@ export const meta = {
<Modal.Content>
<p> <Code>yield</Code> 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。</p>
</Modal.Content>
<Modal.Action passive>放弃使用</Modal.Action>
<Modal.Action>明白了</Modal.Action>
<Modal.Action passive onClick={() => setVisible(false)}>放弃使用</Modal.Action>
<Modal.Action onClick={() => setVisible(false)}>明白了</Modal.Action>
</Modal>
</>
)
@@ -127,8 +127,8 @@ export const meta = {
<Modal.Content>
<p> <Code>yield</Code> 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。</p>
</Modal.Content>
<Modal.Action passive>继续学习</Modal.Action>
<Modal.Action disabled>尝试箭头函数</Modal.Action>
<Modal.Action passive onClick={({ close }) => close()}>继续学习</Modal.Action>
<Modal.Action disabled onClick={close => close()}>尝试箭头函数</Modal.Action>
</Modal>
</>
)
@@ -158,6 +158,30 @@ export const meta = {
`}
/>
<Playground
title="加载中"
scope={{ Modal, Button, useModal, Code }}
code={`
() => {
const { visible, setVisible, bindings } = useModal()
return (
<>
<Button auto onClick={() => setVisible(true)}>Show Modal</Button>
<Modal {...bindings}>
<Modal.Title>箭头函数</Modal.Title>
<Modal.Subtitle>它不能用作构造函数</Modal.Subtitle>
<Modal.Content>
<p> <Code>yield</Code> 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。</p>
</Modal.Content>
<Modal.Action passive onClick={() => setVisible(false)}>取消</Modal.Action>
<Modal.Action loading>提交</Modal.Action>
</Modal>
</>
)
}
`}
/>
<Attributes edit="/pages/zh-cn/components/modal.mdx">
<Attributes.Title>Modal.Props</Attributes.Title>
@@ -189,12 +213,6 @@ export const meta = {
| ---- | -------- | ---------------- | ------------------------ | ---- |
| ... | 原生属性 | `HTMLAttributes` | `'id', 'className', ...` | - |
<Attributes.Title>Modal.Actions.Props</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
| ---- | ---- | ---- | ------ | ---- |
| - | - | - | - | - |
<Attributes.Title>Modal.Action.Props</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
@@ -202,6 +220,7 @@ export const meta = {
| **passive** | 以消极的状态显示按钮 | `boolean` | - | `false` |
| **disabled** | 禁用按钮 | `boolean` | - | `false` |
| **onClick** | 按钮的点击事件 | [(event: ModalActionEvent) => void](#modalactionevent) | - | - |
| **loading** | 显示加载状态指示器 | `boolean` | - | `false` |
| ... | 原生属性 | `ButtonHTMLAttributes` | `'id', 'className', ...` | - |
<Attributes.Title>useModal</Attributes.Title>

View File

@@ -1,5 +1,5 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Select, Spacer, Code } from 'components'
import { Select, Spacer, Code, Modal, useModal, Button } from 'components'
export const meta = {
title: '选择器 Select',
@@ -142,25 +142,56 @@ export const meta = {
`}
/>
<Playground
title="设置渲染容器"
desc="你可以指定下拉框的元素渲染的容器。"
scope={{ Select, Spacer, useModal, Modal, Button, Code }}
code={`
() => {
const { visible, setVisible, bindings } = useModal()
return (
<>
<Button auto onClick={() => setVisible(true)}>显示选择器</Button>
<Modal {...bindings}>
<Modal.Title>Modal</Modal.Title>
<Modal.Content id="customModalSelect">
<Select placeholder="选择语言" initialValue="1"
getPopupContainer={() => document.getElementById('customModalSelect')}>
<Select.Option value="1"><Code>TypeScript</Code></Select.Option>
<Select.Option value="2"><Code>JavaScript</Code></Select.Option>
</Select>
<p>滚动以查看下拉框的工作方式。(超出弹出层会被隐藏)</p>
<div style={{ height: '1200px' }}></div>
<p>滚动以查看下拉框的工作方式。</p>
</Modal.Content>
<Modal.Action passive onClick={() => setVisible(false)}>取消</Modal.Action>
</Modal>
</>
)
}
`}
/>
<Attributes edit="/pages/zh-cn/components/select.mdx">
<Attributes.Title>Select.Props</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
| --------------------- | ---------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
| **value** | 手动设置选择器的值 | `string`, `string[]` | - | - |
| **initialValue** | 选择器初始值 | `string`, `string[]` | - | - |
| **placeholder** | 占位文本内容 | `string` | - | - |
| **width** | 组件的 CSS 宽度值 | `string` | - | `initial` |
| **size** | 选择器组件大小 | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **icon** | 右侧图标组件 | `ComponentType` | - | `SVG Component` |
| **pure** | 隐藏右侧图标组件 | `boolean` | - | `false` |
| **multiple** | 是否支持多选 | `boolean` | - | `false` |
| **disabled** | 禁用所有的交互 | `boolean` | - | `false` |
| **onChange** | 选项被选中所触发的事件 | <Code>(val: string &#124; string[]) => void </Code> | - | - |
| **dropdownClassName** | 下拉框的自定义类名 | `string` | - | - |
| **dropdownStyle** | 下拉框的自定义样式 | `object` | - | - |
| **disableMatchWidth** | 禁止 Option 跟随单选框的宽度 | `boolean` | - | `false` |
| ... | 原生属性 | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
| --------------------- | --------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
| **value** | 手动设置选择器的值 | `string`, `string[]` | - | - |
| **initialValue** | 选择器初始值 | `string`, `string[]` | - | - |
| **placeholder** | 占位文本内容 | `string` | - | - |
| **width** | 组件的 CSS 宽度值 | `string` | - | `initial` |
| **size** | 选择器组件大小 | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
| **icon** | 右侧图标组件 | `ComponentType` | - | `SVG Component` |
| **pure** | 隐藏右侧图标组件 | `boolean` | - | `false` |
| **multiple** | 是否支持多选 | `boolean` | - | `false` |
| **disabled** | 禁用所有的交互 | `boolean` | - | `false` |
| **onChange** | 选项被选中所触发的事件 | <Code>(val: string &#124; string[]) => void </Code> | - | - |
| **dropdownClassName** | 下拉框的自定义类名 | `string` | - | - |
| **dropdownStyle** | 下拉框的自定义样式 | `object` | - | - |
| **disableMatchWidth** | 禁止 Option 跟随单选框的宽度 | `boolean` | - | `false` |
| **getPopupContainer** | 下拉框渲染的父元素,默认是 `body` | `() => HTMLElement` | - | - |
| ... | 原生属性 | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
<Attributes.Title>Select.Option.Props</Attributes.Title>

View File

@@ -84,14 +84,17 @@ export const meta = {
<Attributes edit="/pages/zh-cn/components/snippet.mdx">
<Attributes.Title>Snippet.Props</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
| ---------- | ------------------ | ----------------------------- | -------------------------------- | --------- |
| **text** | 命令文本 | `string` `Array<string>` | - | - |
| **type** | 组件类型 | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes) | `default` |
| **filled** | 填充风格的样式 | `boolean` | - | `false` |
| **width** | 设置 CSS 宽度 | `string` | - | `initial` |
| **copy** | 拷贝按钮的工作方式 | `CopyTypes` | `'default', 'slient', 'prevent'` | `default` |
| ... | 原生属性 | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
| ------------- | ------------------ | ----------------------------- | -------------------------------- | ---------------------- |
| **text** | 命令文本 | `string` `Array<string>` | - | - |
| **type** | 组件类型 | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes) | `default` |
| **filled** | 填充风格的样式 | `boolean` | - | `false` |
| **width** | 设置 CSS 宽度 | `string` | - | `initial` |
| **copy** | 拷贝按钮的工作方式 | `CopyTypes` | `'default', 'slient', 'prevent'` | `default` |
| **symbol** | 组件左侧显示的字符 | `string` | - | `$` |
| **toastText** | 拷贝提示的字符 | `string` | - | `Copied to clipboard!` |
| **toastType** | 拷贝提示的类型 | `NormalTypes` | [NormalTypes](#normaltypes) | `success` |
| ... | 原生属性 | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
<Attributes.Title>SnippetTypes</Attributes.Title>
@@ -99,6 +102,12 @@ export const meta = {
type SnippetTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error' | 'dark' | 'lite'
```
<Attributes.Title>NormalTypes</Attributes.Title>
```ts
type NormalTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error'
```
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>

View File

@@ -131,7 +131,7 @@ export const meta = {
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
| ----------------- | -------------- | --------- | ------ | ------- |
| **label**(必须的) | 选项卡标签文字 | `string` | - | - |
| **value** | 唯一鉴别值 | `string` | - | - |
| **value**(必须的) | 唯一鉴别值 | `string` | - | - |
| **disabled** | 禁用当前选项卡 | `boolean` | - | `false` |
<Attributes.Title>useTabs</Attributes.Title>

View File

@@ -99,6 +99,29 @@ export const meta = {
`}
/>
<Playground
title="命令式 API"
desc="使用非受控方式更新组件。"
scope={{ Textarea, Spacer, Button }}
code={`
() => {
const ref = React.useRef(null)
const setChange = () => {
ref && (ref.current.value = Math.random().toString(32))
}
return (
<>
<Textarea initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
<Spacer y={.5} />
<Button auto type="secondary" size="small"
onClick={setChange}>设置值</Button>
</>
)
}
`}
/>
<Attributes edit="/pages/zh-cn/components/textarea.mdx">
<Attributes.Title alias="Input.Textarea">Textarea.Props</Attributes.Title>

View File

@@ -125,6 +125,17 @@ interface ToastAction {
}
```
<Attributes.Title>NormalTypes</Attributes.Title>
```ts
type NormalTypes = 'default'
| 'secondary'
| 'success'
| 'warning'
| 'error'
```
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>