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] Fix and improve visibility of tab scroll buttons using the IntersectionObserver API #36071

Merged
merged 25 commits into from
Jul 23, 2023

Conversation

SaidMarar
Copy link
Contributor

@SaidMarar SaidMarar commented Feb 5, 2023

Fixes the issues below:
Fixes #31936
Fixes #19673
Fixes #33349
Fixes #23711
Fixes #38035

This fix handle visibility of scroll buttons specially in dynamic content behavior and also it reduces the complexity of the code (calculation logic).

before: https://codesandbox.io/s/interesting-before-b3kdvn?file=/src/App.js
after: https://codesandbox.io/s/interesting-after-zf7ryz

https://deploy-preview-36071--material-ui.netlify.app/material-ui/react-tabs/

@mui-bot
Copy link

mui-bot commented Feb 5, 2023

Netlify deploy preview

https://deploy-preview-36071--material-ui.netlify.app/

Bundle size report

Details of bundle changes (Toolpad)
Details of bundle changes

Generated by 🚫 dangerJS against ca7b7b2

@SaidMarar SaidMarar marked this pull request as draft February 6, 2023 01:35
@oliviertassinari oliviertassinari added the component: tabs This is the name of the generic UI component, not the React module! label Feb 9, 2023
@SaidMarar
Copy link
Contributor Author

I've implemented a new method to handle the visibility of scroll buttons, using the IntersectionObserver. Essentially, it allows us to detect when a particular element (in this case, a tab) is in view on the screen.

With this new method, we're observing only the first and last tabs. If either the first or last tab is partially visible or completely invisible, we'll show the appropriate scroll button. For example, if the first tab is only partially visible,we'll show the scroll button that allows the user to scroll to the left and reveal more of the first tab. If the last tab is completely invisible, we'll show the scroll button that allows the user to scroll to the right and reveal the last tab.

By using the IntersectionObserver to only observe the first and last tabs, we're optimizing our code and reducing unnecessary calculations especially in dynamic content scenarios. This new method, the code should be easier to read and maintain.

@SaidMarar SaidMarar marked this pull request as ready for review February 14, 2023 23:05
@zannager zannager requested a review from mnajdova February 15, 2023 08:29
@SaidMarar
Copy link
Contributor Author

Hi @mnajdova if you have a moment, could you kindly take a look and review this PR please.

@JoinerDev
Copy link

JoinerDev commented Mar 23, 2023

@mnajdova @SaidMarar Are there any updates on this issue?

@SaidMarar
Copy link
Contributor Author

@mnajdova @SaidMarar Are there any updates on this issue?

@JoinerDev the PR needs only to be reviewed, so we can advance.

Copy link
Member

@ZeeshanTamboli ZeeshanTamboli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SaidMarar Thanks for the PR.

In the demos, when scroll you to the extreme right (ITEM 7), the right scroll button is still available, whereas in production, it hides the scroll button.

Besides, I merged with the latest master branch for easier review.

Also, argos detected visual changes. What happens is that on the initial render, the tabs are in a different position, and then they shift. Argos captures the screenshot on the initial mount. You can reproduce it on the page refresh. This should not happen.

@ZeeshanTamboli ZeeshanTamboli added the bug 🐛 Something doesn't work label May 3, 2023
@SaidMarar
Copy link
Contributor Author

SaidMarar commented Jun 24, 2023

Hello @ZeeshanTamboli , Thank you a lot for your review 😄

  1. For the first issue you mentioned in the demos, i don't reproduce it with the latest changes, i have the same behavior as the one in prod. Please check the screenrecords below:

    • Dev:

      dev

    • Prod:

      prod

  2. For the second issue in Argos, i just fixed it by removing debounce on both observers, because it causes a delay on render for the buttons.

@SaidMarar SaidMarar requested a review from ZeeshanTamboli June 25, 2023 13:22
@SaidMarar
Copy link
Contributor Author

SaidMarar commented Jun 26, 2023

PR is ready, @ZeeshanTamboli review is welcome 😃

@ZeeshanTamboli
Copy link
Member

ZeeshanTamboli commented Jun 30, 2023

For the first issue you mentioned in the demos, i don't reproduce it with the latest changes, i have the same behavior as the one in prod.

I can still see it. Can you tell me your operating system, browser, and its version? I'm using Windows 10 and Chrome Version 114.0.5735.199 (64-bit). I am also seeing it on Firefox.

@SaidMarar
Copy link
Contributor Author

SaidMarar commented Jun 30, 2023

@ZeeshanTamboli Could you please post your resolutions, my environment test on Ubuntu 22.04:

  1. Firefox 115.0 (64 bits), resolution 1920px 995px
  2. Chrome Version 114.0.5735.198 (Build officiel) (64 bits), resolution 1920px 995px

@ZeeshanTamboli
Copy link
Member

My screen resolution is 1920px 1080px.

@SaidMarar
Copy link
Contributor Author

@ZeeshanTamboli it works also on iPhone.

trim.43D8479F-1641-4A24-9465-EDD23FC097A6.MOV

@SaidMarar
Copy link
Contributor Author

@ZeeshanTamboli can you screenrecord,maybe it will help me to reproduce.

@ZeeshanTamboli
Copy link
Member

On Windows 10, Chrome Browser:

03.07.2023_18.44.36_REC.mp4

On Android Chrome:

Screenrecorder-2023-07-03-18-53-20-92.mp4

@SaidMarar
Copy link
Contributor Author

SaidMarar commented Jul 6, 2023

@ZeeshanTamboli When you reach the last element could you please print the getBoundingClientRect (value of the attribute right) of:

  1. The last element in tabs the button (item seven)
  2. The element with class MuiTabs-scroller

packages/mui-material/src/Tabs/Tabs.js Outdated Show resolved Hide resolved
packages/mui-material/src/Tabs/Tabs.test.js Outdated Show resolved Hide resolved
packages/mui-material/src/Tabs/Tabs.test.js Outdated Show resolved Hide resolved
packages/mui-material/src/Tabs/Tabs.test.js Outdated Show resolved Hide resolved
@SaidMarar
Copy link
Contributor Author

SaidMarar commented Jul 21, 2023

@ZeeshanTamboli if updateScrollButtons will remain accessible for developers who are relying on it, why we dont just change its old implementation to use IntersectionObserverAPI. The code will be simpler.

we create a state that is responsible to re-init the observer:

const [updateScrollObserver, setUpdateScrollObserver] = React.useState(false);

we change the updateScrollButtonState implementation to this:

  const updateScrollButtonState = useEventCallback(() =>
    setUpdateScrollObserver(!updateScrollObserver),
  );

Finally we just append the Observer useEffect dependencies:

[scrollable, scrollButtons, updateScrollObserver, childrenProp?.length];

What do you think ?

@ZeeshanTamboli
Copy link
Member

I think I understand what you mean. Whenever the updateScrollButtons action is triggered by the developer, you want to run the IntersectionObserver i.e the useEffect? Sounds like a good idea. Feel free to apply the changes. But when will you reset this scroll observer state?

@SaidMarar
Copy link
Contributor Author

@ZeeshanTamboli Yes the IntersectionObserver effect will run, when the developer triggers updateScrollButtons action.
Sorry, I don't understand what you mean by when i will reset the scroll observer state ?

@ZeeshanTamboli
Copy link
Member

Sorry, I don't understand what you mean by when i will reset the scroll observer state ?

Does the updateScrollObserver state, triggered by updateScrollButtons action, reset to false after the effect finishes running?

@SaidMarar
Copy link
Contributor Author

Does the updateScrollObserver state, triggered by updateScrollButtons action, reset to false after the effect finishes running?

No need to reset, because the updateScrollObserver state is used only to be toggled every time we call updateScrollButtons, ensuring the re-run of the effect, which means rebinding the IntersectionObserver to detect the visibility of buttons.

@ZeeshanTamboli ZeeshanTamboli added the package: material-ui Specific to @mui/material label Jul 23, 2023
Copy link
Member

@ZeeshanTamboli ZeeshanTamboli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SaidMarar It looks good! Thanks for your great contribution!

@ZeeshanTamboli ZeeshanTamboli changed the title [Tabs] Fix and improve visibility of scrollButtons using IntersectionObserver [Tabs] Fix and improve visibility of tab scroll buttons using the IntersectionObserver API Jul 23, 2023
@ZeeshanTamboli ZeeshanTamboli merged commit 569a43e into mui:master Jul 23, 2023
@SaidMarar
Copy link
Contributor Author

@SaidMarar It looks good! Thanks for your great contribution!

Thanks to you too for your assistance and help 👍🏻

const tabs = getAllByRole('tab');
const lastTab = tabs[tabs.length - 1];
fireEvent.click(lastTab);
await pause(400);
Copy link
Member

@oliviertassinari oliviertassinari Jul 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never use the real-time in these tests, to revert back to mocking time, for higher control and speed.

Suggested change
await pause(400);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't make the test cover that bug without this, but I'm aware of it. Do you have any alternative solutions by faking the time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same i tried requestAnimationFrame but it doesn't work so i did the pause(400)

Copy link
Contributor Author

@SaidMarar SaidMarar Jul 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oliviertassinari Another way to do it ?
image

I tried to simulate the unit test for the old implementation version 5.14.1, and i see that scrollButton is blinking when we delete a tab programmatically, it is not acting like a real user scenario. please see the codesandbox without the setTimeout you will not see the bug line 27.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look, my process was to: 1. revert all the test changes 2. focus on one failing test, 3. make it pass. Here is what I come up with:

oliviertassinari@76209fd#diff-1d60104297e2e5decebdb9cef7bdbf776affd72d75fa7f263d2d5e387358499aR673

We should be able to fix all the other tests like this. I removed the time mocking in this instance, as it wasn't relevant to the test (what we need to depend on is not time but rafs)

Could we fix the rest? Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what we need to depend on is not time but rafs

Is it because waitFor should be used only in JSDom, and requestAnimationFrame is used in the browser environment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what we need to depend on is not time but rafs

Is it because waitFor should be used only in JSDom, and requestAnimationFrame is used in the browser environment?

Other tests i used the raf and it worked, but for this specific unit test i don't know why we must delay the delete click.

Copy link
Contributor Author

@SaidMarar SaidMarar Jul 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oliviertassinari @ZeeshanTamboli
Created a PR for this #38208.

I revert to the old implementation of unit tests then I refacto + added the last unit that fails in old implementation of Tabs before IntersectionObserver.

The last unit test i bind event transitionend before delete a tab, i don't know if it is a good solution than the setimeout.

Comment on lines -512 to -538
it('should response to scroll events', function test() {
if (isJSDOM) {
this.skip();
}
const { container, forceUpdate, getByRole } = render(tabs);
const tablistContainer = getByRole('tablist').parentElement;

Object.defineProperty(tablistContainer, 'clientWidth', { value: 200 - 40 * 2 });
tablistContainer.scrollLeft = 10;
Object.defineProperty(tablistContainer, 'scrollWidth', { value: 216 });
Object.defineProperty(tablistContainer, 'getBoundingClientRect', {
value: () => ({
left: 0,
right: 50,
}),
});
forceUpdate();
clock.tick(1000);
expect(hasLeftScrollButton(container)).to.equal(true);
expect(hasRightScrollButton(container)).to.equal(true);
tablistContainer.scrollLeft = 0;
fireEvent.scroll(container.querySelector(`.${classes.scroller}.${classes.scrollableX}`));
clock.tick(166);

expect(hasLeftScrollButton(container)).to.equal(false);
expect(hasRightScrollButton(container)).to.equal(true);
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we lose test coverage for this?

Copy link
Member

@ZeeshanTamboli ZeeshanTamboli Jul 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scroll event is not being triggered now. Is it still relevant? #36071 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a PR for this #38208.

packages/mui-material/src/Tabs/Tabs.js Show resolved Hide resolved
packages/mui-material/src/Tabs/Tabs.js Show resolved Hide resolved
packages/mui-material/src/Tabs/Tabs.js Show resolved Hide resolved
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
Projects
None yet
5 participants