diff --git a/packages/vkui/src/components/ModalCard/ModalCard.e2e-playground.tsx b/packages/vkui/src/components/ModalCard/ModalCard.e2e-playground.tsx index 2bf4da9ee7a..7a16d19283a 100644 --- a/packages/vkui/src/components/ModalCard/ModalCard.e2e-playground.tsx +++ b/packages/vkui/src/components/ModalCard/ModalCard.e2e-playground.tsx @@ -94,7 +94,12 @@ export const ModalCardPlayground = (props: ComponentPlaygroundProps) => { {(props: ModalCardProps) => (
- +
diff --git a/packages/vkui/src/components/ModalCard/ModalCard.module.css b/packages/vkui/src/components/ModalCard/ModalCard.module.css index 6d116db21bd..7a9f8b07b1a 100644 --- a/packages/vkui/src/components/ModalCard/ModalCard.module.css +++ b/packages/vkui/src/components/ModalCard/ModalCard.module.css @@ -10,6 +10,10 @@ align-items: flex-end; } +.ModalCard:focus { + outline: none; +} + .ModalCard__in { width: 100%; margin-left: auto; diff --git a/packages/vkui/src/components/ModalCard/ModalCard.tsx b/packages/vkui/src/components/ModalCard/ModalCard.tsx index 792ee6506b7..e730ff7b481 100644 --- a/packages/vkui/src/components/ModalCard/ModalCard.tsx +++ b/packages/vkui/src/components/ModalCard/ModalCard.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; import { useAdaptivityWithJSMediaQueries } from '../../hooks/useAdaptivityWithJSMediaQueries'; +import { useExternRef } from '../../hooks/useExternRef'; import { usePlatform } from '../../hooks/usePlatform'; import { getNavId, NavIdProps } from '../../lib/getNavId'; import { warnOnce } from '../../lib/warnOnce'; @@ -33,6 +34,7 @@ export const ModalCard = ({ nav, id, size, + getRootRef, ...restProps }: ModalCardProps) => { const { isDesktop } = useAdaptivityWithJSMediaQueries(); @@ -40,10 +42,18 @@ export const ModalCard = ({ const modalContext = React.useContext(ModalRootContext); const { refs } = useModalRegistry(getNavId({ nav, id }, warn), ModalType.CARD); + const rootRef = useExternRef(getRootRef, refs.modalElement); + + const contextValue = React.useMemo(() => ({ labelId: `${id}-label` }), [id]); return ( { const generatingId = useId(); @@ -113,6 +115,7 @@ export const ModalPage = ({ const modalContext = React.useContext(ModalRootContext); const { refs } = useModalRegistry(getNavId({ nav, id }, warn), ModalType.PAGE); + const rootRef = useExternRef(getRootRef, refs.modalElement); const contextValue = React.useMemo(() => ({ labelId: `${id}-label` }), [id]); @@ -120,6 +123,8 @@ export const ModalPage = ({ { + let modalPageRef: React.RefObject = React.createRef(); + let modalCardRef: React.RefObject = React.createRef(); + let modalPageWithInputRef: React.RefObject = React.createRef(); + let inputInnerModalPageRef: React.RefObject = + React.createRef(); + let modals: React.ReactElement[] = []; + + beforeEach(() => { + modalPageRef = React.createRef(); + modalCardRef = React.createRef(); + modalPageWithInputRef = React.createRef(); + inputInnerModalPageRef = React.createRef(); + modals = [ + , + , + + + , + ]; + }); + + it('should focus on modal container if controllable element does not exist or is not focused', () => { + const component = render({modals}, { + baseElement: document.documentElement, + }); + runAllTimers(); + + expect(modalPageRef.current).toHaveFocus(); + + component.rerender({modals}); + runAllTimers(); + + expect(modalCardRef.current).toHaveFocus(); + }); + + it('should focus on controllable element instead modal if it is focused', () => { + render({modals}, { + baseElement: document.documentElement, + }); + runAllTimers(); + + expect(modalPageWithInputRef.current).not.toHaveFocus(); + expect(inputInnerModalPageRef.current).toHaveFocus(); + }); + + it('should focus on controllable element only (if it is focused)', () => { + const component = render( + + {modals} + , + { + baseElement: document.documentElement, + }, + ); + runAllTimers(); + + expect(modalPageRef.current).not.toHaveFocus(); + + component.rerender( + + {modals} + , + ); + runAllTimers(); + + expect(modalPageWithInputRef.current).not.toHaveFocus(); + expect(inputInnerModalPageRef.current).toHaveFocus(); + }); + }); }); diff --git a/packages/vkui/src/components/ModalRoot/ModalRoot.tsx b/packages/vkui/src/components/ModalRoot/ModalRoot.tsx index 219e0a9956c..6a9f656c578 100644 --- a/packages/vkui/src/components/ModalRoot/ModalRoot.tsx +++ b/packages/vkui/src/components/ModalRoot/ModalRoot.tsx @@ -107,14 +107,15 @@ class ModalRootTouchComponent extends React.Component< // transition phase 3: animate entering modal if (this.props.enteringModal && this.props.enteringModal !== prevProps.enteringModal) { - const { enteringModal } = this.props; - const enteringState = this.props.getModalState(enteringModal); + const enteringState = this.props.getModalState(this.props.enteringModal); this.props.onEnter(); this.waitTransitionFinish(enteringState, () => { - if (enteringState?.innerElement) { - enteringState.innerElement.style.transitionDelay = ''; + if (enteringState) { + if (enteringState.innerElement) { + enteringState.innerElement.style.transitionDelay = ''; + } + this.onEntered(enteringState); } - this.props.onEntered(enteringModal); }); if (enteringState?.innerElement) { @@ -219,6 +220,18 @@ class ModalRootTouchComponent extends React.Component< } }; + onEntered({ id, modalElement }: ModalsStateEntry) { + if ( + !this.props.noFocusToDialog && + modalElement && + !modalElement.contains(this.document.activeElement) + ) { + modalElement.focus(); + } + + this.props.onEntered(id); + } + closeModal(id: string) { // Сбрасываем состояния, которые могут помешать закрытию модального окна this.setState({ touchDown: false }); @@ -598,12 +611,6 @@ class ModalRootTouchComponent extends React.Component< return ( { - const modalState = this.props.getModalState(modalId); - if (modalState) { - modalState.modalElement = e; - } - }} onClose={this.props.onExit} timeout={this.timeout} className={classNames( diff --git a/packages/vkui/src/components/ModalRoot/ModalRootDesktop.tsx b/packages/vkui/src/components/ModalRoot/ModalRootDesktop.tsx index 261bb1f6d81..101a5cf2289 100644 --- a/packages/vkui/src/components/ModalRoot/ModalRootDesktop.tsx +++ b/packages/vkui/src/components/ModalRoot/ModalRootDesktop.tsx @@ -20,6 +20,7 @@ const warn = warnOnce('ModalRoot'); export const ModalRootDesktop = ({ activeModal: activeModalProp, children, + noFocusToDialog = false, onOpen, onOpened, onClose, @@ -38,7 +39,7 @@ export const ModalRootDesktop = ({ getModalState, enteringModal, onEnter, - onEntered, + onEntered: onEnteredProp, onExited, history, delayEnter, @@ -91,6 +92,14 @@ export const ModalRootDesktop = ({ }); }; + const onEntered = ({ id, modalElement }: ModalsStateEntry) => { + if (!noFocusToDialog && modalElement && !modalElement.contains(document!.activeElement)) { + modalElement.focus(); + } + + onEnteredProp(id); + }; + const openModal = () => { if (!enteringModal || !prevProps) { return; @@ -102,16 +111,10 @@ export const ModalRootDesktop = ({ // Анимация открытия модального окна if (!prevProps.exitingModal) { requestAnimationFrame(() => { - if (enteringModal === enteringModal) { - waitTransitionFinish( - enteringState?.innerElement, - () => onEntered(enteringModal), - timeout, - ); + if (enteringModal === enteringModal && enteringState) { + waitTransitionFinish(enteringState.innerElement, () => onEntered(enteringState), timeout); animateModalOpacity(enteringState, true); - if (enteringState) { - setMaskOpacity(enteringState, 1); - } + setMaskOpacity(enteringState, 1); } }); @@ -127,7 +130,9 @@ export const ModalRootDesktop = ({ } }); - onEntered(enteringModal); + if (enteringState) { + onEntered(enteringState); + } }; const closeModal = (id: string) => { diff --git a/packages/vkui/src/components/ModalRoot/types.ts b/packages/vkui/src/components/ModalRoot/types.ts index adf9c9d8555..2f439624d15 100644 --- a/packages/vkui/src/components/ModalRoot/types.ts +++ b/packages/vkui/src/components/ModalRoot/types.ts @@ -94,6 +94,11 @@ export interface ModalRootProps { * Будет вызвано при окончательном закрытии активной модалки с её id */ onClosed?(modalId: string): void; + + /** + * Отключает фокус на контейнер диалогового окна при открытии. + */ + noFocusToDialog?: boolean; } export interface ModalRootWithDOMProps extends HasPlatform, ModalRootProps, DOMContextInterface {