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

Post visibility panel better keyboard interaction. #1886

Closed
wants to merge 4 commits into from

Conversation

afercia
Copy link
Contributor

@afercia afercia commented Jul 13, 2017

This PR is a POC to try to address keyboard interaction on the Post Visibility panel:

To my understanding, PostVisibility is mounted when the sidebar opens so I'm not sure withFocusReturn can be used here. If only something similar to react-click-outside existed also for focusOut, things would be simpler. Any feedback for a better approach very welcome.

  • adds a keydown event to close the panel when pressing Escape
  • adds an blur event to close the panel when tabbing away (uses event.relatedTarget)
  • moves focus back to the toggle when closing the panel

The code here is mainly to share and discuss better approaches. I haven't found better ways to do this, so any help would be greatly appreciated 🙂

Also to note: seems it doesn't work very well when using Safari+VoiceOver, while it works nicely with Firefox+NVDA.

I also get a warning A component is changing an uncontrolled input of type text to be controlled... but this seems to happen also on master.

Fixes #1885

@afercia afercia requested a review from aduth July 13, 2017 17:01
@aduth aduth added [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). General Interface Parts of the UI which don't fall neatly under other labels. labels Jul 14, 2017
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

If we moved the dialog portion of the PostVisibility into a separate component, could we then apply withFocusReturn to the new component?

Thinking the Popover component could help manage these behaviors. Need to revisit #1204.


componentDidMount() {
const node = findDOMNode( this );
node.addEventListener( 'keydown', this.handleKeyDown, false );
Copy link
Member

Choose a reason for hiding this comment

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

Was there a reason we couldn't apply an onKeyDown prop to the rendered <div /> ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was wondering about this, I've used the same pattern used in the DropdownMenu. Should then be changed to an onKeyDown prop there too?

Copy link
Member

Choose a reason for hiding this comment

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

Should then be changed to an onKeyDown prop there too?

Probably.

@@ -71,6 +74,45 @@ class PostVisibility extends Component {
this.setState( { opened: false } );
}

handleBlur( event ) {
if ( this.state.opened && event.relatedTarget !== null ) {
const wrapper = findDOMNode( this );
Copy link
Member

Choose a reason for hiding this comment

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

This is a case where we could avoid findDOMNode by instead assigning a ref to the rendered element.

https://facebook.github.io/react/docs/refs-and-the-dom.html

If you have absolutely no control over the child component implementation, your last option is to use findDOMNode(), but it is discouraged.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will try.

@afercia
Copy link
Contributor Author

afercia commented Jul 16, 2017

If we moved the dialog portion of the PostVisibility into a separate component, could we then apply withFocusReturn to the new component?
Thinking the Popover component could help manage these behaviors. Need to revisit #1204.

Standardizing and simplifying makes a lot of sense. We should then always make the Popover behave like a modal: constraining tabbing within the component, add a control to close it, etc. There's some discussion on the issue about this. While moving focus back on the toggle makes sense when pressing Escape, we're unsure about what should happen on blur. Wondering if just closing the Popover and let tabbing order work natively would be a best option.

Note: looks like withFocusReturn must be used with care, see #1917 and #1918

@afercia
Copy link
Contributor Author

afercia commented Jul 17, 2017

OK so testing with Safari + VoiceOver, event.relatedTarget returns null, only when using VoiceOver 😞 No idea why. This makes closing the popover on blur fail. I've tested also with Safari Technology Preview + VoiceOver and it works perfectly. Any suggestions for alternative methods to check if the element that receives focus is within the popover very welcome.

@afercia
Copy link
Contributor Author

afercia commented Jul 17, 2017

Refactored and simplified to use an onKeyDown prop on the popover dialog and refs to get DOM elements. Also, changed the behavior on blur: focus is not moved back to the toggle now; instead, the natural tabbing order is preserved.

The issue with event.relatedTarget on Safari+VoiceOver stands though.

@@ -10,7 +10,8 @@ import { withInstanceId } from 'components';
* WordPress dependencies
*/
import { __ } from 'i18n';
import { Component } from 'element';
import { Component, findDOMNode } from 'element';
Copy link
Member

Choose a reason for hiding this comment

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

Tests fail here because this findDOMNode is unused.

>
{ getVisibilityLabel( visibility ) }
</button>

{ this.state.opened &&
<div className="editor-post-visibility__dialog">
<div className="editor-post-visibility__dialog"
onKeyDown={ this.handleKeyDown }
Copy link
Member

Choose a reason for hiding this comment

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

Warning:

138:6   warning  Visible, non-interactive elements should not have mouse or keyboard event listeners

Might explain why this was avoided for DropdownMenu . Is it fair to disable the rule here?

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 think this rule should be improved a bit. While the intent is good, it should be allowed to use keyboard events this way when one knows what he's doing. Also, why doesn't catch onBlur then? Also onBlur can indicate keyboard interaction.
https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md

Also, some of the "solutions" mentioned in the docs, like adding role="presentation" to elements that already have no semantic meaning, don't look right to me and are against the ARIA spec.

Will disable the rule (also remove the previous disabling and comment for htmlFor).

Copy link
Member

Choose a reason for hiding this comment

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

If the rule causes more harm or confusion than it helps, I'd also be okay with disabling it globally from the root .eslintrc.json (i.e. "jsx-a11y/no-static-element-interactions": "off").

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm I think the rule makes perfectly sense for example to avoid to use <div> or <span> elements as UI controls instead of buttons. Re: the warning: it should probably be evaluated on a case by case basis. Our onKeyDown here is actually used for the radio buttons and input field. Maybe the rule should be improved to check if a <div> is focusable and actually able to receive a keyboard event. Anyways, it's good to have it.

@@ -71,6 +74,21 @@ class PostVisibility extends Component {
this.setState( { opened: false } );
}

handleBlur( event ) {
if ( this.state.opened && event.relatedTarget !== null && ! this.dialog.contains( event.relatedTarget ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

While explicitness could be handy, I don't know that there's ever a case that event.relatedTarget is null and this.dialog.contains( null ) would ever be true. In other words, we could safely drop && event.relatedTarget !== null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This needs to be changed anyways, since relatedTarget doesn't work on Safari 10 when using VoiceOver (see comment on the issue). We need to find an alternative solution.

Copy link
Member

Choose a reason for hiding this comment

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

Apparently Firefox has some issues with relatedTarget as well? There's an interesting idea here for determining the related target via a setTimeout( () => { const relatedTarget = document.activeElement } , 0 ) when the blur occurs.

facebook/react#2011 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes the setTimeout 0 trick is the only possible solution I've found so far, not so spectacularly happy to have to use setTimeout. I guess other solutions would be fragile though. Re: Firefox, I think at the time that comments were posted, Firefox was not supporting relatedTarget. It does now, since version 48 https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget
Instead, IE 11 could be a problem but I can't test in IE 11 at the moment 🙂 see #1397 and #1863

@@ -11,6 +11,7 @@ import { withInstanceId } from 'components';
*/
import { __ } from 'i18n';
import { Component } from 'element';
import { ESCAPE } from 'utils/keycodes';
Copy link
Member

Choose a reason for hiding this comment

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

Note with #2172, these need to be updated to prefix the dependencies with @wordpress/. You will need to perform a rebase against the latest version of master and apply your changes:

git fetch origin
git rebase origin/master

Further, we should not import from nested paths. See #941. Instead, import { keycodes } from '@wordpress/utils'; and destructure from the keycodes object:

const { ESCAPE } = keycodes;

@afercia afercia force-pushed the update/post-visibility-keyboard-interaction branch from 63cd684 to 84d7587 Compare August 3, 2017 17:00
@afercia
Copy link
Contributor Author

afercia commented Aug 3, 2017

Rebased and restored (more or less...) the focus/blur behavior. However, this didn't work so well also before the latest changes. I'm wondering if the whole thing should work instead on the Popover component, since now it's focusable (it uses tabindex="0" by default). Maybe also clickOutside should work on the Popover? /cc @aduth

@codecov
Copy link

codecov bot commented Aug 3, 2017

Codecov Report

Merging #1886 into master will decrease coverage by 0.07%.
The diff coverage is 0%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1886      +/-   ##
==========================================
- Coverage    22.5%   22.43%   -0.08%     
==========================================
  Files         139      139              
  Lines        4332     4391      +59     
  Branches      729      736       +7     
==========================================
+ Hits          975      985      +10     
- Misses       2833     2878      +45     
- Partials      524      528       +4
Impacted Files Coverage Δ
editor/sidebar/post-visibility/index.js 0% <0%> (ø) ⬆️
editor/header/tools/preview-button.js 65.38% <0%> (-29.06%) ⬇️
blocks/library/button/index.js 26.66% <0%> (ø) ⬆️
editor/sidebar/table-of-contents/index.js 0% <0%> (ø) ⬆️
blocks/library/cover-text/index.js 32.43% <0%> (+0.85%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b9deeb8...84d7587. Read the comment docs.

@aduth
Copy link
Member

aduth commented Aug 7, 2017

Some ongoing effort in #2160 to move "click outside" behavior into the popover. I think Escape-handling might be well suited there as well, though maybe as a subsequent enhancement.

@afercia
Copy link
Contributor Author

afercia commented Aug 9, 2017

With the relevant changes in #2160 I think this PR can be closed. The related issue should stay open though, as keyboard interaction is still a big problem. See also #2306

@afercia
Copy link
Contributor Author

afercia commented Aug 29, 2017

Closing as the PopOver component has seen big changes and the pending issues should be addressed with a new approach, see #2306

@afercia afercia closed this Aug 29, 2017
@afercia afercia deleted the update/post-visibility-keyboard-interaction branch August 29, 2017 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). General Interface Parts of the UI which don't fall neatly under other labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Post visibility panel better keyboard interaction
2 participants