-
Notifications
You must be signed in to change notification settings - Fork 8
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
base: main
Are you sure you want to change the base?
Scrollable tabs #2440
Conversation
tabindex="-1" | ||
@click="${x => x.onScrollLeftClick()}" | ||
${ref('leftScrollButton')} | ||
> |
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.
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.
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 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.
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 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.
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.
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).
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.
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/storybook/src/nimble/anchor-tabs/anchor-tabs-matrix.stories.ts
Outdated
Show resolved
Hide resolved
/** | ||
* @internal | ||
*/ | ||
public readonly leftScrollButton!: Element; |
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.
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.", |
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 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' }); |
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.
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.
packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.spec.ts
Outdated
Show resolved
Hide resolved
} | ||
}); | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public activeidChanged(_oldValue: string, _newValue: string): void { |
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.
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.
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.
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
.
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.
For the anchor tabs too? That wasn't the behavior I saw in Edge.
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 ofgrid
(the original use ofgrid
wasn't clear to me).ResizeObserver
vsIntersectionObserver
I currently have opted to use a
ResizeObserver
for controlling when to show/hide the scroll buttons. The reason for this is two-fold:contentRect
for the element containing the tabs changes. This will occur if theTabs
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.Two different approaches were used/attempted with the
IntersectionObserver
. The first involved observing eachTab
element. While this worked, the implementation was cumbersome and harder to reason about, as it involved extra state that theTabs
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, theIntersectionObserver
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