[EuiDataGrid] Change header actions trigger element and enable interactive children#7898
Conversation
abfe7af to
6516ff5
Compare
1Copenut
left a comment
There was a problem hiding this comment.
@mgadewoll I tested this today with NVDA (Chrome, Firefox) and JAWS (Chrome). The results were very good. Your live regions announced properly and the aria-describedby attributes did too. No axe-core issues detected. This is a big improvement for our users who navigate with screen readers. Thank you!
packages/eui/src/components/datagrid/body/header/data_grid_header_cell.tsx
Outdated
Show resolved
Hide resolved
b7290ba to
5ec851c
Compare
|
@1Copenut I don't know if this is just me but it feels like a net keyboard UX loss to have to press Enter twice now to toggle header cell actions as opposed to just once (production). @mgadewoll Do you think there's any way if we can detect if there's interactive children other than the cell header actions and only create a focus trap then, but otherwise auto trigger the cell actions on enter keypress if it's the only interactive child? |
packages/eui/src/components/datagrid/body/header/data_grid_header_cell.tsx
Show resolved
Hide resolved
packages/eui/src/components/datagrid/body/header/data_grid_header_cell_wrapper.tsx
Show resolved
Hide resolved
packages/eui/src/components/datagrid/body/header/_data_grid_header_row.scss
Show resolved
Hide resolved
packages/eui/src/components/datagrid/body/header/data_grid_header_cell.tsx
Outdated
Show resolved
Hide resolved
| const focusables = headerEl ? focusable(headerEl) : []; | ||
| const interactives = focusables.filter( | ||
| (element) => !element.hasAttribute('data-focus-guard') | ||
| ); |
There was a problem hiding this comment.
HandleInteractiveChildren already iterates through tabbable children here:
eui/packages/eui/src/components/datagrid/body/cell/focus_utils.tsx
Lines 44 to 50 in 5c40315
Instead of adding another O(n) iteration through the DOM, can we create/pass an optional onInteractiveChildrenFound callback that passes the array of interactive children? e.g.,
const interactiveChildren = disableInteractives(cellEl);
onInteractiveChildrenFound?.(interactiveChildren);
if (renderFocusTrap) {
setHasInteractiveChildren(interactiveChildren!.length > 0);
}And then this component can then simply re-use it/inspect it for the actions button.
There was a problem hiding this comment.
The main issue here is that disableInteractives uses tabbable which excludes tab-index="-1". What happens (already considering your proposal):
- on mount
renderFocusTrapisfalsebecause initially we don't know ifhasInteractiveChildrenistrue/false disableInteractivesreturns the correct array of interactive children andonInteractiveChildrenFoundcan be used to update thehasInteractivesstate in the header wrapper component which updatesrenderFocusTrapprop onHandleInteractives- this then triggers
disableInteractivesagain through theuseEffect, which now returns an empty array because the run on mount disabled the children, that's why I usedfocusablehere which does not excludetab-index="-1"-> this empty array triggers thehasInteractivesinfocus_utilsto be false and return the children instead of a focus trap here
We do need some way to distinguish this, but only for header cells as it's specific to them. That's why I moved the functionality to the wrapper initially to separate it and shouldDisableInteractives would be the condition to switch between that behavior.
You're right that we can just use focus_utils anyway, but I think we still need shouldDisableInteractives to prevent re-running disableInteractives for header cells once renderFocusTrap updates on hasInteractives state update.
There was a problem hiding this comment.
I updated to use the suggested approach of reusing the functionality in HandleInteractiveChildren here
🗒️ This still uses shouldDisableInteractives to ensure we prevent duplicate runs of disableInteractives when unwanted as explained above.
There was a problem hiding this comment.
Ahh gotcha. I think we can avoid all this simply by removing the if (renderFocusTrap) conditional which triggers the useEffect to re-run. To be totally honest I'm not really sure why I added it in the first place. Can you try this and see if it works?
// On mount, disable all interactive children
useEffect(() => {
if (cellEl) {
const interactiveChildren = disableInteractives(cellEl);
setHasInteractiveChildren(interactiveChildren!.length > 0);
onInteractiveChildrenFound?.(interactiveChildren);
}
}, [cellEl, onInteractiveChildrenFound]);Note that this will require wrapping onInteractiveChildrenFound in a useCallback to not trigger the effect repeatedly, but we should be memoizing everything we can in EuiDataGrid in any case.
There was a problem hiding this comment.
Hmm, looking at the logic I think renderFocusTrap is needed.
- on mount
disableInteractivesreturns elements for all cells as they initially are not disabled yet at the same time we haverenderFocusTrap=falseandhasInteractiveChildren=false - if we remove the
renderFocusTrapcheck, then on mount all cells would be set to interactive which is not the expected behavior
There was a problem hiding this comment.
Since we have two distinct variants (interactive/non-interactive) I'd say the the labelling would need to be conditional now. What I'm thinking is something like this:
// EuiDataGridHeaderCell.tsx
// actionsButton
<button
...
onFocus={() => setIsActionsButtonFocused(true)}
onBlur={() => setIsActionsButtonFocused(false)}
aria-labelledby={
isInteractive && isActionsButtonFocused ? contentAriaId : undefined
} // adds cell title/content as context to the button e.g. `Name`
aria-describedby={
!isInteractive || isActionsButtonFocused ? actionsAriaId : undefined
} // adds instructions `Click to view header actions`
aria-hidden={
!isInteractive || isActionsButtonFocused ? 'false' : 'true'
} // hacky way to prevent button from being read on header cell focus
>
...
</button>
// EuiDataGridHeaderCellWrapper
<EuiDataGridHeaderCellWrapper
...
aria-label={isInteractive ? title : undefined} // use the aria-label only for interactive cells to ensure reading the title first for context
aria-describedby={sortingAriaId}
onInteractivesFound={(interactive) => setInteractive(interactive)}
>
...
</EuiDataGridHeaderCellWrapper>There was a problem hiding this comment.
Screen reader testing is really throwing me for a loop. I'm not sure I'm headed in the right direction, so just thought I'd throw this spike your way: https://github.com/mgadewoll/eui/compare/datagrid/7660-change-header-action-trigger-element...cee-chen:eui:datagrid/7660-change-header-action-trigger-element?w=1
I added an optional render prop to the cell wrapper that passes back whether a focus trap has been rendered or not - not sure if that feels better or worse than a onInteractivesFound callback to you, no strong feelings here either way.
The one big change I want to push for is that we cannot use only title for the aria-label. displayAsText is an optional prop and IMO we cannot rely on it being the thing we present to users - it's far more likely to fall back to a very human unreadable id string (which can be incredibly long for some production datagrids, e.g. a random hash etc).
The thing I'm not sure I'm testing right is I'm getting a bunch of really weird VoiceOver behavior when setting aria-labelledby and aria-describedby - it repeats child content at the end unless I explicitly set aria-hidden on it, which is frustrating. It also repeats the sorting text twice for no discernible reason that I can tell. I might need Trevor to see how JAWS and NVDA behaves and if VO is the incorrect edge case in this scenario.
There was a problem hiding this comment.
🗒️ We agreed to continue with the suggestion from @cee-chen for now and run tests to see the behavior in JAWS and NVDA first before trying to fix for VoiceOver specifically in case the others are fine (since the majority usage is not in VoiceOver)
I pinged @1Copenut for another review with this current state.
There was a problem hiding this comment.
Based on the test results done by @1Copenut we needed to update the code to ensure a better screen reader output. I tried to achieve what we initially had as output with the latest code status in this commit.
One slightly spicy point of change here: We use displayAsText as aria-label if available to ensure we have a text-only label. If it's not passed, then the content is read after the cell description. We can't use the arbitrary content as label since we don't know what it will be.
That means the following output for an example column for "Name" (on VoiceOver, macOS):
// with `display` for interactive content and with `displayAsText` additionally
"Name, Press the Enter key to interact with this cell's contents. table 6 columns, 11 rows"
// with `display` for interactive content but without `displayAsText`
"Press the Enter key to interact with this cell's contents. name table 5 columns, 11 rows"
There was a problem hiding this comment.
Based on Windows screen reader testing, the latest update (commit) links the hint messages via aria-describedby to the cell element as that's the only way to ensure it's read out without duplication on Windows screen readers.
For VoiceOver it does not read out the cell on re-focus after exiting an interactive cell. We removed the aria-live workaround as we optimize for Windows (majority of usage) and accept that there is no feedback given on leaving a cell.
packages/eui/src/components/datagrid/body/header/_data_grid_header_row.scss
Outdated
Show resolved
Hide resolved
- trying to prevent flaky test behavior by ensuring the actions button is visible
- the change is in line-height only by ~1px, as the title is not wrapped in a button anymore
770ed5a to
837b58a
Compare
…es back focus trap information
|
@mgadewoll I took this for another drive with NVDA and JAWS. I'm hearing a lot of repeated information both when the heading has focus and when I move to cells in the column. Here's a brief recap:
I'm attaching a screenshot with some notes and highlights of how I think this could be improved. Please ping me with any questions.
|
@1Copenut Thanks for the check! I agree that we should not have duplicate or unexpected information being read. I'll check how we can bring it back to the initial experience we had for your first check. Some additional information for context:
|
- prevents duplicate or unexpected output. Since we need to support two versions we need to have a state for the button focus to distinguish if it's hidden or available to be read - additionally makes more use of displayAsText to add an accessible label to header cells if available, otherwise the cell content is read as default after the description text
|
I took another pass and my notes weren't as clear as I'd hoped, causing a slight regression. I put a 30 minute invite together to live review markup and that should get us to the finish line quickly. |
- uses aria-describedby over aria-live to ensure expected beahvior for Windows screen readers
|
@mgadewoll && @cee-chen I just retested with NVDA and JAWS, and wow. This is the experience I was hoping for. The native traversal method in Forms/Focus Mode gives clear direction for cell entry and exit, and handles the nested content perfectly. The tabular traversal method (my guess is this is a secondary means of traversal) is pretty solid and gives enough clues that users can easily drop into content and are automatically moved to Forms/Focus mode when they exit a cell using |
- sort vars by concern/usage
cee-chen
left a comment
There was a problem hiding this comment.
🎉 Final code diff readthrough looks amazing!! Really great work on this Lene, and thanks for the quick responses to all my feedback!
💚 Build Succeeded
History
|
`v95.9.0`⏩`v95.10.1` > [!note] > **EuiDataGrid**'s header cells have received a major UX change in order to support interactive children within header content. Column header actions now must be hovered and then clicked directly, or opened with the Enter key, as opposed to being able to click the entire header cell to see the actions popover. _[Questions? Please see our Kibana upgrade FAQ.](https://github.com/elastic/eui/blob/main/wiki/eui-team-processes/upgrading-kibana.md#faq-for-kibana-teams)_ --- ## [`v95.10.0`](https://github.com/elastic/eui/releases/v95.10.0) - Updated `EuiDataGrid` to support interactive header cell content ([#7898](elastic/eui#7898)) - Updated `EuiSearchBar`'s `field_value_selection` filter type with a new `autoSortOptions` config, allowing consumers to configure whether or not selected options are automatically sorted to the top of the filter list ([#7958](elastic/eui#7958)) - Updated `getDefaultEuiMarkdownPlugins` to support the following new default plugin configurations: ([#7985](elastic/eui#7985)) - `parsingConfig.linkValidator`, which allows configuring `allowRelative` and `allowProtocols` - `parsingConfig.emoji`, which allows configuring emoticon parsing - `processingConfig.linkProps`, which allows configuring rendered links with any props that `EuiLink` accepts - See our **Markdown plugins** documentation for example `EuiMarkdownFormat` and `EuiMarkdownEditor` usage - Updated `EuiDatePicker` to support `append` and `prepend` nodes in its form control layout ([#7987](elastic/eui#7987)) **Bug fixes** - Fixed border rendering bug with inline `EuiDatePicker`s with `shadow={false}` ([#7987](elastic/eui#7987)) - Fixed `EuiSuperSelect`'s placeholder text color to match other form controls ([#7995](elastic/eui#7995)) **Accessibility** - Improved the keyboard navigation and screen reader output for `EuiDataGrid` header cells ([#7898](elastic/eui#7898)) ## [`v95.10.1`](https://github.com/elastic/eui/releases/v95.10.1) **Bug fixes** - Fixed a visual bug in compact density `EuiDataGrid`s, where the header cell height would increase when the actions button became visible ([#7999](elastic/eui#7999)) --------- Co-authored-by: Lene Gadewoll <lene.gadewoll@elastic.co>


Summary
closes #7660
This PR updates how
EuiDataGridheader cells behave to support interactive cell content.There are 2 separate scenarios for standalone header cell navigations with this update:
a) the header cell only has no or only 1 actions button and no other interactive children
b) the header cell has at least 1 interactive child other than the actions button
For scenario a) the navigation works the same as before: Focusing the header cell announces that
Enterkeypress will trigger the actions button and open the actions popover.For scenario b) the change enables support for interactive content navigation the same way as it's already used for body cells: focusing the header cell announces that
Enterkeypress will enter the cell to navigate cell content, navigating the cell content follows default DOM navigation and pressing theEscapekey exits the cell and focuses the header cell.This PR additionally adds
aria-liveoutput on leaving cells to provide feedback.To ensure WCAG compliancy for interactive targets, the actions button size was increased from
16pxto24pxand a visual hover state was added.Screen.Recording.2024-08-05.at.13.39.24.mov
QA
General checklist
Checked in both light and dark modesChecked in mobileAdded documentationProps have proper autodocs (using@defaultif default values are missing) and playground togglesChecked Code Sandbox works for any docs examplesIf applicable, added the breaking change issue label (and filled out the breaking change checklist)If applicable, file an issue to update EUI's Figma library with any corresponding UI changes. (This is an internal repo, if you are external to Elastic, ask a maintainer to submit this request)