-
Notifications
You must be signed in to change notification settings - Fork 22
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
APP-7268: Add nav dropdown #615
base: main
Are you sure you want to change the base?
Conversation
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.
Additional notes from in-person
} else if (activeIndex >= 0) { | ||
const selectedOption = options[activeIndex]; | ||
if (selectedOption) { | ||
window.location.href = selectedOption.href; |
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 think my confusion around using app/navigation stemmed from trying to navigate to the appropriate href when someone hit the enter key. Is window.location appropriate here?
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 think this is on the right track, but the short version is:
- We should minimize or eliminate any navigation via JavaScript
- If we do need to navigate, we should pass
navigate
in as a prop so we don't accidentally bail out of an app's client-side navigation
The key here is to make sure that we hook into native browser behaviors as much as possible and only add keydown
handlers for stuff that's extra. I've just spent some time experimenting, reading through MDN, and reading through the navigation menubar accessibility spec, and this is what I've seen:
Native browser behaviors:
Enter
- Button: trigger click handler
- Link: follow link and trigger click handler
- Conclusion: we don't need any
Enter
handlers
Space
- Button: trigger click handler
- Link: nothing
- Conclusion: we might want add a
Space
handler on<a>
elements - After some research, trying to navigate on
Space
will be tricky so we probably just want to skip it
Escape
- No native behaviors related to
<button>
nor<a>
- No a11y needed for
<button>
- if menu is open, focus is on one of the link elements, not the button - Need to close the menu if focus is on an
<a>
and Escape is pressed - Conclusion: we need an
Escape
handler on the link elements
- No native behaviors related to
- Arrow keys
- No native behaviors related to
<button>
nor<a>
- No a11y behaviors needed for
<button>
- if menu is open, focus is on one of the link elements, not the button - Need to move focus between
<a>
elements onArrowDown
,ArrowUp
- Conclusion: we need
ArrowUp/Down
handlers on the<a>
elements to move focus
- No native behaviors related to
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.
Very happy with this PR in terms of the visuals and semantics! Keyboard navigation isn't in an accessible state yet and is going to take some more work and experimentation. Rather than drag this PR out, let's punt on the keyboard interactions so we can merge the stuff that's working well
Keyboard nav in these sorts of components is not an easy thing to get "right;" all of our existing components have one deficiency or another in this department.
With the structure set and tests in place, it'll be easier to followup with keyboard interactions in isolation. Plus, in the intermediate state, the menu will be keyboard navigable with tab + enter. This does not follow the menubar a11y recommendations, but is better than nothing!
} else if (activeIndex >= 0) { | ||
const selectedOption = options[activeIndex]; | ||
if (selectedOption) { | ||
window.location.href = selectedOption.href; |
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 think this is on the right track, but the short version is:
- We should minimize or eliminate any navigation via JavaScript
- If we do need to navigate, we should pass
navigate
in as a prop so we don't accidentally bail out of an app's client-side navigation
The key here is to make sure that we hook into native browser behaviors as much as possible and only add keydown
handlers for stuff that's extra. I've just spent some time experimenting, reading through MDN, and reading through the navigation menubar accessibility spec, and this is what I've seen:
Native browser behaviors:
Enter
- Button: trigger click handler
- Link: follow link and trigger click handler
- Conclusion: we don't need any
Enter
handlers
Space
- Button: trigger click handler
- Link: nothing
- Conclusion: we might want add a
Space
handler on<a>
elements - After some research, trying to navigate on
Space
will be tricky so we probably just want to skip it
Escape
- No native behaviors related to
<button>
nor<a>
- No a11y needed for
<button>
- if menu is open, focus is on one of the link elements, not the button - Need to close the menu if focus is on an
<a>
and Escape is pressed - Conclusion: we need an
Escape
handler on the link elements
- No native behaviors related to
- Arrow keys
- No native behaviors related to
<button>
nor<a>
- No a11y behaviors needed for
<button>
- if menu is open, focus is on one of the link elements, not the button - Need to move focus between
<a>
elements onArrowDown
,ArrowUp
- Conclusion: we need
ArrowUp/Down
handlers on the<a>
elements to move focus
- No native behaviors related to
class="relative flex items-center px-2 py-1.5 hover:bg-gray-1" | ||
class:bg-gray-1={i === activeIndex} | ||
role="menuitem" | ||
aria-current={i === activeIndex ? 'page' : 'false'} |
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.
issue: this looks like it confuses "active" and "current"
- active - which link is currently highlighted as the user keyboard navigates through
- current - which link matches the current URL
I think something like this is what we need
aria-current={i === activeIndex ? 'page' : 'false'} | |
aria-current={href === selectedHref ? 'page' : 'false'} |
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.
Whoops, thanks for the correction
<a | ||
{href} | ||
class="relative flex items-center px-2 py-1.5 hover:bg-gray-1" | ||
class:bg-gray-1={i === activeIndex} |
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.
suggestion (keyboard): Should we move this styling to a focus:bg-gray-1
instead? That way, once we set up focus via keyboard events properly, it will all work
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.
Updated
export let selectedHref: string; | ||
|
||
let isOpen = false; | ||
let activeIndex = -1; |
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.
issue (keyboard): when isOpen
becomes true, the WAI example moves focus move automatically to the first item. Should we make sure we set activeIndex
to 0
when isOpen
becomes 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.
Updated, makes sense, ty!
expect(screen.getByRole('menu')).toBeInTheDocument(); | ||
}); | ||
|
||
it('navigates options with arrow keys', async () => { |
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.
issue (keyboard): we'll to make sure this test tracks focus state and tabindex
to ensure the different <a>
elements receive focus as the user presses the arrow keys
For context, there are a few strategies one can use to deal with the "active" item when keyboard navigating through a UI like this. The WAI calls out two strategies:
- Managing Focus Within Components Using a Roving tabindex
- Easier option
- Sticks to browser defaults more closely
- Managing Focus in Composites Using
aria-activedescendant
- Much harder to get right
- Required if the the visually focused element needs to be different than the native browser focused element
(2) is used in <SearchableSelect>
, which has the constraint of requiring browser focus to stay in the text input so the user can search, while visual focus moves through the select items. We have no such constraint in this component, which is a simple menu of links, so we should use (1)
bind:this={buttonElement} | ||
class="relative z-[2] h-7.5 w-full grow appearance-none border border-light bg-white py-1.5 pl-2 pr-1 text-xs leading-tight outline-none group-hover:border-gray-6" | ||
on:click={toggleDropdown} | ||
on:keydown={handleKeydown} |
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.
issue (keyboard): it'll be much harder (though not impossible!) to get keyboard interactions to be accessible if we only attach keyboard handlers to the main control button - thus requiring the browser's focus
to remain on the button at all times. Instead, I think this will be easier if we:
- Move focus to the
<a>
menu-items when the menu is open - Attach the keyboard handlers we need to the menu-items rather than the control button
We should be able to get all the control button interactions we need with just default browser behaviors and the on:click
handler
<div | ||
class="w-full overflow-auto border border-gray-6 bg-white shadow-sm focus:outline-none" | ||
role="menu" | ||
use:clickOutside={closeDropdown} |
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.
issue (annoying clickOutside
shenanigans): IIRC this may prevent you from using the open/close button to toggle the menu closed, because clicking the control button counts as a "click outside," racing the click outside behavior with the button's own toggle behavior.
You may need to add a little guard to this use:clickOutside
handler to avoid running if the clicked element is inside buttonElement
. For reference, I think <FloatingMenu>
in the app has to deal with 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.
Oh, I didn't even realize that was an issue. Thanks, updated to handle it like floating menu does.
Overview
This PR introduces a new
NavDropdown
component that will be used in app to navigate between different versions of fragments. This dropdown allows for version selection while displaying additional data like timestamps and descriptions.Ticket: https://viam.atlassian.net/browse/APP-7268
Figma: https://www.figma.com/design/g7uvrGb1Wwdasr4tw6kPpu/Fragments?node-id=6231-12788&t=3olT5Jeslga6pn75-4