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] Implement draggable column headers #8015

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

mgadewoll
Copy link
Contributor

@mgadewoll mgadewoll commented Sep 11, 2024

Summary

closes #7136

This PR implements reordering EuiDataGrid columns via draggable column headers.
This implementation is opt-in keeping parity with current usages.

Todo

  • rebase/update after completed EuiDataGrid Emotion conversion
  • update VRT images
  • add docs update to EUI+ docs (DataGrid content is not yet available, EUI+ is still in need to be fully migrated/released)

Changes

  • adds prop columnDragDrop on EuiDataGrid to toggle draggable column headers - default value is false
  • implements EuiDragDropContext and EuiDroppable in EuiGridHeaderRow to enable a drop zone for all non-control columns
  • implements EuiDraggable in EuiDataGridHeaderCell to enable draggable column headers
    • requires reparenting via portal for dragged items to ensure correct positioning of the position: fixed element inside a transform context
  • updates the customDragHandle prop value on EuiDraggable to be boolean | 'custom'
  • adds cypress tests for the dragging behavior

Accessibility

The Draggable implementation comes with great accessibility out of the box (adding hints and announcements where needed) BUT for the use case of datagrid headers it was not completely fitting and needed a small adjustment.

The expected behavior is, that focussing a column header, we want the columnheader role to be read with all its expected labels and attached descriptions.
EuiDraggable currently always added a role on the drag container (button or group) which will be read instead of the content (effectively losing us semantic information). To solve this we optionally remove the added role on the drag container, which results in the content being read on focus as wanted.

This was tested and optimized on Win11 for JAWS and NVDA.

Screen reader output

// JAWS
// draggable, non-interactive

1. focus of a header cell titled "Name"

Name Press the Enter key to view this column’s actions column header
Press space bar to start a drag. When dragging you can use the arrow keys to move the item around and escape to cancel. Some screen readers may require you to be in focus mode or to use your pass through key

2. press Enter to open the actions menu popover

Tabular Content / EuiDataGrid - Playground ⋅ Storybook - Google Chrome dialog
EuiDataGrid; Page 1 of 1.

3. press Escape to close the actions menu popover

Name Press the Enter key to view this column’s actions column header
Press space bar to start a drag. When dragging you can use the arrow keys to move the item around and escape to cancel. Some screen readers may require you to be in focus mode or to use your pass through key

4. press Space to drag
5. press ArrowRight reorder

You have lifted an item in position 2
You have moved the item from position 2 to position 3

6. press Space to confirm the drag and reorder

You have dropped the item. You have moved the item from position 2 to position 3
// JAWS
// draggable, interactive

1. focus of a header cell titled "Name"

Name, column header
Press the Enter key to interact with this cell’s contents. Press space bar to start a drag. When dragging you can use the arrow keys to move the item around and escape to cancel. Some screen readers may require you to be in focus mode or to use your pass through key

2. press Enter to enter the cell (focussing the first interactive child element)

Additional information Button
To activate press Enter.
tooltip content

3. Press Tab to focus the next child element

Name. Click to view column header actions. Button
To activate press Enter.

4. press Escape to leave the cell

Name, column header
Exited cell content. Press the Enter key to interact with this cell’s contents.

4. press Space to drag
5. press ArrowRight reorder

You have lifted an item in position 1
You have moved the item from position 1 to position 2

6. press Space to confirm the drag and reorder

You have dropped the item. You have moved the item from position 1 to position 2
Press the Enter key to interact with this cell’s contents.
// NVDA
// draggable, non-interactive

1. focus of a header cell titled "Name"

Name Press the Enter key to view this column's actions  column header  Press space bar to start a drag. When dragging you can use the arrow keys to move the item around and escape to cancel. Some screen readers may require you to be in focus mode or to use your pass through key  row 1  column 2

2. press Enter to open the actions menu popover

You are in a dialog. 
Press Escape, or tap/click outside the dialog to close.To navigate through the   list of column actions, press the Tab or Up and Down arrow keys.

3. press Escape to close the actions menu popover

EuiDataGrid; Page 1 of 1.  table
Name Press the Enter key to view this column's actions  column header  Press space bar to start a drag. When dragging you can use the arrow keys to move the item around and escape to cancel. Some screen readers may require you to be in focus mode or to use your pass through key

4. press Space to drag
5. press ArrowRight reorder

space
You have lifted an item in position 2 
You have moved the item from position 2 to position 3 

6. press Space to confirm the drag and reorder

space
You have dropped the item. You have moved the item from position 2 to position 3
// NVDA
// draggable, interactive

1. focus of a header cell titled "Name"

Name,  column header  Press the Enter key to interact with this cell's contents. Press space bar to start a drag. When dragging you can use the arrow keys to move the item around and escape to cancel. Some screen readers may require you to be in focus mode or to use your pass through key  row 1  column 1

2. press Enter to enter the cell (focussing the first interactive child element)

Additional information  button  
tooltip content

3. Press Tab to focus the next child element

Name. Click to view column header actions.  button  

4. press Escape to leave the cell

Name,  column header  Exited cell content. Press the Enter key to interact with this cell's contents.

4. press Space to drag
5. press ArrowRight reorder

space
You have lifted an item in position 1 
You have moved the item from position 1 to position 2 

6. press Space to confirm the drag and reorder

space
You have dropped the item. You have moved the item from position 1 to position 2

Screenshots

mouse usage

Screen.Recording.2024-09-10.at.18.01.57.mov

keyboard usage

Screen.Recording.2024-09-10.at.18.05.00.mov

resizing

Screen.Recording.2024-09-10.at.18.06.38.mov

QA

Storybook: https://eui.elastic.co/pr_8015/storybook/?path=/story/tabular-content-euidatagrid--playground&args=columnDragDrop:!true

EUI docs: https://eui.elastic.co/pr_8015/index.html#/tabular-content/data-grid-schema-columns#draggable-columns

  • verify columnDragDrop prop works to en/disable draggable columns
  • verify drag behavior works with mouse
    • draggable columns indicate drag behavior on hover
    • clicking draggable columns sets a focus state
    • draggable columns can be dragged and reorder of columns works as expected
    • opening the actions menu popover works and that clicking on another draggable column closes it
  • verify that the drag behavior works with keyboard
    • ArrowLeft/Right keys focus draggable header cells
    • Space key starts dragging a cell
      • ArrowLeft/Right move a dragged cell to another position
      • Space drops the dragged cell in the current position
      • Escape key aborts the drag
    • Enter key on interactive header cells enters the cell content
      • Escape in an interactive cell exits the cell and focuses it
  • verify that resizing a draggable cell works as expected

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
  • 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)

@@ -110,6 +115,8 @@ export const EuiDraggable: FunctionComponent<EuiDraggableProps> = ({
role={
hasInteractiveChildren
? 'group'
: customDragHandle === 'custom'
Copy link
Contributor Author

@mgadewoll mgadewoll Sep 11, 2024

Choose a reason for hiding this comment

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

The idea here is to provide means to unset the role for specific use cases. Using customDragHandle=true for that would have impact on current usages, hence I'm suggesting to add a third custom option as this should not be a default.

The reasoning here is this:
EuiDraggable adds a drag wrapper around its content, which has role="button|group" - this works for most cases where the drag element should be the interactive/focused element. The impact of this is, that the content of this element is not read as standalone semantic elements. E.g. role="button" removes additional semantic content and just reads visible text content.
For datagrid column headers we rather want the columnheader to still be the element to be read via screen readers as it holds all semantic information. Instead we want the draggable accessible information added to the column header instead of the wrapper being read only.
To ensure the columnheader role element is read, the draggable wrapper needs to have no role that removes the content semantics. Hence the decision to provide means to unset it for this case.

@mgadewoll mgadewoll force-pushed the datagrid/7136-draggable-column-headers branch from 9e23fdd to 2ccfb4d Compare September 13, 2024 11:51
Copy link
Contributor Author

@mgadewoll mgadewoll Sep 13, 2024

Choose a reason for hiding this comment

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

All VRT images updates are related to this change which means the "Account" title now sits flush within the header cell padding instead of 2px offset due to the gap (which applied because the action elements are only visually hidden but available in the DOM)

Screenshot 2024-09-13 at 14 06 30

@mgadewoll mgadewoll changed the title [DRAFT] [EuiDataGrid] Implement draggable column headers [EuiDataGrid] Implement draggable column headers Sep 13, 2024
@mgadewoll mgadewoll marked this pull request as ready for review September 13, 2024 16:05
@mgadewoll mgadewoll requested a review from a team as a code owner September 13, 2024 16:05
@mgadewoll mgadewoll marked this pull request as draft September 13, 2024 16:33
@mgadewoll mgadewoll marked this pull request as ready for review September 16, 2024 08:47
Copy link
Member

Choose a reason for hiding this comment

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

Could we see an example of a draggable column header with interactive children? a la https://eui.elastic.co/storybook/?path=/story/tabular-content-euidatagrid--custom-header-content?

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 the example to have some interactive columns to show both cases (commit)

Copy link
Member

@cee-chen cee-chen Sep 19, 2024

Choose a reason for hiding this comment

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

Thank you! Linking the Storybook setup here just for helpfulness: https://eui.elastic.co/pr_8015/storybook/?path=/story/tabular-content-euidatagrid--custom-header-content&args=columnDragDrop:!true

To be totally honest, something about it feels a little a little off to me. Going to CC @MichaelMarcialis into this because I'm getting into UX designer territory:

It feels odd to me that the column actions have such a relatively smaller hitbox / interaction area, but the drag action has such a large one (literally the entire cell except for interactive children). It feels disproportionate in terms of UX emphasis - while reordering is an important feature, sorting a column is arguably even more important for tabular data and is now relatively harder to find/click on.

How would we feel about moving the draggable interaction area to just the handle on hover, instead of the entire cell backdrop? That way in terms of clickability, the drag handle, actions popover, and tooltip children all have roughly equal discoverability and one doesn't eclipse the other.

Am I way off in that thinking?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the ping, @cee-chen. It's a good question. Personally, I'm a bit torn between the two directions. There are pros and cons to both.

In my own personal experiences, I've seen it done both ways, though the majority of those experiences (Google Sheets, AirTable, etc.) have the entire header cell as a draggable zone, rather than relegating the action to a smaller drag handle. This extra draggable real estate is nice to bring the reordering of columns feature to the forefront. However, it could potentially be annoying if misclicks on other actions in the cell result in inadvertent drag actions.

On the other hand, relegating the drag to the drag handle would prevent the misclick issue. But at the same time, it dramatically reduces the drag real estate and may cause frustration for users attempting to drag the column when the cursor is not resting on the handle.

For my part, in playing around with the current approach in Storybook, I quite like it and don't find it particularly odd or frustrating to work with (even when interacting with the additional cell actions). I'd say lets proceed as is and just monitor for feedback to the contrary. In case anyone else has any opinions, I'll CC @elastic/platform-design to see if there are opposing thoughts.

Copy link
Contributor

@ryankeairns ryankeairns Sep 20, 2024

Choose a reason for hiding this comment

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

Seems fine, but I suggest we test this in Kibana > Discover before merging.
With the One Discover work, there are more and more extension points and some can involve actions and tooltips in the column headers. Poking around in Discover's data grid - for each solution - seems like a smart if not necessary thing for us to do.

@@ -0,0 +1,2 @@
- Added `columnDragDrop` prop on `EuiDataGrid` which enables reordering columns via draggable header cells
Copy link
Member

@cee-chen cee-chen Sep 18, 2024

Choose a reason for hiding this comment

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

I'm tempted to nest this under an existing setting rather than adding yet another top level prop to EuiDataGrid. How do you feel about gridStyle: { canDragDropColumns: true }?

Or maybe the columnVisibility prop, since that does affect column order? e.g.

columnVisibility={{
  visibleColumns: [...],
  setVisibleColumns: () => {},
  canDragAndDropColumns: true,
}}

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 think gridStyle might work, considering it also holds stickyFooter.
columnVisibility seems a bit too specific to visibility to add drag behavior? 🤔

Copy link
Member

Choose a reason for hiding this comment

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

columnVisibility actually does make sense to me given its prop description (emphasis mine):

Defines which columns are intitially visible in the grid and the order they are displayed

I'm also assuming we can't drag and drop leading/trailing control columns, correct? If so, columnVisibility actually makes that clearer because it does not include control columns in its logic/array (whereas gridStyle affects all columns).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right. It does not apply to leading/trailing columns indeed.
I'll update to move the prop to columnVisibility then 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in this commit

// Draggable prevents FocusTrap onOutsideClick to be called.
// We manually close the popover for draggable cells and
// update the focus index onBlur to ensure execution order
// as closePopover() focuses its own cells first on close.
Copy link
Member

Choose a reason for hiding this comment

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

I need to run for today but I'm planning on coming back to this file and doing a closer review tomorrow (also delaying because I'm guessing this might change significantly if we need to support #8015 (comment)).

Just curious, do we know exactly why the draggable library interferes with our existing click events?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interactive children work as expected with the draggable header columns (example story).

The general problem is that clicking the draggable column will click the draggable wrapper (and does not receive focus), not the column header. And somewhere the focus is caught, because the focus listener on the header cell that handles the focus context does not trigger.
This starts happening as soon as the provided.draggableProps are added. I did not find the root cause in code so far. 🤔

- draggable cells prevent onOutsideClick to be triggered, we need to manually update focus to ensure expected behavior

- moves columnResizer element to ensure drag and resize actions stay separate
- add columnDragDrop control to custom ehader story for testing with interactive headers
- ensures the columnheader element is read instead of the wrapping draggable container; this requires the draggable wrapper to not have a role as the default roles button/group remove any semantics from their content when focused which results in the content not fully being read
- this reparenting approach is required due to transform context of datagrid which interfers with the positioning of dragged items
- use unique ids

- remove position style override as it's not needed for the reparented/portalled approach
- the changes are only related to the conditionally added gap on header cells
- prevents error about not finishing drop animation as there were duplicate elements being dragged
- prevents duplicate SR output in entering the cell
…e elements

- ensure we use the rowing tabindex and don't add additional unwanted tab stops
- includes column header with interactive cell content
@mgadewoll mgadewoll force-pushed the datagrid/7136-draggable-column-headers branch from 141c294 to deceb15 Compare September 18, 2024 09:02
@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

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.

[EuiDataGrid] Implement draggable columns with table headers
5 participants