Skip to content

Commit

Permalink
feat(tabs): updated tabs to use the new resize observer API
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Sep 1, 2020
1 parent dc3f4df commit 052b3f2
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 59 deletions.
14 changes: 11 additions & 3 deletions packages/tabs/src/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
InteractionStatesOptions,
useInteractionStates,
} from "@react-md/states";
import { bem } from "@react-md/utils";
import { bem, useResizeObserver } from "@react-md/utils";
import { TabConfig } from "./types";
import { useUpdateIndicatorStyles } from "./useTabIndicatorStyle";

export interface TabProps
extends TabConfig,
Expand Down Expand Up @@ -64,7 +65,7 @@ const Tab = forwardRef<HTMLButtonElement, TabProps>(function Tab(
enablePressedAndRipple,
...props
},
ref
propRef
) {
const { ripples, className, handlers } = useInteractionStates({
handlers: props,
Expand All @@ -78,12 +79,19 @@ const Tab = forwardRef<HTMLButtonElement, TabProps>(function Tab(
rippleContainerClassName,
enablePressedAndRipple,
});
// TODO: Look into removing this resize observer. This is only required if
// someone manually updates the width of the tab (dev utils) or if the width
// was not changed due to the tabs container element resizing (iffy)
const updateIndicatorStyles = useUpdateIndicatorStyles();
const [, refHandler] = useResizeObserver(updateIndicatorStyles, {
ref: propRef,
});

return (
<button
{...props}
{...handlers}
ref={ref}
ref={active ? refHandler : propRef}
aria-selected={active}
aria-controls={panelId}
type="button"
Expand Down
79 changes: 44 additions & 35 deletions packages/tabs/src/TabsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import cn from "classnames";
import { applyRef, bem, useIsUserInteractionMode } from "@react-md/utils";

import { TabsConfig } from "./types";
import useTabIndicatorStyle from "./useTabIndicatorStyle";
import useTabIndicatorStyle, {
UpdateIndicatorStylesProvider,
} from "./useTabIndicatorStyle";
import useTabsMovement from "./useTabsMovement";

export interface TabsListProps
Expand Down Expand Up @@ -78,7 +80,12 @@ const TabsList = forwardRef<HTMLDivElement, TabsListProps>(function TabsList(
onActiveIndexChange,
automatic,
});
const [mergedStyle, ref, tabsRef] = useTabIndicatorStyle({
const [
mergedStyle,
tabsRefHandler,
tabsRef,
updateIndicatorStyles,
] = useTabIndicatorStyle({
style,
ref: forwardedRef,
align,
Expand Down Expand Up @@ -113,41 +120,43 @@ const TabsList = forwardRef<HTMLDivElement, TabsListProps>(function TabsList(
}, [activeIndex]);

return (
<div
{...props}
aria-orientation={orientation}
style={mergedStyle}
role="tablist"
className={cn(
block({
[align]: true,
padded,
vertical: !horizontal,
animate: !disableTransition && (!automatic || !isKeyboard),
}),
className
)}
ref={ref}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
{Children.map(tabs, (child, i) => {
if (!isValidElement(child)) {
return child;
}
<UpdateIndicatorStylesProvider value={updateIndicatorStyles}>
<div
{...props}
aria-orientation={orientation}
style={mergedStyle}
role="tablist"
className={cn(
block({
[align]: true,
padded,
vertical: !horizontal,
animate: !disableTransition && (!automatic || !isKeyboard),
}),
className
)}
ref={tabsRefHandler}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
{Children.map(tabs, (child, i) => {
if (!isValidElement(child)) {
return child;
}

const tab = Children.only(child);
let ref: Ref<HTMLElement> = itemRefs[i];
if (tab.props.ref) {
ref = (instance: HTMLElement | null) => {
itemRefs[i].current = instance;
applyRef(instance, tab.props.ref);
};
}
const tab = Children.only(child);
let ref: Ref<HTMLElement> = itemRefs[i];
if (tab.props.ref) {
ref = (instance: HTMLElement | null) => {
itemRefs[i].current = instance;
applyRef(instance, tab.props.ref);
};
}

return cloneElement(tab, { ref });
})}
</div>
return cloneElement(tab, { ref });
})}
</div>
</UpdateIndicatorStylesProvider>
);
});

Expand Down
59 changes: 38 additions & 21 deletions packages/tabs/src/useTabIndicatorStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
useMemo,
useRef,
useState,
createContext,
useContext,
} from "react";
import { ItemRefList, useEnsuredRef, useResizeObserver } from "@react-md/utils";
import { ItemRefList, useResizeObserver } from "@react-md/utils";

interface Options {
style: React.CSSProperties | undefined;
Expand All @@ -27,12 +29,14 @@ interface CSSVariables {
}

type CSSProperties = React.CSSProperties & CSSVariables;
type UpdateIndicatorStyles = () => void;

type MergedTabRef = (instance: HTMLDivElement | null) => void;
type ReturnValue = [
CSSProperties,
MergedTabRef,
MutableRefObject<HTMLDivElement | null>
MutableRefObject<HTMLDivElement | null>,
UpdateIndicatorStyles
];

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

/**
* @since 2.3.0
* @private
* @internal
*/
const context = createContext<UpdateIndicatorStyles>(() => {});

/**
* @since 2.3.0
* @private
* @internal
*/
export const { Provider: UpdateIndicatorStylesProvider } = context;

/**
* @since 2.3.0
* @private
* @internal
*/
export function useUpdateIndicatorStyles(): UpdateIndicatorStyles {
return useContext(context);
}

/**
* This hook will merge the provided style object along with the required css
* variables for the active tab underline moving to the correct location. The
Expand Down Expand Up @@ -96,30 +123,20 @@ export default function useTabIndicatorStyle({
// will be incorrect for that.
}, [activeIndex, itemRefs, updateCSSVars, align]);

const [ref, refHandler] = useEnsuredRef(propRef);
useResizeObserver({
target: ref,
onResize() {
// whenever the tabs container element is resized, it _probably_ means
// that the tabs will be resized or moved. this means the indicator will
// be in the wrong place so we need to fix it here.
updateCSSVars(itemRefs, activeIndex);
},
});
const updateStyles = useCallback(() => {
updateCSSVars(itemRefs, activeIndex);
}, [itemRefs, activeIndex, updateCSSVars]);

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

const mergedStyle = useMemo(() => ({ ...style, ...cssVars }), [
style,
cssVars,
]);
return [mergedStyle, refHandler, ref];
return [mergedStyle, tabsRefHandler, tabsRef, updateStyles];
}

0 comments on commit 052b3f2

Please sign in to comment.