Refactor listview to always focus the row#3000
Conversation
Felt that the previous modifications to GridKeyboardDelegate were too specific to ListView so split out the changes. Also fixes pageUp/Down/Home/End when you are focused on the ListView row (we forgot to pass the layout to the delegate previously)
needed to override getItemRect for the ListView keyboard delegate so that it always uses the row key, namely due to the gridcell keys being unavailable in the listlayout. Changed getItem and getItemRect to protected funcs in GridKeyboardDelegate so I could do that
still need to fix pageUp/down
needed to override getKeyLeftOf/getKeyRightOf so they dont use getFirstKey or getLastKey or else focus would jump to the last row instead of wrapping inside the cell
| }); | ||
| let layout = useListLayout(state, props.density || 'regular'); | ||
| let keyboardDelegate = useMemo(() => new GridKeyboardDelegate({ | ||
| let keyboardDelegate = useMemo(() => new ListGridKeyboardDelegate({ |
There was a problem hiding this comment.
Note that the focusMode of the gridState and the keyboard delegate is still cell, I think I could change it to row but it was just easier to go ahead and delete onFocus from useGridRow rather than from useGridCell. The onFocus from useGridCell has some additional logic merged in from useSelectableItem
| "version": "3.0.0-alpha.1", | ||
| "private": true, |
There was a problem hiding this comment.
I forget, should I directly modify this or will the version/private be updated upon publish?
| ref={rowRef} | ||
| aria-label={item.textValue}> | ||
| <div | ||
| // TODO: refactor the css here now that we are focusing the row? |
There was a problem hiding this comment.
Will be looking at refactoring the css after a couple of other ListView PRs w/ heavy css updates are merged
| import {Key} from 'react'; | ||
| import {Rect} from '@react-stately/virtualizer'; | ||
|
|
||
| export class ListGridKeyboardDelegate<T> extends GridKeyboardDelegate<T, GridCollection<T>> { |
There was a problem hiding this comment.
What are the differences between this and ListKeyboardDelegate now?
There was a problem hiding this comment.
ListKeyboardDelegate will skip over disabled items whereas this one doesn't. Also handles the getKeyLeftOf/getKeyRightOf that useGridCell calls which ListKeyboardDelegate doesn't
There was a problem hiding this comment.
hmm perhaps we could add getKeyLeftOf/getKeyRightOf to ListKeyboardDelegate? I don't really see why adding them would break anything. And we could possibly add an option to include disabled items too, that seems useful for other types of lists in some cases as well.
My main concern with this as currently implemented is that it's not really a grid, so we're having to override a lot of things. Easy to break later if we change the behavior of grid in some way, and then also need to override that here. Also simplifies the API to have just one ListKeyboardDelegate.
There was a problem hiding this comment.
Sure I can go ahead and try modifying ListKeyboardDelegate instead. Only concern is if some grid keyboard delegate logic will spill over
There was a problem hiding this comment.
I've done some poking around and have come up with f2ef4ef, which leverages ListLayout as the keyboard delegate instead of the new ListGridKeyboardDelegate. I feel a bit iffy about it and it still has some finicky behavior that I can't resolve without bringing in some grid specific logic into ListLayout.
Pain points:
- An effort has been made to make the tracked
focusedKeyalways be a row key. However, due to the nature ofuseGridCell, it is possible for thefocusedKeyto be a cell key (e.g. if a user clicks on a row button), resulting in some broken behavior since the ListLayout wasn't designed to handle such a distinction. If I get rid of theonFocusfromuseGridCellthat would fix the problem offocusedKeybeing set to a cell key but would then mean focus wouldn't update properly if the user clicks on said row buttons. Focus would also get highjacked from a menu button after closing the menu and returned onto the row sinceuseGridRowdoesn't have it's own dedicatedfocusfunc that handles these kinds of situations. - the list collection only has access to the
rowkeys, not thecellkeys. This makes usingListKeyboardDelegate/ListLayouttricky since they don't know what to do with thecellkeys unless I modify those functions so they always try to grab therowkey from the provided grid collection - This mixture of using a grid collection with layouts/delegates that don't have a concept of
rowvscellis pretty painful in general ListKeyboarddDelegatemakes an assumption that the component isn't virtualized and uses query selectors to find row height information
I found the old approach w/ the new keyboard delegate to be a bit easier to navigate since it was extending off the GridKeyboardDelegate and I was able to ensure the focusedKey would always be the cell key meaning there were less pitfalls
| @@ -0,0 +1,31 @@ | |||
| { | |||
| "name": "@react-aria/list", | |||
| "version": "3.0.0-alpha.1", | |||
There was a problem hiding this comment.
Removed the private since it was failing lint (published package uses this). Lemme know if I should bump this version to 3.0.0
|
Build successful! 🎉 |
| let stately = `@react-stately/${basename}`; | ||
| let types = `@react-types/${basename}`; | ||
|
|
||
| if (scope === '@react-spectrum' && isDepUsed(aria, globSrc)) { |
There was a problem hiding this comment.
Updated this lint step since it was complaining that I wasn't importing react-stately/list in react-aria/list even though react-stately/list wasn't used in react-aria/list at all. Now it checks that @react-aria/${basename}/@react-stately/${basename}/@react-types/${basename} is actually being imported somewhere in the package's files before asserting that it exists in the package.json.
Open to suggestions on the changes here and/or I've misunderstood the purpose of these checks
There was a problem hiding this comment.
how was this not a problem before? i know we have many aria packages that don't use a stately package
There was a problem hiding this comment.
dunno, I'd have to go through the aria packages we have to check if this is the first instance of this problem or not. If it isn't then I'm mystified why it was caught here specifically
There was a problem hiding this comment.
Ah right, so the reason why this wasn't a problem was because of this part of the check being passed to the softAssert: !pkgNames[stately] || json.dependencies[stately]. For a package like @react-aria/accordion, the first part will resolve to true since the @react-stately/accordion package doesn't exist (aka pkgNames doesn't contain @react-stately/accordion). However, for the newly introduced @react-aria/list package (ListView specific), @react-stately/list does exist so it expects it to be in the dependencies even though it isn't being used.
I could rename the listview package name to be listview instead of list which should side step this problem but it kinda breaks the previous naming conventions (e.g. TableView's package is table). Also I think that the update to this script still makes sense, doesn't feel like the previous logic was making a correct assumption.
There was a problem hiding this comment.
yep, thanks for looking into that! i think when I wrote that I assumed naming parity
so maybe we should still reconsider the name of the package, however, the change you made also makes sense
| let stately = `@react-stately/${basename}`; | ||
| let types = `@react-types/${basename}`; | ||
|
|
||
| if (scope === '@react-spectrum' && isDepUsed(aria, globSrc)) { |
There was a problem hiding this comment.
how was this not a problem before? i know we have many aria packages that don't use a stately package
|
Build successful! 🎉 |
|
One issue I see: In the dynamic items story, if you focus one of the buttons in the first row, tab out of the list view and shift tab back in, it focuses a different row rather than the first one. Didn't seem to happen previously. Seems like it's going to the last one that is visible. |
|
Strange, doesn't seem to be consistent either. Maybe something with useSelectableCollection and how focuses the last focusable element + GridCell behavior? I'll dig |
…listview_row_focus_2
|
Build successful! 🎉 |
|
@devongovett think I've narrowed it down to it being specifically a story issue since tabbing out of the ListView moves you out of the iframe but I'll go into the details. So the code that is causing the last visible row to be focused is react-spectrum/packages/@react-aria/grid/src/useGridCell.ts Lines 206 to 208 in a7642bf focusedKey. Typically useGridCell's onFocus will ignore the focus event that happens if you are in keyboard modality, which for the most part works out for us when the user tabs/shift-tabs into the ListView since we have a document level keyboard listener that shift modality to "keyboard".
For this story however, tabbing out of the ListView will bring you out of the iframe. If you then move the pointer within the story iframe, modality will shift to "pointer". The subsequent Shift + Tab operation to bring focus back into the ListView happens outside of the iframe and thus the event listener responsible for shifting back to keyboard modality doesn't fire, resulting in Here are some 100% reproduction steps (TableView story since it isn't just for ListView):
TLDR: This is pre-existing behavior and only happens if your focus leaves the document entirely + you shift into pointer modality before tabbing back into the ListView. It won't typically happen if you have focusable elements preceding and following the ListView/TableView. |
helps avoid weird test cases like #3000 (comment)
…listview_row_focus_2
this makes it so the listview doesnt get squished on mobile
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
* initial work to focus the row instead of the cell * cleanup * tentative approach to always skip focusing the cell * retaining focus on Listview focusable child when it is clicked * making ListView specific keyboard delegate Felt that the previous modifications to GridKeyboardDelegate were too specific to ListView so split out the changes. Also fixes pageUp/Down/Home/End when you are focused on the ListView row (we forgot to pass the layout to the delegate previously) * fixing pageUp/Down operations when within the ListView row needed to override getItemRect for the ListView keyboard delegate so that it always uses the row key, namely due to the gridcell keys being unavailable in the listlayout. Changed getItem and getItemRect to protected funcs in GridKeyboardDelegate so I could do that * adding tests * fixing tests after rebase * refactor to have outer div behave as both gridcell and grid row still need to fix pageUp/down * fixing Home/End needed to override getKeyLeftOf/getKeyRightOf so they dont use getFirstKey or getLastKey or else focus would jump to the last row instead of wrapping inside the cell * fixing test and getting rid of erroneous data id * cleanup * lint * removing todo * updating lint so we check that a dep is actually needed before throwing * removing private * pulling triggerPress outside the act * tentative approach to use ListLayout instead of making a ListGrid keyboard delegate * fixing build * removing useGridRow to ensure the focused key will always be a row also reverts changes to other files that arent necessary anymore. Adds tests for disabled keys * cleanup * adding listview story decorator for before and after focusable elements helps avoid weird test cases like #3000 (comment) * adding tests for refactor * creating list aria and types packages * adding useListSelectionCheckbox and exporting types * adding tests for rowindex and colindex * replacing grid hooks with list hooks also clean up and fixes from testing * replace useSelectableCollection call with useSelectableList * fixing docs build? * cleanup and fixing docs build again * making the story input fields only appear at certain screen widths this makes it so the listview doesnt get squished on mobile * addressing review comments * fixing dep version * fixing test failures * ignoring type for collection node for now * small story fix found when testing screen reader announcements
Closes #2946
Alternate approach from #2993, mainly to avoid adding more complexity to useGridCell
This approach essentially makes the outer ListView div behave as if it is the gridcell so we can easily navigate to the focusable children but still applies gridrow specific labeling to it. The inner gridcell div becomes decorative mainly
✅ Pull Request Checklist:
📝 Test Instructions:
Test the ListView stories in all browsers/AT combinations. Test that tab/keyboard navigation always focuses the outer row element and that focus is never set onto the inner grid cell. Contents of the row should NOT be announced all at once when focusing a row (may depend on screen reader), the row's text content should be announced specifically.
All other previous behavior should be the same (selection, navigation, etc)
🧢 Your Project:
RSP