Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tabs] After removing tab scrollable mode is triggered (bad behavior of scrollable mode in case of dynamic tabs) #39394

Open
konrazem opened this issue Oct 11, 2023 · 1 comment · Fixed by #39415
Assignees
Labels
bug 🐛 Something doesn't work component: tabs This is the name of the generic UI component, not the React module! package: material-ui Specific to @mui/material regression A bug, but worse

Comments

@konrazem
Copy link

konrazem commented Oct 11, 2023

Steps to reproduce

Link to live example: (required)
https://codesandbox.io/p/sandbox/frosty-snow-tlq78l?file=%2Fpackage.json%3A6%2C32

Steps:

  1. Remove "Item one"
    image

  2. After remove there is scrollable mode with arrow on the right
    image

Current behavior

After removing last tab, scrollable mode is trigger when there is no need

Expected behavior

After remove tab there should be no arrows for scroll as width of tabs is smaller then width of the wrapper component.

Context

It looks like a BUG as arrow is suddenly shown at the end of tabs toolbar.

Your environment

npx @mui/envinfo
  Don't forget to mention which browser you used.
  Output from `npx @mui/envinfo` goes here.

Search keywords: tabs, scrollable
Order ID: 46820

@konrazem konrazem added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Oct 11, 2023
@noraleonte noraleonte transferred this issue from mui/mui-x Oct 11, 2023
@zannager zannager added the component: tabs This is the name of the generic UI component, not the React module! label Oct 11, 2023
@brijeshb42 brijeshb42 added bug 🐛 Something doesn't work package: material-ui Specific to @mui/material labels Oct 12, 2023
@brijeshb42 brijeshb42 assigned brijeshb42 and unassigned mnajdova Oct 12, 2023
brijeshb42 added a commit to brijeshb42/material-ui that referenced this issue Oct 12, 2023
@ZeeshanTamboli ZeeshanTamboli removed the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Oct 14, 2023
brijeshb42 added a commit to brijeshb42/material-ui that referenced this issue Oct 17, 2023
@oliviertassinari oliviertassinari added the regression A bug, but worse label Nov 22, 2023
@oliviertassinari
Copy link
Member

oliviertassinari commented Nov 22, 2023

This is a regression, it used to work correctly in v5.14.1 and got broken in v5.14.2. #36071 or #38133 broke this.


I'm reopening as #39415 doesn't include a regression test, the real fix is the regression test IMHO. Now, since this is a regression, and we will need to move that logic to Base UI, I think we definitely need a test.


As for the fix, it seems that the origin is the intersection observer, a minimal reproduction: https://codesandbox.io/p/sandbox/inspiring-star-hc3vns?file=%2Fsrc%2FApp.js%3A16%2C9. So I think we can solve this with simply ignoring elements that are from a previous render:

diff --git a/packages/mui-material/src/Tabs/Tabs.js b/packages/mui-material/src/Tabs/Tabs.js
index d4d41ef720..904d187e1e 100644
--- a/packages/mui-material/src/Tabs/Tabs.js
+++ b/packages/mui-material/src/Tabs/Tabs.js
@@ -619,13 +619,16 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
       };

       const handleScrollButtonStart = (entries) => {
-        setDisplayStartScroll(!entries[0].isIntersecting);
+        const isInDom = entries[0].target.parentElement !== null;
+        console.log('entries[0].isIntersecting', entries[0].isIntersecting)
+        setDisplayStartScroll(isInDom && !entries[0].isIntersecting);
       };
       const firstObserver = new IntersectionObserver(handleScrollButtonStart, observerOptions);
       firstObserver.observe(firstTab);

       const handleScrollButtonEnd = (entries) => {
-        setDisplayEndScroll(!entries[0].isIntersecting);
+        const isInDom = entries[0].target.parentElement !== null;
+        setDisplayEndScroll(isInDom && !entries[0].isIntersecting);
       };
       const lastObserver = new IntersectionObserver(handleScrollButtonEnd, observerOptions);
       lastObserver.observe(lastTab);

simpler fix.

But then, this is still hiding the root problem, we shouldn't observe an element that is unmounted. This fix makes more sense to me:

diff --git a/packages/mui-material/src/Tabs/Tabs.js b/packages/mui-material/src/Tabs/Tabs.js
index d4d41ef720..b44225cf58 100644
--- a/packages/mui-material/src/Tabs/Tabs.js
+++ b/packages/mui-material/src/Tabs/Tabs.js
@@ -17,6 +17,7 @@ import useEventCallback from '../utils/useEventCallback';
 import tabsClasses, { getTabsUtilityClass } from './tabsClasses';
 import ownerDocument from '../utils/ownerDocument';
 import ownerWindow from '../utils/ownerWindow';
+import useEnhancedEffect from '../utils/useEnhancedEffect';

 const nextItem = (list, item) => {
   if (list === item) {
@@ -597,48 +598,6 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
     };
   }, [updateIndicatorState]);

-  /**
-   * Toggle visibility of start and end scroll buttons
-   * Using IntersectionObserver on first and last Tabs.
-   */
-  React.useEffect(() => {
-    const tabListChildren = Array.from(tabListRef.current.children);
-    const length = tabListChildren.length;
-
-    if (
-      typeof IntersectionObserver !== 'undefined' &&
-      length > 0 &&
-      scrollable &&
-      scrollButtons !== false
-    ) {
-      const firstTab = tabListChildren[0];
-      const lastTab = tabListChildren[length - 1];
-      const observerOptions = {
-        root: tabsRef.current,
-        threshold: 0.99,
-      };
-
-      const handleScrollButtonStart = (entries) => {
-        setDisplayStartScroll(!entries[0].isIntersecting);
-      };
-      const firstObserver = new IntersectionObserver(handleScrollButtonStart, observerOptions);
-      firstObserver.observe(firstTab);
-
-      const handleScrollButtonEnd = (entries) => {
-        setDisplayEndScroll(!entries[0].isIntersecting);
-      };
-      const lastObserver = new IntersectionObserver(handleScrollButtonEnd, observerOptions);
-      lastObserver.observe(lastTab);
-
-      return () => {
-        firstObserver.disconnect();
-        lastObserver.disconnect();
-      };
-    }
-
-    return undefined;
-  }, [scrollable, scrollButtons, updateScrollObserver, childrenProp?.length]);
-
   React.useEffect(() => {
     setMounted(true);
   }, []);
@@ -707,6 +666,48 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) {
     });
   });

+  /**
+   * Toggle visibility of start and end scroll buttons
+   * Using IntersectionObserver on first and last Tabs.
+   */
+  useEnhancedEffect(() => {
+    const tabListChildren = Array.from(tabListRef.current.children);
+    const length = tabListChildren.length;
+
+    if (
+      typeof IntersectionObserver !== 'undefined' &&
+      length > 0 &&
+      scrollable &&
+      scrollButtons !== false
+    ) {
+      const firstTab = tabListChildren[0];
+      const lastTab = tabListChildren[length - 1];
+      const observerOptions = {
+        root: tabsRef.current,
+        threshold: 0.99,
+      };
+
+      const handleScrollButtonStart = (entries) => {
+        setDisplayStartScroll(!entries[0].isIntersecting);
+      };
+      const firstObserver = new IntersectionObserver(handleScrollButtonStart, observerOptions);
+      firstObserver.observe(firstTab);
+
+      const handleScrollButtonEnd = (entries) => {
+        setDisplayEndScroll(!entries[0].isIntersecting);
+      };
+      const lastObserver = new IntersectionObserver(handleScrollButtonEnd, observerOptions);
+      lastObserver.observe(lastTab);
+
+      return () => {
+        firstObserver.disconnect();
+        lastObserver.disconnect();
+      };
+    }
+
+    return undefined;
+  }, [scrollable, scrollButtons, updateScrollObserver, children.length]);
+
   const handleKeyDown = (event) => {
     const list = tabListRef.current;
     const currentFocus = ownerDocument(list).activeElement;

Potentially, the effect could be on children instead of children.length.

@oliviertassinari oliviertassinari changed the title After removing tab scrollable mode is triggered (bad behavior of scrollable mode in case of dynamic tabs) [tabs] After removing tab scrollable mode is triggered (bad behavior of scrollable mode in case of dynamic tabs) Nov 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something doesn't work component: tabs This is the name of the generic UI component, not the React module! package: material-ui Specific to @mui/material regression A bug, but worse
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants