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

Components: Add onFocusOutside replacement to Popover onClickOutside #14851

Merged
merged 4 commits into from
Aug 2, 2019

Conversation

aduth
Copy link
Member

@aduth aduth commented Apr 5, 2019

Fixes #14754, fixes #14849.

This pull request seeks to refactor popovers to avoid relying on "click outside" behaviors in popovers, deprecating the Popover component's onClickOutside prop in favor of a substitute onFocusOutside. This accounts for more reasons for focus transitions not previously reflected in onClickOutside, such as that described by the bug of #14754.

Status: While functionally complete, I'm considering this to be blocked by an apparent incompatibility with a tangentially-related bug described at #14849 . Notably, when toggling an option in the More Options menu, the menu will no longer become closed again from clicks elsewhere in the page.

Testing instructions:

Repeat Steps to Reproduce from #14754 , verifying that the menu is closed as expected.

Verify there are no changes in behavior from other Popover usage, particularly specialized cases such as:

  • Clicking a DropdownMenu toggle button should toggle itself closed when currently opened
  • Clicking a result in the link suggestions autocomplete does not dismiss the URL popover

@aduth aduth added [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). [Feature] UI Components Impacts or related to the UI component system [Package] Components /packages/components labels Apr 5, 2019
@gziolo
Copy link
Member

gziolo commented Apr 10, 2019

Clicking a result in the link suggestions autocomplete does not dismiss the URL popover

There seems to be a different behavior when using mouse and keyboard. Well, there might be something wrong with the URL popover and autocomplete:

  • when I use an arrow key, select one of the results and press space it doesn't get selected
  • when I use an arrow key, select one of the results and press enter it gets selected and focus goes back to RichText field

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

I will finish this review tomorrow. It's looking great so far. I left some questions.

*/
import clickOutside from 'react-click-outside';
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated question to this PR. Can we also refactor Modal component to stop using react-click-outside? :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Unrelated question to this PR. Can we also refactor Modal component to stop using react-click-outside? :)

I would like to, yes.

( #6261 (comment) 😞 )

Copy link
Member

Choose a reason for hiding this comment

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

There is still hope then :)

Copy link
Member

Choose a reason for hiding this comment

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

https://unpkg.com/[email protected]/index.js

Well, it looks like react-click-outside is a simplified version of our own HOC :)

Copy link
Member

Choose a reason for hiding this comment

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

I opened a follow-up to address it – #16878.

*
* @param {FocusEvent} event Focus event from onFocusOutside.
*/
onFocusOutside( event ) {
Copy link
Member

Choose a reason for hiding this comment

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

How can I test this shim? By updating one of the places with usage?

Copy link
Member Author

Choose a reason for hiding this comment

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

You could locally revert one or both of 55ca76a or 3e8ffc4 to restore those components' use of onClickOutside.

@aduth
Copy link
Member Author

aduth commented May 22, 2019

This one was a pain to rebase, in good part because of #15053 (cc @youknowriad).

Worth noting that this will resolve some lingering errors after #11360 logged frequently in StrictMode concerning findDOMNode from react-click-outside (related: kentor/react-click-outside#45). Also related to prior conversation #14851 (comment).

@aduth aduth mentioned this pull request May 22, 2019
12 tasks
}

render() {
return this.props.children;
}
}

export default clickOutside( PopoverDetectOutside );
export default withFocusOutside( PopoverDetectOutside );
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a nice hook const useOnFocusOutside( ref, handler )

Copy link
Member

@noisysocks noisysocks left a comment

Choose a reason for hiding this comment

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

I rebased this PR to fix some conflicts that existed in the CHANGELOG.md files. You may want to double check that the changes to CHANGELOG.md make sense.

I followed the testing steps in the description and everything seems okay. I also played with some Popover and DropdownMenu components and couldn't see anything off. I was not able to follow the original steps in the linked issue as that issue was fixed via a different approach.

Would love to get this in as it fixes a bug that's blocking #16582.

@aduth
Copy link
Member Author

aduth commented Jul 29, 2019

Thanks @noisysocks for the assist here. Looking again over the code and with some final testing, I think this is in good shape for merge. 👍

@aduth
Copy link
Member Author

aduth commented Jul 29, 2019

Hmm, the end-to-end test failures may be legitimate.

@aduth
Copy link
Member Author

aduth commented Jul 29, 2019

The end-to-end tests seem to be getting stuck when trying to switch from "Code" editor back to "Visual" in the container blocks test cases. I can't see any reason why it would be such a specific issue here.

My hunches are:

Separately, I stumbled upon this code, which was introduced more recently and seems like something we might want to update:

onClickOutside={ onClickOutside() }

@noisysocks
Copy link
Member

noisysocks commented Jul 31, 2019

I figured out how to recreate the bug with some simple steps, but I'm no closer to figuring out what's causing it:

  1. Create a new post
  2. Open the More tools & options menu
  3. Click any menu item, e.g. enable Spotlight mode
  4. Click outside of the menu to dismiss it. The menu does not close

Any thoughts, @aduth?

@noisysocks
Copy link
Member

Figured it out! It's because clicking on the menu item causes a focus loss. This was reported in #14849. I'll include a fix here.

Switching between `Button` and `IconButton` causes React to remount the
`MenuItem` component. This causes a focus loss as well as E2E failures.

There's no need to use a `Button` for `MenuItems` that are unselected,
since we can simply pass `icon={ undefined }` to the `IconButton`.
@noisysocks
Copy link
Member

I pushed up 738d987 which fixes the E2E test failures and fixes #14849.

Would you mind giving this a quick test, @gziolo?


Testing instructions:

Verify that #14849 is fixed:

  1. Navigate to Posts > Add New
  2. Click or otherwise activate the "More Options" menu in the top-right
  3. Press Spacebar (assumes first menu item focused)
  4. Note focus loss

Verify there are no changes in behavior from other Popover usage, particularly specialized cases such as:

  • Clicking a DropdownMenu toggle button should toggle itself closed when currently opened
  • Clicking a result in the link suggestions autocomplete does not dismiss the URL popover

tagName,
{
return (
<IconButton
Copy link
Member

Choose a reason for hiding this comment

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

It's a bit stretched use of IconButton in the case when there is no icon 😅

However, I can't think of another simple way of handling it where this doesn't trigger re-render of the button. I noticed that this behavior is broken only because we swap IconButton with Button for no reason when only the icon gets removed/added.

Copy link
Member

Choose a reason for hiding this comment

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

Yep, exactly! Switching between the two components causes React to remount the component and lose focus. Looking at IconButton, it seemed to me that icon={} is an optional prop so I felt comfortable doing it this way.

Copy link
Contributor

Choose a reason for hiding this comment

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

One thing we keep discussing here and there is why not just add an icon prop to the Button block and deprecate the IconButton (or make it just a shortcut)

Copy link
Member

Choose a reason for hiding this comment

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

I'm sure @drw158 @cburton4 have some good ideas on how to approach it :)

I'm fine with anything which makes it less confusing. I guess having more flexible Button and IconButton as a backward-compatible alias makes a lot of sense.

Copy link
Contributor

Choose a reason for hiding this comment

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

We have an issue open to discuss combining IconButton and Button in #16541

We are leaning towards keeping them separate, but I'll look at this closer to understand the disadvantage.

Copy link
Contributor

@davewhitley davewhitley Aug 2, 2019

Choose a reason for hiding this comment

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

I'm assuming from the code that this is an issue because Button does not have an icon option. Switching between icon and non-icon buttons for Menu Items is causing technical problems.

Could this be solved by adding an icon option to the Button component, but still keep IconButton a separate component? That way, the unchecked and checked versions of the Menu Item could use just a single component, Button.

Copy link
Contributor

Choose a reason for hiding this comment

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

To be fair, we already do the opposite here. Not pass the "icon" prop to the IconButton (so it behaves like a Button). If it doesn't make sense to merge the two components into one, I feel we shouldn't do anything specific about it.

@gziolo
Copy link
Member

gziolo commented Aug 1, 2019

@afercia, would you mind running some sanity check for the Manage All Reusable Blocks link in the menu. I have issues confirming that it gets announced as link as expected. I outputted its HTML content and it is still an anchor with role menuitem as expected but I can't confirm it with VoiceOVer.

Aside: yes, I still keep it in mind that we should have no links in the dropdown menu :) I need to find a related issue where @mapk proposes we move it to the Manage Blocks modal and tackle it at last.

Regardless, I think we should move forward with this PR given that it makes More Menu finally usable for keyboard users. The last step to make the experience good is to stop closing this menu when opening modals 🎉

@gziolo
Copy link
Member

gziolo commented Aug 1, 2019

This PR will also partially resolve #15501. There is a remaining issue with focus loss when switching between Code and Visual editor when the post is empty.

@afercia
Copy link
Contributor

afercia commented Aug 1, 2019

Thanks for working on this! @gziolo I tested a bit and seems to me everything works as expected. Just one small thing: should the small horizontal "jump" of menuitems with the check icon be fixed in this PR? See animated GIF:

jump

Regarding the way screen readers announce Manage All Reusable Blocks: it has a menuitem role so it's announced as a "menu button" (or similar, depending on the browser / screen reader). ARIA roles override the native role.

However, it still creates some issues. For example: in VoiceOver, this menuitem is not listed in the "Form Controls" list while the other ones are:

Screenshot 2019-08-01 at 13 40 40

  • open the More menu
  • press Command Option U to open the VoiceOver "rotor"
  • right / left arrows to go to the "Form Controls" panel
  • down / up arrows to browse the list of form controls
  • see that Manage All Reusable Blocks is missing

For clarity, menuitems can be links. However, an ARIA menu is supposed to be either a menu of actions or a navigation menu. When it's a navigation menu, it should be wrapped in a <nav> element, see the W3C example:

Menu or Menu bar design pattern:
https://www.w3.org/TR/wai-aria-practices-1.1/#menu

Navigation Menubar Example:
https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html

Since the menubar presents a site navigation system, it is wrapped in a navigation region implemented with a nav element that has an aria-label that matches the label on the menubar.

The point is that an ARIA menu can't be a mix of the two things. In this case, all the menuitems perform an action in the current page except Manage All Reusable Blocks, which triggers navigation to another page. This is unexpected for users and there's no way to communicate it properly. See #13390

@gziolo
Copy link
Member

gziolo commented Aug 1, 2019

Thanks for working on this! @gziolo I tested a bit and seems to me everything works as expected. Just one small thing: should the small horizontal "jump" of menuitems with the check icon be fixed in this PR?

Good catch, it needs to be fixed, it would be a regression.

@noisysocks
Copy link
Member

Thanks for testing all!

The point is that an ARIA menu can't be a mix of the two things. In this case, all the menuitems perform an action in the current page except Manage All Reusable Blocks, which triggers navigation to another page. This is unexpected for users and there's no way to communicate it properly. See #13390

Thanks for going into this. Let's address the issues with Manage All Reusable Blocks as part of #13390. I agree we should remove the menu item altogether.

Just one small thing: should the small horizontal "jump" of menuitems with the check icon be fixed in this PR?

This bug exists in master, so I'll create a separate PR to fix it. I'm keen to get this PR merged as it is blocking some other work.

@noisysocks noisysocks merged commit 5b01c97 into master Aug 2, 2019
@noisysocks noisysocks deleted the remove/popover-on-click-outside branch August 2, 2019 01:46
@gziolo gziolo added this to the Gutenberg 6.3 milestone Aug 6, 2019
gziolo pushed a commit that referenced this pull request Aug 29, 2019
…14851)

* Components: Add onFocusOutside alternative to Popover onClickOutside

* Components: Refactor Dropdown to use onFocusOutside

* Format Library: Refactor inline link to use onFocusOutside

* MenuItem: Always use an IconButton component so as to avoid focus loss.

Switching between `Button` and `IconButton` causes React to remount the
`MenuItem` component. This causes a focus loss as well as E2E failures.

There's no need to use a `Button` for `MenuItems` that are unselected,
since we can simply pass `icon={ undefined }` to the `IconButton`.
gziolo pushed a commit that referenced this pull request Aug 29, 2019
…14851)

* Components: Add onFocusOutside alternative to Popover onClickOutside

* Components: Refactor Dropdown to use onFocusOutside

* Format Library: Refactor inline link to use onFocusOutside

* MenuItem: Always use an IconButton component so as to avoid focus loss.

Switching between `Button` and `IconButton` causes React to remount the
`MenuItem` component. This causes a focus loss as well as E2E failures.

There's no need to use a `Button` for `MenuItems` that are unselected,
since we can simply pass `icon={ undefined }` to the `IconButton`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] UI Components Impacts or related to the UI component system [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). [Package] Components /packages/components
Projects
None yet
6 participants