Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions packages/vkui/src/components/CalendarHeader/CalendarHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,6 @@ export const CalendarHeader = ({
);
}

const stopPropogationOfEscapeKeyboardEventWhenSelectIsOpen = React.useCallback(
(event: React.KeyboardEvent, isOpen: boolean) => {
if (isOpen && event.key === 'Escape') {
event.stopPropagation();
}
},
[],
);

return (
<RootComponent baseClassName={styles.host} {...restProps}>
{!prevMonthHidden && (
Expand Down Expand Up @@ -271,7 +262,6 @@ export const CalendarHeader = ({
onChange={onMonthsChange}
forceDropdownPortal={false}
selectType="accent"
onInputKeyDown={stopPropogationOfEscapeKeyboardEventWhenSelectIsOpen}
slotProps={{
input: {
'aria-label': changeMonthLabel,
Expand All @@ -292,7 +282,6 @@ export const CalendarHeader = ({
onChange={onYearChange}
forceDropdownPortal={false}
selectType="accent"
onInputKeyDown={stopPropogationOfEscapeKeyboardEventWhenSelectIsOpen}
slotProps={{
input: {
'aria-label': changeYearLabel,
Expand Down
3 changes: 2 additions & 1 deletion packages/vkui/src/components/ChipsSelect/ChipsSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from 'react';
import type { MouseEventHandler } from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useExternRef } from '../../hooks/useExternRef';
import { useGlobalEscKeyDown } from '../../hooks/useGlobalEscKeyDown';
import { useGlobalOnEventOutside } from '../../hooks/useGlobalOnClickOutside';
import { useMergeProps } from '../../hooks/useMergeProps';
import { Keys } from '../../lib/accessibility';
Expand Down Expand Up @@ -438,13 +439,13 @@ export const ChipsSelect = <Option extends ChipOption>({
}
break;
}
case Keys.ESCAPE:
case Keys.TAB:
if (opened) {
setOpened(false);
}
}
};
useGlobalEscKeyDown(opened && !readOnly, () => setOpened(false));

React.useEffect(() => {
if (focusedOptionIndex === null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { useGlobalEscKeyDown } from '../../../hooks/useGlobalEscKeyDown';
import { Keys, pressedKey } from '../../../lib/accessibility';
import type { SelectProps } from '../CustomSelect';
import type { UseFocusedOptionControllerReturn } from './useFocusedOptionController';
Expand Down Expand Up @@ -58,9 +59,6 @@ export function useInputKeyboardController({
open();
}
break;
case Keys.ESCAPE:
close();
break;
case Keys.BACKSPACE:
case Keys.DELETE: {
open();
Expand All @@ -77,9 +75,11 @@ export function useInputKeyboardController({
break;
}
},
[scrollBoxRef, opened, close, focusOption, open, resetFocusedOption, selectFocused],
[scrollBoxRef, opened, focusOption, open, resetFocusedOption, selectFocused],
);

useGlobalEscKeyDown(opened, () => close());

const handleInputKeydown = React.useCallback(
(event: React.KeyboardEvent) => {
onInputKeyDown?.(event, opened);
Expand Down
4 changes: 1 addition & 3 deletions packages/vkui/src/components/DateInput/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,7 @@ export const DateInput = ({
const showCalendarButton = !disableCalendar && (accessible || (!accessible && !value));
const showClearButton = value && !readOnly;

useGlobalEscKeyDown(open && !disableCalendar, closeCalendar, {
capture: false,
});
useGlobalEscKeyDown(open && !disableCalendar, closeCalendar);

return (
<FormField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ describe('DateRangeInput', () => {
it('does not close calendar when Escape is pressed to close inner dropdown (month, year)', async () => {
const onCalendarOpenChangedStub = vi.fn();
const dateNow = new Date();
render(
const { container } = render(
<DateRangeInput
value={[dateNow, addDays(dateNow, 1)]}
accessible
Expand Down Expand Up @@ -550,7 +550,7 @@ describe('DateRangeInput', () => {
expect(screen.queryByRole('listbox')).toBeTruthy();

// закрываем дропдаун с помощью Escape
await userEvent.keyboard('{Escape}');
fireEvent.keyDown(container, { key: 'Escape' });
expect(screen.queryByRole('listbox')).toBeFalsy();

// календарь всё ещё должен быть открыт
Expand All @@ -559,7 +559,7 @@ describe('DateRangeInput', () => {
}

// закрываем календарь с помощью Escape
await userEvent.keyboard('{Escape}');
fireEvent.keyDown(container, { key: 'Escape' });
// календарь закрыт
expect(screen.queryByRole('dialog', { name: 'Календарь' })).toBeFalsy();
expect(onCalendarOpenChangedStub).toHaveBeenCalledTimes(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,7 @@ export const DateRangeInput = ({
const showCalendarButton = !disableCalendar && (accessible || (!accessible && !value));
const showClearButton = value && !readOnly;

useGlobalEscKeyDown(open && !disableCalendar, closeCalendar, {
capture: false,
});
useGlobalEscKeyDown(open && !disableCalendar, closeCalendar);

return (
<FormField
Expand Down
21 changes: 5 additions & 16 deletions packages/vkui/src/components/ModalCard/ModalCardInternal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use client';
/* eslint-disable jsdoc/require-jsdoc */

import { type ComponentType, type KeyboardEvent, type ReactNode, useCallback } from 'react';
import { type ComponentType, type ReactNode } from 'react';
import { classNames, noop } from '@vkontakte/vkjs';
import { useAdaptivityWithJSMediaQueries } from '../../hooks/useAdaptivityWithJSMediaQueries';
import { useExternRef } from '../../hooks/useExternRef';
import { useGlobalEscKeyDown } from '../../hooks/useGlobalEscKeyDown';
import { usePlatform } from '../../hooks/usePlatform';
import { useVirtualKeyboardState } from '../../hooks/useVirtualKeyboardState';
import { Keys, pressedKey } from '../../lib/accessibility';
import { useCSSTransition, type UseCSSTransitionState } from '../../lib/animation';
import { useBottomSheet } from '../../lib/sheet';
import { useScrollLock } from '../AppRoot/ScrollContext';
Expand Down Expand Up @@ -136,24 +136,13 @@ export const ModalCardInternal = ({
}
/>
);
const handleEscKeyDown = useCallback(
(event: KeyboardEvent<HTMLElement>) => {
if (closable && pressedKey(event) === Keys.ESCAPE) {
onClose('escape-key');
}
},
[closable, onClose],
);

useGlobalEscKeyDown(closable, () => onClose('escape-key'));

useScrollLock(!hidden);

return (
<ModalOutlet
hidden={hidden}
isDesktop={isDesktop}
onKeyDown={handleEscKeyDown}
disableModalOverlay={disableModalOverlay}
>
<ModalOutlet hidden={hidden} isDesktop={isDesktop} disableModalOverlay={disableModalOverlay}>
{modalOverlay}
<FocusTrap
rootRef={handleRef}
Expand Down
21 changes: 5 additions & 16 deletions packages/vkui/src/components/ModalPage/ModalPageInternal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
/* eslint-disable jsdoc/require-jsdoc */

import { type ComponentType, type KeyboardEvent, useCallback } from 'react';
import { type ComponentType } from 'react';
import { classNames, noop } from '@vkontakte/vkjs';
import { mergeStyle } from '../../helpers/mergeStyle';
import { useAdaptivity } from '../../hooks/useAdaptivity';
Expand All @@ -10,8 +10,8 @@ import {
useAdaptivityWithJSMediaQueries,
} from '../../hooks/useAdaptivityWithJSMediaQueries';
import { useExternRef } from '../../hooks/useExternRef';
import { useGlobalEscKeyDown } from '../../hooks/useGlobalEscKeyDown';
import { useVirtualKeyboardState } from '../../hooks/useVirtualKeyboardState';
import { Keys, pressedKey } from '../../lib/accessibility';
import { useCSSTransition, type UseCSSTransitionState } from '../../lib/animation';
import { type SnapPoint, type SnapPointChange, useBottomSheet } from '../../lib/sheet';
import type { CSSCustomProperties } from '../../types';
Expand Down Expand Up @@ -150,24 +150,13 @@ export const ModalPageInternal = ({
}
/>
);
const handleEscKeyDown = useCallback(
(event: KeyboardEvent<HTMLElement>) => {
if (closable && pressedKey(event) === Keys.ESCAPE) {
onClose('escape-key');
}
},
[closable, onClose],
);

useGlobalEscKeyDown(closable, () => onClose('escape-key'));

useScrollLock(!hidden);

return (
<ModalOutlet
hidden={hidden}
isDesktop={isDesktop}
onKeyDown={handleEscKeyDown}
disableModalOverlay={disableModalOverlay}
>
<ModalOutlet hidden={hidden} isDesktop={isDesktop} disableModalOverlay={disableModalOverlay}>
{modalOverlay}
<FocusTrap
rootRef={rootRef}
Expand Down
99 changes: 99 additions & 0 deletions packages/vkui/src/hooks/useGlobalEscKeyDown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { useGlobalEscKeyDown } from './useGlobalEscKeyDown';

type EscHandlersFixtureProps = {
onOuterClose: () => void;
onInnerClose: () => void;
};

const EscHandlersFixture = ({ onOuterClose, onInnerClose }: EscHandlersFixtureProps) => {
const [outerOpened, setOuterOpened] = React.useState(true);
const [innerOpened, setInnerOpened] = React.useState(true);

useGlobalEscKeyDown(outerOpened, () => {
onOuterClose();
setOuterOpened(false);
});

useGlobalEscKeyDown(innerOpened, () => {
onInnerClose();
setInnerOpened(false);
});

return <input data-testid="target" />;
};

const SingleEscHandlerFixture = ({
enabled,
onEscKeyDown,
}: {
enabled: boolean;
onEscKeyDown?: (event: KeyboardEvent) => void;
}) => {
useGlobalEscKeyDown(enabled, onEscKeyDown);
return <input data-testid="target" />;
};

describe(useGlobalEscKeyDown, () => {
it('should close layers in stack order', () => {
const onOuterClose = vi.fn();
const onInnerClose = vi.fn();
const { getByTestId } = render(
<EscHandlersFixture onOuterClose={onOuterClose} onInnerClose={onInnerClose} />,
);

const target = getByTestId('target');

fireEvent.keyDown(target, { key: 'Escape' });
expect(onInnerClose).toHaveBeenCalledTimes(1);
expect(onOuterClose).not.toHaveBeenCalled();

fireEvent.keyDown(target, { key: 'Escape' });
expect(onOuterClose).toHaveBeenCalledTimes(1);
});

it('should not call callback for non-escape keys', () => {
const onEscKeyDown = vi.fn();
const { getByTestId } = render(<SingleEscHandlerFixture enabled onEscKeyDown={onEscKeyDown} />);

fireEvent.keyDown(getByTestId('target'), { key: 'Enter' });
expect(onEscKeyDown).not.toHaveBeenCalled();
});

it('should not subscribe when callback is not provided', () => {
const { getByTestId } = render(<SingleEscHandlerFixture enabled={true} />);

expect(() => fireEvent.keyDown(getByTestId('target'), { key: 'Escape' })).not.toThrow();
});

it('should re-subscribe when enabled state changes', () => {
const onEscKeyDown = vi.fn();
const { getByTestId, rerender } = render(
<SingleEscHandlerFixture enabled={false} onEscKeyDown={onEscKeyDown} />,
);

fireEvent.keyDown(getByTestId('target'), { key: 'Escape' });
expect(onEscKeyDown).not.toHaveBeenCalled();

rerender(<SingleEscHandlerFixture enabled onEscKeyDown={onEscKeyDown} />);

fireEvent.keyDown(getByTestId('target'), { key: 'Escape' });
expect(onEscKeyDown).toHaveBeenCalledTimes(1);
});

it('should call latest callback after rerender', () => {
const firstCallback = vi.fn();
const secondCallback = vi.fn();
const { getByTestId, rerender } = render(
<SingleEscHandlerFixture enabled onEscKeyDown={firstCallback} />,
);

rerender(<SingleEscHandlerFixture enabled onEscKeyDown={secondCallback} />);

fireEvent.keyDown(getByTestId('target'), { key: 'Escape' });
expect(firstCallback).not.toHaveBeenCalled();
expect(secondCallback).toHaveBeenCalledTimes(1);
});
});
Loading
Loading