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

[a11y] Adding a screen reader live region and announcements for actions #320

Closed
wants to merge 7 commits into from
Closed

[a11y] Adding a screen reader live region and announcements for actions #320

wants to merge 7 commits into from

Conversation

cee-chen
Copy link
Member

@cee-chen cee-chen commented Jul 9, 2019

Description

This is an an accessibility PR that provides more context and information to screen reader users. The goal is to announce the results/consequences of certain actions being taken to users, so that visually impaired users aren't left wondering what happened after pressing a button.

For example:

  • After clicking the "+ More" button in the filters sidebar, a screen reader user should know how many total filters are now being shown. (This was originally spiked out here).
  • After pressing the search button, a screen reader user should know how many results are being shown and how many total results were returned.
  • After navigating to another page of results, a screen reader user should have the results/paging info read out to them (same as above)
  • After changing how many results are being shown per-page, a screen reader user should have the results/paging info read out to them (same as above)

Screencaps

Search results announcements:

search-results-screencap

+ More Filters annoucements:

more-filters-screencap

List of changes

I recommend following along commit-by-commit if possible!

  • Adds a new ScreenReaderStatus render prop helper
    • This creates a one-time live region in the app, and reads out any updates in that region to screen readers. This region can be used by multiple different components.
    • Note: Because this directly modifies the client DOM, a check needs to be added to this helper to ensure it doesn't crash for SSR React apps.
  • Updates MultiCheckboxFacet's "+ More" button to announce shown filters to ScreenReaderStatus
  • Updates PagingInfo to announce changes in results shown to ScreenReaderStatus

QA

  • Tested on MacOS Voiceover

(Note - uncovered lines are same as before, and all coverage %s went up slightly)


Screen Shot 2019-07-09 at 12 38 10 PM

Future

I'll continue to investigate other opportunities to use this new live region (the next likely being the SingleLinksFacet component), but for now I'd like to keep this PR fairly small and to the two lowest hanging fruit for screen reader announcements.

@cee-chen cee-chen requested a review from JasonStoltz July 9, 2019 19:37
@cee-chen cee-chen requested a review from yakhinvadim July 9, 2019 19:41
@@ -1,17 +1,27 @@
import PropTypes from "prop-types";
import React from "react";

import { appendClassName } from "./view-helpers";
import { appendClassName, ScreenReaderStatus } from "./view-helpers";
Copy link
Member Author

Choose a reason for hiding this comment

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

One potential downside to adding this to the <PagingInfo /> component is that if users roll their own view, they're inadvertently making their app less accessible - so it's possible that adding this to the react-search-ui container may be a bit more robust. Definitely open to thoughts on this!

Copy link
Member

Choose a reason for hiding this comment

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

In this project it's often a bit fuzzy to me what should go into a container and what should go into a view.

The guidance we give users for customizing views is to use the default view as a base and customize from there. That could be a bit of a foot gun if they forget to call "announceToScreenReader", as you point out.

The goal of having swappable views, is to give users a very easy way to use custom markup for a component. The less they have to reproduce in the view, the better. To that end, it would seem more ideal to do this in a container. We could either do it completely transparently, or we could roll all of the logic into a single prop that we pass down.

function PagingInfo({ className, end, searchTerm, start, totalResults, screenReaderAnnouncement }) {
  end = Math.min(end, totalResults);
  return (
    <>
      <div className={appendClassName("sui-paging-info", className)}>
        Showing{" "}
        <strong>
          {start} - {end}
        </strong>{" "}
        out of <strong>{totalResults}</strong> for: <em>{searchTerm}</em>
      </div>
      {screenReaderAnnouncement(someCustomMessageFunction)}
    </>
  );
}

It could be something like, if you don't call it at all, the default announcement will be used. if you call it with a custom function, screenReaderAnnouncement(), then that overrides the default message with a custom message.

Copy link
Member

Choose a reason for hiding this comment

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

I also envisioned a future state where users could use react-search-ui without including react-search-ui-views at all, so they could have a completely custom view layer. Having this be part of the container would make that a challenge 🤔.

onClick={() => {
onMoreClick();

const newLimit = options.length + 10;
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a bit of a magic constant (from the parent Facet.js container) - feel free to shout if you think it should be converted to a prop or something a bit less static

Copy link
Member

@JasonStoltz JasonStoltz left a comment

Choose a reason for hiding this comment

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

This works great, I just have a whole bunch of larger architectural questions about how we best make this fit into Search UI. I left some open ended thoughts. Hoping we can put some of these thoughts through the old rock tumbler and come up with a clean answer.

return render(announceToScreenReader);
};

export default ScreenReaderStatus;
Copy link
Member

Choose a reason for hiding this comment

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

I think the name of this file should match the name of the default export, so ScreenReaderStatus in this case. Just makes it easier to find things.

+ More
</button>
<ScreenReaderStatus
render={announceToScreenReader => (
Copy link
Member

Choose a reason for hiding this comment

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

A couple of thoughts:

  1. I like the render prop pattern for this, but did you consider using the "children" property rather than an explicit "render" property? I think it just makes the syntax a little more concise.
  2. Rather than a render prop, did you consider creating a custom hook?
  3. If a user is creating a custom view, how do we expect them to use this? Do we expect them to import it from "@elastic/react-search-ui-view/view-helpers"? If so, we should document that in the "customization" section or something. Let's do that documentation in a separate PR though (Document "view-helpers" in view customization guide. #323). There's also probably a larger discussion there as well, of how we expose helpers for views... require users to import from "view-helpers", or pass relevant helpers through as props from the container?
  4. Instead of a view helper, could this be its own view component?

Copy link
Member Author

@cee-chen cee-chen Jul 11, 2019

Choose a reason for hiding this comment

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

  1. I've used both in the past, just tend to use render out of habit. Good with switching to children or whatever we prefer!

  2. After reading your comments down below I think I'm actually leaning towards moving away from a render prop (if we do indeed want to make this usable to non-React apps). Re: custom hooks specifically - I ran into crashes/errors when I tried to use hooks with the MultiCheckboxFacet component (I think related to the previous hook issues we've seen?) - let me try to create a reproducible branch.

  3. Yeah, I definitely didn't think too much about making this work for developers using custom views/not using react views, etc. I think we're starting to think about moving away from this in other comments.

  4. See previous comments - I think we're discussing moving away from this being React dependent, correct me if I'm wrong? (although I'm definitely not 100% on this, so happy to get clarification)

Copy link
Member Author

@cee-chen cee-chen Jul 11, 2019

Choose a reason for hiding this comment

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

Re 2. - here's an example branch/commit with a very basic hook usage that creates the following console error:

Screen Shot 2019-07-11 at 8 24 18 AM

I think this is related to #276 but I've been wrong before :)

@@ -1,17 +1,27 @@
import PropTypes from "prop-types";
import React from "react";

import { appendClassName } from "./view-helpers";
import { appendClassName, ScreenReaderStatus } from "./view-helpers";
Copy link
Member

Choose a reason for hiding this comment

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

In this project it's often a bit fuzzy to me what should go into a container and what should go into a view.

The guidance we give users for customizing views is to use the default view as a base and customize from there. That could be a bit of a foot gun if they forget to call "announceToScreenReader", as you point out.

The goal of having swappable views, is to give users a very easy way to use custom markup for a component. The less they have to reproduce in the view, the better. To that end, it would seem more ideal to do this in a container. We could either do it completely transparently, or we could roll all of the logic into a single prop that we pass down.

function PagingInfo({ className, end, searchTerm, start, totalResults, screenReaderAnnouncement }) {
  end = Math.min(end, totalResults);
  return (
    <>
      <div className={appendClassName("sui-paging-info", className)}>
        Showing{" "}
        <strong>
          {start} - {end}
        </strong>{" "}
        out of <strong>{totalResults}</strong> for: <em>{searchTerm}</em>
      </div>
      {screenReaderAnnouncement(someCustomMessageFunction)}
    </>
  );
}

It could be something like, if you don't call it at all, the default announcement will be used. if you call it with a custom function, screenReaderAnnouncement(), then that overrides the default message with a custom message.

@@ -1,17 +1,27 @@
import PropTypes from "prop-types";
import React from "react";

import { appendClassName } from "./view-helpers";
import { appendClassName, ScreenReaderStatus } from "./view-helpers";
Copy link
Member

Choose a reason for hiding this comment

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

I also envisioned a future state where users could use react-search-ui without including react-search-ui-views at all, so they could have a completely custom view layer. Having this be part of the container would make that a challenge 🤔.

};

const announceToScreenReader = announcement => {
if (hasDOM) {
Copy link
Member

@JasonStoltz JasonStoltz Jul 11, 2019

Choose a reason for hiding this comment

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

Do we need to do explicit DOM manipulation? Could we use a more declarative, stateful approach? Like could announcements be managed in central state somewhere?

For instance, messages could go into their own context store and we could just wire a ScreenReaderStatus component up to that context and re-render when it changes.

We could even take it a step further and make screen reader status part of the core Search UI state, and have an action to update it.

Copy link
Member

Choose a reason for hiding this comment

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

If it were part of the core Search UI state, it would make it available for any library also, whether it's Vue, React, or vanilla js, etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

At some point we'll probably want to do explicit DOM manipulation (we could use a React portal, but that's also essentially just DOM appending/manipulation and will still need a document/window check for SSR apps). Also FWIW, I looked at how downshift was doing announcements as well after noticing their live region, and they're essentially doing what we're doing, which gives me a bit of confidence.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, wait, no, I think I maybe see what you mean by central state. Originally when I was making this, I was really only thinking in terms of putting this in react-search-ui-views, and my hesitation towards putting it in, e.g. <Layout> was something along the lines of "What if the developer isn't using our Layout component".

But if we're moving to react-search-ui instead (correct me if I'm wrong), then we can take advantage of the top level <SearchProvider> component itself to create the region + manage a central state.

Is that what you were thinking?

Copy link
Member Author

@cee-chen cee-chen Jul 11, 2019

Choose a reason for hiding this comment

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

Or, wait, sorry, I'm still catching up again :) You're thinking even higher level than that - putting it in SearchDriver.js (or it's own file - like URLManager?) - correct me if I'm wrong?

I'd love to get a chance to chat with you more about this to clarify architecture a bit more!

Copy link
Member

Choose a reason for hiding this comment

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

@constancecchen Yes, I think this warrants a chat. Will message you later today!

Copy link
Member

Choose a reason for hiding this comment

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

Summary of our chat: #325

@JasonStoltz
Copy link
Member

Also 💯 for this PR.

@cee-chen
Copy link
Member Author

Closing this in favor of a new PR: #355

@cee-chen cee-chen closed this Jul 17, 2019
@cee-chen cee-chen deleted the a11y-screenreader-live branch July 17, 2019 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants