-
Notifications
You must be signed in to change notification settings - Fork 841
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] Fix keyboard navigation not fully scrolling cells into view #5515
Conversation
Preview documentation changes for this PR: https://eui.elastic.co/pr_5515/ |
Firefox (v95.02) is acting weird for me on this one, it seems to process the changes to focus cell really late, even batching a few of the scroll operations together, so I can pretty easily press down a number of times before the selected cell jumps into focus. Safari initially did some weird things where it didn't seem to be scrolling at all, but then it began working and I haven't seen it misbehave since 😕 |
Yeah I've seen that FF issue intermittently here but it works 80% of the time for me (screencaps were taken on FF). I'm tempted to add an |
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.
Fix for navigating up LGTM, fix for navigating down to the last row LGTM. We can at the footer interaction & non-last-row down separately.
Looks good! Just one scenario that may or may not be relevant:
The correct cell is focused, but the focused cell is not in view. Too much of an edge case? |
@thompsongl that specific edge case of focus restoration should be resolved separately in #5530! FYI, I'm still wondering if there's a more robust way to handle downwards navigation + I might poke at this a little bit more to try and address the intermittent scrolling issues we're still seeing, so will hold off on merging this for today |
Just saw that! Thanks |
…using keyboard navigation
+ small conversion example for gridStyles
4a65407
to
6d5adbe
Compare
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.
PHEW, ALRIGHT. That was more math than I've done since high school but I've finally got this this working ✨ perfectly ✨
I banged my head around a couple different approaches and got super super close (accounting for scrollbars, sticky footers, etc), but it didn't really click until I realized I was relying too much on react-window's scrollToItem
API. scrollToItem
is inherently flawed because it doesn't account for sticky headers/footers, BUT I really only need it for cells that are totally out of view. So I stopped using it as a default and instead opted to query for the cell DOM node and use its scroll position/height/width + the grid's scroll container to calculate whether or not the cell actually needed to be fully scrolled into view on any sides.
I'm pretty happy with this final solution as it feels robust and has way less spaghetti/one-offs than other attempts I made, and if the various math positions are a headache to track, I at least tried to comment the everloving shit out of it as well as write (hopefully) helpful unit tests. LMK what you think!
(FYI, I basically scrapped everything from my original PR except the hook test helper, but I kept the commits in the git history just for reference.)
@@ -616,7 +616,7 @@ export class EuiDataGridCell extends Component< | |||
ref={this.cellRef} | |||
{...cellProps} | |||
data-test-subj="dataGridRowCell" | |||
data-gridcell-id={`${this.props.rowIndex},${this.props.colIndex}`} | |||
data-gridcell-id={`${this.props.colIndex},${this.props.visibleRowIndex}`} |
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.
@chandlerprall This is possibly the dicey change in this approach (but unfortunately is necessary, as my entire approach requires being able to obtain the outermost cell DOM node).
I changed the order because it's confusingly the opposite of setFocusedCell
which is [colIndex, rowIndex]
, and I thought the x,y
coordinates made a bit more sense.
I also changed this from rowIndex
to visibleRowIndex
because that's what the focused cell behavior uses for indices, and it feels somewhat more accurate to how react-window
behaves - that is to say, on pagination it doesn't actually destroy the cells that exist but instead replaces its contents.
That being said, if that's too big of a change for this PR, maybe we should add a new data attr instead like data-gridcell-location
? 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.
We can swap to x,y , but it would be a breaking change (+ Kibana has two affected unit tests) which is okay, and I agree that the x,y pattern fits better.
Instead of changing this attribute to be the visible row index, let's use that second data-gridcell-location
idea and provide both.
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.
Per our sync call:
- I've added 4 new props,
data-gridcell-column-id
,data-gridcell-column-index
,data-gridcell-row-index
, anddata-gridcell-visible-row-index
(with code comments for each prop to help indicate their use-cases). - This allows for flexibility in choosing specific cells by the cell location vs cell data, or even choosing entire rows or columns.
data-gridcell-id
will be deprecated in 3 months in favor of using each 4 separated data attrs
// Obtain the outermost wrapper of the current cell in view in order to | ||
// get scroll position/height/width calculations and determine what level | ||
// of scroll adjustment the cell needs | ||
const getCell = () => | ||
outerGridRef.current!.querySelector<HTMLDivElement>( | ||
`[data-gridcell-id="${colIndex},${rowIndex}"]` | ||
); | ||
let cell = getCell(); |
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.
See above comment - .querySelector
was the fastest/easiest way I could think of to get the cell we wanted, but I'm aware it's not the most "React-y" approach. Definitely interested in hearing if y'all can think of some way of either obtaining or passing the current cell node in easily. Otherwise, I don't think plain vanilla JS is the worst here.
Quick note that I did also experiment with document.activeElement
, but it doesn't work for a few reasons:
- It's actually stale and not yet updated to the newly focused cell when this workaround fires
- We focus into cell contents that have interactive elements which means sometimes the active element is not the outermost cell wrapper node (which is what we need to correctly calculate widths/heights/scroll positions)
- I was planning on using this helper for the new
openCellPopover
API we're shortly going to be exposing as part of [EuiDataGrid] Expansion popover needs more customization #5310, which will not be triggering a focus element
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.
Short of adding another context that cells can use to register/deregister through, I don't see another way to solve this. I think querySelector is great for navigating these internal-component contracts.
Preview documentation changes for this PR: https://eui.elastic.co/pr_5515/ |
…atch setFocusedCell - setFocused cell uses `colIndex,rowIndex` not `rowIndex,colIndex` - this updates our data attr to match, and is more common as an x,y coordinates - NOTE: I'm relying heavily on the ability to find a cell via querySelector and this data-attr in my upcoming logic, hence the desire for standardization
- Less based on `scrollIntoView` than before - I now only call that specific API if the cell is completely out of viewport/not rendered - This allows us to stop fighting react-window's incorrect calculations due to not taking the sticky header into account, and only require the grid+cell's dimensions/scroll position to determine whether or not we need to tweak our scroll position
- Not super in love with the E2E tests, but not sure how to meaningfully spy on code otherwise - maybe visual snapshots would work better?
…e grid's dimensions - or for cells that are not *currently* out lf top/left bound but *would be* once the bottom/right bound was adjusted for (i.e., wider or taller than the grid) + simplify scroll adjustments for the left/top pos - we can simply statically use the cell's position instead of doing math relative to the current scroll position
6d5adbe
to
9d950a9
Compare
Preview documentation changes for this PR: https://eui.elastic.co/pr_5515/ |
Preview documentation changes for this PR: https://eui.elastic.co/pr_5515/ |
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.
Wow this is cool and solves so many edge cases! Couple of thoughts, couple of 🥳 , I think there was one actual suggestion somewhere in here.
@@ -616,7 +616,7 @@ export class EuiDataGridCell extends Component< | |||
ref={this.cellRef} | |||
{...cellProps} | |||
data-test-subj="dataGridRowCell" | |||
data-gridcell-id={`${this.props.rowIndex},${this.props.colIndex}`} | |||
data-gridcell-id={`${this.props.colIndex},${this.props.visibleRowIndex}`} |
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.
We can swap to x,y , but it would be a breaking change (+ Kibana has two affected unit tests) which is okay, and I agree that the x,y pattern fits better.
Instead of changing this attribute to be the visible row index, let's use that second data-gridcell-location
idea and provide both.
src/components/datagrid/body/header/data_grid_header_cell_wrapper.tsx
Outdated
Show resolved
Hide resolved
return; // Grid isn't rendered yet or is empty | ||
} | ||
|
||
const gridDoesNotScroll = |
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'll make a note to look into if this does matter in any way, but I wonder if this logic should try to match the same scroll*/client* logic in https://github.com/elastic/eui/blob/main/src/components/datagrid/utils/grid_height_width.ts#L146-L148 ?
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 think there's some overlapping logic here, in this case I think it's more safe to look at whether in the outer/inner elements share the same width/height to determine whether the grid scrolls. It's worth noting that for example in Firefox, I never actually see a scrollbar width (offsetWidth and clientWidth are the same for me because the scrollbars in FF are 'overlays' and don't actually occupy width), whereas in Chrome/Edge/Safari, the scrollbar actually takes up width.
// Obtain the outermost wrapper of the current cell in view in order to | ||
// get scroll position/height/width calculations and determine what level | ||
// of scroll adjustment the cell needs | ||
const getCell = () => | ||
outerGridRef.current!.querySelector<HTMLDivElement>( | ||
`[data-gridcell-id="${colIndex},${rowIndex}"]` | ||
); | ||
let cell = getCell(); |
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.
Short of adding another context that cells can use to register/deregister through, I don't see another way to solve this. I think querySelector is great for navigating these internal-component contracts.
+ update query selectors and Cypress tests to use new dat attrs + deprecate `data-gridcell-id`
This reverts commit 1e5999b.
Preview documentation changes for this PR: https://eui.elastic.co/pr_5515/ |
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.
🎉 changes lgtm, cannot find any bugs in user experience in Chrome, FF, or Safari across the various examples.
General changes
Cell data attributes (f444d56)
4 new data attributes have been added to individual grid cells:
data-gridcell-column-id
- Static column ID name, not affected by column orderdata-gridcell-column-index
- Affected by column reorderingdata-gridcell-row-index
- Index comes from data & is not affected by sorting or paginationdata-gridcell-visible-row-index
- Affected by sorting & paginationThe separated props allows for flexibility in choosing specific cells by (e.g.) cell location vs cell data, or even choosing entire rows or columns.
data-gridcell-id
will be deprecated in 3 months in favor of using the new 4 separate data attrs.Test hook helper (961677c)
I added a new helper for testing custom react hooks in this PR, which we'll continue to use as I write more unit tests for data grid.
Scroll/focus/keyboard nav fixes
closes #3151
closes #4456
closes #5481
closes #5516
Before
Note that using the up arrow to navigate back upwards is in particularly completely broken - see https://elastic.github.io/eui/#/tabular-content/data-grid-virtualization
After
Checklist
- [ ] Check against all themes for compatibility in both light and dark modes- [ ] Props have proper autodocs and playground toggles- [ ] Added documentation- [ ] Checked Code Sandbox works for any docs examples