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

Improve the placeholder instructions accessibility. #45801

Merged
merged 17 commits into from Sep 21, 2023

Conversation

afercia
Copy link
Contributor

@afercia afercia commented Nov 16, 2022

Fixes #42738

What?

#38366 made sure the placeholder instructions are announced by screen readers. It did so by using a <fieldset> element with a <legend> element that holds the instructions text.
However, that appears to be not ideal for the reasons explained in #42738. I'd like to propose a cleaner approach.

Why?

  • A fieldset element establishes a group that groups logically related form controls. A legend element is meant to give the group a meaningful name. It's not the right place for instructions.
  • Some screen readers (e.g. VoiceOver) read out the legend multiple times, each time a form control within the fieldset receives focus. That's very noisy for screen reader users.
  • Some blocks (e.g. the 'Table of contents' block) do have instructions text but don't contain form controls and there's no action to take. The fieldset and legend are rendered regardless. That's a named group that doesn't contain anything.
  • Other blocks don't contain form controls depending on their state, see Improve Placeholder instructions accessibility #42738 (comment). Also in this case a fieldset and legend aren't ideal.
  • Lastly, third-party blocks may provide a placeholder with any kind of content, even with forms and fieldsets, which would result in nested fieldsets. While that's allowed in HTML, it's problematic for assistive technologies.

How?

The only goal of the fieldset and legend added in 38366 was to make the instructions announced by screen readers. It's safe to remove them and provide another way to make screen readers announce the instructions. What this PR does:

  • Removes the fieldset and legend.
  • Adds an ARIA role=status to the instructions so that they're announced just once, when inserting the block.
  • Adjusts the CSS.
  • Adjusts the tests accordingly.

Note: a role=status establishes a live region, that is a region whose content is supposed to be updated. Strictly speaking the usage ot role=status in this case is slightly unsemantic because the instructions text doesn't update.

Testing Instructions

  • Test with a screen reader e.g. Safari + VoiceOver (on macOS) or Firefox + NVDA (on Windows).
  • Edit a post or create a new one.
  • Insert a block that has a Placeholder with instructions, e.g. an Image block.
  • Check that the screen reader announces the instructions.
  • Add a block that doesn't have form controls within the Placeholder, e.g. a Table of Contents block.
  • Check that the screen reader announces the instructions.

Without a screen reader:

  • Check the instructions text container has a role="status" attribute.

Screenshots or screencast

@codesandbox
Copy link

codesandbox bot commented Nov 16, 2022

CodeSandbox logoCodeSandbox logo  Open in CodeSandbox Web Editor | VS Code | VS Code Insiders

@github-actions
Copy link

github-actions bot commented Nov 16, 2022

Size Change: -44 B (0%)

Total Size: 1.62 MB

Filename Size Change
build/components/index.min.js 255 kB -7 B (0%)
build/components/style-rtl.css 11.7 kB -18 B (0%)
build/components/style.css 11.7 kB -19 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.03 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/index.min.js 217 kB
build/block-editor/style-rtl.css 15.4 kB
build/block-editor/style.css 15.4 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 189 B
build/block-library/blocks/heading/style.css 189 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.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 1 kB
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/editor-rtl.css 486 B
build/block-library/blocks/query/editor.css 486 B
build/block-library/blocks/query/style-rtl.css 375 B
build/block-library/blocks/query/style.css 372 B
build/block-library/blocks/query/view.min.js 555 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/editor-rtl.css 12.2 kB
build/block-library/editor.css 12.1 kB
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/index.min.js 205 kB
build/block-library/reset-rtl.css 472 B
build/block-library/reset.css 472 B
build/block-library/style-rtl.css 14 kB
build/block-library/style.css 14 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.3 kB
build/commands/index.min.js 15.5 kB
build/commands/style-rtl.css 921 B
build/commands/style.css 918 B
build/compose/index.min.js 12.7 kB
build/core-commands/index.min.js 2.6 kB
build/core-data/index.min.js 17 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/index.min.js 35.5 kB
build/edit-post/style-rtl.css 7.84 kB
build/edit-post/style.css 7.83 kB
build/edit-site/index.min.js 185 kB
build/edit-site/style-rtl.css 13.9 kB
build/edit-site/style.css 13.9 kB
build/edit-widgets/index.min.js 17 kB
build/edit-widgets/style-rtl.css 4.8 kB
build/edit-widgets/style.css 4.79 kB
build/editor/index.min.js 45.6 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.75 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 3.52 kB
build/patterns/style-rtl.css 302 B
build/patterns/style.css 302 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 975 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

Copy link
Contributor

@alexstine alexstine left a comment

Choose a reason for hiding this comment

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

@afercia Thanks for the PR but sadly this does not work well for Windows screen readers. I gave this a quick test in Firefox.

  1. Enable NVDA.
  2. Add a new post in Firefox.
  3. Insert an image block.
  4. Focus is placed on upload button which I believe is why the message is not being read. role="status" uses aria-live="polite" and because the useFocusFirstElement() hook fires, it cancels out the polite speech.
    https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_role

Any other ideas?

@afercia
Copy link
Contributor Author

afercia commented Nov 17, 2022

@alexstine thanks for testing! Sad it doesn't work for Windows sr.

In the latest commit I took a different approach. The instructions text is now passed to a11y.speak and debounced a bit.
I tested in depth with the Windows screen readers I can test with:

  • Works with Firefox and NVDA
  • Works with Chrome and NVDA
  • Works with Chrome and JAWS 2019
  • Does not work with JAWS 2019 and Firefox. Note that all live regions seem to not work with this combo, e.g. when searching and adding Tags to a post. Can't test with more recent versions of JAWS.

I'd appreciate some quick test.

Note: I'm not sure there's a way to avoid the instructions to be read again when transforming a block. For example, when transforming an Image block to a Columns block (which actually wraps the Image within a Column block), it appears the Image block re-mounts and useEffect runs again. I'd tend to think it's an edge case and a minor annoyance. /Cc @youknowriad any idea on how to avoid this? When you have a chance 🙏

Copy link
Contributor

@alexstine alexstine left a comment

Choose a reason for hiding this comment

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

@afercia Getting closer. One question below.

if ( instructions ) {
debouncedSpeak( instructions );
}
}, [ instructions, debouncedSpeak ] );
Copy link
Contributor

@alexstine alexstine Nov 17, 2022

Choose a reason for hiding this comment

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

Should this run every time the block is selected? As of now, the instructions are only provided on insert.

Suggested change
}, [ instructions, debouncedSpeak ] );
}, [ debouncedSpeak ] );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, the pre-commit linter forced me to add both props as dependencies. I would have gone with an empty array otherwise.

Copy link
Contributor

Choose a reason for hiding this comment

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

With an empty dependency array, if the instructions were to be updated, they would not be announced again — in that sense, having both dependencies looks correct to me?

Regarding debouncedSpeak, the useDebounce hook internally uses useMemo, and therefore the debounceSpeak dependency should not really change unless the speak function changes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

instructions doesn't change as well, at least there are no cases or dynamic instructions so far. So, regardless f whether we use an empty array or not, the announcement is not repeated. Anyways, the pre-commit linter check requires to add both dependencies :) can't commit without adding them.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ciampo Is there anything we can use in place of useDebounce() to still have the timeout for the speak() but can announce on every render? I guess we could attach onFocus callback and trigger the announcement that way?

Copy link
Contributor

Choose a reason for hiding this comment

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

It sounds like the first step here is to understand the desired behavior — should the announcement be heard or every render? Only when the block is added? Every time the block is focused? Every time the block is focused?

Once there is an agreement there, we can think about what's the best way to implement the desired behaviour

Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally we should hear it when the block is focused, because that way when adding placeholders, saving, then going back some time later to add the actual content, the user will know how to interact with the block.

Also worth considering the use case of interacting with an existing template where all the blocks are already in place; the person that fills in the content might not even be the same as the one who adds the blocks.

If the problem with the fieldset/legend combo is that not all placeholders have controls, I wonder if we could come up with a different markup structure for those, and some sort of flag to let the component know when to use either? This has probably been suggested already but nothing else occurs to me 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

@tellthemachines I think what we really need is to tie the Placeholder and block selection together. That way the announcement is triggered on block select. I am just not sure how to do this with different components at play. Does the Placeholder render if the block is not selected?

Copy link
Contributor

Choose a reason for hiding this comment

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

Placeholder renders whenever a new block is inserted or, after save and reload, if the block hasn't been changed since insertion. It'll render whether the block is focused or not, because it's always meant to be visually present on the page.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tellthemachines Think it is possible to add onFocus callback to the wrapping div and just call the speak() function when focus is actually placed inside the block content?

@skorasaurus skorasaurus added the [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). label Nov 17, 2022
@youknowriad
Copy link
Contributor

it appears the Image block re-mounts and useEffect runs again. I'd tend to think it's an edge case and a minor annoyance. /Cc @youknowriad any idea on how to avoid this? When you have a chance 🙏

It seems to be that it's normal that the instructions are announced again when you transform blocks, instructions could be different between blocks.

@mirka mirka added the [Package] Components /packages/components label Nov 18, 2022
@mirka mirka added this to In progress (owned) ⏳ in WordPress Components via automation Nov 18, 2022
@afercia
Copy link
Contributor Author

afercia commented Nov 18, 2022

when you transform blocks,

Yep, but in the example I made above the Image block isn't actually 'transformed'. It gets wrapped within a Columns block. The Image block instructions will be announced again, even though the actual selected block is the Columns one. It's an edge case and we can maybe live with it, I was just wondering if there's a way to avoid the new announcement.

@youknowriad
Copy link
Contributor

@afercia I don't think there's if we tie the announcement with the component. Even if we tie it to the block instead it's very hard to do as sometimes the "clientId" of the block changes which means there's no way for us to know that it's the same block in all situations.

@afercia
Copy link
Contributor Author

afercia commented Nov 21, 2022

if you go back to edit a post later and it has been some time since you interacted with the block, a reminder on how to interact with it may be needed

@alexstine I'm not sure the Placeholder and instructions are rendered again when editing a block. For example, if you actually set an image on an Image block, save, and then edit again the block, there are no instructions. Even the current fieldset + legend aren't rendered. To my understanding, there's only one case when the instructions are available again and that's when a block is left 'empty' e.g. an Image block with no image. If we opt for making the instructions announced again when editing a block that's totally reasonable but I'm not sure it can rely on the Placeholder component.

Maybe I'm missing something: is there any case where editing a block shows the Placeholder with instructions again?

@alexstine
Copy link
Contributor

@afercia That is actually a valid point. I am not sure there is a block with content with a placeholder. It would be nice to get confirmation on this before merging though.

@getdave @tellthemachines @talldan Any ideas?

@tellthemachines
Copy link
Contributor

I am not sure there is a block with content with a placeholder.

There's none that I know of. Would we still want to announce instructions for blocks that already have content? Might that not be more confusing?

@alexstine
Copy link
Contributor

@tellthemachines I was under the impression that Placeholder component was still there when the block had content, but guess not. My concern is no longer a problem. Sorry for the confusion.

@talldan
Copy link
Contributor

talldan commented Nov 22, 2022

I'm not completely sure I understand the question.

The media and text block can have its inner blocks edited while it's still showing a placeholder, but I don't think it counts because I think you're referring to a block that still renders the placeholder after it has been used to add content. In all cases that I'm aware of, the placeholder is removed from the DOM after it's used.

@alexstine
Copy link
Contributor

@talldan Yes. There are two issues being discussed.

  1. Whether or not the Placeholder is persistent after you add content to a block. Seems like the answer is no, so no need to worry about announcing the Placeholder after the block is inserted.
  2. The second issue is one that @tellthemachines brought up and it is a good point. If the Placeholder renders after a block is inserted, it could be reading the info for the inserted block before the user is ready to use it if I am understanding this correctly. I think what we're getting at is we need to find a way to fire the speak() function on block select if the block is still empty. Adding onFocus to the wrapping div of the Placeholder to call speak() sounds like a possible solution. When the block is selected, focus will pass through the wrapper and fire the announcement.

Thoughts?

@talldan
Copy link
Contributor

talldan commented Nov 22, 2022

The second issue is one that @tellthemachines brought up and it is a good point. If the Placeholder renders after a block is inserted, it could be reading the info for the inserted block before the user is ready to use it if I am understanding this correctly. I think what we're getting at is we need to find a way to fire the speak() function on block select if the block is still empty. Adding onFocus to the wrapping div of the Placeholder to call speak() sounds like a possible solution. When the block is selected, focus will pass through the wrapper and fire the announcement.

That's a difficult problem to solve, but only announcing on focus seems like a good idea. Not sure what other options there are. It's a shame there isn't a wrapper element that could have an aria-description, but I suppose that would need to be focusable. 🤔

@alexstine
Copy link
Contributor

@talldan aria-description is currently blocked because of failing unit tests. See my below PR for further context. Need React 18 upgrade first.

#39558

@alexstine alexstine self-assigned this Nov 24, 2022
@alexstine
Copy link
Contributor

I'll try to improve this a little more when I catch a free moment. If I get something reasonable, I'll do a single commit so we can all evaluate and revert if needed. 👍

@alexstine
Copy link
Contributor

@afercia Done. I think it works better now. I am using state to detect the first time the component gets focus instead of based off timing alone.

Thoughts?

@github-actions
Copy link

github-actions bot commented Jan 7, 2023

Flaky tests detected in 37bda2d.
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/6253419745
📝 Reported issues:

within( placeholder ).getByRole( 'group' );
// Test for empty content. When the content is empty,
// the only way to query the div is with `querySelector`
// eslint-disable-next-line testing-library/no-node-access
Copy link
Contributor

@alexstine alexstine Jan 7, 2023

Choose a reason for hiding this comment

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

Is there a better way or is it okay to ignore?

Copy link
Contributor

Choose a reason for hiding this comment

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

Mhh, there isn't an obvious better choice. Maybe @tyxla can think of something here?

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the ping @ciampo!

Nope, if we remove the fieldset in favor of a div (which I agree it makes sense), there's no other way to query for the empty div. There are other DOM traversing alternatives, but this one is the most precise.

Therefore, since I agree with removing the fieldset in favor of a div in this use case, I'm fine with the ignore. Making an element inaccessible intentionally is the ideal case for this type of ESLint ignore.

@afercia
Copy link
Contributor Author

afercia commented Jan 16, 2023

@alexstine thanks for the ping. Good idea using the state, it works better now! There's still a problem though. Block with a Placeholder that doesn't contain focusable element won't trigger the onFocus callback thus the instructions aren't announced. For example, the Table of contents block:

  • Add a Table of contents block
  • When there's no heading on the page, this block instructions text is: 'Start adding Heading blocks to create a table of contents. Headings with HTML anchors will be linked here.'
  • the Placeholder doesn't contain any focusable element.
  • The instructions aren't announced.
  • The speak polite live region is empty.

@alexstine
Copy link
Contributor

@ciampo Are these failures related? Looks like maybe not. These E2E logs are too hard for me to read...

@ciampo ciampo enabled auto-merge (squash) September 21, 2023 08:45
@ciampo
Copy link
Contributor

ciampo commented Sep 21, 2023

I don't think they are related — in any case, I've pushed a couple of commits to solve a merge conflict, which will cause the CI checks to re-run. Let's see if they fail again.

@ciampo ciampo merged commit 955aef6 into trunk Sep 21, 2023
51 checks passed
WordPress Components automation moved this from In progress (owned) ⏳ to Done 🎉 Sep 21, 2023
@ciampo ciampo deleted the fix/placeholder-instructions-accessibility branch September 21, 2023 09:20
@github-actions github-actions bot added this to the Gutenberg 16.8 milestone Sep 21, 2023
@ciampo ciampo added Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) Backport to WP Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta labels Sep 21, 2023
@mikachan mikachan removed the Backport to WP Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Sep 22, 2023
mikachan pushed a commit that referenced this pull request Sep 22, 2023
* Improve the placeholder instructions accessibility.

* Use a speak message for the placeholder instructions.

* Update test.

* Try to only announce message after initial focus.

* Fix unit tests.

* Announce message on render and add test.

* Changelog entry.

* Reviewer feedback.

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

* Fix unit tests.

* Fix changelog.

* Remove duplicate CHANGELOG entry

---------

Co-authored-by: Alex Stine <alex.stine@yourtechadvisors.com>
Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com>
@mikachan
Copy link
Member

I just cherry-picked this PR to the release/16.7 branch to get it included in the next release: af78d3f

@mikachan mikachan removed the Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) label Sep 22, 2023
@mikachan
Copy link
Member

mikachan commented Oct 2, 2023

👋 There's a report from the testing for WP 6.4 Beta 1 that may be related to this change:

[56710] leads to my plugin's block breaking, when inserting it into the block editor.
I get
This block has encountered an error and cannot be previewed.
...
So, somehow the a11y package has a problem with the component, it seems?

There's also an error log included:

Uncaught TypeError: message.replace is not a function
    at filterMessage (a11y.js?ver=b5ff61edc2245a1950cb:153:21)
    at speak (a11y.js?ver=b5ff61edc2245a1950cb:227:13)
    at components.js?ver=954ab867bc33b1510106:69680:49
    at commitHookEffectListMount (react-dom.js?ver=18.2.0:23160:28)
    at commitPassiveMountOnFiber (react-dom.js?ver=18.2.0:24936:15)
    at commitPassiveMountEffects_complete (react-dom.js?ver=18.2.0:24901:11)
    at commitPassiveMountEffects_begin (react-dom.js?ver=18.2.0:24888:9)
    at commitPassiveMountEffects (react-dom.js?ver=18.2.0:24876:5)
    at flushPassiveEffectsImpl (react-dom.js?ver=18.2.0:27049:5)
    at flushPassiveEffects (react-dom.js?ver=18.2.0:26994:16)
filterMessage @ a11y.js?ver=b5ff61edc2245a1950cb:153
speak @ a11y.js?ver=b5ff61edc2245a1950cb:227
(anonymous) @ components.js?ver=954ab867bc33b1510106:69680
commitHookEffectListMount @ react-dom.js?ver=18.2.0:23160
commitPassiveMountOnFiber @ react-dom.js?ver=18.2.0:24936
commitPassiveMountEffects_complete @ react-dom.js?ver=18.2.0:24901
commitPassiveMountEffects_begin @ react-dom.js?ver=18.2.0:24888
commitPassiveMountEffects @ react-dom.js?ver=18.2.0:24876
flushPassiveEffectsImpl @ react-dom.js?ver=18.2.0:27049
flushPassiveEffects @ react-dom.js?ver=18.2.0:26994
performSyncWorkOnRoot @ react-dom.js?ver=18.2.0:26086
flushSyncCallbacks @ react-dom.js?ver=18.2.0:12052
commitRootImpl @ react-dom.js?ver=18.2.0:26969
commitRoot @ react-dom.js?ver=18.2.0:26692
finishConcurrentRender @ react-dom.js?ver=18.2.0:25991
performConcurrentWorkOnRoot @ react-dom.js?ver=18.2.0:25819
workLoop @ react.js?ver=18.2.0:2653
flushWork @ react.js?ver=18.2.0:2626
performWorkUntilDeadline @ react.js?ver=18.2.0:2920
react-dom.js?ver=18.2.0:18697


The above error occurred in the <Placeholder> component:

    at Placeholder (http://localhost:8001/wp-includes/js/dist/components.js?ver=954ab867bc33b1510106:69649:5)
    at div
    at edit (http://localhost:8001/wp-content/plugins/tablepress/blocks/table/build/index.js?ver=a76551f3faef1d6732c6:1:2168)
    at Edit (http://localhost:8001/wp-includes/js/dist/block-editor.js?ver=1f941e81f7f064707f6a:16442:5)
    at http://localhost:8001/wp-includes/js/dist/block-editor.js?ver=1f941e81f7f064707f6a:15565:11
[...]

I'm wondering if anyone could help debug this. Thank you 🙇

cc. @afercia @ciampo

@ciampo
Copy link
Contributor

ciampo commented Oct 2, 2023

Hey @mikachan , reading the stack trace, it looks like the error is coming from this call to the speak() function inside the Placeholder component.

Since there is an if statement guarding that call, I wonder if the plugin is passing a value for the instructions prop that is not a string — that would explain the fact that the call to speak still gets executed, but fails because we're passing something different than a string, thus resulting in the message.replace is not a function error.

Are you able to investigate further on with the person who filed the report?


Update: I quickly tried to render a Placeholder component in Storybook and pass (mistakenly) a React Element instead of a string for the instruction prop:

<Placeholder {...props} instructions={ <span>These are the instructions</span> } />

And I got the exact same message. If that was the case, it means that whoever got this error was using the component incorrectly, since instructions was always meant to be a string (as per the documentation and the TypeScript types)

@alexstine
Copy link
Contributor

@ciampo I wonder if we should type check this anyway in the wordpress/a11y package? Make sure we're dealing with type of string? If so, happy to make a quick follow-up.

@TobiasBg
Copy link
Contributor

TobiasBg commented Oct 2, 2023

Hi @ciampo, I'm the guy who reported this in Core Trac.
Here's my instructions prop: https://github.com/TablePress/TablePress/blob/2.1.7/blocks/table/src/edit.js#L88-L96

@alexstine
Copy link
Contributor

@TobiasBg Its the <> and </> wrapping tags inside the instructions prop. I would move that logic into variables so it can be a passed in string.

https://github.com/WordPress/gutenberg/tree/trunk/packages/components/src/placeholder#instructions-string

Line 27: https://github.com/WordPress/gutenberg/blob/e3a253e890b9397fd75e290ee7dc45e3a9feca43/packages/components/src/placeholder/types.ts

@ciampo
Copy link
Contributor

ciampo commented Oct 2, 2023

@ciampo I wonder if we should type check this anyway in the wordpress/a11y package? Make sure we're dealing with type of string? If so, happy to make a quick follow-up.

@alexstine we could, and that would be an extra safety net. At the same time, we do add docs and TypeScript types exactly to help folks to use our components. If we had to always check for every prop to have the correct type at runtime, then we'd need to rewrite most of our components.

Its the <> and </> wrapping tags inside the instructions prop. I would move that logic into variables so it can be a passed in string.

Hey @TobiasBg , as @alexstine also mentioned, the instructions prop is meant to accept raw strings. Instead, in your code you're passing a React element. It may have accidentally worked in previous versions of the Gutenberg plugin/WordPress, but the prop was always meant to be a string. Hope this helps to fix your issue!

@TobiasBg
Copy link
Contributor

TobiasBg commented Oct 3, 2023

Thanks for the details and explanation, @ciampo! That makes perfect sense!
I was able to move the logic to variables as per @alexstine's suggestion, so everything is working fine again.
Thanks for your help!
@mikachan, thanks for guiding me to this issue from the Core Trac!

@Firsh
Copy link

Firsh commented Oct 6, 2023

Hey @TobiasBg , as @alexstine also mentioned, the instructions prop is meant to accept raw strings. Instead, in your code you're passing a React element. It may have accidentally worked in previous versions of the Gutenberg plugin/WordPress, but the prop was always meant to be a string. Hope this helps to fix your issue!

The same issue happened to me too. I want to put HTML as instructions (it just has a bulleted list). HTML tags are rendered as-is in the instructions though. I used to use

wp.element.createElement(wp.components.Placeholder, {
    icon: gridIcon,
    label: 'Justified Image Grid',
    instructions: wp.element.RawHTML({
        children: txt.placeholderInstructions,
    }),
});

From now on, I either skip the Placeholder component entirely and only output raw HTML, or put everything in the preview prop instead of of instructions. That includes moving the output of icon and label to be in the raw HTML as well, as the icon and label show up after the preview's output.

@ciampo
Copy link
Contributor

ciampo commented Oct 6, 2023

Hey @Firsh ,

I want to put HTML as instructions (it just has a bulleted list)

HTML is not supported by the instructions prop, as it expects to receive unformatted text (a string) — that is because that same string of text will be announced by assistive technology.

The preview prop accepts ReactNode, and therefore it should be able to render HTML correctly — although I'd still recommend passing clear instructions via the instructions prop as raw text as an accessibility improvement.

if ( instructions ) {
speak( instructions );
}
}, [ instructions ] );
Copy link
Member

Choose a reason for hiding this comment

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

On page load, all block with placeholders with instructions are now all "speaking" at once. Is this by intention?

Copy link
Contributor

Choose a reason for hiding this comment

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

@ellatrix No, it needs to be fixed.

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). [Package] Components /packages/components
Projects
Development

Successfully merging this pull request may close these issues.

Improve Placeholder instructions accessibility