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

Fix/screen reader text #20607

Merged
merged 17 commits into from
Mar 17, 2020
Merged

Conversation

adamziel
Copy link
Contributor

@adamziel adamziel commented Mar 3, 2020

Description

This PR solves #18167

See #18022 which introduced the VisuallyHidden component. The usage of screen-reader-text assumes a WordPress context with the proper CSS already defined. This may not always be the case, especially when pieces of the Gutenberg project are used elsewhere.

How has this been tested?

Tested locally. This PR touches a number of blocks and components. For each of them, you need to check if the new <VisuallyHidden> part behaves in the same way as the old .screen-reader-text. For example, this PR touches a gallery block caption:

https://github.com/WordPress/gutenberg/pull/20607/files#diff-e25f8ba6320e6c1650e6ec1c0ae5ba34L43-L102

To test it, let's follow some steps:

  1. Create a new gallery block
  2. Confirm the caption is hidden when it's empty and the block is not selected
  3. Confirm the caption is visible then it's empty and the block is selected
  4. Confirm the caption is visible when it's not empty and the block is not selected
  5. Confirm the caption is visible then it's not empty and the block is selected

Without this PR applied it should use CSS class screen-reader-text (gif)

With this PR applied it should use the new VisuallyHidden wrapper (gif)

Types of changes

Non-breaking change which fixes #18167

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
@adamziel adamziel force-pushed the fix/screen-reader-text branch 2 times, most recently from 8ee8a21 to 222e5b3 Compare March 4, 2020 10:38
@adamziel adamziel marked this pull request as ready for review March 4, 2020 10:58
@talldan talldan added [Feature] UI Components Impacts or related to the UI component system [Type] Task Issues or PRs that have been broken down into an individual action to take [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). labels Mar 4, 2020
>
{ __( 'Search for a block' ) }
</label>
<VisuallyHidden>
Copy link
Contributor

@talldan talldan Mar 5, 2020

Choose a reason for hiding this comment

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

It looks like this can be defined this as:

<VisuallyHidden as="label" htmlFor={ `block-editor-inserter__search-${ instanceId }` }>
    { __( 'Search for a block' ) }
</VisuallyHidden>

which avoids the extra div around the label. I saw a few other places in the PR where that would be beneficial.

I noticed the VisuallyHidden docs don't mention this, could be a good follow up PR to update those docs 😄

@talldan talldan added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Mar 5, 2020
Copy link
Contributor

@tellthemachines tellthemachines left a comment

Choose a reason for hiding this comment

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

Thanks for taking this on! I left a bunch of comments below to indicate places where it's particularly important to preserve existing semantics, but the same recommendation applies to practically all of the instances where<VisuallyHidden> is being introduced: unless the existing element to be hidden is a <div>, we should always use the as prop to make <VisuallyHidden> itself render the semantically appropriate element, instead of wrapping the existing markup with it.

The readme for VisuallyHidden doesn't really explain its correct usage, but the Storybook demo may be helpful (you might have to use devtools to inspect the generated markup though, as some of it is hidden 😄 )

>
{ __( 'Search for a block' ) }
</label>
<VisuallyHidden>
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid wrapping the label in an unnecessary <div>, we should instead replace the label with <VisuallyHidden as="label" htmlFor={...} />

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is extra <div> a bad thing though? I'd say a clear distinction between <VisuallyHidden> and a <label> is more readable.

Copy link
Contributor

Choose a reason for hiding this comment

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

We might have to work on the docs, but it should be clear enough that <VisuallyHidden> will render as whatever element it's passed with the as prop 😅
As a general principle though, adding extra markup for code readability is not advisable because it has a direct impact on performance for the end user. It's best to keep markup to a minimum and use spacing and (non-rendering) comments where needed for clarification.

<legend className="screen-reader-text">
{ __( 'Currently selected link settings' ) }
</legend>
<VisuallyHidden>
Copy link
Contributor

Choose a reason for hiding this comment

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

We need <VisuallyHidden as="legend"> here. A <legend> must be the first direct descendant of a <fieldset>, so we can't have any intermediate wrappers.

@@ -43,5 +31,14 @@
}

.block-editor-responsive-block-control .components-base-control__help {
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't feel right; if we need these styles here we should be applying them with whatever classname we're currently using to hide stuff from vision. Or we should be using <VisuallyHidden> if possible. Probably something to tackle in a follow-up PR though 😅. I don't think ResponsiveBlockControl is being used anywhere atm.

@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Basic rendering should render with required props 1`] = `"<fieldset class=\\"block-editor-responsive-block-control\\"><legend class=\\"block-editor-responsive-block-control__title\\">Padding</legend><div class=\\"block-editor-responsive-block-control__inner\\"><div class=\\"components-base-control components-toggle-control block-editor-responsive-block-control__toggle\\"><div class=\\"components-base-control__field\\"><span class=\\"components-form-toggle is-checked\\"><input class=\\"components-form-toggle__input\\" id=\\"inspector-toggle-control-0\\" type=\\"checkbox\\" aria-describedby=\\"inspector-toggle-control-0__help\\" checked=\\"\\"><span class=\\"components-form-toggle__track\\"></span><span class=\\"components-form-toggle__thumb\\"></span><svg width=\\"2\\" height=\\"6\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 2 6\\" class=\\"components-form-toggle__on\\" role=\\"img\\" aria-hidden=\\"true\\" focusable=\\"false\\"><path d=\\"M0 0h2v6H0z\\"></path></svg></span><label for=\\"inspector-toggle-control-0\\" class=\\"components-toggle-control__label\\">Use the same padding on all screensizes.</label></div><p id=\\"inspector-toggle-control-0__help\\" class=\\"components-base-control__help\\">Toggle between using the same value for all screen sizes or using a unique value per screen size.</p></div><div class=\\"block-editor-responsive-block-control__group\\"><div class=\\"components-base-control\\"><div class=\\"components-base-control__field\\"><label class=\\"components-base-control__label\\" for=\\"inspector-select-control-0\\"><span aria-describedby=\\"rbc-desc-0\\">All</span><span class=\\"screen-reader-text\\" id=\\"rbc-desc-0\\">Controls the padding property for All viewports.</span></label><select id=\\"inspector-select-control-0\\" class=\\"components-select-control__input\\"><option value=\\"\\">Please select</option><option value=\\"small\\">Small</option><option value=\\"medium\\">Medium</option><option value=\\"large\\">Large</option></select></div></div><p id=\\"all\\">All is used here for testing purposes to ensure we have access to details about the device.</p></div></div></fieldset>"`;
exports[`Basic rendering should render with required props 1`] = `"<fieldset class=\\"block-editor-responsive-block-control\\"><legend class=\\"block-editor-responsive-block-control__title\\">Padding</legend><div class=\\"block-editor-responsive-block-control__inner\\"><div class=\\"components-base-control components-toggle-control block-editor-responsive-block-control__toggle\\"><div class=\\"components-base-control__field\\"><span class=\\"components-form-toggle is-checked\\"><input class=\\"components-form-toggle__input\\" id=\\"inspector-toggle-control-0\\" type=\\"checkbox\\" aria-describedby=\\"inspector-toggle-control-0__help\\" checked=\\"\\"><span class=\\"components-form-toggle__track\\"></span><span class=\\"components-form-toggle__thumb\\"></span><svg width=\\"2\\" height=\\"6\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 2 6\\" class=\\"components-form-toggle__on\\" role=\\"img\\" aria-hidden=\\"true\\" focusable=\\"false\\"><path d=\\"M0 0h2v6H0z\\"></path></svg></span><label for=\\"inspector-toggle-control-0\\" class=\\"components-toggle-control__label\\">Use the same padding on all screensizes.</label></div><p id=\\"inspector-toggle-control-0__help\\" class=\\"components-base-control__help\\">Toggle between using the same value for all screen sizes or using a unique value per screen size.</p></div><div class=\\"block-editor-responsive-block-control__group\\"><div class=\\"components-base-control\\"><div class=\\"components-base-control__field\\"><label class=\\"components-base-control__label\\" for=\\"inspector-select-control-0\\"><span aria-describedby=\\"rbc-desc-0\\">All</span><div class=\\"components-visually-hidden\\" id=\\"rbc-desc-0\\">Controls the padding property for All viewports.</div></label><select id=\\"inspector-select-control-0\\" class=\\"components-select-control__input\\"><option value=\\"\\">Please select</option><option value=\\"small\\">Small</option><option value=\\"medium\\">Medium</option><option value=\\"large\\">Large</option></select></div></div><p id=\\"all\\">All is used here for testing purposes to ensure we have access to details about the device.</p></div></div></fieldset>"`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated to this changeset, but these snapshots are not very readable. I wonder why all the spacing has been removed 🤔

inlineToolbar
/>
{ ! isSelected && RichText.isEmpty( caption ) ? (
<VisuallyHidden>{ richTextElem }</VisuallyHidden>
Copy link
Contributor

Choose a reason for hiding this comment

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

This presents a problem, because <figcaption> has to be a direct descendant of <figure>. I suspect the best way forward here will be to just pass the components-visually-hidden class to RichText directly. We have to support direct use of the class anyway because of the php files 🤷‍♀

Copy link
Contributor

@talldan talldan Mar 5, 2020

Choose a reason for hiding this comment

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

It's a bit mad, but this should work:

<VisuallyHidden as={ RichText } tagName="figcaption" // other RichText props ... />

It might be cleanest to extract the logic into a separate component so that the props can be spread onto either RichText or VisuallyHidden.

But looking at the VisuallyHidden implementation, it doesn't look like it accepts additional class names. I may knock together a quick PR to fix that.

Copy link
Contributor

@talldan talldan Mar 5, 2020

Choose a reason for hiding this comment

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

PR to better handle class names in VisuallyHidden - #20649

Copy link
Member

Choose a reason for hiding this comment

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

Yes, let's fix it :)

In general, you should be able to pass anything you want as as and all props should be passed down.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@talldan I think that makes sense! I added a helper component inline since it's pretty specific to this file, and I prepared a more general helper in #20928

<VisuallyHidden as="legend">
{ __( 'Color value in HSL' ) }
<VisuallyHidden>
<legend>{ __( 'Color value in HSL' ) }</legend>
</VisuallyHidden>
Copy link
Contributor

Choose a reason for hiding this comment

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

This change and the one above will break form semantics; best leave things as they were.

@@ -35,7 +35,7 @@ export function ExternalLink(
ref={ ref }
>
{ children }
<VisuallyHidden as="span">
<VisuallyHidden>
Copy link
Contributor

Choose a reason for hiding this comment

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

While <div> and <span> have no semantic meaning, they do have user agent styling differences, so best not change this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does the user agent styling matters if <VisuallyHidden> is always visually hidden?

Copy link
Contributor

Choose a reason for hiding this comment

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

In practice it won't make a difference because the styles for hiding position the element absolutely, so it's removed from the flow, and it doesn't matter if its display type is block or inline.
But there's no good reason for making this change either, so best keep the changeset to what is strictly necessary 🙂

</legend>
<VisuallyHidden>
<legend>{ __( 'Font size' ) }</legend>
</VisuallyHidden>
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above.

@@ -27,7 +27,7 @@ function render_block_core_search( $attributes ) {
);
} else {
$label_markup = sprintf(
'<label for="%s" class="wp-block-search__label screen-reader-text">%s</label>',
'<label for="%s" class="wp-block-search__label components-visually-hidden">%s</label>',
Copy link
Contributor

Choose a reason for hiding this comment

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

There might be some issues using component class name in this way, since a component's class name isn't meant to be used outside of the component.

This html is also going to be output to a WordPress site's frontend, so would the components styles be loaded? Not sure, I'd need to test it. 😄

It might be preferable for it to continue using the screen-reader-text global style or for the block to have its own visually hidden style.

It may also be an idea to split some of the changes that need more discussion into a separate PR so that they don't hold up the other changes.

Copy link
Member

Choose a reason for hiding this comment

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

It might be preferable for it to continue using the screen-reader-text global style or for the block to have its own visually hidden style.

Yes, correct. PHP part should be left as is.

@talldan talldan mentioned this pull request Mar 5, 2020
6 tasks
@gziolo
Copy link
Member

gziolo commented Mar 5, 2020

function VisuallyHidden( { as = 'div', ...props } ) {
return renderAsRenderProps( {
as,
className: 'components-visually-hidden',
...props,
} );

VisuallyHidden it's a simple wrapper that defaults to div. All existing divs should be replaced with VisuallyHidden when they use the WP version of the class name.

In case of any other tag name or component is used, you should pass it as as prop.

In the case of PHP implementation, it should use the class name that WordPress core supports to ensure it works properly on the frontend.

>
{ label }
</label>
) }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think #20928 would be a better solution here

Copy link
Member

Choose a reason for hiding this comment

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

Keep in mind that you still need to apply a different class name when it's visually hidden so there is going to be some extra logic involved.

@adamziel
Copy link
Contributor Author

@talldan @mkaz @gziolo @tellthemachines I addressed your feedback and this one is ready for re-review :-)

Copy link
Contributor

@tellthemachines tellthemachines left a comment

Choose a reason for hiding this comment

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

Couple of minor comments below; looks good overall 🙂

return isHidden ? (
<VisuallyHidden as={ RichText } { ...richTextProps } />
) : (
<RichText className={ className } { ...richTextProps } />
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't the className be passed in anyway as part of richTextProps, or am I missing something?

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 wanted to avoid passing className to <VisuallyHidden />, but when I looked into it more .block-gallery-caption shouldn't interfere with any styles we have there so I'll simplify this.

@@ -19,7 +19,9 @@ describe( 'AdminNotices', () => {
<p>My <strong>notice</strong> text</p>
<p>My second line of text</p>
<button type="button" class="notice-dismiss">
<span class="screen-reader-text">Dismiss this notice.</span>
<span class="components-visually-hidden">
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're keeping the original classname as per this thead then this needn't change either.

@@ -40,10 +41,6 @@ export const Gallery = ( props ) => {
images,
} = attributes;

const captionClassNames = classnames( 'blocks-gallery-caption', {
Copy link
Member

Choose a reason for hiding this comment

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

@tellthemachines, can you explain why we need still to render RichText when it's hidden visually? I see you introduced this behavior in #17101. In practice, it will never get focused because as soon as you move the focus to the Gallery block, then this condition will evaluate to false because isSelected will become true and the placeholder will show up when the caption is empty.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, I did that so it'll show up on the list of form controls for screen reader users. So if you're using a screen reader, you can navigate straight to the caption element through the forms list, even if the gallery block isn't focused. Of course, as soon as you navigate to the element it becomes focused and visible.

I notice that this is inconsistent with how we're treating the citation in the Quote block (which disappears from the DOM when empty not focused) and how we treat empty Paragraph blocks (which have visible placeholder text at all times). I'm not 100% sure which of these solutions is best accessibility and usability-wise. Perhaps this could be discussed further in the weekly accessibility chat? It would be best to have a consistent behaviour for all these cases.

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 definitely something worth discussing with the bigger group 👍

It would be nice to be more consistent if other blocks always show placeholders then maybe we should follow that as well. However, I'm sure it's something that always has trade-offs. In the case of Paragraph without the placeholder, you could miss the fact that the block is even there when using mouse navigation :) On the other hand, if you would display caption placeholders for all images in the Gallery block then it would be very loaded. I think it also translates to the screen read case, too many controls exposed might make it harder to navigate to the currently selected block.

Copy link
Contributor

Choose a reason for hiding this comment

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

Update: a similar issue is being discussed in #15308 and seems to be going in the direction of making placeholders visible by default.

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.

Nice work @adamziel. I don't want to disturb the process for this PR, I think it's ready to go.

We should clarify the issue with the wrapper for VisuallyHidden separately. I'd like to learn first if it is really necessary or we can simplify the code instead.

@gziolo gziolo merged commit 61cbfa5 into WordPress:master Mar 17, 2020
@github-actions github-actions bot added this to the Gutenberg 7.8 milestone Mar 17, 2020
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 First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). [Type] Task Issues or PRs that have been broken down into an individual action to take
4 participants