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

[EuiDataGrid] Change header actions trigger element and enable interactive children #7898

Conversation

mgadewoll
Copy link
Contributor

@mgadewoll mgadewoll commented Jul 22, 2024

Summary

closes #7660

This PR updates how EuiDataGrid header 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 Enter keypress 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 Enter keypress will enter the cell to navigate cell content, navigating the cell content follows default DOM navigation and pressing the Escape key exits the cell and focuses the header cell.
This PR additionally adds aria-live output on leaving cells to provide feedback.

To ensure WCAG compliancy for interactive targets, the actions button size was increased from 16px to 24px and a visual hover state was added.

Screen.Recording.2024-08-05.at.13.39.24.mov

QA

  • review EuiDataGrid storybook and verify that the header cells look as expected and keyboard navigation works

General checklist

  • Browser QA
    • Checked in both light and dark modes
    • Checked in mobile
    • Checked in Chrome, Safari, Edge, and Firefox
    • Checked for accessibility including keyboard-only and screenreader modes
    • Checked in VoiceOver, NVDA, JAWS, for screen reader behavior
  • Docs site QA
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately.
    • If applicable, added the breaking change issue label (and filled out the breaking change checklist)
  • Designer 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)

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.

@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!

@mgadewoll mgadewoll changed the title [DRAFT] [EuiDataGrid] Change header actions trigger element and enable interactive children [EuiDataGrid] Change header actions trigger element and enable interactive children Jul 29, 2024
@mgadewoll mgadewoll force-pushed the datagrid/7660-change-header-action-trigger-element branch from b7290ba to 5ec851c Compare July 30, 2024 07:56
@mgadewoll mgadewoll marked this pull request as ready for review July 30, 2024 13:58
@mgadewoll mgadewoll requested a review from a team as a code owner July 30, 2024 13:58
@cee-chen
Copy link
Member

cee-chen commented Aug 1, 2024

@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?

Comment on lines 62 to 65
const focusables = headerEl ? focusable(headerEl) : [];
const interactives = focusables.filter(
(element) => !element.hasAttribute('data-focus-guard')
);
Copy link
Member

@cee-chen cee-chen Aug 8, 2024

Choose a reason for hiding this comment

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

HandleInteractiveChildren already iterates through tabbable children here:

if (cellEl) {
const interactiveChildren = disableInteractives(cellEl);
if (renderFocusTrap) {
setHasInteractiveChildren(interactiveChildren!.length > 0);
}
}

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main issue here is that disableInteractives uses tabbable which excludes tab-index="-1". What happens (already considering your proposal):

  • on mount renderFocusTrap is false because initially we don't know if hasInteractiveChildren is true/false
  • disableInteractives returns the correct array of interactive children and onInteractiveChildrenFound can be used to update the hasInteractives state in the header wrapper component which updates renderFocusTrap prop on HandleInteractives
  • this then triggers disableInteractives again through the useEffect, which now returns an empty array because the run on mount disabled the children, that's why I used focusable here which does not exclude tab-index="-1" -> this empty array triggers the hasInteractives in focus_utils to 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.

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 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.

Copy link
Member

@cee-chen cee-chen Aug 9, 2024

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, looking at the logic I think renderFocusTrap is needed.

  • on mount disableInteractives returns elements for all cells as they initially are not disabled yet at the same time we have renderFocusTrap=false and hasInteractiveChildren=false
  • if we remove the renderFocusTrap check, then on mount all cells would be set to interactive which is not the expected behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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>

Copy link
Member

@cee-chen cee-chen Aug 14, 2024

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

@mgadewoll mgadewoll Aug 15, 2024

Choose a reason for hiding this comment

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

🗒️ 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.

Copy link
Contributor Author

@mgadewoll mgadewoll Aug 27, 2024

Choose a reason for hiding this comment

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

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"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

@cee-chen cee-chen force-pushed the datagrid/7660-change-header-action-trigger-element branch from 770ed5a to 837b58a Compare August 15, 2024 03:14
@1Copenut
Copy link
Contributor

@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:

  1. When focus in on a heading cell, I'm hearing the cell name and "Additional information" which is the aria-label for the icon. I was really looking for hearing the cell name only.
  2. There's an empty aria-describedby P tag that is referenced by the heading cell.
  3. There's a live region that announces the prompt to press Enter to interact with the cell's contents.
  4. Column cells that reference the heading cell announce the entire name + "Additional Information" and the Enter prompt. I would expect these to only announce the name.

I'm attaching a screenshot with some notes and highlights of how I think this could be improved. Please ping me with any questions.

Screenshot 2024-08-26 at 2 37 50 PM

@mgadewoll
Copy link
Contributor Author

mgadewoll commented Aug 27, 2024

@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:

  1. When focus in on a heading cell, I'm hearing the cell name and "Additional information" which is the aria-label for the icon. I was really looking for hearing the cell name only.
  2. There's an empty aria-describedby P tag that is referenced by the heading cell.
  3. There's a live region that announces the prompt to press Enter to interact with the cell's contents.
  4. Column cells that reference the heading cell announce the entire name + "Additional Information" and the Enter prompt. I would expect these to only announce the name.

I'm attaching a screenshot with some notes and highlights of how I think this could be improved. Please ping me with any questions.

Screenshot 2024-08-26 at 2 37 50 PM

@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:

  • we have to label the header cell with all its content (instead of just the title span) as the content can be passed and hence is unknown to us
  • the empty p tag is the optional sorting description text (not new)
  • the live region was added to support the additional "exited cell" functionality

- 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
Copy link
Member

@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.

SR code changes LGTM! If @1Copenut re-reviews this and finds it works in screen readers without duplicated copy, let's ship it!! 🚢

@1Copenut
Copy link
Contributor

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.

@mgadewoll mgadewoll removed skip-changelog a11yReviewNeeded Accessibility design or code review labels Sep 2, 2024
@1Copenut
Copy link
Contributor

1Copenut commented Sep 3, 2024

@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 ESC. I'm very happy with this latest iteration! 🚢 it.

- sort vars by concern/usage
Copy link
Member

@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.

🎉 Final code diff readthrough looks amazing!! Really great work on this Lene, and thanks for the quick responses to all my feedback!

@mgadewoll mgadewoll enabled auto-merge (squash) September 3, 2024 17:13
@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

@mgadewoll mgadewoll merged commit 57bea12 into elastic:main Sep 3, 2024
5 checks passed
@mgadewoll mgadewoll deleted the datagrid/7660-change-header-action-trigger-element branch September 5, 2024 09:31
jbudz pushed a commit to elastic/kibana that referenced this pull request Sep 10, 2024
`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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[EuiDataGrid] Change header cell popover menu trigger element to be the verticalBoxes icon button
4 participants