-
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
base: multi_loader_support
Are you sure you want to change the base?
Changes from all commits
e964eea
2eecd6e
4651c91
1beaefc
66a087c
fe0aaa2
054934d
ced8859
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
|
||
import {AriaLabelingProps, DOMAttributes, DOMProps, Key, KeyboardDelegate, RefObject} from '@react-types/shared'; | ||
import {filterDOMProps, mergeProps, useId} from '@react-aria/utils'; | ||
import {getItemCount} from '@react-stately/collections'; | ||
import {GridCollection} from '@react-types/grid'; | ||
import {GridKeyboardDelegate} from './GridKeyboardDelegate'; | ||
import {gridMap} from './utils'; | ||
|
@@ -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 commentThe 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 |
||
gridProps['aria-colcount'] = state.collection.columnCount; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -24,6 +24,7 @@ import { | |||
RefObject | ||||
} from '@react-types/shared'; | ||||
import {filterDOMProps, mergeProps, useId} from '@react-aria/utils'; | ||||
import {getItemCount} from '@react-stately/collections'; | ||||
import {listMap} from './utils'; | ||||
import {ListState} from '@react-stately/list'; | ||||
import {useGridSelectionAnnouncement, useHighlightSelectionDescription} from '@react-aria/grid'; | ||||
|
@@ -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 commentThe 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" There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||
gridProps['aria-colcount'] = 1; | ||||
} | ||||
|
||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,7 +121,9 @@ export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: R | |
if (isVirtualized) { | ||
let index = Number(item?.index); | ||
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)); | ||
Comment on lines
123
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these are kinda weird in RAC/S2 when there are sections, the |
||
} | ||
|
||
let onAction = data?.onAction ? () => data?.onAction?.(key) : undefined; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ import {getChildNodes} from './getChildNodes'; | |
|
||
const cache = new WeakMap<Iterable<unknown>, number>(); | ||
|
||
export function getItemCount<T>(collection: Collection<Node<T>>): number { | ||
export function getItemCount<T>(collection: Collection<Node<T>>, isValidItem: ((node: Node<T>) => boolean) = (node) => node.type === 'item'): number { | ||
let count = cache.get(collection); | ||
if (count != null) { | ||
return count; | ||
|
@@ -25,11 +25,16 @@ export function getItemCount<T>(collection: Collection<Node<T>>): number { | |
let counter = 0; | ||
let countItems = (items: Iterable<Node<T>>) => { | ||
for (let item of items) { | ||
if (item.type === 'section') { | ||
countItems(getChildNodes(item, collection)); | ||
} else if (item.type === 'item') { | ||
if (isValidItem(item)) { | ||
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 commentThe 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 commentThe 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 |
||
// the header and body when the collection is iterated on, but old collections will return the body rows... | ||
// I could forgo using getItemCount, but the collection.rows is different between old and new collection too... (old includes header row) | ||
if (item.type === 'section' || item.type === 'tablebody') { | ||
countItems(getChildNodes(item, collection)); | ||
} | ||
} | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -262,7 +262,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte | |||
y = 0; | ||||
} | ||||
|
||||
for (let node of collection) { | ||||
for (let node of collectionNodes) { | ||||
let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap; | ||||
// Skip rows before the valid rectangle unless they are already cached. | ||||
if (node.type === 'item' && y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) { | ||||
|
@@ -279,12 +279,13 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte | |||
} | ||||
|
||||
// Build each loader that exists in the collection that is outside the visible rect so that they are persisted | ||||
// at the proper estimated location | ||||
if (y > this.requestedRect.maxY) { | ||||
// 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) { | ||||
Comment on lines
+282
to
+284
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still basically how it worked previously,
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 |
||||
let lastProcessedIndex = collectionNodes.indexOf(node); | ||||
for (let loaderNode of loaderNodes) { | ||||
let loaderNodeIndex = collectionNodes.indexOf(loaderNode); | ||||
// Subtract by an addition 1 since we've already added the current item's height to y | ||||
// Subtract by an additional 1 since we've already added the current item's height to y | ||||
y += (loaderNodeIndex - lastProcessedIndex - 1) * rowHeight; | ||||
let loader = this.buildChild(loaderNode, this.padding, y, null); | ||||
nodes.push(loader); | ||||
|
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...