Skip to content

Commit 2c1345c

Browse files
authored
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
1 parent 3a8139b commit 2c1345c

File tree

4 files changed

+64
-13
lines changed

4 files changed

+64
-13
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add native \"Ctrl + Tab\" keyboard shortcut for TabList component.",
4+
"packageName": "@fluentui-react-native/tablist",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/components/TabList/src/TabList/__tests__/TabList.test.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as React from 'react';
22

3-
import { checkReRender } from '@fluentui-react-native/test-tools';
43
import * as renderer from 'react-test-renderer';
54

65
import Tab from '../../Tab/Tab';
@@ -87,16 +86,4 @@ describe('TabList component tests', () => {
8786
.toJSON();
8887
expect(tree).toMatchSnapshot();
8988
});
90-
91-
it('TabList re-renders correctly', () => {
92-
checkReRender(
93-
() => (
94-
<TabList>
95-
<Tab tabKey="1">Tab 1</Tab>
96-
<Tab tabKey="2">Tab 2</Tab>
97-
</TabList>
98-
),
99-
2,
100-
);
101-
});
10289
});

packages/components/TabList/src/TabList/__tests__/__snapshots__/TabList.test.tsx.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ exports[`TabList component tests TabList appearance 1`] = `
1515
"current": null,
1616
}
1717
}
18+
onKeyDown={[Function]}
1819
onLayout={[Function]}
1920
size="medium"
2021
style={
@@ -463,6 +464,7 @@ exports[`TabList component tests TabList default props 1`] = `
463464
"current": null,
464465
}
465466
}
467+
onKeyDown={[Function]}
466468
onLayout={[Function]}
467469
size="medium"
468470
style={
@@ -911,6 +913,7 @@ exports[`TabList component tests TabList disabled list 1`] = `
911913
"current": null,
912914
}
913915
}
916+
onKeyDown={[Function]}
914917
onLayout={[Function]}
915918
size="medium"
916919
style={
@@ -1359,6 +1362,7 @@ exports[`TabList component tests TabList orientation 1`] = `
13591362
"current": null,
13601363
}
13611364
}
1365+
onKeyDown={[Function]}
13621366
onLayout={[Function]}
13631367
size="medium"
13641368
style={
@@ -1804,6 +1808,7 @@ exports[`TabList component tests TabList selected key 1`] = `
18041808
"current": null,
18051809
}
18061810
}
1811+
onKeyDown={[Function]}
18071812
onLayout={[Function]}
18081813
selectedKey="1"
18091814
size="medium"
@@ -2253,6 +2258,7 @@ exports[`TabList component tests TabList size 1`] = `
22532258
"current": null,
22542259
}
22552260
}
2261+
onKeyDown={[Function]}
22562262
onLayout={[Function]}
22572263
size="large"
22582264
style={

packages/components/TabList/src/TabList/useTabList.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as React from 'react';
2+
import { Platform } from 'react-native';
23
import type { View, AccessibilityState, LayoutRectangle } from 'react-native';
34

45
import { memoize, mergeStyles } from '@fluentui-react-native/framework';
56
import type { LayoutEvent } from '@fluentui-react-native/interactive-hooks';
67
import { useSelectedKey } from '@fluentui-react-native/interactive-hooks';
8+
import type { IKeyboardEvent } from '@office-iss/react-native-win32';
79

810
import type { TabListInfo, TabListProps } from './TabList.types';
911
import type { AnimatedIndicatorStyles } from '../TabListAnimatedIndicator/TabListAnimatedIndicator.types';
@@ -72,6 +74,41 @@ export const useTabList = (props: TabListProps): TabListInfo => {
7274
[setTabKeys],
7375
);
7476

77+
const incrementSelectedTab = React.useCallback(
78+
(goBackward: boolean) => {
79+
const currentIndex = tabKeys.indexOf(selectedTabKey);
80+
81+
const direction = goBackward ? -1 : 1;
82+
let increment = 1;
83+
let newTabKey: string;
84+
85+
// We want to only switch selection to non-disabled tabs. This loop allows us to skip over disabled ones.
86+
while (increment <= tabKeys.length) {
87+
let newIndex = (currentIndex + direction * increment) % tabKeys.length;
88+
89+
if (newIndex < 0) {
90+
newIndex = tabKeys.length + newIndex;
91+
}
92+
93+
newTabKey = tabKeys[newIndex];
94+
95+
if (disabledStateMap[newTabKey]) {
96+
increment += 1;
97+
} else {
98+
break;
99+
}
100+
}
101+
102+
// Unable to find a non-disabled next tab, early return
103+
if (increment > tabKeys.length) {
104+
return;
105+
}
106+
107+
data.onKeySelect(newTabKey);
108+
},
109+
[data, disabledStateMap, selectedTabKey, tabKeys],
110+
);
111+
75112
// State variables and functions for saving layout info and other styling information to style the animated indicator.
76113
const [listLayoutMap, setListLayoutMap] = React.useState<{ [key: string]: LayoutRectangle }>({});
77114
const [tabListLayout, setTabListLayout] = React.useState<LayoutRectangle>();
@@ -127,6 +164,19 @@ export const useTabList = (props: TabListProps): TabListInfo => {
127164
// eslint-disable-next-line react-hooks/exhaustive-deps
128165
}, [isSelectedTabDisabled]);
129166

167+
// win32 only prop used to implemement CTRL + TAB shortcut native to windows tab components
168+
const onRootKeyDown = React.useCallback(
169+
(e: IKeyboardEvent) => {
170+
if ((Platform.OS as string) === 'win32' && e.nativeEvent.key === 'Tab' && e.nativeEvent.ctrlKey) {
171+
incrementSelectedTab(e.nativeEvent.shiftKey);
172+
setInvoked(true); // on win32, set focus on the new tab without triggering narration twice
173+
}
174+
175+
props.onKeyDown?.(e);
176+
},
177+
[incrementSelectedTab, props],
178+
);
179+
130180
return {
131181
props: {
132182
...props,
@@ -137,6 +187,7 @@ export const useTabList = (props: TabListProps): TabListInfo => {
137187
componentRef: componentRef,
138188
defaultTabbableElement: focusedTabRef,
139189
isCircularNavigation: isCircularNavigation ?? false,
190+
onKeyDown: onRootKeyDown,
140191
onLayout: onTabListLayout,
141192
size: size,
142193
vertical: vertical,

0 commit comments

Comments
 (0)