-
Notifications
You must be signed in to change notification settings - Fork 367
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
Changes from all commits
33dbb4d
9c2faf0
a3118be
8ad0791
31dd3ad
0f8be39
9086bcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,11 @@ import React from "react"; | |
import deepEqual from "deep-equal"; | ||
|
||
import { FacetValue, FilterValue } from "./types"; | ||
import { appendClassName, getFilterValueDisplay } from "./view-helpers"; | ||
import { | ||
appendClassName, | ||
getFilterValueDisplay, | ||
ScreenReaderStatus | ||
} from "./view-helpers"; | ||
|
||
function MultiCheckboxFacet({ | ||
className, | ||
|
@@ -12,6 +16,7 @@ function MultiCheckboxFacet({ | |
onRemove, | ||
onSelect, | ||
options, | ||
optionsCount, | ||
showMore, | ||
values, | ||
showSearch, | ||
|
@@ -82,14 +87,28 @@ function MultiCheckboxFacet({ | |
</div> | ||
|
||
{showMore && ( | ||
<button | ||
type="button" | ||
className="sui-multi-checkbox-facet__view-more" | ||
onClick={onMoreClick} | ||
aria-label="Show more options" | ||
> | ||
+ More | ||
</button> | ||
<ScreenReaderStatus | ||
render={announceToScreenReader => ( | ||
<button | ||
type="button" | ||
className="sui-multi-checkbox-facet__view-more" | ||
aria-label="Show more options" | ||
onClick={() => { | ||
onMoreClick(); | ||
|
||
const newLimit = options.length + 10; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
const showingAll = newLimit >= optionsCount; | ||
const message = `${ | ||
showingAll ? `All ${optionsCount}` : newLimit | ||
} options shown.`; | ||
|
||
announceToScreenReader(message); | ||
}} | ||
> | ||
+ More | ||
</button> | ||
)} | ||
/> | ||
)} | ||
</fieldset> | ||
); | ||
|
@@ -102,6 +121,7 @@ MultiCheckboxFacet.propTypes = { | |
onSelect: PropTypes.func.isRequired, | ||
onSearch: PropTypes.func.isRequired, | ||
options: PropTypes.arrayOf(FacetValue).isRequired, | ||
optionsCount: PropTypes.number, | ||
showMore: PropTypes.bool.isRequired, | ||
values: PropTypes.arrayOf(FilterValue).isRequired, | ||
className: PropTypes.string, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,27 @@ | ||
import PropTypes from "prop-types"; | ||
import React from "react"; | ||
|
||
import { appendClassName } from "./view-helpers"; | ||
import { appendClassName, ScreenReaderStatus } from "./view-helpers"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One potential downside to adding this to the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also envisioned a future state where users could use |
||
|
||
function PagingInfo({ className, end, searchTerm, start, totalResults }) { | ||
end = Math.min(end, totalResults); | ||
return ( | ||
<div className={appendClassName("sui-paging-info", className)}> | ||
Showing{" "} | ||
<strong> | ||
{start} - {Math.min(end, totalResults)} | ||
</strong>{" "} | ||
out of <strong>{totalResults}</strong> for: <em>{searchTerm}</em> | ||
</div> | ||
<> | ||
<div className={appendClassName("sui-paging-info", className)}> | ||
Showing{" "} | ||
<strong> | ||
{start} - {end} | ||
</strong>{" "} | ||
out of <strong>{totalResults}</strong> for: <em>{searchTerm}</em> | ||
</div> | ||
<ScreenReaderStatus | ||
render={announceToScreenReader => { | ||
let message = `Showing ${start} to ${end} results out of ${totalResults}`; | ||
if (searchTerm) message += `, searching for "${searchTerm}".`; | ||
return announceToScreenReader(message); | ||
}} | ||
/> | ||
</> | ||
); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,57 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`does not render a higher end than the total # of results 1`] = ` | ||
<div | ||
className="sui-paging-info" | ||
> | ||
Showing | ||
|
||
<strong> | ||
0 | ||
- | ||
15 | ||
</strong> | ||
|
||
out of | ||
<strong> | ||
15 | ||
</strong> | ||
for: | ||
<em> | ||
grok | ||
</em> | ||
</div> | ||
<Fragment> | ||
<div | ||
className="sui-paging-info" | ||
> | ||
Showing | ||
|
||
<strong> | ||
0 | ||
- | ||
15 | ||
</strong> | ||
|
||
out of | ||
<strong> | ||
15 | ||
</strong> | ||
for: | ||
<em> | ||
grok | ||
</em> | ||
</div> | ||
<ScreenReaderStatus | ||
render={[Function]} | ||
/> | ||
</Fragment> | ||
`; | ||
|
||
exports[`renders correctly 1`] = ` | ||
<div | ||
className="sui-paging-info" | ||
> | ||
Showing | ||
|
||
<strong> | ||
0 | ||
- | ||
20 | ||
</strong> | ||
|
||
out of | ||
<strong> | ||
1000 | ||
</strong> | ||
for: | ||
<em> | ||
grok | ||
</em> | ||
</div> | ||
<Fragment> | ||
<div | ||
className="sui-paging-info" | ||
> | ||
Showing | ||
|
||
<strong> | ||
0 | ||
- | ||
20 | ||
</strong> | ||
|
||
out of | ||
<strong> | ||
1000 | ||
</strong> | ||
for: | ||
<em> | ||
grok | ||
</em> | ||
</div> | ||
<ScreenReaderStatus | ||
render={[Function]} | ||
/> | ||
</Fragment> | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* @jest-environment node | ||
*/ | ||
import React from "react"; | ||
import { shallow } from "enzyme"; | ||
import { ScreenReaderStatus } from "../../view-helpers"; | ||
|
||
it("does not crash or create errors in server-side rendered apps", () => { | ||
shallow( | ||
<ScreenReaderStatus | ||
render={announceToScreenReader => { | ||
announceToScreenReader("Test"); | ||
return null; | ||
}} | ||
/> | ||
); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of thoughts:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used both in the past, just tend to use
render
out of habit. Good with switching tochildren
or whatever we prefer!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.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.
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)
There was a problem hiding this comment.
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:
I think this is related to #276 but I've been wrong before :)