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 ( -

+
+