1
1
import React from 'react' ;
2
2
import { I18nManager , Platform } from 'react-native' ;
3
+ import type { LayoutRectangle } from 'react-native' ;
3
4
4
5
import type { LayoutEvent , PressablePropsExtended } from '@fluentui-react-native/interactive-hooks' ;
5
6
@@ -29,6 +30,8 @@ export function useTabAnimation(
29
30
const { addTabLayout, selectedKey, layout, updateAnimatedIndicatorStyles, vertical } = context ;
30
31
const { tabKey } = props ;
31
32
33
+ const [ tabLayoutRect , setTabLayoutRect ] = React . useState < LayoutRectangle > ( ) ;
34
+
32
35
// If we're the selected tab, we style the TabListAnimatedIndicator with the correct token value set by the user
33
36
React . useEffect ( ( ) => {
34
37
if ( tabKey === selectedKey && updateAnimatedIndicatorStyles ) {
@@ -37,6 +40,41 @@ export function useTabAnimation(
37
40
// eslint-disable-next-line react-hooks/exhaustive-deps
38
41
} , [ tabKey , selectedKey , tokens . indicatorColor ] ) ;
39
42
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
+
40
78
/**
41
79
* 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
42
80
* want to trigger a re-render by needlessly updating the TabList state.
@@ -51,44 +89,27 @@ export function useTabAnimation(
51
89
( e : LayoutEvent ) => {
52
90
if (
53
91
e . nativeEvent . layout &&
92
+ layout ?. tablist &&
54
93
// Following checks are for win32 only, will be removed after addressing scrollview layout bug
55
94
( Platform . OS !== ( 'win32' as any ) ||
56
- ( layout ?. tablist &&
57
- layout ?. tablist . width > 0 &&
95
+ ( layout . tablist . width > 0 &&
58
96
e . nativeEvent . layout . height <= layout . tablist . height &&
59
97
e . nativeEvent . layout . height < RENDERING_HEIGHT_LIMIT ) )
60
98
) {
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 ) ;
88
103
}
89
104
} ,
90
- [ addTabLayout , layout , tabKey , tokens . borderWidth , tokens . indicatorMargin , tokens . indicatorThickness , vertical ] ,
105
+ [ calculateAndUpdateAnimationLayoutInfo , layout . tablist ] ,
91
106
) ;
92
107
108
+ React . useEffect ( ( ) => {
109
+ if ( tabLayoutRect && layout . tablist ) {
110
+ calculateAndUpdateAnimationLayoutInfo ( tabLayoutRect , layout . tablist ) ;
111
+ }
112
+ } , [ layout . tablist ] ) ;
113
+
93
114
return React . useMemo ( ( ) => ( { ...rootProps , onLayout : onTabLayout } ) , [ rootProps , onTabLayout ] ) ;
94
115
}
0 commit comments