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

Allow Modals to optionally ignore the Close button when determining where to place focus #54296

Closed
wants to merge 26 commits into from

Conversation

getdave
Copy link
Contributor

@getdave getdave commented Sep 8, 2023

What?

Updates all instances of <Modal> in order that they ignore the Close button when determining where to place focus when mounting.

Alternative to #54590

Closes #54106

Why?

As discussed in #54106 dialogs (including Modal) should ideally place focus on the first focusable element on mount. Currently all Modals find that the Close button is the first element in the DOM inside the Modal that is focusable and thus it receives focus. A11y feedback has been that this is unhelpful.

How?

Updates the useFocusOnMount hook API to (optionally) accept a callback which is passed a list of "tabbable" elements within the Modal. The consumer can then use this to programmatically select the element which should received focus.

Previously the API only allowed for "first element".

The <Modal> component is then updated to utilise this API to ensure that it selects the first focusable element which is not the Close button

This appraoch is inline with that @ciampo suggested in #54106 (comment).

Questions/Concerns

  • this change has a wide scope of impact on all Modals. Should we first test it in isolation the Block Rename Modal instance and if it's ok then roll out to the underlying component?
  • @youknowriad cautioned that exposing such an "open" API could be a concern for backwards compatibility. We should also explore whether it's possible to simply this by moving the Close button outside of the focusOnMount ref whilst remaining within the constrained tabbing ref. @ciampo felt this would be very difficult however.
  • what should happen if there are no other focusable nodes other than the Close button? Is that the developer's problem? Or should we focus the Close button in that case?
  • how do I fix the lint warning where it complains that focusOnMount.current is not callable?
  • how can we simplify the code to avoid duplicating the setTimeout() portion?

Testing Instructions

  • New Post
  • Add some blocks and then Group them.
  • Open List View
  • Click options menu on Griup block and click Rename.
  • See focus placed on input and not on Close button
  • verify it's still possible to reach the Close button and constrained tabbing is still active
  • open some other Modals and verify the behaviour still makes sense in other contexts.

Testing Instructions for Keyboard

Screenshots or screencast

Screen.Capture.on.2023-09-08.at.11-43-30.mp4

@getdave getdave self-assigned this Sep 8, 2023
@getdave getdave added [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). [Package] Components /packages/components [Feature] Component System WordPress component system labels Sep 8, 2023
@getdave
Copy link
Contributor Author

getdave commented Sep 8, 2023

Also pinging @andrewhayward who was involved in original discussions.

packages/compose/README.md Outdated Show resolved Hide resolved
timerId.current = setTimeout( () => {
const tabbables = focus.tabbable.find( node );

const elementToFocus = focusOnMountRef.current( tabbables );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What should happen if tabbables or elementToFocus is empty?

Copy link
Contributor

@draganescu draganescu Sep 11, 2023

Choose a reason for hiding this comment

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

Focus on close? I figure this works for when modal is a dialog for info only?
nothing.

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 wonder if we should provide a better DX by console.info or something? Might be overthinking...

// Modals should ignore the `Close` button which is the first focusable element.
// Remap `true` to select the next focusable element instead.
const focusOnMountRef = useFocusOnMount(
focusOnMount === true ? focusFirstNonCloseButtonElement : focusOnMount
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should throw an error or something because this code inadvertantly supports functions as props for the Modal component.

Copy link
Contributor

Choose a reason for hiding this comment

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

what if the focusOnMount prop of the Modal component also supported passing a callback, same way as the hook?

Copy link
Contributor Author

@getdave getdave Sep 8, 2023

Choose a reason for hiding this comment

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

It did that until 1f71011 😄

See #54296 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

I wanted to avoid opening too much APIs but I guess I may be on the minority here. It's fine.

Copy link
Contributor

@ciampo ciampo Sep 8, 2023

Choose a reason for hiding this comment

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

I see!

To recap: until this PR, if I'm not mistaken:

  • passing false would disable the focus on mount functionality
  • passing true would focus the Modal's content wrapper
  • passing firstElement would focus the first focusable element in terms of DOM order inside the Modal (ie. usually the close button)

What we could do, could be to:

We could also start without the callback option and do everything else, and add the callback only if we find it to be needed in real world usage.

Copy link
Contributor Author

@getdave getdave Sep 8, 2023

Choose a reason for hiding this comment

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

add the callback only if we find it to be needed in real world usage.

I'd agree with that.

...tweak the behaviour when passing firstElement, focusing the first element ignoring the header

Is it counter intuitive to have firstElement not actually focus the first element though?

I wanted to avoid opening too much APIs but I guess I may be on the minority here. It's fine.

I would also like to avoid opening up too many APIs. Let's start with the minimal changes we need to allow us to address the issue and then consider opening up more if/when use cases become apparent.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it counter intuitive to have firstElement not actually focus the first element though?

We could specify that it will focus the first element inside the Modal's content (ie. excluding the header)?

I would also like to avoid opening up too many APIs. Let's start with the minimal changes we need to allow us to address the issue and then consider opening up more if/when use cases become apparent.

Sounds good.

Copy link
Contributor

Choose a reason for hiding this comment

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

tweak the behaviour when passing firstElement, focusing the first element ignoring the header
We could specify that it will focus the first element inside the Modal's content (ie. excluding the header)?

Yes, that's my thinking too. Changing the "true" behavior is more disruptive and is harder to explain.

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've updated the code. However the following comment still applies right?

I wonder if we should throw an error or something because this code inadvertantly supports functions as props for the Modal component.

So if someone passes a function as focusOnMount to the Modal shall we coerce to true and then console.warn or just throw?

Copy link
Contributor

Choose a reason for hiding this comment

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

Both the docs and the TypeScript types should suggest that the focusOnMount prop doesn't support a function — in that sense, I don't think that any action is required. If folks want to try hacking around and pass a function, they're deciding to use the component in an unsupported way and they may incur in runtime errors.

The same would happen on any other component when passing prop values that are not supported

@andrewhayward
Copy link
Contributor

I said this in a comment, but just for greater visibility, is it explicitly the 'close' button we want to skip, or any actionable header item? Is the intent actually to skip anything in the "top corner", and find the first focusable "content" node?

A modal dialog with a "star" button instead of a close button, circled in red with a question mark next to it.

@ciampo
Copy link
Contributor

ciampo commented Sep 11, 2023

One more thought — is it possible that a Modal component with focusOnMount = "firstElement" could be rendered with just the close button as the interactive element? In that case, I believe that the close button should still be focused.

Basically, we should not "ignore" the close button if it's only available tabbable element.

@getdave
Copy link
Contributor Author

getdave commented Sep 11, 2023

One more thought — is it possible that a Modal component with focusOnMount = "firstElement" could be rendered with just the close button as the interactive element? In that case, I believe that the close button should still be focused.

Basically, we should not "ignore" the close button if it's only available tabbable element.

@ciampo Yes I agree. As mentioned under Questions in the description I was concerned abiout this. I think your suggestion is correct.

@getdave getdave changed the title Update all Modals to ignore the Close button when determining where to place focus Allow Modals to optionally ignore the Close button when determining where to place focus Sep 11, 2023
Comment on lines +109 to +111
const focusOnMountRef = useFocusOnMount(
focusOnMount === 'firstElement' ? getFirstTabbableElement : focusOnMount
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note with this change consumers of Modal will have to opt into this behaviour by passing firstElement. The default of Modal is true.

@github-actions
Copy link

github-actions bot commented Sep 11, 2023

Size Change: +1.26 kB (0%)

Total Size: 1.52 MB

Filename Size Change
build/block-editor/index.min.js 216 kB +75 B (0%)
build/block-library/blocks/query/editor-rtl.css 478 B +28 B (+6%) 🔍
build/block-library/blocks/query/editor.css 477 B +28 B (+6%) 🔍
build/block-library/blocks/query/view.min.js 555 B -4 B (-1%)
build/block-library/editor-rtl.css 12.1 kB +17 B (0%)
build/block-library/editor.css 12.1 kB +17 B (0%)
build/block-library/index.min.js 204 kB +310 B (0%)
build/components/index.min.js 255 kB -158 B (0%)
build/compose/index.min.js 12.1 kB +31 B (0%)
build/core-data/index.min.js 16.8 kB +35 B (0%)
build/edit-post/index.min.js 35.5 kB +66 B (0%)
build/edit-site/index.min.js 91.8 kB +653 B (+1%)
build/edit-site/style-rtl.css 13.5 kB +56 B (0%)
build/edit-site/style.css 13.5 kB +59 B (0%)
build/edit-widgets/index.min.js 16.9 kB +25 B (0%)
build/edit-widgets/style-rtl.css 4.8 kB -5 B (0%)
build/edit-widgets/style.css 4.79 kB -4 B (0%)
build/editor/index.min.js 45.5 kB +27 B (0%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 955 B
build/annotations/index.min.js 2.69 kB
build/api-fetch/index.min.js 2.28 kB
build/autop/index.min.js 2.1 kB
build/blob/index.min.js 451 B
build/block-directory/index.min.js 7.01 kB
build/block-directory/style-rtl.css 1.02 kB
build/block-directory/style.css 1.02 kB
build/block-editor/content-rtl.css 4.25 kB
build/block-editor/content.css 4.24 kB
build/block-editor/default-editor-styles-rtl.css 381 B
build/block-editor/default-editor-styles.css 381 B
build/block-editor/style-rtl.css 15.1 kB
build/block-editor/style.css 15 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 90 B
build/block-library/blocks/archives/style.css 90 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 122 B
build/block-library/blocks/audio/style.css 122 B
build/block-library/blocks/audio/theme-rtl.css 126 B
build/block-library/blocks/audio/theme.css 126 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 104 B
build/block-library/blocks/avatar/style.css 104 B
build/block-library/blocks/block/editor-rtl.css 305 B
build/block-library/blocks/block/editor.css 305 B
build/block-library/blocks/button/editor-rtl.css 584 B
build/block-library/blocks/button/editor.css 582 B
build/block-library/blocks/button/style-rtl.css 629 B
build/block-library/blocks/button/style.css 628 B
build/block-library/blocks/buttons/editor-rtl.css 337 B
build/block-library/blocks/buttons/editor.css 337 B
build/block-library/blocks/buttons/style-rtl.css 332 B
build/block-library/blocks/buttons/style.css 332 B
build/block-library/blocks/calendar/style-rtl.css 239 B
build/block-library/blocks/calendar/style.css 239 B
build/block-library/blocks/categories/editor-rtl.css 113 B
build/block-library/blocks/categories/editor.css 112 B
build/block-library/blocks/categories/style-rtl.css 124 B
build/block-library/blocks/categories/style.css 124 B
build/block-library/blocks/code/editor-rtl.css 53 B
build/block-library/blocks/code/editor.css 53 B
build/block-library/blocks/code/style-rtl.css 121 B
build/block-library/blocks/code/style.css 121 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 421 B
build/block-library/blocks/columns/style.css 421 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 199 B
build/block-library/blocks/comment-template/style.css 198 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 840 B
build/block-library/blocks/comments/editor.css 839 B
build/block-library/blocks/comments/style-rtl.css 637 B
build/block-library/blocks/comments/style.css 636 B
build/block-library/blocks/cover/editor-rtl.css 647 B
build/block-library/blocks/cover/editor.css 650 B
build/block-library/blocks/cover/style-rtl.css 1.69 kB
build/block-library/blocks/cover/style.css 1.68 kB
build/block-library/blocks/details/editor-rtl.css 65 B
build/block-library/blocks/details/editor.css 65 B
build/block-library/blocks/details/style-rtl.css 98 B
build/block-library/blocks/details/style.css 98 B
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 410 B
build/block-library/blocks/embed/style.css 410 B
build/block-library/blocks/embed/theme-rtl.css 126 B
build/block-library/blocks/embed/theme.css 126 B
build/block-library/blocks/file/editor-rtl.css 316 B
build/block-library/blocks/file/editor.css 316 B
build/block-library/blocks/file/style-rtl.css 311 B
build/block-library/blocks/file/style.css 312 B
build/block-library/blocks/file/view.min.js 318 B
build/block-library/blocks/footnotes/style-rtl.css 201 B
build/block-library/blocks/footnotes/style.css 199 B
build/block-library/blocks/freeform/editor-rtl.css 2.61 kB
build/block-library/blocks/freeform/editor.css 2.61 kB
build/block-library/blocks/gallery/editor-rtl.css 947 B
build/block-library/blocks/gallery/editor.css 952 B
build/block-library/blocks/gallery/style-rtl.css 1.53 kB
build/block-library/blocks/gallery/style.css 1.53 kB
build/block-library/blocks/gallery/theme-rtl.css 108 B
build/block-library/blocks/gallery/theme.css 108 B
build/block-library/blocks/group/editor-rtl.css 654 B
build/block-library/blocks/group/editor.css 654 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 336 B
build/block-library/blocks/html/editor.css 337 B
build/block-library/blocks/image/editor-rtl.css 834 B
build/block-library/blocks/image/editor.css 833 B
build/block-library/blocks/image/style-rtl.css 1.42 kB
build/block-library/blocks/image/style.css 1.41 kB
build/block-library/blocks/image/theme-rtl.css 126 B
build/block-library/blocks/image/theme.css 126 B
build/block-library/blocks/image/view-interactivity.min.js 1.83 kB
build/block-library/blocks/latest-comments/style-rtl.css 357 B
build/block-library/blocks/latest-comments/style.css 357 B
build/block-library/blocks/latest-posts/editor-rtl.css 213 B
build/block-library/blocks/latest-posts/editor.css 212 B
build/block-library/blocks/latest-posts/style-rtl.css 478 B
build/block-library/blocks/latest-posts/style.css 478 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 505 B
build/block-library/blocks/media-text/style.css 503 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 668 B
build/block-library/blocks/navigation-link/editor.css 669 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 296 B
build/block-library/blocks/navigation-submenu/editor.css 295 B
build/block-library/blocks/navigation/editor-rtl.css 2.26 kB
build/block-library/blocks/navigation/editor.css 2.26 kB
build/block-library/blocks/navigation/style-rtl.css 2.23 kB
build/block-library/blocks/navigation/style.css 2.22 kB
build/block-library/blocks/navigation/view.min.js 984 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 401 B
build/block-library/blocks/page-list/editor.css 401 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 235 B
build/block-library/blocks/paragraph/editor.css 235 B
build/block-library/blocks/paragraph/style-rtl.css 335 B
build/block-library/blocks/paragraph/style.css 335 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 508 B
build/block-library/blocks/post-comments-form/style.css 508 B
build/block-library/blocks/post-date/style-rtl.css 61 B
build/block-library/blocks/post-date/style.css 61 B
build/block-library/blocks/post-excerpt/editor-rtl.css 71 B
build/block-library/blocks/post-excerpt/editor.css 71 B
build/block-library/blocks/post-excerpt/style-rtl.css 141 B
build/block-library/blocks/post-excerpt/style.css 141 B
build/block-library/blocks/post-featured-image/editor-rtl.css 588 B
build/block-library/blocks/post-featured-image/editor.css 586 B
build/block-library/blocks/post-featured-image/style-rtl.css 319 B
build/block-library/blocks/post-featured-image/style.css 319 B
build/block-library/blocks/post-navigation-link/style-rtl.css 215 B
build/block-library/blocks/post-navigation-link/style.css 214 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 314 B
build/block-library/blocks/post-template/style.css 314 B
build/block-library/blocks/post-terms/style-rtl.css 96 B
build/block-library/blocks/post-terms/style.css 96 B
build/block-library/blocks/post-time-to-read/style-rtl.css 69 B
build/block-library/blocks/post-time-to-read/style.css 69 B
build/block-library/blocks/post-title/style-rtl.css 100 B
build/block-library/blocks/post-title/style.css 100 B
build/block-library/blocks/preformatted/style-rtl.css 125 B
build/block-library/blocks/preformatted/style.css 125 B
build/block-library/blocks/pullquote/editor-rtl.css 135 B
build/block-library/blocks/pullquote/editor.css 135 B
build/block-library/blocks/pullquote/style-rtl.css 335 B
build/block-library/blocks/pullquote/style.css 335 B
build/block-library/blocks/pullquote/theme-rtl.css 168 B
build/block-library/blocks/pullquote/theme.css 168 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 302 B
build/block-library/blocks/query-pagination/style.css 299 B
build/block-library/blocks/query-title/style-rtl.css 63 B
build/block-library/blocks/query-title/style.css 63 B
build/block-library/blocks/query/style-rtl.css 370 B
build/block-library/blocks/query/style.css 368 B
build/block-library/blocks/quote/style-rtl.css 222 B
build/block-library/blocks/quote/style.css 222 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 149 B
build/block-library/blocks/rss/editor.css 149 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 178 B
build/block-library/blocks/search/editor.css 178 B
build/block-library/blocks/search/style-rtl.css 607 B
build/block-library/blocks/search/style.css 607 B
build/block-library/blocks/search/theme-rtl.css 114 B
build/block-library/blocks/search/theme.css 114 B
build/block-library/blocks/search/view.min.js 468 B
build/block-library/blocks/separator/editor-rtl.css 146 B
build/block-library/blocks/separator/editor.css 146 B
build/block-library/blocks/separator/style-rtl.css 234 B
build/block-library/blocks/separator/style.css 234 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 323 B
build/block-library/blocks/shortcode/editor.css 323 B
build/block-library/blocks/site-logo/editor-rtl.css 754 B
build/block-library/blocks/site-logo/editor.css 754 B
build/block-library/blocks/site-logo/style-rtl.css 204 B
build/block-library/blocks/site-logo/style.css 204 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 116 B
build/block-library/blocks/site-title/editor.css 116 B
build/block-library/blocks/site-title/style-rtl.css 57 B
build/block-library/blocks/site-title/style.css 57 B
build/block-library/blocks/social-link/editor-rtl.css 184 B
build/block-library/blocks/social-link/editor.css 184 B
build/block-library/blocks/social-links/editor-rtl.css 682 B
build/block-library/blocks/social-links/editor.css 681 B
build/block-library/blocks/social-links/style-rtl.css 1.44 kB
build/block-library/blocks/social-links/style.css 1.43 kB
build/block-library/blocks/spacer/editor-rtl.css 348 B
build/block-library/blocks/spacer/editor.css 348 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 432 B
build/block-library/blocks/table/editor.css 432 B
build/block-library/blocks/table/style-rtl.css 639 B
build/block-library/blocks/table/style.css 639 B
build/block-library/blocks/table/theme-rtl.css 146 B
build/block-library/blocks/table/theme.css 146 B
build/block-library/blocks/tag-cloud/style-rtl.css 251 B
build/block-library/blocks/tag-cloud/style.css 253 B
build/block-library/blocks/template-part/editor-rtl.css 403 B
build/block-library/blocks/template-part/editor.css 403 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/term-description/style-rtl.css 111 B
build/block-library/blocks/term-description/style.css 111 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 99 B
build/block-library/blocks/verse/style.css 99 B
build/block-library/blocks/video/editor-rtl.css 552 B
build/block-library/blocks/video/editor.css 555 B
build/block-library/blocks/video/style-rtl.css 185 B
build/block-library/blocks/video/style.css 185 B
build/block-library/blocks/video/theme-rtl.css 126 B
build/block-library/blocks/video/theme.css 126 B
build/block-library/classic-rtl.css 179 B
build/block-library/classic.css 179 B
build/block-library/common-rtl.css 1.1 kB
build/block-library/common.css 1.1 kB
build/block-library/editor-elements-rtl.css 75 B
build/block-library/editor-elements.css 75 B
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/reset-rtl.css 472 B
build/block-library/reset.css 472 B
build/block-library/style-rtl.css 13.9 kB
build/block-library/style.css 13.9 kB
build/block-library/theme-rtl.css 688 B
build/block-library/theme.css 693 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.87 kB
build/blocks/index.min.js 51.4 kB
build/commands/index.min.js 15.5 kB
build/commands/style-rtl.css 921 B
build/commands/style.css 918 B
build/components/style-rtl.css 11.7 kB
build/components/style.css 11.7 kB
build/core-commands/index.min.js 2.6 kB
build/customize-widgets/index.min.js 12 kB
build/customize-widgets/style-rtl.css 1.48 kB
build/customize-widgets/style.css 1.48 kB
build/data-controls/index.min.js 640 B
build/data/index.min.js 8.84 kB
build/date/index.min.js 17.8 kB
build/deprecated/index.min.js 451 B
build/dom-ready/index.min.js 324 B
build/dom/index.min.js 4.64 kB
build/edit-post/classic-rtl.css 544 B
build/edit-post/classic.css 545 B
build/edit-post/style-rtl.css 7.84 kB
build/edit-post/style.css 7.83 kB
build/editor/style-rtl.css 3.53 kB
build/editor/style.css 3.52 kB
build/element/index.min.js 4.82 kB
build/escape-html/index.min.js 537 B
build/format-library/index.min.js 7.71 kB
build/format-library/style-rtl.css 554 B
build/format-library/style.css 553 B
build/hooks/index.min.js 1.55 kB
build/html-entities/index.min.js 448 B
build/i18n/index.min.js 3.58 kB
build/interactivity/index.min.js 11.3 kB
build/is-shallow-equal/index.min.js 527 B
build/keyboard-shortcuts/index.min.js 1.72 kB
build/keycodes/index.min.js 1.87 kB
build/list-reusable-blocks/index.min.js 2.2 kB
build/list-reusable-blocks/style-rtl.css 836 B
build/list-reusable-blocks/style.css 836 B
build/media-utils/index.min.js 2.9 kB
build/notices/index.min.js 948 B
build/nux/index.min.js 1.99 kB
build/nux/style-rtl.css 735 B
build/nux/style.css 732 B
build/patterns/index.min.js 2.7 kB
build/patterns/style-rtl.css 240 B
build/patterns/style.css 240 B
build/plugins/index.min.js 1.79 kB
build/preferences-persistence/index.min.js 1.84 kB
build/preferences/index.min.js 1.24 kB
build/primitives/index.min.js 943 B
build/priority-queue/index.min.js 1.52 kB
build/private-apis/index.min.js 958 B
build/react-i18n/index.min.js 615 B
build/react-refresh-entry/index.min.js 9.47 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.7 kB
build/reusable-blocks/index.min.js 2.7 kB
build/reusable-blocks/style-rtl.css 243 B
build/reusable-blocks/style.css 243 B
build/rich-text/index.min.js 10.2 kB
build/router/index.min.js 1.78 kB
build/server-side-render/index.min.js 1.94 kB
build/shortcode/index.min.js 1.39 kB
build/style-engine/index.min.js 1.97 kB
build/sync/index.min.js 53.8 kB
build/token-list/index.min.js 582 B
build/url/index.min.js 3.73 kB
build/vendors/inert-polyfill.min.js 2.48 kB
build/vendors/react-dom.min.js 41.8 kB
build/vendors/react.min.js 4.02 kB
build/viewport/index.min.js 958 B
build/warning/index.min.js 249 B
build/widgets/index.min.js 7.16 kB
build/widgets/style-rtl.css 1.15 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.02 kB

compressed-size-action

@getdave getdave force-pushed the try/augment-focus-on-mount-api branch from f506d7e to 0403a21 Compare September 11, 2023 11:16
@github-actions
Copy link

github-actions bot commented Sep 11, 2023

Flaky tests detected in 9fec0cf.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6170906328
📝 Reported issues:

Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Great job so far! I tested quickly on my local storybook instance and the Modal component seems to behave as advertised.

I've got a couple of extra comments, on top of the inline ones:

  1. We could improve the Storybook controls to make it easier to test for different values of focusOnMount
Here's how:
diff --git a/packages/components/src/modal/stories/index.story.tsx b/packages/components/src/modal/stories/index.story.tsx
index 8405a6eb01..fe43db1ad6 100644
--- a/packages/components/src/modal/stories/index.story.tsx
+++ b/packages/components/src/modal/stories/index.story.tsx
@@ -28,7 +28,8 @@ const meta: Meta< typeof Modal > = {
 			control: { type: null },
 		},
 		focusOnMount: {
-			control: { type: 'boolean' },
+			options: [ true, false, 'firstElement' ],
+			control: { type: 'select' },
 		},
 		role: {
 			control: { type: 'text' },
  1. I think that we should add a couple of unit tests to certify the behaviour:
  • focusOnMount = true focuses the modal wrapper
  • focusOnMount = false doesn't move focus
  • focusOnMount = firstElement moves focus to the first element in the modal content (ignoring header)
  • focusOnMount = firstElement moves focus to the close button if there aren't any focusable elements in the modal content

packages/components/src/modal/README.md Outdated Show resolved Hide resolved
packages/compose/src/hooks/use-focus-on-mount/index.js Outdated Show resolved Hide resolved
// Modals should ignore the `Close` button which is the first focusable element.
// Remap `true` to select the next focusable element instead.
const focusOnMountRef = useFocusOnMount(
focusOnMount === true ? focusFirstNonCloseButtonElement : focusOnMount
Copy link
Contributor

Choose a reason for hiding this comment

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

Both the docs and the TypeScript types should suggest that the focusOnMount prop doesn't support a function — in that sense, I don't think that any action is required. If folks want to try hacking around and pass a function, they're deciding to use the component in an unsupported way and they may incur in runtime errors.

The same would happen on any other component when passing prop values that are not supported

packages/compose/src/hooks/use-focus-on-mount/index.js Outdated Show resolved Hide resolved
packages/compose/src/hooks/use-focus-on-mount/index.js Outdated Show resolved Hide resolved
@andrewhayward
Copy link
Contributor

  • focusOnMount = true focuses the modal wrapper
  • focusOnMount = false doesn't move focus
  • focusOnMount = firstElement moves focus to the first element in the modal content (ignoring header)
  • focusOnMount = firstElement moves focus to the close button if there aren't any focusable elements in the modal content

I'd not really thought about it until it was spelled out explicitly, but what is the scenario that we're supporting where focus shouldn't move (focusOnMount={false})? If a dialog is opened modally, it should by definition be the only content actively available on the page, so there should be nowhere else for the focus to go.

@getdave getdave force-pushed the try/augment-focus-on-mount-api branch from 5ecb1b6 to 22e1d8c Compare September 12, 2023 10:18
@getdave getdave added the Needs Dev Note Requires a developer note for a major WordPress release cycle label Sep 12, 2023
@getdave
Copy link
Contributor Author

getdave commented Sep 12, 2023

Note to self that I still need to update the Storybook entries.

Also to fix the e2e test.

@getdave
Copy link
Contributor Author

getdave commented Sep 13, 2023

@alexstine Just flagging this PR. This allows consumers of Modal to optionally opt-in to having the first element within the contents of the Modal (i.e. not the Close button) be focused on mount.

We've achieved this by changing the behaviour of focusOnMount prop of Modal so that if you pass firstElement it will exhibit the behaviour described above.

The behaviour of true (focus on the first focusable thing - likely the dialog itself) and false (don't move focus at all) are preserved as they are curently on trunk.

Note that contributors have to opt into this behaviour by passing firstElement.

getdave and others added 2 commits September 13, 2023 13:25
Co-authored-by: Ben Dwyer <ben@scruffian.com>
Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Code changes LGTM and test well as per instructions 🚀

Left a couple of extra comments, feel free to merge once those are addressed :)

Thank you for working on this!

@@ -2,6 +2,10 @@

## Unreleased

### Breaking changes
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually not sure I would call this a breaking change, maybe more of an enhancement?

packages/components/src/modal/index.tsx Outdated Show resolved Hide resolved

// If focusOnMount is `firstElement`, Modals should ignore the `Close` button which is the first focusable element.
// Remap `true` to select the next focusable element instead.
const focusOnMountRef = useFocusOnMount(
Copy link
Member

Choose a reason for hiding this comment

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

Forgive me if it's already mentioned above but it's a long PR and I'm not on my laptop 😅 .

Could we instead just assign the focusOnMountRef to the <div> with childrenContainerRef on L320 when focusOnMount === 'firstElement'? This way we avoid the use-focusOnMount API change and the awkward CSS class name selector etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

That would also change the focus behaviour when the focusOnMount prop is set to true, because the children container would gain focus instead of the dialog container — in my opinion, that behaviour is not correct as we'd like to focus the dialog component.

Copy link
Member

Choose a reason for hiding this comment

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

when focusOnMount === 'firstElement'

I mean only change the ref callback to be assigned to the children container div if and only if foucsOnMount === 'firstElement'. Is it still going to be incorrect?

Copy link
Contributor

Choose a reason for hiding this comment

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

when focusOnMount === 'firstElement'

I mean only change the ref callback to be assigned to the children container div if and only if foucsOnMount === 'firstElement'. Is it still going to be incorrect?

I wouldn't know off the top of my head, but I guess we could try the approach keeping the current set of unit tests and see how it goes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would this give us the flexibility to find the first focusable node outside of the content wrapper if there's nothing appropriate within it? Feels like we still need the "global" context, and moving the ref would take that away.

Copy link
Member

Choose a reason for hiding this comment

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

True! FWIW, I would prefer to add a new type like firstContentElement instead of patching firstElement. It's also clearer for folks that don't read the doc/changelog or have false (but fair) assumptions.

That said, I don't really have a strong preference here. I'm just sharing a suggestion, and I'm okay with whatever solution we end up with! ❤️

Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer to add a new type like firstContentElement instead of patching firstElement

That could also be an option, I wouldn't be opposed to that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using firstContentElement would avoid regressions. I'm tempted to try that approach next week alongside the suggestion to conditionally move the ref when firstContentElement is set.

I knew this wouldn't be easy 😅

Copy link
Member

Choose a reason for hiding this comment

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

You all are doing a great job! Happy to help in any way when I have the capacity! 💪

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kevin940726 Alternative now available in #54590

Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com>
@ciampo
Copy link
Contributor

ciampo commented Sep 21, 2023

Closing in favour of #54590

@ciampo ciampo closed this Sep 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Component System WordPress component system [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). Needs Dev Note Requires a developer note for a major WordPress release cycle [Package] Components /packages/components
Projects
Development

Successfully merging this pull request may close these issues.

Modal itself receiving focus rather than first tabbable element
7 participants