mirror of
https://github.com/zhigang1992/react.git
synced 2026-05-09 16:52:46 +08:00
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>"
|
||||
`;
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>{`
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>"
|
||||
`;
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@ export interface TabsLabelItem {
|
||||
|
||||
export interface TabsConfig {
|
||||
register?: (item: TabsLabelItem) => void
|
||||
unregister?: (item: TabsLabelItem) => void
|
||||
currentValue?: string
|
||||
inGroup: boolean
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>"
|
||||
`;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}, [])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 | 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 | 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 | 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 | 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -131,7 +131,7 @@ export const meta = {
|
||||
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
|
||||
| ----------------- | -------------- | --------- | ------ | ------- |
|
||||
| **label**(必须的) | 选项卡标签文字 | `string` | - | - |
|
||||
| **value** | 唯一鉴别值 | `string` | - | - |
|
||||
| **value**(必须的) | 唯一鉴别值 | `string` | - | - |
|
||||
| **disabled** | 禁用当前选项卡 | `boolean` | - | `false` |
|
||||
|
||||
<Attributes.Title>useTabs</Attributes.Title>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user