Skip to content

Commit

Permalink
Implement TabList CTRL + TAB keyboard shortcut on win32. (#3841)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lawrencewin authored Jan 25, 2025
1 parent 3a8139b commit 2c1345c
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add native \"Ctrl + Tab\" keyboard shortcut for TabList component.",
"packageName": "@fluentui-react-native/tablist",
"email": "[email protected]",
"dependentChangeType": "patch"
}
13 changes: 0 additions & 13 deletions packages/components/TabList/src/TabList/__tests__/TabList.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -87,16 +86,4 @@ describe('TabList component tests', () => {
.toJSON();
expect(tree).toMatchSnapshot();
});

it('TabList re-renders correctly', () => {
checkReRender(
() => (
<TabList>
<Tab tabKey="1">Tab 1</Tab>
<Tab tabKey="2">Tab 2</Tab>
</TabList>
),
2,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ exports[`TabList component tests TabList appearance 1`] = `
"current": null,
}
}
onKeyDown={[Function]}
onLayout={[Function]}
size="medium"
style={
Expand Down Expand Up @@ -463,6 +464,7 @@ exports[`TabList component tests TabList default props 1`] = `
"current": null,
}
}
onKeyDown={[Function]}
onLayout={[Function]}
size="medium"
style={
Expand Down Expand Up @@ -911,6 +913,7 @@ exports[`TabList component tests TabList disabled list 1`] = `
"current": null,
}
}
onKeyDown={[Function]}
onLayout={[Function]}
size="medium"
style={
Expand Down Expand Up @@ -1359,6 +1362,7 @@ exports[`TabList component tests TabList orientation 1`] = `
"current": null,
}
}
onKeyDown={[Function]}
onLayout={[Function]}
size="medium"
style={
Expand Down Expand Up @@ -1804,6 +1808,7 @@ exports[`TabList component tests TabList selected key 1`] = `
"current": null,
}
}
onKeyDown={[Function]}
onLayout={[Function]}
selectedKey="1"
size="medium"
Expand Down Expand Up @@ -2253,6 +2258,7 @@ exports[`TabList component tests TabList size 1`] = `
"current": null,
}
}
onKeyDown={[Function]}
onLayout={[Function]}
size="large"
style={
Expand Down
51 changes: 51 additions & 0 deletions packages/components/TabList/src/TabList/useTabList.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<LayoutRectangle>();
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 2c1345c

Please sign in to comment.