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

[SpeedDial] Fix navigation between SpeedDialActions #12725

Merged
merged 15 commits into from
Oct 3, 2018

Conversation

eps1lon
Copy link
Member

@eps1lon eps1lon commented Aug 30, 2018

Fixes navigation in SpeedDial between actions to the behavior with alternative B described in this spec.

There were quite some props which had no effect on the component.

onKeyDown was used both in SpeedDialAction and Button but SpeedDialAction never handled those. Edit: The intented behavior (navigating with arrow keys in SpeedDial) was broken and should be fixed now.

className was forwarded to Tooltip which was accepted prior to #12085 but not applied anywhere I can see (Tooltip prior to #12085].

Overall I think every test should have at least one complete mount with the basic setup.

Not sure if we should introduce a root wrapper for the SpeedDialAction to enable className or classes.root

Todo:

  • Write tests

Partial fix of #12159
Closes #12361

Followup:

@oliviertassinari oliviertassinari added the package: lab Specific to @mui/lab label Aug 31, 2018
@eps1lon
Copy link
Member Author

eps1lon commented Aug 31, 2018

@mbrookes Since this is quite the rewrite of the old behavior I pushed the fix without tests to get some early feedback and writing tests for this behavior is quite tedious. I tested this locally in the lab docs. I can focus the dial fab and then navigate with arrow keys through the actions.

@mbrookes
Copy link
Member

@eps1lon Thanks for working on it. While basic keyboard interaction is back, some important accessibility requirements have been lost.

To a visually impaired screen-reader user this is a component is effectively a drop-down menu, so:

  1. Down-arrow should select the first action (action closest the the speed dial - i.e the action next in the DOM).
  2. If down arrow selected the first item, then up should go back to the FAB.
  3. Ditto for two presses of each.
  4. It shouldn't wrap.

For sighted users, the opposite behavior to 1 & 2 applies for the default orientation.

This was baked into the logic, (and explained in the comments), but unfortunately wasn't taken into account in the "direction" PR (possibly because keyboard interaction had already been broken by then). How to address that is going to take a little more thought.

@eps1lon
Copy link
Member Author

eps1lon commented Aug 31, 2018

I did not agree with the rationale. However I did not know that this is considered common behavior concerning a11y.

The new implementation was just so tempting because it did not have to branch for different orientations.

@mbrookes Does down still select the first item even if the orientation is horizontal and should we set aria-orientation?

@mbrookes
Copy link
Member

I did not agree with the rationale.

Perhaps then you could explain how best you think it should operate?

The new implementation was just so tempting

That sounds closer to the truth. 😉

@eps1lon
Copy link
Member Author

eps1lon commented Sep 1, 2018

@mbrookes I should not have changed the behavior just because it was easy.

I'm going to look at the spec, other components within this repo and other implementations of the SpeedDial and see how they solved this.

@eps1lon
Copy link
Member Author

eps1lon commented Sep 1, 2018

Could not find a section about navigation in speed dial actions in the spec. Material web components don't have this implemented either. Does anyone know a google product that has FABs with action items that can be navigated with the keyboard?

Angular has a demo but either this is bugged or highly confusing:

  • the first action is selected on any arrow key as you said
  • if direction is left and I press left to select the first action left, up and down navigate to the left
  • if direction is down and I press up to select the first action (i.e. navigating down) subsequent up will still navigate up on subsequent presses.

If you consider the first key stroke you have 4 (directions) * 4 (first arrow keys) = 16 navigation behaviors that you have to cover. If I press down on an up-wards oriented speed dial should down now move up-wards because screen reader users consider this a drop-down menu?

Concerning wrapping:
Although https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html is about menus it still recommends wrapping and I would follow this recommendation.

Taking all this into considerations I would propose the following specification:

  • let up, right, down, left identify an arrowKey
  • let upwards, rightwards, downwards, leftwards identify the direction in which the nextItem is located visually
  • let horizontal, vertical identify the orientation
  • let action item indices be ordered from closest (in the DOM) to farthest.
  • let nextItemIndex = (currentIndex + 1) % itemCount.
  • let previousItemIndex = (currentIndex -1) % itemCount.
  • if direction is upwards or downwards orientation is vertical
  • if direction is rightwards or leftwards orientation is horizontal
  • if FAB is focused and orientation is vertical up and down focus the first item
  • if FAB is focused and orientation is horizontal left and right focus the first item
  • alternative A for navigation behavior:
    • if any item is focused up and right focus the nextItemIndex
    • if any item is focused down and left focus the previousItemIndex
  • alternative B for navigation behavior:
    • if any item is focused the key pressed when focusing the first item navigates to nextItemIndex
    • if any item is focused the inverse of the key pressed when focusing the first item navigates to previousItemIndex
  • alternative C for navigation behavior:
    • arrowKey equivalent to direction means nextItem
    • arrowKey equivalent to reverse direction means previousItem

Problems with A:

  • there is no aria-direction-ish attribute. I don't know what best practice is for navigating menus that are pointed upwards. I could not find examples for those situations
  • consistent but confusing behavior because coming from a left-to-right language and speed dials usually being placed at the bottom pointing upwards this looks wrong.

Problems with B:
Can be confusing if the first key stroke on FAB focus was accidental.

Problems with C:
How do screen reader users identify the direction?

A taken from https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_slider_role
B seems to be the indented behavior prior to this PR.
C is implemented in this PR.

@mbrookes What's your opinion on this?

@mbrookes
Copy link
Member

mbrookes commented Sep 1, 2018

@eps1lon I absolutely love how you defined the specification, and I agree 100% with your conclusion on problems. This is the reason the current implementation was chosen (in terms of the logic - the code can always be improved.)

Given the new (since the original implementation) requirement WRT orientation, the use of aria-orientation combined with constraining the arrow keys to those relevant to the orientation minimizes the chance for confusion.

I stand corrected WRT to W3 view on wrapping - I was of the impression that without an index, a NOP signalling the end of a range was preferable. This is how a native HTML select, and native OS menus operate. I agree that we should follow the W3 recommendation, however counter-intuitive it might appear.

@eps1lon
Copy link
Member Author

eps1lon commented Sep 1, 2018

@mbrookes ubuntu gnome menus do wrap that's why this felt natural to me. Probably everybody who is used to a certain behavior considers the other behavior counter-intuitive.

So what I'm taking out this conversation is that we:

  • do enable wrapping
  • ignore arrow keys that are orthogonal to the orientation
  • consider the first accepted arrow key to point towards the nextItemIndex and its inverse to point towards previousItemIndex (this resetted everytime the actions collapse)

Looks like a good tradeoff to me.

@mbrookes
Copy link
Member

mbrookes commented Sep 1, 2018

The reason why the W3 ARIA recommendation seems counter-intuitive Is that a totally blind person has no visual reference as to when a menu wraps, so has to depend on memorising the first option.

It is perhaps a trade-off with ease of returning to the first item, having reviewed the available options.

@eps1lon
Copy link
Member Author

eps1lon commented Sep 1, 2018

That makes sense. But the recommendation lists Home and End as keys to go to the first and last item in menus so this seems redundant. However the material spec explicitly states that there should be no more than 6 actions so wrapping or not is probably not that big of a deal.

@eps1lon eps1lon changed the title [SpeedDial] Fix warnings [SpeedDial] Fix navigation between SpeedDialActions Sep 2, 2018
@eps1lon eps1lon changed the title [SpeedDial] Fix navigation between SpeedDialActions [WIP] [SpeedDial] Fix navigation between SpeedDialActions Sep 2, 2018
@eps1lon
Copy link
Member Author

eps1lon commented Sep 2, 2018

@mbrookes I implemented the behavior according to the proposed spec with alternative B. This should now match the old behavior.

I'm going to checkout a previous working version and see if this is indeed true and then add some tests to avoid future regression.

I would delay the className issue to a future PR since this is mainly an issue with the Tooltip component.

className and classes.root was not applied to anything or forwarded to
anything in the past. They should be implemented in the future however.

A missing tooltipTitle caused a warning further down the component tree.
onKeyDown was applied to the button and the SpeedDialAction. However the
Action did not process onKeyDown and passing the same
event listener to different components is ambiguous.
Following the proposed spec with alternative B
@eps1lon eps1lon changed the title [WIP] [SpeedDial] Fix navigation between SpeedDialActions [SpeedDial] Fix navigation between SpeedDialActions Sep 3, 2018
@eps1lon
Copy link
Member Author

eps1lon commented Sep 3, 2018

@mbrookes
I added test cases for the navigation behavior. PR is ready for review.

@mbrookes
Copy link
Member

mbrookes commented Sep 7, 2018

@eps1lon Sorry for the delay.

With the custom close icon example, how does a user keyboard-navigate back to the FAB? (I have some accessibility concerns about that feature anyway - with the role of the FAB changing between its closed and open states, but that's a different concern).

Also, you mentioned the Home and End keys. Should we add those here, or is that best left for another PR (creature feep...)?

@mbrookes
Copy link
Member

mbrookes commented Sep 7, 2018

With the ESC key.

Except that Escape closes the SpeedDial...

In the case of the custom close icon example, when the speed-dial is open, the FAB becomes one of the actions (hence me accessibility concerns), but with the current wrapping there's no way to navigate back to it without closing the speed-dial and opening it again.

Should it wrap back to the FAB in that scenario?

@eps1lon
Copy link
Member Author

eps1lon commented Sep 7, 2018

A custom icon does not imply that the FAB emits an action. If anything there should be a prop to indicate that and only then should this action be included in the arrow key navigation.

I still don't understand why you think you have to open it again. If I navigate within the menu and press ESC the FAB is already focused and ENTER would trigger its onClick. If it would emit another action then the FAB would actually emit two actions: open the menu and emit whatever the user defined which is a bad idea imo.

I understand now. The FAB should only emit an action if the custom icon is displayed. But this brings me back to my first point. It should be an extra prop and the example should include the intention of the custom icon

@mbrookes
Copy link
Member

mbrookes commented Sep 7, 2018

The action could simply be triggered in the onClick handler when open is true, so technically no need for a specific prop (I guess we could have added that to the demo, but it's pretty self explanatory).

That, though, still leaves the question as to when to include the FAB in the wrap cycle (if that's the approach we're taking). So, if we have to add a prop one way or the other, it might as well host the FAB action logic too.

The alternative is that we ignore w3C on this one, and make the behavior consistent with how native <select> functions in the majority of browsers and OSes (i.e. no wrap).

@eps1lon
Copy link
Member Author

eps1lon commented Sep 8, 2018

The alternative is that we ignore w3C on this one, and make the behavior consistent with how native <select> functions in the majority of browsers and OSes (i.e. no wrap).

We still have to decide whether the FAB should be included in the navigation.

I think we can take a little freedom from the w3c recommendation here. Just include the FAB in the navigation and call it a day. Adding another prop to control the navigation just adds complexity.

@mbrookes
Copy link
Member

mbrookes commented Sep 8, 2018

The alternative is that we ignore w3C on this one, and make the behavior consistent with how native <select> functions in the majority of browsers and OSes (i.e. no wrap).

We still have to decide whether the FAB should be included in the navigation.

My point was, if you don't wrap, then the FAB is already included in the navigation, since if you press a direction key n times, then press the opposite direction key the same number of times, you return to the FAB.

Adding another prop to control the navigation just adds complexity.

The idea here was that if you're have to add another prop (forced by wrap), then rather than it being to control the navigation, it is the onClick prop for the FAB action, that only fires when the FAB is open (moving that logic into the component), and that controlling the navigation based on whether this prop is used is secondary.

I think you're now saying to include the FAB in the wrap regardless of whether the FAB triggers an action. Personally I would prefer to revert to the previous behavior (no wrap), which matches how the native HTML select and Material-UI Select & Menu behave.

@eps1lon
Copy link
Member Author

eps1lon commented Sep 12, 2018

@mbrookes
So you mean we add onOpenClick which only fires if the dial is open i.e. the secondary action and we only include the dial in the navigation if this prop is set?

@mbrookes
Copy link
Member

@eps1lon Sorry, fixed the quoting (needed to escape <select>.

So you mean we add onOpenClick which only fires if the dial is open i.e. the secondary action and we only include the dial in the navigation if this prop is set?

Yes, for that scenario, but it wouldn't be my first choice.

@eps1lon
Copy link
Member Author

eps1lon commented Sep 13, 2018

Considering everything I think we should remove the wrapping behavior and include the FAB when navigating with arrow keys. Makes it easier to reason about and since there are very few items in the menu to begin with wrapping is not that important in my opinion.

@eps1lon eps1lon changed the title [SpeedDial] Fix navigation between SpeedDialActions [WIP] [SpeedDial] Fix navigation between SpeedDialActions Sep 17, 2018
- FAB is included in navigation
- Fixed potential issues when mixing keyboard and mouse navigation or
  refocusing a SpeedDial
@eps1lon eps1lon changed the title [WIP] [SpeedDial] Fix navigation between SpeedDialActions [SpeedDial] Fix navigation between SpeedDialActions Sep 17, 2018
@oliviertassinari oliviertassinari added component: speed dial This is the name of the generic UI component, not the React module! and removed package: lab Specific to @mui/lab labels Sep 22, 2018
@eps1lon
Copy link
Member Author

eps1lon commented Oct 1, 2018

Merged master. CI is still passing.

Anything left to do @mbrookes ?

@mbrookes
Copy link
Member

mbrookes commented Oct 1, 2018

@eps1lon No, I think we're good. (Other than codecov..._)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: speed dial This is the name of the generic UI component, not the React module!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants