-
-
Notifications
You must be signed in to change notification settings - Fork 128
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
MenuBar: do not focus on hover #607
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.
Left some inline comments
// Update focus to new active index | ||
if ( | ||
this._activeIndex >= 0 && | ||
this.contentNode.childNodes[this._activeIndex] | ||
) { | ||
(this.contentNode.childNodes[this._activeIndex] as HTMLElement).focus(); | ||
} | ||
|
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.
This block of code was added via #187.
However, this code presents two problems for me. For one, I have found side effects in setters to be problematic. Another, bigger problem is that it causes :focus-visible
styling to be activated after page load even if the user hasn't touched the keyboard.** As far as I can tell, all browsers right now have this behavior (why, I don't know). Here it happens because node.focus() gets called when this.activeIndex
is changed during a mousedown
event. It makes it impossible for me to get the kind of behavior that I want for :focus-visible
, which is to only show the focus ring when the user is using the keyboard instead of the mouse to interact with the page—in other words, to match the behavior that you get in a Windows file menu bar.
There's another issue, too, that I try to address explicitly in this PR by being more explicit about the definition of activeIndex
. It's very hard to make activeIndex
always consistent with focus. The consistency we had before this PR and which we continue to have after this PR is:
If an item node at index
i
in the menu bar has focus, thenactiveIndex
is equal toi
.
This is assured by the onfocus
handler attached to each item node. But the reverse was (and is) not always true. If activeIndex
is equal to i
(not -1), the item node at index i
may or may not be focussed, depending on whether the menu at i
is open.
** You can verify this behavior for yourself. Go to the ReadTheDocs Lumino DockPanel example, and before pressing any keys or clicking anywhere else, use your mouse to move and click around the menu bar. You should see your browser's default focus ring appearing even though you've only been using your mouse, and even though the MenuBar doesn't have any HTML elements that get a focus ring by default.
@@ -648,7 +662,6 @@ export class MenuBar extends Widget { | |||
|
|||
// Stop the propagation of the event. Immediate propagation is | |||
// also stopped so that an open menu does not handle the event. | |||
event.preventDefault(); |
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 we always call preventDefault() on mousedown, it prevents the node from receiving browser focus unless we call node.focus() ourselves, but if we call focus on mousedown we cause current browsers to activate :focus-visible stylings. In other words, if we call focus on mousedown, then a focus ring appears around the items in the menu bar (on initial page load) even if the user hasn't touched the keyboard. I have a code pen that demos this behavior.
packages/widgets/src/menubar.ts
Outdated
@@ -408,7 +422,7 @@ export class MenuBar extends Widget { | |||
*/ | |||
protected onActivateRequest(msg: Message): void { | |||
if (this.isAttached) { | |||
this.activeIndex = 0; | |||
this._focusItem(0); |
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.
Instead of having focus happen as a side-effect of setting activeIndex
, I rewrote this code to rely on the fact that the item's onfocus handler sets the active index.
Note: if menubar.activate()
is called on page load or after a mouse event, this will cause :focus-visible styling to be applied.
this.activeIndex = -1; | ||
this.node.blur(); |
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.
There is no way currently to focus the menu bar node, so there is no way to blur it either.
The escape key has not been able to blur the menu bar node since #465. Before that PR, the menu bar node could be focussed (because tabindex
). But after that PR, the menu bar node is just a div with no tabindex
value, so it cannot ever be focussed.
private _evtMouseLeave(event: MouseEvent): void { | ||
// Reset the active index if there is no open menu. | ||
if (!this._childMenu) { | ||
private _evtFocusOut(event: FocusEvent): 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.
The MenuBar class was originally written to handle mouse and keyboard interaction; however, it really wasn't concerned with browser focus. For example, in the original version, you could use the left and right arrow keys to "move" from one item to another in the menu bar, but browser focus did not move when you pressed those keys.
There was no way to even put browser focus on individual item nodes in the menu bar until #187. As a result, it made sense to set the active index to -1 when the mouse left the menu bar. But now that the active index is closely tied to focus, to be consistent we shouldn't reset the active index just because the mouse leaves the menu bar.
However, we can safely reset the active index if the menu bar loses focus.
97424bf
to
8e91f4b
Compare
@@ -489,7 +513,10 @@ describe('@lumino/widgets', () => { | |||
|
|||
context('keydown', () => { | |||
it('should bail on Tab', () => { | |||
let event = new KeyboardEvent('keydown', { keyCode: 9 }); | |||
let event = new KeyboardEvent('keydown', { | |||
cancelable: 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.
If you don't set cancelable
to true
then it's pointless to check event.preventDefault
because it will always be false.
I searched the codebase for defaultPrevented
and fortunately there were only a few places, all fixed in this PR.
Thank you so much for working on this - and for the super thorough descriptions, research, and codepens! It really helps to have all the context with this stuff cause it can be sort of subtle. I haven't tested this locally but as the person that added a lot of the changes that you're addressing in this PR, I approve! I also might recommend someone that's worked a bit more with Lumino checks this out too (@afshin) cause it's been a while since I made changes to Lumino. |
Hey @gabalafou would you mind running: yarn api
yarn lint To fix the CI? |
@fcollonval done! But running |
bc50bd9
to
888038f
Compare
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.
Hey @gabalafou
Thanks for pushing on this. I rebased (this was the reason of so many api doc changes) and added the menubar to the doc to ease testing.
i only have one comment otherwise it looks good to me.
Co-authored-by: Frédéric Collonval <[email protected]>
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.
Thanks @gabalafou
When the MenuBar class was created, it was designed so that whenever a user hovers over an item in the menu with a mouse, the active index would change to the index of the item being hovered. The setter for the active index also had a side effect that would update the DOM in order to apply the active CSS class to the item being hovered so that clients consuming this class could style the item (just like the
:hover
pseudo-class).Later, in an attempt to make the Lumino menu bar more accessible, tabindex=0 values were added to the menubar items via #187. With the menu bar items keyboard-focusable (but with no CSS styling to indicate it), it made sense to try to synchronize the active index with the item focussed.
However, this had one unfortunate side effect, which was that whenever a user moved their mouse over a menu bar item, the app would move browser focus from anywhere else in the app onto that item.
This PR is an attempt to fix that issue and to put the menu bar's focus management on better footing.
Originally I refactored the menu bar class to simply not change
activeIndex
onmousemove
, but I realized that that would probably break a lot of downstream code. For years, since the beginning it seems, the convention of the Lumino menu bar has been that if an item in the menu bar is hovered, then Lumino will apply thelm-mod-active
class. In the interest of not breaking people's code, I decided to keep that convention.