From bb575498fed75709e465bee84b2e15f206662cfa Mon Sep 17 00:00:00 2001 From: witt Date: Fri, 14 Aug 2020 16:59:49 +0800 Subject: [PATCH] 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 Co-authored-by: yqrashawn Co-authored-by: Zhao Lei --- .../__snapshots__/index.test.tsx.snap | 15 +- .../__snapshots__/icon.test.tsx.snap | 115 +++++- components/button/__tests__/icon.test.tsx | 7 + components/button/button-icon.tsx | 11 +- components/button/button.tsx | 5 +- components/button/utils.tsx | 7 + components/description/description.tsx | 1 + components/image/__tests__/browser.test.tsx | 2 +- components/input/input.tsx | 24 +- .../__snapshots__/index.test.tsx.snap | 2 +- components/link/link.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 235 +++++++++--- components/modal/__tests__/index.test.tsx | 35 ++ components/modal/modal-action.tsx | 141 ++++--- components/modal/modal-actions.tsx | 2 +- components/modal/modal-content.tsx | 1 + components/modal/modal-wrapper.tsx | 58 ++- components/popover/popover-item.tsx | 7 +- components/select/select-dropdown.tsx | 8 +- components/select/select.tsx | 5 +- .../__snapshots__/backdrop.test.tsx.snap | 20 +- components/shared/__tests__/dropdown.test.tsx | 20 + components/shared/backdrop.tsx | 28 +- components/shared/dropdown.tsx | 32 +- .../__snapshots__/index.test.tsx.snap | 116 ++++++ components/snippet/__tests__/index.test.tsx | 23 ++ components/snippet/snippet.tsx | 19 +- components/table/__tests__/index.test.tsx | 37 ++ components/table/table-column.tsx | 8 +- components/table/table-context.ts | 2 +- components/table/table.tsx | 18 +- components/tabs/__tests__/index.test.tsx | 48 +-- components/tabs/tabs-context.ts | 1 - components/tabs/tabs-item.tsx | 12 +- components/tabs/tabs.tsx | 30 +- .../__snapshots__/index.test.tsx.snap | 360 +++++++++--------- components/textarea/textarea.tsx | 219 ++++++----- .../__snapshots__/index.test.tsx.snap | 6 +- components/utils/collections.ts | 13 + components/utils/use-portal.ts | 11 +- lib/components/menu/menu-links.tsx | 2 +- package.json | 2 +- pages/en-us/components/button.mdx | 2 +- pages/en-us/components/input.mdx | 22 ++ pages/en-us/components/link.mdx | 2 +- pages/en-us/components/modal.mdx | 31 +- pages/en-us/components/select.mdx | 65 +++- pages/en-us/components/snippet.mdx | 41 +- pages/en-us/components/tabs.mdx | 2 +- pages/en-us/components/textarea.mdx | 22 ++ pages/en-us/components/toast.mdx | 10 + pages/zh-cn/components/button.mdx | 2 +- pages/zh-cn/components/input.mdx | 22 ++ pages/zh-cn/components/link.mdx | 2 +- pages/zh-cn/components/modal.mdx | 43 ++- pages/zh-cn/components/select.mdx | 65 +++- pages/zh-cn/components/snippet.mdx | 25 +- pages/zh-cn/components/tabs.mdx | 2 +- pages/zh-cn/components/textarea.mdx | 23 ++ pages/zh-cn/components/toast.mdx | 11 + 60 files changed, 1507 insertions(+), 596 deletions(-) diff --git a/components/button-group/__tests__/__snapshots__/index.test.tsx.snap b/components/button-group/__tests__/__snapshots__/index.test.tsx.snap index 06fd2bb..130961b 100644 --- a/components/button-group/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/button-group/__tests__/__snapshots__/index.test.tsx.snap @@ -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; diff --git a/components/button/__tests__/__snapshots__/icon.test.tsx.snap b/components/button/__tests__/__snapshots__/icon.test.tsx.snap index 62c0e08..b0e470b 100644 --- a/components/button/__tests__/__snapshots__/icon.test.tsx.snap +++ b/components/button/__tests__/__snapshots__/icon.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ButtonIcon should render correctly 1`] = ` -"" +`; + +exports[`ButtonIcon should work without text 1`] = ` +"
" `; diff --git a/components/modal/__tests__/index.test.tsx b/components/modal/__tests__/index.test.tsx index d73d135..7e6d2c3 100644 --- a/components/modal/__tests__/index.test.tsx +++ b/components/modal/__tests__/index.test.tsx @@ -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 + , + ) + 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) + }) }) diff --git a/components/modal/modal-action.tsx b/components/modal/modal-action.tsx index 60e5f24..ce06339 100644 --- a/components/modal/modal-action.tsx +++ b/components/modal/modal-action.tsx @@ -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 & { close: () => void @@ -20,66 +28,83 @@ const defaultProps = { disabled: false, } -type NativeAttrs = Omit, keyof Props> -export type ModalActionProps = Props & typeof defaultProps & NativeAttrs +export type ModalActionProps = Props & typeof defaultProps & Omit -const ModalAction: React.FC = ({ - className, - children, - onClick, - passive, - disabled, - ...props -}) => { - const theme = useTheme() - const { close } = useModalContext() - const clickHandler = (event: MouseEvent) => { - if (disabled) return - const actionEvent = Object.assign({}, event, { - close: () => close && close(), - }) - onClick && onClick(actionEvent) - } +const ModalAction = React.forwardRef>( + ( + { className, children, onClick, passive, disabled, ...props }, + ref: React.Ref, + ) => { + const theme = useTheme() + const btnRef = useRef(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) => { + 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 ( - <> - - - - ) -} +type ModalActionComponent = React.ForwardRefExoticComponent< + PropsWithoutRef

& RefAttributes +> -export default withDefaults(ModalAction, defaultProps) +type ComponentProps = Partial & + Omit & + Partial> + +ModalAction.defaultProps = defaultProps + +export default ModalAction as ModalActionComponent diff --git a/components/modal/modal-actions.tsx b/components/modal/modal-actions.tsx index 0c2d088..443c3ac 100644 --- a/components/modal/modal-actions.tsx +++ b/components/modal/modal-actions.tsx @@ -22,7 +22,7 @@ const ModalActions: React.FC> = ({ 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}; } diff --git a/components/modal/modal-content.tsx b/components/modal/modal-content.tsx index ac7c45b..43561bf 100644 --- a/components/modal/modal-content.tsx +++ b/components/modal/modal-content.tsx @@ -26,6 +26,7 @@ const ModalContent: React.FC = ({ 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) { diff --git a/components/modal/modal-wrapper.tsx b/components/modal/modal-wrapper.tsx index 568f81e..35e79b3 100644 --- a/components/modal/modal-wrapper.tsx +++ b/components/modal/modal-wrapper.tsx @@ -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> = ({ ...props }) => { const theme = useTheme() + const modalContent = useRef(null) + const tabStart = useRef(null) + const tabEnd = useRef(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) => { + 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 ( -

+
+