-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: Support keyboard focusing loading spinners in collection components #8326
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
Conversation
Build successful! 🎉 |
optionProps['aria-posinset'] = Number.isNaN(index) ? undefined : index + 1; | ||
optionProps['aria-setsize'] = getItemCount(state.collection); | ||
// TODO: this is not great, but the loader sentinel is always in the collection even when loading isn't currently in progress. | ||
// This same issue applies to the other collection elements, namely the row counts calculated for the top level parent element | ||
optionProps['aria-setsize'] = getItemCount(state.collection, (node) => node.type === 'item' || (node.type === 'loader' && node.props.isLoading)); |
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.
these are kinda weird in RAC/S2 when there are sections, the setsize
is still based on the whole collection but the posinset
is based off the index within the section since the collection calculated index is different in new collections
Build successful! 🎉 |
Build successful! 🎉 |
const diff: number = (cachedItemNodes?.length ?? 0) - (itemNodes?.length ?? 0); | ||
// Look up the start item's index in the cached node array. We can't rely on the node's index | ||
// because it maybe in a section and thus have a index relative to that section | ||
// TODO: the collection keys/item are not guarenteed to be in order | ||
// (aka the loader will be the first node in the keymap if the collection was empty and then loaded items) | ||
// this means we should ideally traverse through the key's prevKey until we find a focusable node but | ||
// that is a bigger refactor, will handle later. For now continue assuming that the loader is the last element | ||
let startItemIndex = 0; | ||
if (startItem) { |
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.
To avoid further bloat, will be handling this refactor later. Perhaps something can be done in the RAC collection level but otherwise we should maybe consider refactoring this to use the old focused key's prevKey
instead of index since the collection's keymap order is not necessarily in index order.
Will need to discuss where exactly we want to restore focus (in roughly the same place? or should it prefer the closest contextually matching row regardless of where that maybe in the collection?)
Build successful! 🎉 |
@@ -59,7 +59,7 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key | |||
} | |||
|
|||
private isDisabled(item: Node<unknown>) { | |||
return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key)); | |||
return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key) || (item.type === 'loader' && !item.props.isLoading)); |
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.
more places where we have to account for the fact that the loading sentinel is in the collection but not actually loading...
@@ -175,7 +176,7 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection< | |||
); | |||
|
|||
if (isVirtualized) { | |||
gridProps['aria-rowcount'] = state.collection.size; | |||
gridProps['aria-rowcount'] = getItemCount(state.collection, (node) => node.type === 'item' || (node.type === 'loader' && node.props.isLoading)); |
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.
again, bit unfortunate that the collection's size is not longer reflective of the number of rows since we don't actually want to count the loading sentinel if it isn't actually loading. Shouldn't be that much more expensive hopefully since getItemCount
does some caching
// at the proper estimated location. If the node.type is "section" then we don't do this shortcut since we have to | ||
// build the sections to see how tall they are. | ||
if ((node.type === 'item' || node.type === 'loader') && y > this.requestedRect.maxY) { |
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.
This is still basically how it worked previously,
if (node.type === 'item' && y > this.requestedRect.maxY) { |
We only used this shortcut when processing items, if there are sections then iterating through the collection will only return sections since we don't support sections and sectionless items at the same time
@@ -167,7 +168,7 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T | |||
); | |||
|
|||
if (isVirtualized) { | |||
gridProps['aria-rowcount'] = state.collection.size; | |||
gridProps['aria-rowcount'] = getItemCount(state.collection, (node) => node.type === 'item' || (node.type === 'loader' && node.props.isLoading)); |
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.
line 154 i think has to check this too since the loader is "tabbable"
also line 166?
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.
oh that right, didn't noticed since useSelectableCollection actually still ensures we have the right tabindex in this case. Relates to the last comment I made below, ugh
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.
Hrm, this opened up a can of worms since I had incorrectly assumed that all the other collection.size
checks wouldn't be a problem since they all are empty collection checks but we do need the same kind of logic there for correctness. Rather than propagate this getItemCount
in all those places perhaps we should update the collection so that collection.size
does a similar check to above? Or maybe we should consider trying to pull the loader out of the collection? Poking around, but I'm leaning towards updating collection's .size
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.
@devongovett any initial opinions on this? We had discussed previously that checks like
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader'); |
counter++; | ||
} | ||
|
||
// TODO: New collections vs old collection is different here. New collections for table will only return |
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.
still a TODO here? or are things working and you just want alternatives?
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.
The latter, feels ok for now but a bit wary about how the loading spinners being included in the collections now means we can't lean on collection.size anymore
* Update tree and listlayout to handle multi loaders * adapting other stories to new loader api and adding useAsync example story * add tests * fix story for correctness, should only need to provide a dependecy at the top most collection * restoring focus back to the tree if the user was focused on the loader * fixing estimated loader position if sections exist taken from #8326 * skip test for now
…m into focus_loading_spinners
…m into focus_loading_spinners
Build successful! 🎉 |
## API Changes
react-aria-components/react-aria-components:UNSTABLE_GridListLoadingSentinel UNSTABLE_GridListLoadingSentinel <T extends {}> {
children?: ReactNode
- className?: string
+ className?: string | ((GridListLoadingSentinelRenderProps & {
+ defaultClassName: string | undefined
+})) => string
isLoading?: boolean
onLoadMore?: () => any
scrollOffset?: number = 1
- style?: CSSProperties
+ style?: CSSProperties | ((GridListLoadingSentinelRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
} /react-aria-components:UNSTABLE_ListBoxLoadingSentinel UNSTABLE_ListBoxLoadingSentinel <T extends {}> {
children?: ReactNode
- className?: string
+ className?: string | ((ListBoxLoadingSentinelRenderProps & {
+ defaultClassName: string | undefined
+})) => string
isLoading?: boolean
onLoadMore?: () => any
scrollOffset?: number = 1
- style?: CSSProperties
+ style?: CSSProperties | ((ListBoxLoadingSentinelRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
} /react-aria-components:UNSTABLE_TableLoadingSentinel UNSTABLE_TableLoadingSentinel <T extends {}> {
children?: ReactNode
- className?: string
+ className?: string | ((TableLoadingSentinelRenderProps & {
+ defaultClassName: string | undefined
+})) => string
isLoading?: boolean
onLoadMore?: () => any
scrollOffset?: number = 1
- style?: CSSProperties
+ style?: CSSProperties | ((TableLoadingSentinelRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
} /react-aria-components:UNSTABLE_TreeLoadingIndicator-UNSTABLE_TreeLoadingIndicator <T extends {}> {
- children?: ReactNode | ((UNSTABLE_TreeLoadingIndicatorRenderProps & {
- defaultChildren: ReactNode | undefined
-})) => ReactNode
- className?: string | ((UNSTABLE_TreeLoadingIndicatorRenderProps & {
- defaultClassName: string | undefined
-})) => string
- style?: CSSProperties | ((UNSTABLE_TreeLoadingIndicatorRenderProps & {
- defaultStyle: CSSProperties
-})) => CSSProperties | undefined
-} /react-aria-components:UNSTABLE_TreeLoadingSentinel+UNSTABLE_TreeLoadingSentinel <T extends {}> {
+ children?: ReactNode | ((TreeLoadingSentinelRenderProps & {
+ defaultChildren: ReactNode | undefined
+})) => ReactNode
+ className?: string | ((TreeLoadingSentinelRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ isLoading?: boolean
+ onLoadMore?: () => any
+ scrollOffset?: number = 1
+ style?: CSSProperties | ((TreeLoadingSentinelRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} @react-stately/collections/@react-stately/collections:getItemCount getItemCount <T> {
collection: Collection<Node<T>>
+ isValidItem: (Node<T>) => boolean
returnVal: undefined
} |
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.
Note: will need to handle the case where the user is using Left/Right to change the selected key when the listbox is closed and focus is on the Picker. If the loading spinner is present, it will just show an empty option, should prevent the loader from being selectable in this case
closing for now, will require a larger refactor than expected since ideally we'd have each node contain information about it's own focusability/selectability |
Closes
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project:
RSP