Skip to content

Commit 5fd255f

Browse files
committed
Fix RTL rendering on mac
1 parent 67e7fdc commit 5fd255f

File tree

2 files changed

+59
-34
lines changed

2 files changed

+59
-34
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { I18nManager, Platform } from 'react-native';
3+
import type { LayoutRectangle } from 'react-native';
34

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

@@ -29,6 +30,8 @@ export function useTabAnimation(
2930
const { addTabLayout, selectedKey, layout, updateAnimatedIndicatorStyles, vertical } = context;
3031
const { tabKey } = props;
3132

33+
const [tabLayoutRect, setTabLayoutRect] = React.useState<LayoutRectangle>();
34+
3235
// If we're the selected tab, we style the TabListAnimatedIndicator with the correct token value set by the user
3336
React.useEffect(() => {
3437
if (tabKey === selectedKey && updateAnimatedIndicatorStyles) {
@@ -37,6 +40,41 @@ export function useTabAnimation(
3740
// eslint-disable-next-line react-hooks/exhaustive-deps
3841
}, [tabKey, selectedKey, tokens.indicatorColor]);
3942

43+
// Function to calculate indicator positioning and dimensions for the animated indicator.
44+
const calculateAndUpdateAnimationLayoutInfo = React.useCallback(
45+
(tabLayout: LayoutRectangle, tablistLayout: LayoutRectangle) => {
46+
const { width: tabWidth, height: tabHeight, y: tabY } = tabLayout;
47+
let indicatorWidth: number, indicatorHeight: number;
48+
// Total Indicator inset consists of the horizontal/vertical margin of the indicator, the space taken up by the tab's focus border, and the
49+
// existing padding between the focus border and the tab itself. Multiply by 2 to account for the start + end margin/border/padding.
50+
const focusBorderPadding = 1;
51+
const totalIndicatorInset = 2 * (tokens.indicatorMargin + tokens.borderWidth + focusBorderPadding);
52+
// we can calculate the dimensions of the indicator using token values we have access to.
53+
if (vertical) {
54+
indicatorWidth = tokens.indicatorThickness;
55+
indicatorHeight = tabHeight - totalIndicatorInset;
56+
} else {
57+
indicatorWidth = tabWidth - totalIndicatorInset;
58+
indicatorHeight = tokens.indicatorThickness;
59+
}
60+
let tabX = tabLayout.x;
61+
// For RTL users on win32 and mac, we adjust the x position of each tab to be relative to the entire tablist starting right to left
62+
// (e.g. 0 = 0 from the right, 100 = 100 from the right, rather than from the left)
63+
if (I18nManager.isRTL) {
64+
tabX = tablistLayout.width - (tabX + tabWidth);
65+
}
66+
addTabLayout(tabKey, {
67+
x: tabX,
68+
y: tabY,
69+
width: indicatorWidth,
70+
height: indicatorHeight,
71+
tabBorderWidth: tokens.borderWidth,
72+
startMargin: tokens.indicatorMargin,
73+
});
74+
},
75+
[addTabLayout, tabKey, tokens.borderWidth, tokens.indicatorMargin, tokens.indicatorThickness, vertical],
76+
);
77+
4078
/**
4179
* 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
4280
* want to trigger a re-render by needlessly updating the TabList state.
@@ -51,44 +89,27 @@ export function useTabAnimation(
5189
(e: LayoutEvent) => {
5290
if (
5391
e.nativeEvent.layout &&
92+
layout?.tablist &&
5493
// Following checks are for win32 only, will be removed after addressing scrollview layout bug
5594
(Platform.OS !== ('win32' as any) ||
56-
(layout?.tablist &&
57-
layout?.tablist.width > 0 &&
95+
(layout.tablist.width > 0 &&
5896
e.nativeEvent.layout.height <= layout.tablist.height &&
5997
e.nativeEvent.layout.height < RENDERING_HEIGHT_LIMIT))
6098
) {
61-
const { width: tabWidth, height: tabHeight, y: tabY } = e.nativeEvent.layout;
62-
let indicatorWidth: number, indicatorHeight: number;
63-
// Total Indicator inset consists of the horizontal/vertical margin of the indicator, the space taken up by the tab's focus border, and the
64-
// existing padding between the focus border and the tab itself. Multiply by 2 to account for the start + end margin/border/padding.
65-
const focusBorderPadding = 1;
66-
const totalIndicatorInset = 2 * (tokens.indicatorMargin + tokens.borderWidth + focusBorderPadding);
67-
// we can calculate the dimensions of the indicator using token values we have access to.
68-
if (vertical) {
69-
indicatorWidth = tokens.indicatorThickness;
70-
indicatorHeight = tabHeight - totalIndicatorInset;
71-
} else {
72-
indicatorWidth = tabWidth - totalIndicatorInset;
73-
indicatorHeight = tokens.indicatorThickness;
74-
}
75-
let tabX = e.nativeEvent.layout.x;
76-
// For RTL users, we adjust the x position of each tab to be relative to the entire tablist starting right to left
77-
if (Platform.OS == ('win32' as any) && I18nManager.isRTL) {
78-
tabX = layout.tablist.width - (tabX + tabWidth);
79-
}
80-
addTabLayout(tabKey, {
81-
x: tabX,
82-
y: tabY,
83-
width: indicatorWidth,
84-
height: indicatorHeight,
85-
tabBorderWidth: tokens.borderWidth,
86-
startMargin: tokens.indicatorMargin,
87-
});
99+
calculateAndUpdateAnimationLayoutInfo(e.nativeEvent.layout, layout.tablist);
100+
} else if (!layout.tablist) {
101+
// We need the tablist layout rectangle for the layout calculation, so we save the tab rect given and defer the calculation to the useEffect below
102+
setTabLayoutRect(e.nativeEvent.layout);
88103
}
89104
},
90-
[addTabLayout, layout, tabKey, tokens.borderWidth, tokens.indicatorMargin, tokens.indicatorThickness, vertical],
105+
[calculateAndUpdateAnimationLayoutInfo, layout.tablist],
91106
);
92107

108+
React.useEffect(() => {
109+
if (tabLayoutRect && layout.tablist) {
110+
calculateAndUpdateAnimationLayoutInfo(tabLayoutRect, layout.tablist);
111+
}
112+
}, [layout.tablist]);
113+
93114
return React.useMemo(() => ({ ...rootProps, onLayout: onTabLayout }), [rootProps, onTabLayout]);
94115
}

packages/experimental/TabList/src/TabListAnimatedIndicator/useAnimatedIndicatorStyles.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Animated, Easing } from 'react-native';
2+
import { Animated, Easing, I18nManager } from 'react-native';
33
import type { ViewStyle } from 'react-native';
44

55
import type { AnimatedIndicatorProps, AnimatedIndicatorStyles } from './TabListAnimatedIndicator.types';
@@ -30,13 +30,17 @@ export function useAnimatedIndicatorStyles(props: AnimatedIndicatorProps): Anima
3030
let scaleValue: number, translateValue: number, translateOffset: number;
3131
if (vertical) {
3232
scaleValue = selectedIndicatorLayout.height / startingIndicatorLayout.height;
33-
translateValue = selectedIndicatorLayout.y - startingIndicatorLayout.y;
3433
translateOffset = (selectedIndicatorLayout.height - startingIndicatorLayout.height) / 2;
34+
translateValue = selectedIndicatorLayout.y - startingIndicatorLayout.y + translateOffset;
3535
} else {
3636
scaleValue = selectedIndicatorLayout.width / startingIndicatorLayout.width;
37-
translateValue = selectedIndicatorLayout.x - startingIndicatorLayout.x;
3837
translateOffset = (selectedIndicatorLayout.width - startingIndicatorLayout.width) / 2;
38+
translateValue = selectedIndicatorLayout.x - startingIndicatorLayout.x + translateOffset;
39+
if (I18nManager.isRTL) {
40+
translateValue *= -1;
41+
}
3942
}
43+
translateValue = translateValue + translateOffset;
4044
Animated.parallel([
4145
Animated.timing(indicatorScale, {
4246
toValue: scaleValue,
@@ -45,7 +49,7 @@ export function useAnimatedIndicatorStyles(props: AnimatedIndicatorProps): Anima
4549
useNativeDriver: true,
4650
}),
4751
Animated.timing(indicatorTranslate, {
48-
toValue: translateValue + translateOffset,
52+
toValue: translateValue,
4953
duration: 300,
5054
easing: Easing.bezier(0, 0, 0, 1),
5155
useNativeDriver: true,

0 commit comments

Comments
 (0)