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

Scrollable tabs #2440

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open

Scrollable tabs #2440

wants to merge 25 commits into from

Conversation

atmgrifter00
Copy link
Contributor

@atmgrifter00 atmgrifter00 commented Oct 16, 2024

Pull Request

🤨 Rationale

👩‍💻 Implementation

Adding buttons to template which will show/hide based on width of the container for the tabs either exceeding visible area or not. Layout of tabs is now using flex instead of grid (the original use of grid wasn't clear to me).

ResizeObserver vs IntersectionObserver

I currently have opted to use a ResizeObserver for controlling when to show/hide the scroll buttons. The reason for this is two-fold:

  1. The observer only fires when the contentRect for the element containing the tabs changes. This will occur if the Tabs component changes its width, when tabs or added or removed (if all tabs are visible), or when the content of a tab is changed (when all tabs are visible), all of which are valid conditions to re-evaluate whether we need to show or hide the scroll buttons.
  2. The implementation is easy to reason about, and there is no extra state necessary.

Two different approaches were used/attempted with the IntersectionObserver. The first involved observing each Tab element. While this worked, the implementation was cumbersome and harder to reason about, as it involved extra state that the Tabs component had to maintain (a map that tracked which tabs were currently visible or not), and "calculating" the visible condition from the state kept in the map. This was required to handle cases such as removing the last tab, where it was only partially visible (and thus was currently showing the scroll buttons), which, when removed, the IntersectionObserver would only report a change for the removed tab, but the visibility should still be updated.

The other approach attempted was trying to simply observe the container element of the tabs (whose root was simply its containing element), which wouldn't have required any extra state. However, this requires the observer to fire both when the container element becomes partially visible and when it becomes fully visible. Supplying thresholds of [0, 1] (one pixel and 100% visible), or only 0, did not result in the needed behavior. It would simply fire once, and then never again.

🧪 Testing

Added unit tests, and matrix tests.

✅ Checklist

  • I have updated the project documentation to reflect my changes or determined no changes are needed.

packages/nimble-components/src/tabs/template.ts Outdated Show resolved Hide resolved
tabindex="-1"
@click="${x => x.onScrollLeftClick()}"
${ref('leftScrollButton')}
>
Copy link
Contributor

Choose a reason for hiding this comment

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

Both of these buttons should have text content for the accessible name, e.g. "scroll tabs left". This also mean that we need to add those strings to the core label provider and create design tokens for them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suppose there could be some debate on this. The buttons are not reachable via the keyboard, and arguably we could/should opt to simply set aria-hidden="true". Having a tooltip on hover for these buttons doesn't seem that helpful, and accidentally pressing a button is ultimately innocuous.

Copy link
Contributor

Choose a reason for hiding this comment

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

I considered that. A focusable element must not have aria-hidden="true", but the converse is not true, i.e. just because an element isn't focusable doesn't mean it should be hidden from accessibility. From what I was reading, unless the element is purely decorative or would be redundant if announced by a screen reader, we shouldn't hide it. But I'm no expert, so I'm curious what others think, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the very least I would think whatever decision we arrived at for these buttons would apply to things like the inc/dec buttons on the number field (which do not provide text content).

Copy link
Contributor

@m-akinc m-akinc Oct 17, 2024

Choose a reason for hiding this comment

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

Sir, I refute you.

...Hmm.. maybe not. Apparently we are also setting aria-hidden="true" on them. 🤔Now I kind of remember us deciding they shouldn't be exposed to accessibility, so I'm not sure why we added localizable labels.

packages/nimble-components/src/tabs/styles.ts Outdated Show resolved Hide resolved
packages/nimble-components/src/tabs/styles.ts Outdated Show resolved Hide resolved
packages/nimble-components/src/tabs/index.ts Outdated Show resolved Hide resolved
packages/nimble-components/src/tabs/tests/tabs.spec.ts Outdated Show resolved Hide resolved
packages/nimble-components/src/tabs/tests/tabs.spec.ts Outdated Show resolved Hide resolved
@m-akinc m-akinc marked this pull request as ready for review October 17, 2024 21:40
/**
* @internal
*/
public readonly leftScrollButton!: Element;
Copy link
Contributor

Choose a reason for hiding this comment

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

leftScrollButton shouldn't be defined as not-null (!) when it is conditionally shown in the template. Same for the regular (non-anchor) tabs.

@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Scrollable tabs for Tabs and AnchorTabs.",
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't debug the issue, but playing around with the Tabs story in storybook, there seems to be an issue related to the check for when the scroll buttons can be removed.

Steps to reproduce:

  • Make the nimble-tabs have a fixed width of 200px, which makes the scroll buttons appear
  • In the DOM, rename the first tab from "Tab One" to "1"
  • In the DOM, rename the second tab from "Tab Two" to "2"
  • Notice the scroll buttons are still visible
  • In the DOM, rename the third tab from "Tab Three" to "3"
  • Notice the scroll buttons disappear
  • In the DOM, rename the third tab back to "Tab Three"
  • Notice the scroll buttons don't reappear even though they were previously shown for the same 3 tab names

/**
* @internal
*/
public activeidChanged(_oldValue: string, _newValue: string): void {
if (this.$fastController.isConnected) {
this.setTabs();
this.activetab?.scrollIntoView({ block: 'nearest' });
Copy link
Contributor

Choose a reason for hiding this comment

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

If you have a long tab name, scrolling into view from the right (i.e. clicking the left arrow button) feels odd because only the end of the tab name becomes visible. I think if you used inline: 'start' here, you might get more intuitive behavior.

}
});
}

/**
* @internal
*/
public activeidChanged(_oldValue: string, _newValue: string): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

For the anchor tabs, would it make more sense to scroll a tab into view when it is navigated to rather than waiting for the activeid to change? As is, you can't always see what tab you're going to activate when you press Enter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, navigating will already bring the tab into view (via arrow keys). All this implementation does is bring the tab into view via a programmatic change to activeid.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the anchor tabs too? That wasn't the behavior I saw in Edge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants