Skip to content

Commit 052b3f2

Browse files
committed
feat(tabs): updated tabs to use the new resize observer API
1 parent dc3f4df commit 052b3f2

File tree

3 files changed

+93
-59
lines changed

3 files changed

+93
-59
lines changed

packages/tabs/src/Tab.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
InteractionStatesOptions,
66
useInteractionStates,
77
} from "@react-md/states";
8-
import { bem } from "@react-md/utils";
8+
import { bem, useResizeObserver } from "@react-md/utils";
99
import { TabConfig } from "./types";
10+
import { useUpdateIndicatorStyles } from "./useTabIndicatorStyle";
1011

1112
export interface TabProps
1213
extends TabConfig,
@@ -64,7 +65,7 @@ const Tab = forwardRef<HTMLButtonElement, TabProps>(function Tab(
6465
enablePressedAndRipple,
6566
...props
6667
},
67-
ref
68+
propRef
6869
) {
6970
const { ripples, className, handlers } = useInteractionStates({
7071
handlers: props,
@@ -78,12 +79,19 @@ const Tab = forwardRef<HTMLButtonElement, TabProps>(function Tab(
7879
rippleContainerClassName,
7980
enablePressedAndRipple,
8081
});
82+
// TODO: Look into removing this resize observer. This is only required if
83+
// someone manually updates the width of the tab (dev utils) or if the width
84+
// was not changed due to the tabs container element resizing (iffy)
85+
const updateIndicatorStyles = useUpdateIndicatorStyles();
86+
const [, refHandler] = useResizeObserver(updateIndicatorStyles, {
87+
ref: propRef,
88+
});
8189

8290
return (
8391
<button
8492
{...props}
8593
{...handlers}
86-
ref={ref}
94+
ref={active ? refHandler : propRef}
8795
aria-selected={active}
8896
aria-controls={panelId}
8997
type="button"

packages/tabs/src/TabsList.tsx

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import cn from "classnames";
1313
import { applyRef, bem, useIsUserInteractionMode } from "@react-md/utils";
1414

1515
import { TabsConfig } from "./types";
16-
import useTabIndicatorStyle from "./useTabIndicatorStyle";
16+
import useTabIndicatorStyle, {
17+
UpdateIndicatorStylesProvider,
18+
} from "./useTabIndicatorStyle";
1719
import useTabsMovement from "./useTabsMovement";
1820

1921
export interface TabsListProps
@@ -78,7 +80,12 @@ const TabsList = forwardRef<HTMLDivElement, TabsListProps>(function TabsList(
7880
onActiveIndexChange,
7981
automatic,
8082
});
81-
const [mergedStyle, ref, tabsRef] = useTabIndicatorStyle({
83+
const [
84+
mergedStyle,
85+
tabsRefHandler,
86+
tabsRef,
87+
updateIndicatorStyles,
88+
] = useTabIndicatorStyle({
8289
style,
8390
ref: forwardedRef,
8491
align,
@@ -113,41 +120,43 @@ const TabsList = forwardRef<HTMLDivElement, TabsListProps>(function TabsList(
113120
}, [activeIndex]);
114121

115122
return (
116-
<div
117-
{...props}
118-
aria-orientation={orientation}
119-
style={mergedStyle}
120-
role="tablist"
121-
className={cn(
122-
block({
123-
[align]: true,
124-
padded,
125-
vertical: !horizontal,
126-
animate: !disableTransition && (!automatic || !isKeyboard),
127-
}),
128-
className
129-
)}
130-
ref={ref}
131-
onClick={handleClick}
132-
onKeyDown={handleKeyDown}
133-
>
134-
{Children.map(tabs, (child, i) => {
135-
if (!isValidElement(child)) {
136-
return child;
137-
}
123+
<UpdateIndicatorStylesProvider value={updateIndicatorStyles}>
124+
<div
125+
{...props}
126+
aria-orientation={orientation}
127+
style={mergedStyle}
128+
role="tablist"
129+
className={cn(
130+
block({
131+
[align]: true,
132+
padded,
133+
vertical: !horizontal,
134+
animate: !disableTransition && (!automatic || !isKeyboard),
135+
}),
136+
className
137+
)}
138+
ref={tabsRefHandler}
139+
onClick={handleClick}
140+
onKeyDown={handleKeyDown}
141+
>
142+
{Children.map(tabs, (child, i) => {
143+
if (!isValidElement(child)) {
144+
return child;
145+
}
138146

139-
const tab = Children.only(child);
140-
let ref: Ref<HTMLElement> = itemRefs[i];
141-
if (tab.props.ref) {
142-
ref = (instance: HTMLElement | null) => {
143-
itemRefs[i].current = instance;
144-
applyRef(instance, tab.props.ref);
145-
};
146-
}
147+
const tab = Children.only(child);
148+
let ref: Ref<HTMLElement> = itemRefs[i];
149+
if (tab.props.ref) {
150+
ref = (instance: HTMLElement | null) => {
151+
itemRefs[i].current = instance;
152+
applyRef(instance, tab.props.ref);
153+
};
154+
}
147155

148-
return cloneElement(tab, { ref });
149-
})}
150-
</div>
156+
return cloneElement(tab, { ref });
157+
})}
158+
</div>
159+
</UpdateIndicatorStylesProvider>
151160
);
152161
});
153162

packages/tabs/src/useTabIndicatorStyle.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
useMemo,
77
useRef,
88
useState,
9+
createContext,
10+
useContext,
911
} from "react";
10-
import { ItemRefList, useEnsuredRef, useResizeObserver } from "@react-md/utils";
12+
import { ItemRefList, useResizeObserver } from "@react-md/utils";
1113

1214
interface Options {
1315
style: React.CSSProperties | undefined;
@@ -27,12 +29,14 @@ interface CSSVariables {
2729
}
2830

2931
type CSSProperties = React.CSSProperties & CSSVariables;
32+
type UpdateIndicatorStyles = () => void;
3033

3134
type MergedTabRef = (instance: HTMLDivElement | null) => void;
3235
type ReturnValue = [
3336
CSSProperties,
3437
MergedTabRef,
35-
MutableRefObject<HTMLDivElement | null>
38+
MutableRefObject<HTMLDivElement | null>,
39+
UpdateIndicatorStyles
3640
];
3741

3842
const getActiveTab = (
@@ -42,6 +46,29 @@ const getActiveTab = (
4246
return (itemRefs[activeIndex] && itemRefs[activeIndex].current) || null;
4347
};
4448

49+
/**
50+
* @since 2.3.0
51+
* @private
52+
* @internal
53+
*/
54+
const context = createContext<UpdateIndicatorStyles>(() => {});
55+
56+
/**
57+
* @since 2.3.0
58+
* @private
59+
* @internal
60+
*/
61+
export const { Provider: UpdateIndicatorStylesProvider } = context;
62+
63+
/**
64+
* @since 2.3.0
65+
* @private
66+
* @internal
67+
*/
68+
export function useUpdateIndicatorStyles(): UpdateIndicatorStyles {
69+
return useContext(context);
70+
}
71+
4572
/**
4673
* This hook will merge the provided style object along with the required css
4774
* variables for the active tab underline moving to the correct location. The
@@ -96,30 +123,20 @@ export default function useTabIndicatorStyle({
96123
// will be incorrect for that.
97124
}, [activeIndex, itemRefs, updateCSSVars, align]);
98125

99-
const [ref, refHandler] = useEnsuredRef(propRef);
100-
useResizeObserver({
101-
target: ref,
102-
onResize() {
103-
// whenever the tabs container element is resized, it _probably_ means
104-
// that the tabs will be resized or moved. this means the indicator will
105-
// be in the wrong place so we need to fix it here.
106-
updateCSSVars(itemRefs, activeIndex);
107-
},
108-
});
126+
const updateStyles = useCallback(() => {
127+
updateCSSVars(itemRefs, activeIndex);
128+
}, [itemRefs, activeIndex, updateCSSVars]);
109129

110-
// TODO: Look into removing this resize observer. This is only required if
111-
// someone manually updates the width of the tab (dev utils) or if the width
112-
// was not changed due to the tabs container element resizing (iffy)
113-
useResizeObserver({
114-
target: () => getActiveTab(itemRefs, activeIndex),
115-
onResize() {
116-
updateCSSVars(itemRefs, activeIndex);
117-
},
130+
// whenever the tabs container element is resized, it _probably_ means
131+
// that the tabs will be resized or moved. this means the indicator will
132+
// be in the wrong place so we need to fix it here.
133+
const [tabsRef, tabsRefHandler] = useResizeObserver(updateStyles, {
134+
ref: propRef,
118135
});
119136

120137
const mergedStyle = useMemo(() => ({ ...style, ...cssVars }), [
121138
style,
122139
cssVars,
123140
]);
124-
return [mergedStyle, refHandler, ref];
141+
return [mergedStyle, tabsRefHandler, tabsRef, updateStyles];
125142
}

0 commit comments

Comments
 (0)