Skip to content

Commit

Permalink
feat(ModalCard,ModalPage): add prop restoreFocus (#8120)
Browse files Browse the repository at this point in the history
  • Loading branch information
EldarMuhamethanov authored Dec 28, 2024
1 parent e31540f commit e249428
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 2 deletions.
56 changes: 56 additions & 0 deletions packages/vkui/src/components/ModalCard/ModalCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { act } from 'react';
import * as React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { baselineComponent, waitCSSTransitionEnd } from '../../testing/utils';
import { Button } from '../Button/Button';
import { ConfigProvider } from '../ConfigProvider/ConfigProvider';
import { ModalCard } from './ModalCard';
import { type ModalCardProps } from './types';

export const waitModalCardCSSTransitionEnd = async (el: HTMLElement) =>
await waitCSSTransitionEnd(el);
Expand Down Expand Up @@ -126,4 +130,56 @@ describe(ModalCard, () => {
expect(onClose).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledWith('click-close-button');
});

describe('check restoreFocus prop', () => {
const Fixture: React.FC<Pick<ModalCardProps, 'restoreFocus'>> = ({ restoreFocus = true }) => {
const [open, setOpen] = React.useState(false);
return (
<>
<ConfigProvider platform="vkcom">
<ModalCard
key="host"
id="host"
open={open}
modalDismissButtonTestId="dismiss-button"
data-testid="host"
restoreFocus={restoreFocus}
/>
<Button onClick={() => setOpen((v) => !v)} data-testid="open-modal">
Открыть
</Button>
</ConfigProvider>
</>
);
};

it.each([true, false])('check restoreFocus=%s', async (restoreFocus) => {
jest.useFakeTimers();
const h = render(<Fixture restoreFocus={restoreFocus} />);
expect(h.queryByTestId('host')).toBeFalsy();

const openButton = h.getByTestId('open-modal');
await act(async () => {
openButton.focus();
});
fireEvent.click(openButton);
expect(openButton).toHaveFocus();

await waitModalCardCSSTransitionEnd(h.getByTestId('host'));
expect(h.queryByTestId('host')).toBeTruthy();
jest.runAllTimers();
expect(h.getByTestId('dismiss-button')).toHaveFocus();

fireEvent.click(openButton);
await waitModalCardCSSTransitionEnd(h.getByTestId('host'));
expect(h.queryByTestId('host')).toBeFalsy();
jest.runAllTimers();

if (restoreFocus) {
expect(openButton).toHaveFocus();
} else {
expect(openButton).not.toHaveFocus();
}
});
});
});
7 changes: 6 additions & 1 deletion packages/vkui/src/components/ModalCard/ModalCardInternal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const ModalCardInternal = ({
dismissButtonMode,
dismissLabel,
noFocusToDialog,
restoreFocus,
onOpen,
onOpened,
onClose = noop,
Expand Down Expand Up @@ -136,7 +137,11 @@ export const ModalCardInternal = ({
);

useScrollLock(!hidden);
useFocusTrap(ref, { autoFocus: !noFocusToDialog, disabled: !opened || hidden });
useFocusTrap(ref, {
autoFocus: !noFocusToDialog,
disabled: !opened || hidden,
restoreFocus,
});

return (
<ModalOutlet hidden={hidden} onKeyDown={handleEscKeyDown}>
Expand Down
4 changes: 3 additions & 1 deletion packages/vkui/src/components/ModalCard/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UIEvent } from 'react';
import { type UseFocusTrapProps } from '../../hooks/useFocusTrap';
import type { NavIdProps } from '../../lib/getNavId';
import type { UseBottomSheetHandlers } from '../../lib/sheet';
import type { ModalCardBaseProps } from '../ModalCardBase/ModalCardBase';
Expand All @@ -11,7 +12,8 @@ export type ModalCardCloseReason =

export interface ModalCardProps
extends NavIdProps,
Omit<ModalCardBaseProps, 'id' | 'onClose' | 'onTransitionEnd' | keyof UseBottomSheetHandlers> {
Omit<ModalCardBaseProps, 'id' | 'onClose' | 'onTransitionEnd' | keyof UseBottomSheetHandlers>,
Pick<UseFocusTrapProps, 'restoreFocus'> {
/**
* Состояние видимости.
*
Expand Down
56 changes: 56 additions & 0 deletions packages/vkui/src/components/ModalPage/ModalPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { act } 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 { AdaptivityProvider } from '../AdaptivityProvider/AdaptivityProvider';
import { Button } from '../Button/Button';
import { ConfigProvider } from '../ConfigProvider/ConfigProvider';
import { type ModalCardProps } from '../ModalCard/types';
import { ModalPage } from './ModalPage';

export const waitModalPageCSSTransitionEnd = async (el: HTMLElement) =>
Expand Down Expand Up @@ -133,4 +137,56 @@ describe(ModalPage, () => {
expect(onClose).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledWith('click-close-button', expect.any(Object));
});

describe('check restoreFocus prop', () => {
const Fixture: React.FC<Pick<ModalCardProps, 'restoreFocus'>> = ({ restoreFocus = true }) => {
const [open, setOpen] = React.useState(false);
return (
<>
<ConfigProvider platform="vkcom">
<ModalPage
key="host"
id="host"
open={open}
restoreFocus={restoreFocus}
modalDismissButtonTestId="dismiss-button"
data-testid="host"
/>
<Button onClick={() => setOpen((v) => !v)} data-testid="open-modal">
Открыть
</Button>
</ConfigProvider>
</>
);
};

it.each([true, false])('check restoreFocus=%s', async (restoreFocus) => {
jest.useFakeTimers();
const h = render(<Fixture restoreFocus={restoreFocus} />);
expect(h.queryByTestId('host')).toBeFalsy();

const openButton = h.getByTestId('open-modal');
await act(async () => {
openButton.focus();
});
fireEvent.click(openButton);
expect(openButton).toHaveFocus();

await waitModalPageCSSTransitionEnd(h.getByTestId('host'));
expect(h.queryByTestId('host')).toBeTruthy();
jest.runAllTimers();
expect(h.getByTestId('dismiss-button')).toHaveFocus();

fireEvent.click(openButton);
await waitModalPageCSSTransitionEnd(h.getByTestId('host'));
expect(h.queryByTestId('host')).toBeFalsy();
jest.runAllTimers();

if (restoreFocus) {
expect(openButton).toHaveFocus();
} else {
expect(openButton).not.toHaveFocus();
}
});
});
});
2 changes: 2 additions & 0 deletions packages/vkui/src/components/ModalPage/ModalPageInternal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const ModalPageInternal = ({
hideCloseButton,
preventClose,
disableContentPanningGesture,
restoreFocus,
onOpen,
onOpened,
onClose = noop,
Expand Down Expand Up @@ -166,6 +167,7 @@ export const ModalPageInternal = ({
<FocusTrap
{...restProps}
autoFocus={!noFocusToDialog}
restoreFocus={restoreFocus}
role="dialog"
aria-modal="true"
disabled={!opened || hidden}
Expand Down
2 changes: 2 additions & 0 deletions packages/vkui/src/components/ModalPage/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CSSProperties, ReactNode, Ref, UIEvent } from 'react';
import { type UseFocusTrapProps } from '../../hooks/useFocusTrap';
import type { NavIdProps } from '../../lib/getNavId';
import type { HTMLAttributesWithRootRef } from '../../types';

Expand All @@ -15,6 +16,7 @@ type OmittedStyleAttribute = {
export interface ModalPageProps
extends NavIdProps,
Omit<HTMLAttributesWithRootRef<HTMLDivElement>, 'id' | 'style'>,
Pick<UseFocusTrapProps, 'restoreFocus'>,
OmittedStyleAttribute {
/**
* Состояние видимости.
Expand Down

0 comments on commit e249428

Please sign in to comment.