Skip to content

Commit 942da04

Browse files
authored
Re-structure TabList Animated Indicator code to work better with RTL layouts (#3236)
* Fix RTL styling on win32 * Change files * Fix RTL rendering on mac * Fix snapshot tests failing * Re-add root slot to fix indicator positioning * Rewrite animated indicator to work better with RTL This commit simplifies the logic of the animated indicator. Rather than positioning the indicator using a calculated `start` value, we position it using absolute layout values within the tablist. Now, the `useTabAnimation` hook does all the math to figure out the top and left offsets of the indicator, and the styling hook for the animatedIndicator simply uses them to position the `top` and `left` layout props. The container slot of the animated indicator is also removed. * Clean-up: refactoring + adding comments * Fix snapshot tests * Fix tablist test page title again * Fix wrong token setting indicator border radius
1 parent d05effc commit 942da04

16 files changed

+2173
-2090
lines changed

apps/fluent-tester/src/TestComponents/TabList/TabListTest.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,5 +227,5 @@ export const TabListTest: React.FunctionComponent = () => {
227227

228228
const description = 'With Tabs, users can navigate to another view.';
229229

230-
return <Test name="TabsV1 Test" description={description} sections={sections} status={status} e2eSections={e2eSections} />;
230+
return <Test name="TabList Test" description={description} sections={sections} status={status} e2eSections={e2eSections} />;
231231
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Change TabList test page title to be correct",
4+
"packageName": "@fluentui-react-native/tablist",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Fix RTL styling on win32",
4+
"packageName": "@fluentui-react-native/tester",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/experimental/TabList/src/Tab/Tab.styling.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,14 @@ export const useTabSlotProps = (props: TabProps, tokens: TabTokens, theme: Theme
6969
display: 'flex',
7070
alignItems: 'center',
7171
flexDirection: 'row',
72+
flex: vertical ? 0 : 1,
7273
alignSelf: 'flex-start',
7374
justifyContent: 'center',
7475
marginHorizontal: tokens.stackMarginHorizontal,
7576
marginVertical: tokens.stackMarginVertical,
7677
},
7778
}),
78-
[tokens.stackMarginHorizontal, tokens.stackMarginVertical],
79+
[vertical, tokens.stackMarginHorizontal, tokens.stackMarginVertical],
7980
);
8081

8182
const indicatorContainer = React.useMemo<IViewProps>(

packages/experimental/TabList/src/Tab/__tests__/__snapshots__/Tab.test.tsx.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ exports[`Tab component tests Customized Tab 1`] = `
8181
"alignItems": "center",
8282
"alignSelf": "flex-start",
8383
"display": "flex",
84+
"flex": 1,
8485
"flexDirection": "row",
8586
"justifyContent": "center",
8687
"marginHorizontal": 6,
@@ -217,6 +218,7 @@ exports[`Tab component tests Tab default props 1`] = `
217218
"alignItems": "center",
218219
"alignSelf": "flex-start",
219220
"display": "flex",
221+
"flex": 1,
220222
"flexDirection": "row",
221223
"justifyContent": "center",
222224
"marginHorizontal": 6,
@@ -353,6 +355,7 @@ exports[`Tab component tests Tab disabled 1`] = `
353355
"alignItems": "center",
354356
"alignSelf": "flex-start",
355357
"display": "flex",
358+
"flex": 1,
356359
"flexDirection": "row",
357360
"justifyContent": "center",
358361
"marginHorizontal": 6,
@@ -489,6 +492,7 @@ exports[`Tab component tests Tab render icon + text 1`] = `
489492
"alignItems": "center",
490493
"alignSelf": "flex-start",
491494
"display": "flex",
495+
"flex": 1,
492496
"flexDirection": "row",
493497
"justifyContent": "center",
494498
"marginHorizontal": 6,
@@ -637,6 +641,7 @@ exports[`Tab component tests Tab render icon only 1`] = `
637641
"alignItems": "center",
638642
"alignSelf": "flex-start",
639643
"display": "flex",
644+
"flex": 1,
640645
"flexDirection": "row",
641646
"justifyContent": "center",
642647
"marginHorizontal": 6,

packages/experimental/TabList/src/Tab/useTabAnimation.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Platform } from 'react-native';
2+
import { I18nManager, Platform } from 'react-native';
33

44
import type { LayoutEvent, PressablePropsExtended } from '@fluentui-react-native/interactive-hooks';
55

@@ -32,52 +32,63 @@ export function useTabAnimation(
3232
// If we're the selected tab, we style the TabListAnimatedIndicator with the correct token value set by the user
3333
React.useEffect(() => {
3434
if (tabKey === selectedKey && updateAnimatedIndicatorStyles) {
35-
updateAnimatedIndicatorStyles({ indicator: { backgroundColor: tokens.indicatorColor } });
35+
updateAnimatedIndicatorStyles({ backgroundColor: tokens.indicatorColor, borderRadius: tokens.indicatorRadius });
3636
}
37+
// Disabling warning because effect does not need to fire on `updateAnimatedIndicatorStyles` being changed
3738
// eslint-disable-next-line react-hooks/exhaustive-deps
38-
}, [tabKey, selectedKey, tokens.indicatorColor]);
39+
}, [tabKey, selectedKey, tokens.indicatorColor, tokens.indicatorRadius]);
3940

4041
/**
4142
* This checks to see if we have relevant info to calculate the layout position and dimensions of the indicator. If this check fails, we don't
4243
* want to trigger a re-render by needlessly updating the TabList state.
4344
*
44-
* We also check if the info is good. Info can be bad in some weird cases:
45+
* We also check if the info is good. Info can be bad in some weird cases on win32:
4546
* - Check if width > 0 because there is an on-going issue caused by ScrollViews initially laying out its childrens' width to 0 and height to be a bigger than expected value.
4647
* - ScrollView also negatively affects the initial height values. For vertical TabLists, the initial height value will lay out incorrectly. Sometimes, the styling of the parent
4748
* component combined with the ScrollView issues causes the initial height layout value to be completely unreasonable. Exactly which style that causes this issue isn't known;
4849
* more investigation has to be done.
50+
*
51+
* Once we finish these checks, for each tab, we calculate the layout information of its indicator consisting of (1) its dimensions and (2) its position (x,y) relative to the tablist.
52+
* Afterwards, we save these to feed into the Animated Indicator's layout styles.
4953
*/
5054
const onTabLayout = React.useCallback(
5155
(e: LayoutEvent) => {
5256
if (
53-
e.nativeEvent.layout &&
54-
// Following checks are for win32 only, will be removed after addressing scrollview layout bug
55-
(Platform.OS !== ('win32' as any) ||
56-
(layout?.tablist &&
57-
layout?.tablist.width > 0 &&
58-
e.nativeEvent.layout.height <= layout.tablist.height &&
59-
e.nativeEvent.layout.height < RENDERING_HEIGHT_LIMIT))
57+
(e.nativeEvent.layout &&
58+
// Following checks are for win32 only, will be removed after addressing scrollview layout bug
59+
Platform.OS !== ('win32' as any)) ||
60+
(layout?.tablist &&
61+
layout.tablist.width > 0 &&
62+
e.nativeEvent.layout.height <= layout.tablist.height &&
63+
e.nativeEvent.layout.height < RENDERING_HEIGHT_LIMIT)
6064
) {
61-
let width: number, height: number;
65+
const { width: tabWidth, height: tabHeight, x: tabX, y: tabY } = e.nativeEvent.layout;
66+
let indicatorWidth: number, indicatorHeight: number, indicatorX: number, indicatorY: number;
6267
// Total Indicator inset consists of the horizontal/vertical margin of the indicator, the space taken up by the tab's focus border, and the
63-
// existing padding between the focus border and the tab itself. Multiply by 2 to account for the start + end margin/border/padding.
68+
// existing padding between the focus border and the tab itself.
6469
const focusBorderPadding = 1;
65-
const totalIndicatorInset = 2 * (tokens.indicatorMargin + tokens.borderWidth + focusBorderPadding);
66-
// we can calculate the dimensions of the indicator using token values we have access to.
70+
const totalIndicatorInset = tokens.indicatorMargin + tokens.borderWidth + focusBorderPadding;
6771
if (vertical) {
68-
width = tokens.indicatorThickness;
69-
height = e.nativeEvent.layout.height - totalIndicatorInset;
72+
indicatorWidth = tokens.indicatorThickness;
73+
indicatorHeight = tabHeight - totalIndicatorInset * 2; // multiply inset by 2 to subtract height from top and bottom
74+
indicatorY = tabY + totalIndicatorInset;
75+
if (I18nManager.isRTL) {
76+
// On RTL, the vertical tab indicator should appear to the right of the text
77+
indicatorX = tabX + tabWidth - (tokens.borderWidth + focusBorderPadding + indicatorWidth);
78+
} else {
79+
indicatorX = tabX + tokens.borderWidth + focusBorderPadding;
80+
}
7081
} else {
71-
width = e.nativeEvent.layout.width - totalIndicatorInset;
72-
height = tokens.indicatorThickness;
82+
indicatorWidth = tabWidth - totalIndicatorInset * 2; // multiply inset by 2 to subtract width from left and right
83+
indicatorHeight = tokens.indicatorThickness;
84+
indicatorX = tabX + totalIndicatorInset;
85+
indicatorY = tabHeight + tabY - indicatorHeight - tokens.borderWidth - focusBorderPadding;
7386
}
7487
addTabLayout(tabKey, {
75-
x: e.nativeEvent.layout.x,
76-
y: e.nativeEvent.layout.y,
77-
width: width,
78-
height: height,
79-
tabBorderWidth: tokens.borderWidth,
80-
startMargin: tokens.indicatorMargin,
88+
x: indicatorX,
89+
y: indicatorY,
90+
width: indicatorWidth,
91+
height: indicatorHeight,
8192
});
8293
}
8394
},

packages/experimental/TabList/src/TabList/TabList.styling.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,24 @@ export const stylingSettings: UseStylingOptions<TabListProps, TabListSlotProps,
1010
states: ['vertical'],
1111
slotProps: {
1212
stack: buildProps(
13-
(tokens: TabListTokens, theme: Theme) => ({
13+
(tokens: TabListTokens) => ({
1414
style: {
15+
display: 'flex',
1516
flexDirection: tokens.direction,
17+
flex: 0,
18+
},
19+
}),
20+
['direction'],
21+
),
22+
root: buildProps(
23+
(tokens: TabListTokens, theme: Theme) => ({
24+
style: {
25+
display: 'flex',
26+
alignItems: 'flex-start',
1627
...layoutStyles.from(tokens, theme),
1728
},
1829
}),
19-
['direction', ...layoutStyles.keys],
30+
layoutStyles.keys,
2031
),
2132
},
2233
};

packages/experimental/TabList/src/TabList/TabList.tsx

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const TabList = compose<TabListType>({
2020
slots: {
2121
container: FocusZone,
2222
stack: View,
23+
root: View,
2324
},
2425
useRender: (userProps: TabListProps, useSlots: UseSlots<TabListType>) => {
2526
// configure props and state for tabs based on user props
@@ -43,22 +44,24 @@ export const TabList = compose<TabListType>({
4344
// Passes in the selected key and a hook function to update the newly selected tab and call the client's onTabsClick callback.
4445
value={tablist.state}
4546
>
46-
<Slots.container
47-
disabled={disabled || tablistDisabledState}
48-
defaultTabbableElement={defaultTabbableElement}
49-
focusZoneDirection={vertical ? 'vertical' : 'horizontal'}
50-
isCircularNavigation={isCircularNavigation}
51-
>
52-
<Slots.stack {...mergedProps}>{children}</Slots.stack>
53-
{canShowAnimatedIndicator && (
54-
<TabListAnimatedIndicator
55-
animatedIndicatorStyles={animatedIndicatorStyles}
56-
selectedKey={selectedKey}
57-
tabLayout={layout.tabs}
58-
vertical={vertical}
59-
/>
60-
)}
61-
</Slots.container>
47+
<Slots.root {...mergedProps}>
48+
<Slots.container
49+
disabled={disabled || tablistDisabledState}
50+
defaultTabbableElement={defaultTabbableElement}
51+
focusZoneDirection={vertical ? 'vertical' : 'horizontal'}
52+
isCircularNavigation={isCircularNavigation}
53+
>
54+
<Slots.stack>{children}</Slots.stack>
55+
{canShowAnimatedIndicator && (
56+
<TabListAnimatedIndicator
57+
animatedIndicatorStyles={animatedIndicatorStyles}
58+
selectedKey={selectedKey}
59+
tabLayout={layout.tabs}
60+
vertical={vertical}
61+
/>
62+
)}
63+
</Slots.container>
64+
</Slots.root>
6265
</TabListContext.Provider>
6366
);
6467
};

packages/experimental/TabList/src/TabList/TabList.types.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,15 @@ import type { FocusZoneProps } from '@fluentui-react-native/focus-zone';
66
import type { LayoutTokens } from '@fluentui-react-native/tokens';
77
import type { LayoutRectangle } from '@office-iss/react-native-win32';
88

9-
import type {
10-
AnimatedIndicatorStyles,
11-
AnimatedIndicatorStylesUpdate,
12-
TabLayoutInfo,
13-
} from '../TabListAnimatedIndicator/TabListAnimatedIndicator.types';
9+
import type { AnimatedIndicatorStyles } from '../TabListAnimatedIndicator/TabListAnimatedIndicator.types';
1410

1511
export const tabListName = 'TabList';
1612

1713
export type TabListAppearance = 'transparent' | 'subtle';
1814
export type TabListSize = 'small' | 'medium' | 'large';
1915
export interface TabListLayoutInfo {
2016
tablist: LayoutRectangle;
21-
tabs: { [key: string]: TabLayoutInfo };
17+
tabs: { [key: string]: LayoutRectangle };
2218
}
2319

2420
export interface TabListState {
@@ -30,7 +26,7 @@ export interface TabListState {
3026
/**
3127
* Method to add Tab's layout information for animating the tab indicator
3228
*/
33-
addTabLayout?: (tabKey: string, layout: TabLayoutInfo) => void;
29+
addTabLayout?: (tabKey: string, layout: LayoutRectangle) => void;
3430

3531
/**
3632
* Global state both TabList and Tab use for tracking styling of the animated indicator.
@@ -104,7 +100,7 @@ export interface TabListState {
104100
/**
105101
* Directly update the animated indicator's styles with styles the user supplies for each slot.
106102
*/
107-
updateAnimatedIndicatorStyles?: (updates: AnimatedIndicatorStylesUpdate) => void;
103+
updateAnimatedIndicatorStyles?: (updates: AnimatedIndicatorStyles) => void;
108104

109105
/**
110106
* Updates internal map that keeps track of each of this tablist's tabs disabled state
@@ -184,7 +180,8 @@ export interface TabListInfo {
184180
}
185181
export interface TabListSlotProps {
186182
container?: FocusZoneProps;
187-
stack: React.PropsWithRef<IViewProps>;
183+
stack: IViewProps;
184+
root: React.PropsWithRef<IViewProps>;
188185
}
189186

190187
export interface TabListType {

0 commit comments

Comments
 (0)