Skip to content
Merged
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
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": "winlarry@microsoft.com",
"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
Loading