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

[EuiBasicTable & EuiInMemoryTable] Fix multiple accessibility/axe errors on EuiBasicTable and EuiInMemoryTable pages #5241

Merged
merged 12 commits into from
Oct 5, 2021

Conversation

cee-chen
Copy link
Contributor

@cee-chen cee-chen commented Oct 4, 2021

Summary

a11y/axe tests would have caught the duplicate ID issue in #5237, but we had them skipped in our a11y-testing.js testing due to multiple failures that I'm assuming we didn't have bandwidth to fix at the time.

This PR attempts to put us back on the straight and narrow by re-enabling a11y testing on our table docs pages and fixing the various axe issues that were present on the 2 pages.

I'll post a couple before/after screenshots in individual comments below in a bit, but I also strongly recommend following along by-commit.

Checklist

- [ ] Check against all themes for compatibility in both light and dark modes
- [ ] Checked in mobile
- [ ] Checked in Chrome, Safari, Edge, and Firefox
- [ ] Props have proper autodocs and playground toggles
- [ ] Added documentation
- [ ] Checked Code Sandbox works for any docs examples

- [ ] Checked for breaking changes and labeled appropriately

  • Checked for accessibility including keyboard-only and screenreader modes
  • A changelog entry exists and is marked appropriately

…s on the page

- This is possibly only an issue with our docs and the fact that we use the same dataset repeatedly across multiple examples, but solves the error of duplicated IDs and shouldn't negatively affect production users
… the page

- there's no real need for this popover to have a custom ID instead of a randomized one - remove it

- This is possibly only an issue with our docs and the fact that we use the same dataset repeatedly across examples, but solves the error of duplicated IDs and shouldn't negatively affect production users
- An `aria-label` was being passed to the `PaginationBar` component, but it wasn't actually being correctly used:
  - `PaginationBar` was never passing an `aria-label` prop down to `EuiTablePagination` or `EuiPagination`
  - The i18n typing was passing a react element instead of a string via render prop
…able docs

- While we added an aria-label for each table's paginatoin nav, they still need to be unique for multiple tables on the page, which means adding a tableCaption for each demo
@kibanamachine
Copy link

Preview documentation changes for this PR: https://eui.elastic.co/pr_5241/

- empty `td`s within a `thead` is valid per https://webaim.org/techniques/tables/data's examples, but should not have the `scope` attr set
- I opted to use visually empty headings, populated with EuiScreenReaderOnly text as an example for users looking to implement their own columns

+ fix odd data-test-subjs caused by either undefined or node column names
- missing labels on checkbox elements, duplicate checkbox ID (solved in elastic#5237)

- These are all a11y solutions that come OOTB in EuiBasicTable, but need to be added for the custom example
- these should very likely just be paragraphs, not headings
Copy link
Contributor Author

@cee-chen cee-chen left a comment

Choose a reason for hiding this comment

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

Quick rundown of the source code changes with before/after screenshots - I didn't do those for docs-only fixes, as those felt pretty straightforward and should be easy enough to follow along in the commit messages

Comment on lines -9 to -10
`${root}#/tabular-content/tables`,
`${root}#/tabular-content/in-memory-tables`,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Feel free to test locally yourself with yarn test-a11y!

@@ -1166,7 +1166,7 @@ export class EuiBasicTable<T = any> extends Component<
<EuiI18n token="euiBasicTable.selectThisRow" default="Select this row">
{(selectThisRow: string) => (
<EuiCheckbox
id={`${key}-checkbox`}
id={`${this.tableId}${key}-checkbox`}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Axe error: https://dequeuniversity.com/rules/axe/4.3/duplicate-id-active
Commit: 06fbd23

Before:

After:

NB: This is really only an issue with our docs because we use the same dataset repeatedly across multiple examples - but it's definitely possible other end-users may have multiple isSelectable tables on the same page. Generally, it shouldn't negatively affect production users (who should likely prefer the data-test-subj attr over id to hook into the checkbox if needed)

@@ -368,7 +368,6 @@ export class FieldValueSelectionFilter extends Component<

return (
<EuiPopover
id={`${config.type}_${index}`}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Axe error: https://dequeuniversity.com/rules/axe/4.3/duplicate-id-active
Commit: 08ca142

Before:

After:
Screen Shot 2021-10-04 at 5 11 02 PM

NB: This is really only an issue with our docs because we use the same dataset repeatedly across multiple examples. This shouldn't have any impact on production users that I can reasonably think of - honestly, I'm not totally sure why this ID was necessary in the first place, as EuiPopover generates its own internal ID for screen reader text.

@@ -80,6 +82,7 @@ export const PaginationBar = ({
onChangeItemsPerPage={onPageSizeChange}
onChangePage={onPageChange}
aria-controls={ariaControls}
aria-label={ariaLabel}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A bit of a d'oh moment: while we were passing an aria-label prop from above/EuiBasicTable, we were never actually passing it from the PaginationBar component to the underlying EuiTablePagination->EuiPagination component. This fixes that and allows the pagination nav element to correctly have a unique label.

Before:

After:

Comment on lines 1380 to 1394
<EuiI18n
token="euiBasicTable.tablePagination"
default="Pagination for preceding table: {tableCaption}"
values={{ tableCaption }}
>
{(tablePagination: string) => (
<PaginationBar
pagination={pagination}
onPageSizeChange={this.onPageSizeChange.bind(this)}
onPageChange={this.onPageChange.bind(this)}
aria-controls={this.tableId}
aria-label={tablePagination}
/>
)}
</EuiI18n>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Axe error: https://dequeuniversity.com/rules/axe/4.3/landmark-unique
Commits: 20afa32 and 51c053b

What this does:

  1. Correctly uses EuiI18n's render prop functionality to pass a string to aria-label instead of a React element (type error)

  2. Passes an aria-label to all table pagination nav elements always, regardless of whether the tableCaption prop is set

    • For users who have just one table on a page, this passes the unique label on the nav axe check, and screen readers will read out just "pagination for preceding table" without a title, which sounds fairly natural to me.
    • If users have multiple tables on the same page, they should use tableCaption to pass axe/to differentiate between their tables.

@1Copenut - I played around with aria-labelledby and pointing this at our hidden table caption SR text (picture below), but I ended up not being a huge fan of the behavior. It also doesn't solve the duplicate axe issue when users have multiple tables that happen to have the same row & page count - I think we should consider encouraging tableCaption usage instead. Any thoughts or objections?

Copy link
Contributor

Choose a reason for hiding this comment

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

@constancecchen I forgot to comment earlier. No objection to this approach. I agree with you to provide good defaults and guidance on using tableCaption to create meaningful labels.

@@ -155,6 +155,7 @@ export const EuiTableHeaderCell: FunctionComponent<EuiTableHeaderCellProps> = ({
const styleObj = resolveWidthAsStyle(style, width);

const CellComponent = children ? 'th' : 'td';
const cellScope = CellComponent === 'th' ? scope ?? 'col' : undefined; // `scope` is only valid on `th` elements
Copy link
Contributor Author

@cee-chen cee-chen Oct 5, 2021

Choose a reason for hiding this comment

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

axe error: https://dequeuniversity.com/rules/axe/4.3/scope-attr-valid
commit: b517a77

I've personally run into/seen this error before when running axe-devtools on my previous Kibana plugin so I'm excited to be able to fix it in EUI!

Copy link
Contributor

Choose a reason for hiding this comment

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

I like this a lot. It gives us a default scope="col" but allows for passing scope="row" in cases where we might use TH as a row header.

Comment on lines +164 to +168
name: (
<EuiScreenReaderOnly>
<span>Expand rows</span>
</EuiScreenReaderOnly>
),
Copy link
Contributor Author

@cee-chen cee-chen Oct 5, 2021

Choose a reason for hiding this comment

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

This addition was optional with b517a77, but I added it because:

Demo of visually hidden table column heading with SR text:

sr-table-heading.mp4

Comment on lines +841 to +843
data-test-subj={`tableHeaderCell_${
typeof name === 'string' ? name : ''
}_${index}`}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before:

Bonus: if you passed a react node it rendered as tableHeaderCell_[object Object]_4 lol

After:

@kibanamachine
Copy link

Preview documentation changes for this PR: https://eui.elastic.co/pr_5241/

Copy link
Contributor

@1Copenut 1Copenut left a comment

Choose a reason for hiding this comment

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

Thank you @constancecchen! One change request, one discussion item and we should be good to go.

@@ -155,6 +155,7 @@ export const EuiTableHeaderCell: FunctionComponent<EuiTableHeaderCellProps> = ({
const styleObj = resolveWidthAsStyle(style, width);

const CellComponent = children ? 'th' : 'td';
const cellScope = CellComponent === 'th' ? scope ?? 'col' : undefined; // `scope` is only valid on `th` elements
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this a lot. It gives us a default scope="col" but allows for passing scope="row" in cases where we might use TH as a row header.

pagination={pagination}
onPageSizeChange={this.onPageSizeChange.bind(this)}
onPageChange={this.onPageChange.bind(this)}
aria-controls={this.tableId}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm on the fence about the value of aria-controls. It's supported in JAWS, but not NVDA or VoiceOver at this time. If we have just one pagination table on the page, the UX is fairly straight forward. If there are more than one, it could create unexpected behaviors. If there are accidental duplicate IDs, users may end up in a different table.

I'm okay with this staying in if it's difficult to remove, out of scope, or warrants more discussion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TBH I'm not a super huge expert / haven't used JAWS, so I'm not familiar with how aria-controls is supposed to work 🙈 I'm kind of tempted to leave it in in case aria-controls gets more support in the future. Can we leave it for now and revisit it down the road maybe?

FWIW,

If there are accidental duplicate IDs, users may end up in a different table

isn't possible because the table ID is randomly generated between tables, and we don't allow users to pass in a custom ID for tables.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good, let's leave it for now and keep an ear out for user feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome, thanks a million Trevor! Mind giving this a ✅ if all your change requests have been addressed?

@kibanamachine
Copy link

Preview documentation changes for this PR: https://eui.elastic.co/pr_5241/

@cee-chen
Copy link
Contributor Author

cee-chen commented Oct 5, 2021

jenkins test this

@kibanamachine
Copy link

Preview documentation changes for this PR: https://eui.elastic.co/pr_5241/

Copy link
Contributor

@1Copenut 1Copenut left a comment

Choose a reason for hiding this comment

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

👍 LGTM! Thank you @constancecchen

@cee-chen cee-chen merged commit cbf7c3a into elastic:master Oct 5, 2021
@cee-chen cee-chen deleted the table-axe-fixes branch October 5, 2021 22:22
@@ -741,6 +756,7 @@ export default class extends Component {
<EuiSpacer size="m" />

<EuiTablePagination
tableCaption="Custom EuiTable demo"
Copy link
Contributor

Choose a reason for hiding this comment

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

@constancecchen Question here, is this supposed to be right? In master I'm getting a dev console error that tableCaption is just being passed all the way to the DOM element.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tableCaption is a valid prop that we document and use for accessibility - it looks like we should be pulling it out via ...rest or other spread operator so it doesn't fall through to the DOM. That's not caused by this PR specifically though, it's something that would have already been an issue when tableCaption was first implemented.

I'd offer to open a PR but I'm a little knee-deep in last minute data grid fires ATM, feel free to open an issue and I can hopefully get to it later!

ym pushed a commit to ym/eui that referenced this pull request Oct 29, 2021
…ors on EuiBasicTable and EuiInMemoryTable pages (elastic#5241)

* Fix duplicate checkbox `id`s with multiple isSelectable EuiBasicTables on the page

- This is possibly only an issue with our docs and the fact that we use the same dataset repeatedly across multiple examples, but solves the error of duplicated IDs and shouldn't negatively affect production users

* Fix duplicate popover `id`s with multiple EuiInMemoryTable filters on the page

- there's no real need for this popover to have a custom ID instead of a randomized one - remove it

- This is possibly only an issue with our docs and the fact that we use the same dataset repeatedly across examples, but solves the error of duplicated IDs and shouldn't negatively affect production users

* Fix missing `aria-label` on table pagination

- An `aria-label` was being passed to the `PaginationBar` component, but it wasn't actually being correctly used:
  - `PaginationBar` was never passing an `aria-label` prop down to `EuiTablePagination` or `EuiPagination`
  - The i18n typing was passing a react element instead of a string via render prop

* Fix multiple `landmark-unique` issues on EuiBasicTable & EuiInMemoryTable docs

- While we added an aria-label for each table's paginatoin nav, they still need to be unique for multiple tables on the page, which means adding a tableCaption for each demo

* Fix `scope-attr-valid` errors on empty table column headings

- empty `td`s within a `thead` is valid per https://webaim.org/techniques/tables/data's examples, but should not have the `scope` attr set

* Improve demos with empty table headings

- I opted to use visually empty headings, populated with EuiScreenReaderOnly text as an example for users looking to implement their own columns

+ fix odd data-test-subjs caused by either undefined or node column names

* Fix various axe failures on custom EuiTable example

- missing labels on checkbox elements, duplicate checkbox ID (solved in elastic#5237)

- These are all a11y solutions that come OOTB in EuiBasicTable, but need to be added for the custom example

* Fix unsemantic headings in EuiBasicTable responsive documentation

- these should very likely just be paragraphs, not headings

* Re-enable basic table & in-memory table documentations in a11y tests

* Add changelog entry

* PR feedback: screen reader landmark copy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants