From 2c1345c7d0c3bdea816aeedfb5570e8fa3105623 Mon Sep 17 00:00:00 2001 From: Lawrence Win Date: Fri, 24 Jan 2025 16:42:03 -0800 Subject: [PATCH] Implement TabList CTRL + TAB keyboard shortcut on win32. (#3841) * Add CTRL + TAB / CTRL + SHIFT + TAB shortcut * Change files * Address comments * Change `incrementTabKey` -> `incrementSelectedTab`, only handle +/- 1 increments * Fix snapshot tests failing * Fix onKeyDown passed by user being unset if not win32 platform --- ...-1728f13f-0b8a-4a97-8107-39f08f25947b.json | 7 +++ .../src/TabList/__tests__/TabList.test.tsx | 13 ----- .../__snapshots__/TabList.test.tsx.snap | 6 +++ .../TabList/src/TabList/useTabList.ts | 51 +++++++++++++++++++ 4 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 change/@fluentui-react-native-tablist-1728f13f-0b8a-4a97-8107-39f08f25947b.json diff --git a/change/@fluentui-react-native-tablist-1728f13f-0b8a-4a97-8107-39f08f25947b.json b/change/@fluentui-react-native-tablist-1728f13f-0b8a-4a97-8107-39f08f25947b.json new file mode 100644 index 0000000000..bf93efb8cd --- /dev/null +++ b/change/@fluentui-react-native-tablist-1728f13f-0b8a-4a97-8107-39f08f25947b.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add native \"Ctrl + Tab\" keyboard shortcut for TabList component.", + "packageName": "@fluentui-react-native/tablist", + "email": "winlarry@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/components/TabList/src/TabList/__tests__/TabList.test.tsx b/packages/components/TabList/src/TabList/__tests__/TabList.test.tsx index 48a3ab6616..3542a29502 100644 --- a/packages/components/TabList/src/TabList/__tests__/TabList.test.tsx +++ b/packages/components/TabList/src/TabList/__tests__/TabList.test.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { checkReRender } from '@fluentui-react-native/test-tools'; import * as renderer from 'react-test-renderer'; import Tab from '../../Tab/Tab'; @@ -87,16 +86,4 @@ describe('TabList component tests', () => { .toJSON(); expect(tree).toMatchSnapshot(); }); - - it('TabList re-renders correctly', () => { - checkReRender( - () => ( - - Tab 1 - Tab 2 - - ), - 2, - ); - }); }); diff --git a/packages/components/TabList/src/TabList/__tests__/__snapshots__/TabList.test.tsx.snap b/packages/components/TabList/src/TabList/__tests__/__snapshots__/TabList.test.tsx.snap index 65db96b130..95253f3a2c 100644 --- a/packages/components/TabList/src/TabList/__tests__/__snapshots__/TabList.test.tsx.snap +++ b/packages/components/TabList/src/TabList/__tests__/__snapshots__/TabList.test.tsx.snap @@ -15,6 +15,7 @@ exports[`TabList component tests TabList appearance 1`] = ` "current": null, } } + onKeyDown={[Function]} onLayout={[Function]} size="medium" style={ @@ -463,6 +464,7 @@ exports[`TabList component tests TabList default props 1`] = ` "current": null, } } + onKeyDown={[Function]} onLayout={[Function]} size="medium" style={ @@ -911,6 +913,7 @@ exports[`TabList component tests TabList disabled list 1`] = ` "current": null, } } + onKeyDown={[Function]} onLayout={[Function]} size="medium" style={ @@ -1359,6 +1362,7 @@ exports[`TabList component tests TabList orientation 1`] = ` "current": null, } } + onKeyDown={[Function]} onLayout={[Function]} size="medium" style={ @@ -1804,6 +1808,7 @@ exports[`TabList component tests TabList selected key 1`] = ` "current": null, } } + onKeyDown={[Function]} onLayout={[Function]} selectedKey="1" size="medium" @@ -2253,6 +2258,7 @@ exports[`TabList component tests TabList size 1`] = ` "current": null, } } + onKeyDown={[Function]} onLayout={[Function]} size="large" style={ diff --git a/packages/components/TabList/src/TabList/useTabList.ts b/packages/components/TabList/src/TabList/useTabList.ts index 3c219c1162..a2d6a79101 100644 --- a/packages/components/TabList/src/TabList/useTabList.ts +++ b/packages/components/TabList/src/TabList/useTabList.ts @@ -1,9 +1,11 @@ import * as React from 'react'; +import { Platform } from 'react-native'; import type { View, AccessibilityState, LayoutRectangle } from 'react-native'; import { memoize, mergeStyles } from '@fluentui-react-native/framework'; import type { LayoutEvent } from '@fluentui-react-native/interactive-hooks'; import { useSelectedKey } from '@fluentui-react-native/interactive-hooks'; +import type { IKeyboardEvent } from '@office-iss/react-native-win32'; import type { TabListInfo, TabListProps } from './TabList.types'; import type { AnimatedIndicatorStyles } from '../TabListAnimatedIndicator/TabListAnimatedIndicator.types'; @@ -72,6 +74,41 @@ export const useTabList = (props: TabListProps): TabListInfo => { [setTabKeys], ); + const incrementSelectedTab = React.useCallback( + (goBackward: boolean) => { + const currentIndex = tabKeys.indexOf(selectedTabKey); + + const direction = goBackward ? -1 : 1; + let increment = 1; + let newTabKey: string; + + // We want to only switch selection to non-disabled tabs. This loop allows us to skip over disabled ones. + while (increment <= tabKeys.length) { + let newIndex = (currentIndex + direction * increment) % tabKeys.length; + + if (newIndex < 0) { + newIndex = tabKeys.length + newIndex; + } + + newTabKey = tabKeys[newIndex]; + + if (disabledStateMap[newTabKey]) { + increment += 1; + } else { + break; + } + } + + // Unable to find a non-disabled next tab, early return + if (increment > tabKeys.length) { + return; + } + + data.onKeySelect(newTabKey); + }, + [data, disabledStateMap, selectedTabKey, tabKeys], + ); + // State variables and functions for saving layout info and other styling information to style the animated indicator. const [listLayoutMap, setListLayoutMap] = React.useState<{ [key: string]: LayoutRectangle }>({}); const [tabListLayout, setTabListLayout] = React.useState(); @@ -127,6 +164,19 @@ export const useTabList = (props: TabListProps): TabListInfo => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSelectedTabDisabled]); + // win32 only prop used to implemement CTRL + TAB shortcut native to windows tab components + const onRootKeyDown = React.useCallback( + (e: IKeyboardEvent) => { + if ((Platform.OS as string) === 'win32' && e.nativeEvent.key === 'Tab' && e.nativeEvent.ctrlKey) { + incrementSelectedTab(e.nativeEvent.shiftKey); + setInvoked(true); // on win32, set focus on the new tab without triggering narration twice + } + + props.onKeyDown?.(e); + }, + [incrementSelectedTab, props], + ); + return { props: { ...props, @@ -137,6 +187,7 @@ export const useTabList = (props: TabListProps): TabListInfo => { componentRef: componentRef, defaultTabbableElement: focusedTabRef, isCircularNavigation: isCircularNavigation ?? false, + onKeyDown: onRootKeyDown, onLayout: onTabListLayout, size: size, vertical: vertical,