-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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
[Tabs] Fix and improve visibility of tab scroll buttons using the IntersectionObserver API #36071
Conversation
Netlify deploy previewhttps://deploy-preview-36071--material-ui.netlify.app/ Bundle size reportDetails of bundle changes (Toolpad) |
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. |
Hi @mnajdova if you have a moment, could you kindly take a look and review this PR please. |
@mnajdova @SaidMarar Are there any updates on this issue? |
@JoinerDev the PR needs only to be reviewed, so we can advance. |
There was a problem hiding this 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.
Hello @ZeeshanTamboli , Thank you a lot for your review 😄
|
PR is ready, @ZeeshanTamboli review is welcome 😃 |
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. |
@ZeeshanTamboli Could you please post your resolutions, my environment test on Ubuntu 22.04:
|
My screen resolution is 1920px 1080px. |
@ZeeshanTamboli it works also on iPhone. trim.43D8479F-1641-4A24-9465-EDD23FC097A6.MOV |
@ZeeshanTamboli can you screenrecord,maybe it will help me to reproduce. |
On Windows 10, Chrome Browser: 03.07.2023_18.44.36_REC.mp4On Android Chrome: Screenrecorder-2023-07-03-18-53-20-92.mp4 |
@ZeeshanTamboli When you reach the last element could you please print the getBoundingClientRect (value of the attribute right) of:
|
Co-authored-by: Zeeshan Tamboli <[email protected]> Signed-off-by: SaidMarar <[email protected]>
@ZeeshanTamboli if we create a state that is responsible to re-init the observer:
we change the updateScrollButtonState implementation to this:
Finally we just append the Observer useEffect dependencies:
What do you think ? |
I think I understand what you mean. Whenever the |
@ZeeshanTamboli Yes the IntersectionObserver effect will run, when the developer triggers |
Does the |
No need to reset, because the |
There was a problem hiding this 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!
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); |
There was a problem hiding this comment.
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.
await pause(400); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 ?
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, andrequestAnimationFrame
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.
There was a problem hiding this comment.
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.
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); | ||
}); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
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/