From 0d94b24426013ca90dfb8753c8628df42d41e68e Mon Sep 17 00:00:00 2001 From: EldarMuhamethanov <61377022+EldarMuhamethanov@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:44:05 +0300 Subject: [PATCH] fix(ActionSheetItem): fix navigation by arrows in selectable ActionSheetItem (#7216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit h2. Описание При навигации с помощью стрелочек по `ActionSheetItems` с `selectable` флагом закрывается `ActionSheet` h2. Изменения - Поисследовал проблему с тригером события `click` при навигации по элементам. Нашел вариант как можно ее обойти https://github.com/facebook/react/issues/7407. С помощью проверки можно отличить реальное событие клика. - Добавил обработку нажатия кнопки `Enter`, при котором происходит закрытие `ActionSheet` - Добавил тесты для нового функционала --- .../ActionSheet/ActionSheet.test.tsx | 13 +++- .../components/ActionSheet/ActionSheet.tsx | 2 +- .../ActionSheet/ActionSheetContext.ts | 1 + .../vkui/src/components/ActionSheet/Readme.md | 17 +++-- .../ActionSheetItem/ActionSheetItem.test.tsx | 66 ++++++++++++++++++- .../ActionSheetItem/ActionSheetItem.tsx | 61 ++++++++++++----- .../src/components/ActionSheetItem/helpers.ts | 11 ++++ 7 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 packages/vkui/src/components/ActionSheetItem/helpers.ts diff --git a/packages/vkui/src/components/ActionSheet/ActionSheet.test.tsx b/packages/vkui/src/components/ActionSheet/ActionSheet.test.tsx index ee84b23c1e..7a4b39ecd0 100644 --- a/packages/vkui/src/components/ActionSheet/ActionSheet.test.tsx +++ b/packages/vkui/src/components/ActionSheet/ActionSheet.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { act } from 'react'; -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { ViewWidth } from '../../lib/adaptivity'; import { baselineComponent, @@ -127,7 +127,16 @@ describe(ActionSheet, () => { , ); await waitForFloatingPosition(); - await userEvent.click(screen.getByTestId('item')); + // эмулируем настоящее событие клика(отличается оно тем, что clientX и clientY != 0) + // @see packages/vkui/src/components/ActionSheetItem/helpers.ts + fireEvent( + screen.getByTestId('item'), + new MouseEvent('click', { + clientX: 1, + clientY: 1, + bubbles: true, + }), + ); act(jest.runAllTimers); if (onCloseHandler.mock.calls.length > 0) { diff --git a/packages/vkui/src/components/ActionSheet/ActionSheet.tsx b/packages/vkui/src/components/ActionSheet/ActionSheet.tsx index 41efb8325e..758552ab9a 100644 --- a/packages/vkui/src/components/ActionSheet/ActionSheet.tsx +++ b/packages/vkui/src/components/ActionSheet/ActionSheet.tsx @@ -88,7 +88,7 @@ export const ActionSheet = ({ }, [], ); - const contextValue = useObjectMemo({ onItemClick, mode }); + const contextValue = useObjectMemo({ onItemClick, mode, onClose: onCloseWithOther }); const DropdownComponent = mode === 'menu' ? ActionSheetDropdownMenu : ActionSheetDropdownSheet; diff --git a/packages/vkui/src/components/ActionSheet/ActionSheetContext.ts b/packages/vkui/src/components/ActionSheet/ActionSheetContext.ts index d43aaad8ee..3aa48804c4 100644 --- a/packages/vkui/src/components/ActionSheet/ActionSheetContext.ts +++ b/packages/vkui/src/components/ActionSheet/ActionSheetContext.ts @@ -11,6 +11,7 @@ export type ItemClickHandler = (options: { export type ActionSheetContextType = { onItemClick?: ItemClickHandler; + onClose?: () => void; mode?: 'sheet' | 'menu'; }; diff --git a/packages/vkui/src/components/ActionSheet/Readme.md b/packages/vkui/src/components/ActionSheet/Readme.md index 3d95ada683..3f7b5df3a2 100644 --- a/packages/vkui/src/components/ActionSheet/Readme.md +++ b/packages/vkui/src/components/ActionSheet/Readme.md @@ -32,8 +32,6 @@ const onClose = () => { setOpenedPopoutName(null); }; -const [filter, setFilter] = useState('best'); -const onChange = (e) => setFilter(e.target.value); const baseTargetRef = React.useRef(null); const iconsTargetRef = React.useRef(null); const subtitleTargetRef = React.useRef(null); @@ -140,9 +138,13 @@ const openSubtitle = () => , ); -const openSelectable = () => - openActionSheet( - 'selectable', +const SelectableActionSheet = () => { + const [filter, setFilter] = useState('best'); + const onChange = (e) => { + setFilter(e.target.value); + }; + + return ( > Друзья по вузу - , + ); +}; + +const openSelectable = () => openActionSheet('selectable', ); const openTitle = () => openActionSheet( diff --git a/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.test.tsx b/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.test.tsx index e288fd2c03..dc827080d1 100644 --- a/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.test.tsx +++ b/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.test.tsx @@ -1,5 +1,7 @@ -import { render, screen } from '@testing-library/react'; +import * as React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { baselineComponent } from '../../testing/utils'; +import { ActionSheetContext } from '../ActionSheet/ActionSheetContext'; import { ActionSheetItem, ActionSheetItemProps } from './ActionSheetItem'; const ActionSheetItemTest = (props: ActionSheetItemProps) => ( @@ -27,4 +29,66 @@ describe('ActionSheetItem', () => { render(ActionSheetItem); expect(item().tagName.toLowerCase()).toMatch('label'); }); + + it('should call close callback when Enter keydown', async () => { + const onCloseCallback = jest.fn(); + render( + + + ActionSheetItem + + , + ); + + await React.act(async () => + fireEvent.keyDown(screen.getByTestId('action-item'), { key: 'Enter', code: 'Enter' }), + ); + + expect(onCloseCallback).toHaveBeenCalledTimes(1); + }); + + it('check call onItemClick callback when click to ActionSheetItem with selectable=true', async () => { + const onItemClickCallback = jest.fn(); + + render( + + + ActionSheetItem + + , + ); + + // эмулируем событие клика при навигации стрелочками + await React.act(async () => + fireEvent( + screen.getByTestId('action-item'), + new MouseEvent('click', { + clientX: 0, + clientY: 0, + bubbles: true, + }), + ), + ); + + expect(onItemClickCallback).toHaveBeenCalledTimes(0); + + // эмулируем настоящее событие клика(отличается оно тем, что clientX и clientY != 0) + // @see packages/vkui/src/components/ActionSheetItem/helpers.ts + const newMouseEvent = new MouseEvent('click', { + clientX: 1, + clientY: 1, + bubbles: true, + }); + await React.act(async () => fireEvent(screen.getByTestId('action-item'), newMouseEvent)); + + expect(onItemClickCallback).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.tsx b/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.tsx index 73262d2af2..d9a1fcf96a 100644 --- a/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.tsx +++ b/packages/vkui/src/components/ActionSheetItem/ActionSheetItem.tsx @@ -2,11 +2,13 @@ import * as React from 'react'; import { classNames, noop } from '@vkontakte/vkjs'; import { useAdaptivityWithJSMediaQueries } from '../../hooks/useAdaptivityWithJSMediaQueries'; import { usePlatform } from '../../hooks/usePlatform'; +import { Keys, pressedKey } from '../../lib/accessibility'; import { ActionSheetContext, type ActionSheetContextType } from '../ActionSheet/ActionSheetContext'; import { Tappable } from '../Tappable/Tappable'; import { Subhead } from '../Typography/Subhead/Subhead'; import { Text } from '../Typography/Text/Text'; import { Title } from '../Typography/Title/Title'; +import { isRealClickEvent } from './helpers'; import { Radio } from './subcomponents/Radio/Radio'; import styles from './ActionSheetItem.module.css'; @@ -74,8 +76,11 @@ export const ActionSheetItem = ({ ...restProps }: ActionSheetItemProps): React.ReactNode => { const platform = usePlatform(); - const { onItemClick = () => noop, mode: actionSheetMode } = - React.useContext>(ActionSheetContext); + const { + onItemClick = () => noop, + mode: actionSheetMode, + onClose: onActionSheetClose, + } = React.useContext>(ActionSheetContext); const { sizeY } = useAdaptivityWithJSMediaQueries(); const Component: React.ElementType | undefined = selectable ? 'label' : undefined; @@ -83,20 +88,45 @@ export const ActionSheetItem = ({ const isRich = subtitle || meta || selectable; const isCentered = !isRich && !before && platform === 'ios'; + const onItemClickHandler = React.useCallback( + (e: React.MouseEvent) => { + onItemClick({ + action: onClick, + immediateAction: onImmediateClick, + autoClose: !autoCloseDisabled, + isCancelItem: Boolean(isCancelItem), + })?.(e); + }, + [autoCloseDisabled, isCancelItem, onClick, onImmediateClick, onItemClick], + ); + + const onKeyDown: React.KeyboardEventHandler = React.useCallback( + (event) => { + if (pressedKey(event) === Keys.ENTER) { + onActionSheetClose?.(); + } + }, + [onActionSheetClose], + ); + + const onItemClickImpl: React.MouseEventHandler = React.useCallback( + (event) => { + if (selectable) { + if (isRealClickEvent(event)) { + onItemClickHandler(event); + } + } else { + onItemClickHandler(event); + } + }, + [onItemClickHandler, selectable], + ); + return ( {before &&
{before}
}
{ + return event.type === 'click' && event.clientX !== 0 && event.clientY !== 0; +};