Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ModalPage, ModalCard): add prop disableFocusTrap to fix conflict with several FocusTraps #8248

Merged
41 changes: 39 additions & 2 deletions packages/vkui/src/components/ModalCard/ModalCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { act } from 'react';
import { act, useState } from 'react';
import * as React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { baselineComponent, waitCSSTransitionEnd } from '../../testing/utils';
import { ViewWidth } from '../../lib/adaptivity';
import { baselineComponent, userEvent, waitCSSTransitionEnd } from '../../testing/utils';
import { AdaptivityProvider } from '../AdaptivityProvider/AdaptivityProvider';
import { Button } from '../Button/Button';
import { ConfigProvider } from '../ConfigProvider/ConfigProvider';
import { ModalPageHeader } from '../ModalPageHeader/ModalPageHeader';
Expand Down Expand Up @@ -132,6 +134,41 @@ describe(ModalCard, () => {
expect(onClose).toHaveBeenCalledWith('click-close-button');
});

test('check disable focus trap', async () => {
const Fixture = () => {
const [open, setOpen] = useState(false);
return (
<>
<AdaptivityProvider viewWidth={ViewWidth.SMALL_TABLET} hasPointer>
<ModalCard
key="host"
id="host"
open={open}
modalDismissButtonTestId="dismiss-button"
data-testid="host"
focusTrapDisabled
/>
</AdaptivityProvider>
<Button data-testid="open-button" onClick={() => setOpen(true)} />
</>
);
};

const h = render(<Fixture />);

const openButton = h.getByTestId('open-button');
fireEvent.click(openButton);

await waitModalCardCSSTransitionEnd(h.getByTestId('host'));

const dismissButton = h.getByTestId('dismiss-button');
dismissButton.focus();
expect(dismissButton).toHaveFocus();

await userEvent.tab();
expect(openButton).toHaveFocus();
});

describe('check restoreFocus prop', () => {
const Fixture: React.FC<Pick<ModalCardProps, 'restoreFocus'>> = ({ restoreFocus = true }) => {
const [open, setOpen] = React.useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const ModalCardInternal = ({
onOpened,
onClose = noop,
onClosed,
focusTrapDisabled,
...restProps
}: ModalCardInternalProps): ReactNode => {
const platform = usePlatform();
Expand Down Expand Up @@ -139,7 +140,7 @@ export const ModalCardInternal = ({
useScrollLock(!hidden);
useFocusTrap(ref, {
autoFocus: !noFocusToDialog,
disabled: !opened || hidden,
disabled: !opened || hidden || focusTrapDisabled,
restoreFocus,
});

Expand Down
3 changes: 3 additions & 0 deletions packages/vkui/src/components/ModalCard/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ const Example = () => {
При `open={true}` навигация через `Tab` и `Shift` + `Tab` будет зациклена на содержимом `ModalCard` (допустимо, т.к. к устройству
может быть подключена клавиатура).

> ⚠️ Если вам необходимо открывать модалку поверху другой модалки, необходимо для первой
> использовать свойство `focusTrapDisabled`, чтобы отключить захват фокуса.

andrey-medvedev-vk marked this conversation as resolved.
Show resolved Hide resolved
По умолчанию, пользователь может закрыть **bottom card** с помощью:

- нажатия на `ModalOverlay`, что вызовет событие `onClose` с аргументом `click-overlay`;
Expand Down
6 changes: 6 additions & 0 deletions packages/vkui/src/components/ModalCard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,10 @@ export interface ModalCardProps
* Будет вызвано при окончательном закрытии модалки.
*/
onClosed?: VoidFunction;
/**
* Позволяет отключить захват фокуса.
*
* Нужно использовать, когда поверх одной модалки открывается другая, что два `FocusTrap` не конфликтовали
andrey-medvedev-vk marked this conversation as resolved.
Show resolved Hide resolved
*/
focusTrapDisabled?: UseFocusTrapProps['disabled'];
}
40 changes: 38 additions & 2 deletions packages/vkui/src/components/ModalPage/ModalPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { act } from 'react';
import { act, useState } from 'react';
import * as React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { ViewWidth } from '../../lib/adaptivity';
import { baselineComponent, waitCSSTransitionEnd } from '../../testing/utils';
import { baselineComponent, userEvent, waitCSSTransitionEnd } from '../../testing/utils';
import { AdaptivityProvider } from '../AdaptivityProvider/AdaptivityProvider';
import { Button } from '../Button/Button';
import { ConfigProvider } from '../ConfigProvider/ConfigProvider';
Expand Down Expand Up @@ -139,6 +139,42 @@ describe(ModalPage, () => {
expect(onClose).toHaveBeenCalledWith('click-close-button', expect.any(Object));
});

test('check disable focus trap', async () => {
const Fixture = () => {
const [open, setOpen] = useState(false);
return (
<>
<AdaptivityProvider viewWidth={ViewWidth.SMALL_TABLET} hasPointer>
<ModalPage
key="host"
id="host"
open={open}
modalDismissButtonTestId="dismiss-button"
focusTrapDisabled
noFocusToDialog
data-testid="host"
/>
</AdaptivityProvider>
<Button data-testid="open-button" onClick={() => setOpen(true)} />
</>
);
};

const h = render(<Fixture />);

const openButton = h.getByTestId('open-button');
fireEvent.click(openButton);

await waitModalPageCSSTransitionEnd(h.getByTestId('host'));

const dismissButton = h.getByTestId('dismiss-button');
dismissButton.focus();
expect(dismissButton).toHaveFocus();

await userEvent.tab();
expect(openButton).toHaveFocus();
});

describe('check restoreFocus prop', () => {
const Fixture: React.FC<Pick<ModalCardProps, 'restoreFocus'>> = ({ restoreFocus = true }) => {
const [open, setOpen] = React.useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const ModalPageInternal = ({
onOpened,
onClose = noop,
onClosed,
focusTrapDisabled,
...restProps
}: ModalPageInternalProps) => {
const { hasCustomPanelHeaderAfter } = useConfigProvider();
Expand Down Expand Up @@ -180,7 +181,7 @@ export const ModalPageInternal = ({
restoreFocus={restoreFocus}
role="dialog"
aria-modal="true"
disabled={!opened || hidden}
disabled={!opened || hidden || focusTrapDisabled}
className={classNames(
className,
styles.host,
Expand Down
3 changes: 3 additions & 0 deletions packages/vkui/src/components/ModalPage/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ const Example = () => {
- параметр `size` будет ограничивать максимальную ширину окна;
- навигация через `Tab` и `Shift` + `Tab` будет зациклена на содержимом `ModalPage`.

> ⚠️ Если вам необходимо открывать модалку поверху другой модалки, необходимо для первой
> использовать свойство `focusTrapDisabled`, чтобы отключить захват фокуса.

По умолчанию, пользователь может закрыть **диалоговое окно** с помощью:

- нажатия на кнопку закрытия, что вызовет событие `onClose` с аргументом `click-close-button`;
Expand Down
6 changes: 6 additions & 0 deletions packages/vkui/src/components/ModalPage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,10 @@ export interface ModalPageProps
*
*/
outsideButtons?: React.ReactNode;
/**
* Позволяет отключить захват фокуса.
*
* Нужно использовать, когда поверх одной модалки открывается другая, что два `FocusTrap` не конфликтовали
andrey-medvedev-vk marked this conversation as resolved.
Show resolved Hide resolved
*/
focusTrapDisabled?: UseFocusTrapProps['disabled'];
}
2 changes: 1 addition & 1 deletion packages/vkui/src/hooks/useFocusTrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const useFocusTrap = (

recalculateFocusableNodesRef(parentNode);

if (!autoFocus || arraysEquals(oldFocusableNodes, focusableNodesRef.current)) {
if (disabled || !autoFocus || arraysEquals(oldFocusableNodes, focusableNodesRef.current)) {
return;
}

Expand Down