Skip to content

Commit

Permalink
[Tabs] Fix and improve visibility of tab scroll buttons using the Int…
Browse files Browse the repository at this point in the history
…ersectionObserver API (#36071)

Signed-off-by: SaidMarar <mararsaid@gmail.com>
Co-authored-by: ZeeshanTamboli <zeeshan.tamboli@gmail.com>
  • Loading branch information
SaidMarar and ZeeshanTamboli committed Jul 23, 2023
1 parent 5a64f5f commit 569a43e
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 153 deletions.
96 changes: 58 additions & 38 deletions packages/mui-material/src/Tabs/Tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,9 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {

const [mounted, setMounted] = React.useState(false);
const [indicatorStyle, setIndicatorStyle] = React.useState(defaultIndicatorStyle);
const [displayScroll, setDisplayScroll] = React.useState({
start: false,
end: false,
});
const [displayStartScroll, setDisplayStartScroll] = React.useState(false);
const [displayEndScroll, setDisplayEndScroll] = React.useState(false);
const [updateScrollObserver, setUpdateScrollObserver] = React.useState(false);

const [scrollerStyle, setScrollerStyle] = React.useState({
overflow: 'hidden',
Expand Down Expand Up @@ -508,7 +507,7 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
/>
) : null;

const scrollButtonsActive = displayScroll.start || displayScroll.end;
const scrollButtonsActive = displayStartScroll || displayEndScroll;
const showScrollButtons =
scrollable && ((scrollButtons === 'auto' && scrollButtonsActive) || scrollButtons === true);

Expand All @@ -519,7 +518,7 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
orientation={orientation}
direction={isRtl ? 'right' : 'left'}
onClick={handleStartScrollClick}
disabled={!displayScroll.start}
disabled={!displayStartScroll}
{...TabScrollButtonProps}
className={clsx(classes.scrollButtons, TabScrollButtonProps.className)}
/>
Expand All @@ -534,7 +533,7 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
orientation={orientation}
direction={isRtl ? 'left' : 'right'}
onClick={handleEndScrollClick}
disabled={!displayScroll.end}
disabled={!displayEndScroll}
{...TabScrollButtonProps}
className={clsx(classes.scrollButtons, TabScrollButtonProps.className)}
/>
Expand Down Expand Up @@ -563,23 +562,7 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {

const updateScrollButtonState = useEventCallback(() => {
if (scrollable && scrollButtons !== false) {
const { scrollTop, scrollHeight, clientHeight, scrollWidth, clientWidth } = tabsRef.current;
let showStartScroll;
let showEndScroll;

if (vertical) {
showStartScroll = scrollTop > 1;
showEndScroll = scrollTop < scrollHeight - clientHeight - 1;
} else {
const scrollLeft = getNormalizedScrollLeft(tabsRef.current, theme.direction);
// use 1 for the potential rounding error with browser zooms.
showStartScroll = isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
showEndScroll = !isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
}

if (showStartScroll !== displayScroll.start || showEndScroll !== displayScroll.end) {
setDisplayScroll({ start: showStartScroll, end: showEndScroll });
}
setUpdateScrollObserver(!updateScrollObserver);
}
});

Expand All @@ -593,7 +576,6 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
// replaced by Suspense with a fallback, once React is updated to version 18
if (tabsRef.current) {
updateIndicatorState();
updateScrollButtonState();
}
});
const win = ownerWindow(tabsRef.current);
Expand All @@ -615,29 +597,68 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
resizeObserver.disconnect();
}
};
}, [updateIndicatorState, updateScrollButtonState]);

const handleTabsScroll = React.useMemo(
() =>
debounce(() => {
updateScrollButtonState();
}),
[updateScrollButtonState],
);
}, [updateIndicatorState]);

/**
* Toggle visibility of start and end scroll buttons
* Using IntersectionObserver on first and last Tabs.
*/
React.useEffect(() => {
let firstObserver;
let lastObserver;
const tabListChildren = Array.from(tabListRef.current.children);
const length = tabListChildren.length;
const firstTab = tabListChildren[0];
const lastTab = tabListChildren[length - 1];
const threshold = 0.99;
const observerOptions = {
root: tabsRef.current,
threshold,
};

const handleScrollButtonStart = (entries) => {
let display = false;
entries.forEach(({ isIntersecting }) => {
display = !isIntersecting;
});
setDisplayStartScroll(display);
};

const handleScrollButtonEnd = (entries) => {
let display = false;
entries.forEach(({ isIntersecting }) => {
display = !isIntersecting;
});
setDisplayEndScroll(display);
};

if (
typeof IntersectionObserver !== 'undefined' &&
length > 0 &&
scrollable &&
scrollButtons !== false
) {
firstObserver = new IntersectionObserver(handleScrollButtonStart, observerOptions);
firstObserver.observe(firstTab);

if (length > 1) {
lastObserver = new IntersectionObserver(handleScrollButtonEnd, observerOptions);
lastObserver.observe(lastTab);
}
}

return () => {
handleTabsScroll.clear();
firstObserver?.disconnect();
lastObserver?.disconnect();
};
}, [handleTabsScroll]);
}, [scrollable, scrollButtons, updateScrollObserver, childrenProp?.length]);

React.useEffect(() => {
setMounted(true);
}, []);

React.useEffect(() => {
updateIndicatorState();
updateScrollButtonState();
});

React.useEffect(() => {
Expand Down Expand Up @@ -763,7 +784,6 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
: -scrollerStyle.scrollbarWidth,
}}
ref={tabsRef}
onScroll={handleTabsScroll}
>
{/* The tablist isn't interactive but the tabs are */}
<FlexContainer
Expand Down

0 comments on commit 569a43e

Please sign in to comment.