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

Edit Post: Make sidebar header focusable for button focus normalization #21031

Merged
merged 2 commits into from
Mar 20, 2020

Conversation

aduth
Copy link
Member

@aduth aduth commented Mar 19, 2020

Fixes #20759

This pull request seeks to resolve an issue where clicking the "Close Sidebar" in Firefox for macOS has two problems:

  • Focus will not be returned to the corresponding header toolbar toggle button
  • The "Open document settings" panel at the bottom of the sidebar remains visible

There are a number of contributing factors here, all of which ultimately stem from a particular browser quirk in how Firefox for macOS treats clicks on button elements. Essentially, the problem is that clicking the sidebar dismiss button never registers as the sidebar ever having received focus, so it does not know to either return focus to the header, nor to dismiss the bottom panel of the sidebar.

In other browsers, the normal expected behavior is:

  1. A click on the button bubbles as a focus event captured on withFocusReturn state:

<div
onFocus={ this.setIsFocusedTrue }
onBlur={ this.setIsFocusedFalse }
>

  1. When the (inner) sidebar component is unmounted, it runs the focus return logic if focus was within. This is where Firefox fails, because at this point it does not consider focus as being within the element.

if ( ! isFocused ) {
return;
}
// Defer to the component's own explicit focus return behavior,
// if specified. The function should return `false` to prevent
// the default behavior otherwise occurring here. This allows
// for support that the `onFocusReturn` decides to allow the
// default behavior to occur under some conditions.
if ( onFocusReturn() === false ) {
return;
}

  1. The implementation of the sidebar explicitly shifts focus to the header toolbar toggle button, having the dual effect of shifting focus to a meaningful fallback, and hiding the sidebar panel.

onFocusReturn() {
const button = document.querySelector(
'.edit-post-header__settings [aria-label="Settings"]'
);
if ( button ) {
button.focus();
return false;
}
},

There is some prior art to this proposed solution, specifically in how the sibling inserter captures focus via click on the inserter button:

// While ideally it would be enough to capture the
// bubbling focus event from the Inserter, due to the
// characteristics of click focusing of `button`s in
// Firefox and Safari, it is not reliable.
//
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
tabIndex={ -1 }

Testing Instructions:

Repeat Steps to Reproduce from #20759, verifying that the panel at the bottom of the sidebar no longer remains visible, and that focus is shifted to the sidebar toggle button in the header toolbar.

@aduth aduth added [Type] Bug An existing feature does not function as intended General Interface Parts of the UI which don't fall neatly under other labels. Browser Issues Issues or PRs that are related to browser specific problems [Type] Regression Related to a regression in the latest release [a11y] Keyboard & Focus labels Mar 19, 2020
@github-actions
Copy link

github-actions bot commented Mar 19, 2020

Size Change: -68 B (0%)

Total Size: 856 kB

Filename Size Change
build/components/index.js 191 kB -74 B (0%)
build/edit-post/index.js 91.2 kB +6 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 998 B 0 B
build/annotations/index.js 3.43 kB 0 B
build/api-fetch/index.js 3.39 kB 0 B
build/autop/index.js 2.58 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 6.02 kB 0 B
build/block-directory/style-rtl.css 760 B 0 B
build/block-directory/style.css 760 B 0 B
build/block-editor/index.js 100 kB 0 B
build/block-editor/style-rtl.css 10.9 kB 0 B
build/block-editor/style.css 10.9 kB 0 B
build/block-library/editor-rtl.css 7.21 kB 0 B
build/block-library/editor.css 7.21 kB 0 B
build/block-library/index.js 110 kB 0 B
build/block-library/style-rtl.css 7.42 kB 0 B
build/block-library/style.css 7.43 kB 0 B
build/block-library/theme-rtl.css 669 B 0 B
build/block-library/theme.css 671 B 0 B
build/block-serialization-default-parser/index.js 1.65 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/blocks/index.js 57.5 kB 0 B
build/components/style-rtl.css 15.8 kB 0 B
build/components/style.css 15.7 kB 0 B
build/compose/index.js 6.21 kB 0 B
build/core-data/index.js 10.6 kB 0 B
build/data-controls/index.js 1.04 kB 0 B
build/data/index.js 8.2 kB 0 B
build/date/index.js 5.37 kB 0 B
build/deprecated/index.js 771 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 3.06 kB 0 B
build/edit-post/style-rtl.css 8.47 kB 0 B
build/edit-post/style.css 8.46 kB 0 B
build/edit-site/index.js 5.56 kB 0 B
build/edit-site/style-rtl.css 2.62 kB 0 B
build/edit-site/style.css 2.62 kB 0 B
build/edit-widgets/index.js 4.43 kB 0 B
build/edit-widgets/style-rtl.css 2.58 kB 0 B
build/edit-widgets/style.css 2.58 kB 0 B
build/editor/editor-styles-rtl.css 381 B 0 B
build/editor/editor-styles.css 382 B 0 B
build/editor/index.js 43.8 kB 0 B
build/editor/style-rtl.css 3.97 kB 0 B
build/editor/style.css 3.96 kB 0 B
build/element/index.js 4.44 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/index.js 6.95 kB 0 B
build/format-library/style-rtl.css 502 B 0 B
build/format-library/style.css 502 B 0 B
build/hooks/index.js 1.93 kB 0 B
build/html-entities/index.js 621 B 0 B
build/i18n/index.js 3.49 kB 0 B
build/is-shallow-equal/index.js 711 B 0 B
build/keyboard-shortcuts/index.js 2.3 kB 0 B
build/keycodes/index.js 1.69 kB 0 B
build/list-reusable-blocks/index.js 2.99 kB 0 B
build/list-reusable-blocks/style-rtl.css 226 B 0 B
build/list-reusable-blocks/style.css 226 B 0 B
build/media-utils/index.js 4.83 kB 0 B
build/notices/index.js 1.58 kB 0 B
build/nux/index.js 3.01 kB 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/plugins/index.js 2.54 kB 0 B
build/primitives/index.js 1.5 kB 0 B
build/priority-queue/index.js 780 B 0 B
build/redux-routine/index.js 2.83 kB 0 B
build/rich-text/index.js 14.4 kB 0 B
build/server-side-render/index.js 2.55 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/url/index.js 4.01 kB 0 B
build/viewport/index.js 1.61 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.18 kB 0 B

compressed-size-action

// the sidebar is unmounted, the corresponding "focus return" behavior to
// shift focus back to the heading toolbar would not be run.
//
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we tried this before but forgot the conclusion, why don't we try to normalize it on the Button click handler (or related events).

Copy link
Contributor

Choose a reason for hiding this comment

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

Asking cause it's not the first time we have the button click focus issues and certainly not the last time.

Copy link
Member Author

@aduth aduth Mar 20, 2020

Choose a reason for hiding this comment

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

I've been thinking about this as well.

I think one of the main reluctances I'd have is in breaking expectations about how elements are expected to work, but I'm not sure how well this holds up:

  • The abstraction of a button shouldn't necessarily be known to be tied to expectations around a particular HTML element.
  • The "expected" behavior is clearly not very expected, or at least it's highly inconsistent between browsers and consequently prone to error.

On the technical front, I've been wondering as well just how this would work:

  • If we dispatch a new window.Event, how well does would it interoperate with React's event system, and in particular with portaled rendering which can't rely on DOM event bubbling?

I've also been thinking about a few other options:

  • Is <input type="button"> susceptible to this as well? And if not, could we implement <Button> this way? I expect by virtue of the differences between the two (largely ability to use non-text HTML content in <button>), it may be infeasible.
  • What about something like <span role="button"> ? As I understand it, there are a lot of accessibility challenges in using role="button" effectively, but if it were technically possible, we could absorb that complexity into the implementation of the core component.
  • Wrap every <button> with <span tabindex="-1">, essentially applying the behavior implemented here universally. There are some known issues here (huge proliferation of wrapping elements) and some unknown issues (potential side-effects of so many additional focusable elements).

Copy link
Contributor

Choose a reason for hiding this comment

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

I think @diegohaz might have good thoughts on this problem as something he probably already faced with reakit

Copy link
Member

Choose a reason for hiding this comment

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

On Reakit we opted to ensure consistency across browsers on a Tabbable component (which is used by Button): https://github.com/reakit/reakit/blob/c9f55d7d87f12b58ee409e8a392a9c36c2aadf52/packages/reakit/src/Tabbable/Tabbable.ts#L141-L148

It's important to note that, on Firefox/macOS, calling preventDefault() there also prevents it to get the :active state when clicked (ariakit/ariakit#432). Something like data-active could be used instead. That's the only bad side-effect people noticed so far.

Copy link
Member Author

Choose a reason for hiding this comment

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

@diegohaz Thanks for sharing! It hadn't occurred to me as potentially being as simple as calling .focus() on the button node. About the implementation and your point about preventDefault: Can you explain why preventDefault is necessary at all?

Copy link
Member

Choose a reason for hiding this comment

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

That's because Safari/Firefox on macOS will blur the button if it has focus on mouse down. preventDefault prevents that from happening. But there may be another way to work around that.

@aduth aduth added the Backport to WP 6.7 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Mar 20, 2020
@aduth aduth merged commit f0e3636 into master Mar 20, 2020
@aduth aduth deleted the update/sidebar-header-focusable branch March 20, 2020 12:50
@github-actions github-actions bot added this to the Gutenberg 7.8 milestone Mar 20, 2020
@jorgefilipecosta jorgefilipecosta removed the Backport to WP 6.7 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Mar 27, 2020
@priethor priethor added the [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). label Jul 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Browser Issues Issues or PRs that are related to browser specific problems [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. [Type] Bug An existing feature does not function as intended [Type] Regression Related to a regression in the latest release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

(Firefox) "Open Document Settings" region visible after closing sidebar
5 participants