From 1269d852b6cd8de16ac725e72428ff2211f00e44 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:57:58 -0700 Subject: [PATCH 01/15] update aria-rowindex calculation --- .../gridlist/src/useGridListItem.ts | 44 +++++++++++++++- .../gridlist/src/useGridListSection.ts | 50 +++++++++++++++++-- .../react-aria-components/src/GridList.tsx | 6 ++- .../stories/GridList.stories.tsx | 24 ++++----- 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 2ecfc4f8125..a254a65f4cb 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -277,6 +277,33 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt // }); // } + let sumOfNodes = (node: RSNode): number => { + if (node.prevKey === null) { + if (node.type === 'section') { + return [...state.collection.getChildren!(node.key)].length; + } else if (node.type === 'item') { + return 1; + } + return 0; + } + + let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) : null; + if (parentNode && parentNode.type === 'section') { + return sumOfNodes(parentNode); + } + + let prevNode = state.collection.getItem(node.prevKey!); + if (prevNode) { + if (node.type === 'section') { + return sumOfNodes(prevNode) + [...state.collection.getChildren!(node.key)].length; + } + + return sumOfNodes(prevNode) + 1; + } + + return 0; + }; + let rowProps: DOMAttributes = mergeProps(itemProps, linkProps, { role: 'row', onKeyDownCapture, @@ -294,7 +321,22 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let {collection} = state; let nodes = [...collection]; // TODO: refactor ListCollection to store an absolute index of a node's position? - rowProps['aria-rowindex'] = nodes.find(node => node.type === 'section') ? [...collection.getKeys()].filter((key) => collection.getItem(key)?.type !== 'section').findIndex((key) => key === node.key) + 1 : node.index + 1; + if (nodes.find(node => node.type === 'section')) { + let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) : null; + let isInSection = parentNode && parentNode.type === 'section'; + if (isInSection) { + let diff = [...state.collection.getChildren!(parentNode!.key)].length - node.index - 1; + if (parentNode!.prevKey) { + rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - diff; + } else { + rowProps['aria-rowindex'] = [...state.collection.getChildren!(parentNode!.key)].length - diff; + } + } else { + rowProps['aria-rowindex'] = sumOfNodes(node); + } + } else { + rowProps['aria-rowindex'] = node.index + 1; + } } let gridCellProps = { diff --git a/packages/@react-aria/gridlist/src/useGridListSection.ts b/packages/@react-aria/gridlist/src/useGridListSection.ts index f7d8bce9433..d60c34f6578 100644 --- a/packages/@react-aria/gridlist/src/useGridListSection.ts +++ b/packages/@react-aria/gridlist/src/useGridListSection.ts @@ -10,13 +10,17 @@ * governing permissions and limitations under the License. */ -import {DOMAttributes, RefObject} from '@react-types/shared'; +import {DOMAttributes, RefObject, Node as RSNode} from '@react-types/shared'; import type {ListState} from '@react-stately/list'; import {useLabels, useSlotId} from '@react-aria/utils'; export interface AriaGridListSectionProps { /** An accessibility label for the section. Required if `heading` is not present. */ - 'aria-label'?: string + 'aria-label'?: string, + /** An object representing the section. */ + node: RSNode, + /** Whether the list row is contained in a virtual scroller. */ + isVirtualized?: boolean } export interface GridListSectionAria { @@ -37,20 +41,56 @@ export interface GridListSectionAria { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export function useGridListSection(props: AriaGridListSectionProps, state: ListState, ref: RefObject): GridListSectionAria { - let {'aria-label': ariaLabel} = props; + let {'aria-label': ariaLabel, node, isVirtualized} = props; let headingId = useSlotId(); let labelProps = useLabels({ 'aria-label': ariaLabel, 'aria-labelledby': headingId }); + let rowIndex; + + let sumOfNodes = (node: RSNode): number => { + if (node.prevKey === null) { + if (node.type === 'section') { + return [...state.collection.getChildren!(node.key)].length; + } else if (node.type === 'item') { + return 1; + } + return 0; + } + + let prevNode = state.collection.getItem(node.prevKey!); + if (prevNode) { + if (node.type === 'item') { + return sumOfNodes(prevNode) + 1; + } + + return sumOfNodes(prevNode) + [...state.collection.getChildren!(node.key)].length; + } + + return 0; + }; + + if (isVirtualized) { + if (node.prevKey) { + let prevNode = state.collection.getItem(node.prevKey); + if (prevNode) { + rowIndex = sumOfNodes(prevNode) + 1; + } + } else { + rowIndex = 1; + } + } return { rowProps: { - role: 'row' + role: 'row', + 'aria-rowindex': rowIndex }, rowHeaderProps: { id: headingId, - role: 'rowheader' + role: 'rowheader', + 'aria-colindex': 1 }, rowGroupProps: { role: 'rowgroup', diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index d36cca8d398..6c511f23538 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -582,11 +582,13 @@ export interface GridListSectionProps extends SectionProps {} */ export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, (props: GridListSectionProps, ref: ForwardedRef, item: Node) => { let state = useContext(ListStateContext)!; - let {CollectionBranch} = useContext(CollectionRendererContext); + let {CollectionBranch, isVirtualized} = useContext(CollectionRendererContext); let headingRef = useRef(null); ref = useObjectRef(ref); let {rowHeaderProps, rowProps, rowGroupProps} = useGridListSection({ - 'aria-label': props['aria-label'] ?? undefined + 'aria-label': props['aria-label'] ?? undefined, + node: item, + isVirtualized }, state, ref); let renderProps = useRenderProps({ defaultClassName: 'react-aria-GridListSection', diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index 0d8c197519a..c75e0b02650 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -160,21 +160,21 @@ export const GridListSectionExample = (args) => ( }}> Section 1 - 1,1 - 1,2 - 1,3 + 1,1 + 1,2 + 1,3 Section 2 - 2,1 - 2,2 - 2,3 + 2,1 + 2,2 + 2,3 Section 3 - 3,1 - 3,2 - 3,3 + 3,1 + 3,2 + 3,3 ); @@ -213,7 +213,7 @@ export function VirtualizedGridListSection() { let sections: {id: string, name: string, children: {id: string, name: string}[]}[] = []; for (let s = 0; s < 10; s++) { let items: {id: string, name: string}[] = []; - for (let i = 0; i < 3; i++) { + for (let i = 0; i < 5; i++) { items.push({id: `item_${s}_${i}`, name: `Section ${s}, Item ${i}`}); } sections.push({id: `section_${s}`, name: `Section ${s}`, children: items}); @@ -223,11 +223,11 @@ export function VirtualizedGridListSection() { @@ -236,7 +236,7 @@ export function VirtualizedGridListSection() { {section.name} - {item => {item.name}} + {item => {item.name}} )} From 56ddffa440e23221b769f05d60c538601fafe38f Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:58:05 -0700 Subject: [PATCH 02/15] add tests --- .../test/GridList.test.js | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/packages/react-aria-components/test/GridList.test.js b/packages/react-aria-components/test/GridList.test.js index dc01ea3f93b..367e8957c5e 100644 --- a/packages/react-aria-components/test/GridList.test.js +++ b/packages/react-aria-components/test/GridList.test.js @@ -494,6 +494,85 @@ describe('GridList', () => { expect(items).toHaveLength(2); }); + it('should calculate the correct aria-rowindex when gridlist is made up of only sections', () => { + let sections = []; + for (let s = 0; s < 5; s++) { + let items = []; + for (let i = 0; i < 2; i++) { + items.push({id: `item_${s}_${i}`, name: `Section ${s}, Item ${i}`}); + } + sections.push({id: `section_${s}`, name: `Section ${s}`, children: items}); + } + + jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 300); + + let {getAllByRole} = render( + + + + {section => ( + + {section.name} + + {item => {item.name}} + + + )} + + + + ); + + let rows = getAllByRole('row'); + expect(rows).toHaveLength(15); + expect(rows.map(r => r.textContent)).toEqual(['Section 0', 'Section 0, Item 0', 'Section 0, Item 1', 'Section 1', 'Section 1, Item 0', 'Section 1, Item 1', 'Section 2', 'Section 2, Item 0', 'Section 2, Item 1', 'Section 3', 'Section 3, Item 0', 'Section 3, Item 1', 'Section 4', 'Section 4, Item 0', 'Section 4, Item 1']); + + for (let i = 0; i < 15; i++) { + expect(rows[i]).toHaveAttribute('aria-rowindex'); + expect(rows[i].getAttribute('aria-rowindex')).toEqual(`${i + 1}`); + } + }); + + it('should calculate the correct aria-rowindex when there is a mix of sections and individual items', () => { + jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 300); + + let {getAllByRole} = render( + + + Home + School + + Pets + Cat + Dog + + + Vanilla + Chocolate + + City Hall + Pharmacy + + Plants + Sunflower + Daffodil + + + + ); + + let rows = getAllByRole('row'); + expect(rows).toHaveLength(12); + expect(rows.map(r => r.textContent)).toEqual(['Home', 'School', 'Pets', 'Cat', 'Dog', 'Vanilla', 'Chocolate', 'City Hall', 'Pharmacy', 'Plants', 'Sunflower', 'Daffodil']); + + for (let i = 0; i < 12; i++) { + expect(rows[i]).toHaveAttribute('aria-rowindex'); + expect(rows[i].getAttribute('aria-rowindex')).toEqual(`${i + 1}`); + } + }); + describe('selectionBehavior="replace"', () => { // Required for proper touch detection installPointerEvent(); From 84a3a20887bae93f6a963fe91dd9f3fd3aeb5005 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:59:15 -0700 Subject: [PATCH 03/15] use lastChildKey to calculate rowindex --- .../gridlist/src/useGridListItem.ts | 9 ++++++--- .../gridlist/src/useGridListSection.ts | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index a254a65f4cb..52775cfa832 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -20,6 +20,7 @@ import type {ListState} from '@react-stately/list'; import {SelectableItemStates, useSelectableItem} from '@react-aria/selection'; import type {TreeState} from '@react-stately/tree'; import {useLocale} from '@react-aria/i18n'; +import { CollectionNode } from '@react-aria/collections'; export interface AriaGridListItemOptions { /** An object representing the list item. Contains all the relevant information that makes up the list row. */ @@ -322,10 +323,12 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let nodes = [...collection]; // TODO: refactor ListCollection to store an absolute index of a node's position? if (nodes.find(node => node.type === 'section')) { - let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) : null; + let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode: null; let isInSection = parentNode && parentNode.type === 'section'; - if (isInSection) { - let diff = [...state.collection.getChildren!(parentNode!.key)].length - node.index - 1; + let lastChildKey = parentNode?.lastChildKey; + if (isInSection && lastChildKey) { + let lastChild = state.collection.getItem(lastChildKey) + let diff = lastChild ? lastChild.index - node.index : 0; if (parentNode!.prevKey) { rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - diff; } else { diff --git a/packages/@react-aria/gridlist/src/useGridListSection.ts b/packages/@react-aria/gridlist/src/useGridListSection.ts index d60c34f6578..f0b2731fc60 100644 --- a/packages/@react-aria/gridlist/src/useGridListSection.ts +++ b/packages/@react-aria/gridlist/src/useGridListSection.ts @@ -13,8 +13,9 @@ import {DOMAttributes, RefObject, Node as RSNode} from '@react-types/shared'; import type {ListState} from '@react-stately/list'; import {useLabels, useSlotId} from '@react-aria/utils'; +import {CollectionNode} from '@react-aria/collections'; -export interface AriaGridListSectionProps { +export interface AriaGridListSectionProps { /** An accessibility label for the section. Required if `heading` is not present. */ 'aria-label'?: string, /** An object representing the section. */ @@ -40,7 +41,7 @@ export interface GridListSectionAria { * @param props - Props for the section. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function useGridListSection(props: AriaGridListSectionProps, state: ListState, ref: RefObject): GridListSectionAria { +export function useGridListSection(props: AriaGridListSectionProps, state: ListState, ref: RefObject): GridListSectionAria { let {'aria-label': ariaLabel, node, isVirtualized} = props; let headingId = useSlotId(); let labelProps = useLabels({ @@ -51,8 +52,10 @@ export function useGridListSection(props: AriaGridListSectionProps, state: Li let sumOfNodes = (node: RSNode): number => { if (node.prevKey === null) { - if (node.type === 'section') { - return [...state.collection.getChildren!(node.key)].length; + let lastChildKey = (node as CollectionNode).lastChildKey; + if (node.type === 'section' && lastChildKey) { + let lastChild = state.collection.getItem(lastChildKey); + return lastChild ? lastChild.index + 1 : 0; } else if (node.type === 'item') { return 1; } @@ -64,8 +67,12 @@ export function useGridListSection(props: AriaGridListSectionProps, state: Li if (node.type === 'item') { return sumOfNodes(prevNode) + 1; } - - return sumOfNodes(prevNode) + [...state.collection.getChildren!(node.key)].length; + + let lastChildKey = (node as CollectionNode).lastChildKey; + if (lastChildKey) { + let lastChild = state.collection.getItem(lastChildKey); + return lastChild ? sumOfNodes(prevNode) + lastChild.index + 1 : sumOfNodes(prevNode); + } } return 0; From dd71e547d1374380e5ed3d112fbb5f1eaacdfad4 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:05:20 -0700 Subject: [PATCH 04/15] fix lint --- packages/@react-aria/gridlist/package.json | 1 + packages/@react-aria/gridlist/src/useGridListItem.ts | 6 +++--- packages/@react-aria/gridlist/src/useGridListSection.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/@react-aria/gridlist/package.json b/packages/@react-aria/gridlist/package.json index 2d463eac429..0c164953728 100644 --- a/packages/@react-aria/gridlist/package.json +++ b/packages/@react-aria/gridlist/package.json @@ -26,6 +26,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { + "@react-aria/collections": "3.0.0-rc.4", "@react-aria/focus": "^3.21.0", "@react-aria/grid": "^3.14.3", "@react-aria/i18n": "^3.12.11", diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 52775cfa832..61bf9636546 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -11,6 +11,7 @@ */ import {chain, getScrollParent, mergeProps, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils'; +import {CollectionNode} from '@react-aria/collections'; import {DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared'; import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus'; import {getRowId, listMap} from './utils'; @@ -20,7 +21,6 @@ import type {ListState} from '@react-stately/list'; import {SelectableItemStates, useSelectableItem} from '@react-aria/selection'; import type {TreeState} from '@react-stately/tree'; import {useLocale} from '@react-aria/i18n'; -import { CollectionNode } from '@react-aria/collections'; export interface AriaGridListItemOptions { /** An object representing the list item. Contains all the relevant information that makes up the list row. */ @@ -323,11 +323,11 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let nodes = [...collection]; // TODO: refactor ListCollection to store an absolute index of a node's position? if (nodes.find(node => node.type === 'section')) { - let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode: null; + let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode : null; let isInSection = parentNode && parentNode.type === 'section'; let lastChildKey = parentNode?.lastChildKey; if (isInSection && lastChildKey) { - let lastChild = state.collection.getItem(lastChildKey) + let lastChild = state.collection.getItem(lastChildKey); let diff = lastChild ? lastChild.index - node.index : 0; if (parentNode!.prevKey) { rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - diff; diff --git a/packages/@react-aria/gridlist/src/useGridListSection.ts b/packages/@react-aria/gridlist/src/useGridListSection.ts index f0b2731fc60..813ea8eda98 100644 --- a/packages/@react-aria/gridlist/src/useGridListSection.ts +++ b/packages/@react-aria/gridlist/src/useGridListSection.ts @@ -10,12 +10,12 @@ * governing permissions and limitations under the License. */ +import {CollectionNode} from '@react-aria/collections'; import {DOMAttributes, RefObject, Node as RSNode} from '@react-types/shared'; import type {ListState} from '@react-stately/list'; import {useLabels, useSlotId} from '@react-aria/utils'; -import {CollectionNode} from '@react-aria/collections'; -export interface AriaGridListSectionProps { +export interface AriaGridListSectionProps { /** An accessibility label for the section. Required if `heading` is not present. */ 'aria-label'?: string, /** An object representing the section. */ @@ -41,7 +41,7 @@ export interface GridListSectionAria { * @param props - Props for the section. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function useGridListSection(props: AriaGridListSectionProps, state: ListState, ref: RefObject): GridListSectionAria { +export function useGridListSection(props: AriaGridListSectionProps, state: ListState, ref: RefObject): GridListSectionAria { let {'aria-label': ariaLabel, node, isVirtualized} = props; let headingId = useSlotId(); let labelProps = useLabels({ From ff9e891bdd1db832cb178bca8e1cceb25dfdb679 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:14:43 -0700 Subject: [PATCH 05/15] update lock --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index a1a89e85aac..ba1148ec57e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5728,6 +5728,7 @@ __metadata: version: 0.0.0-use.local resolution: "@react-aria/gridlist@workspace:packages/@react-aria/gridlist" dependencies: + "@react-aria/collections": "npm:3.0.0-rc.4" "@react-aria/focus": "npm:^3.21.0" "@react-aria/grid": "npm:^3.14.3" "@react-aria/i18n": "npm:^3.12.11" From ea5b2f71ef9b99984b9dfd6f6e88f67150be6e91 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:18:39 -0700 Subject: [PATCH 06/15] fix some accessibility stuff, export new GridListHeaderContext --- .../collections/src/BaseCollection.ts | 2 +- .../react-aria-components/src/GridList.tsx | 27 ++++++++++--------- packages/react-aria-components/src/index.ts | 2 +- .../test/GridList.test.js | 12 +++++++-- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/@react-aria/collections/src/BaseCollection.ts b/packages/@react-aria/collections/src/BaseCollection.ts index 739f85995c2..5ec2c353f53 100644 --- a/packages/@react-aria/collections/src/BaseCollection.ts +++ b/packages/@react-aria/collections/src/BaseCollection.ts @@ -254,7 +254,7 @@ export class BaseCollection implements ICollection> { throw new Error('Cannot add a node to a frozen collection'); } - if (node.type === 'item' && this.keyMap.get(node.key) == null) { + if (node.type === 'item' && this.keyMap.get(node.key) == null || node.type === 'header' && this.keyMap.get(node.key) == null) { this.itemCount++; } diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 9acffb9ad9e..cbf0d6abb93 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -21,7 +21,6 @@ import {DraggableCollectionState, DroppableCollectionState, Collection as IColle import {FieldInputContext, SelectableCollectionContext} from './context'; import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils'; import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; -import {HeaderContext} from './Header'; import {ListStateContext} from './ListBox'; import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react'; import {TextContext} from './Text'; @@ -580,11 +579,11 @@ export interface GridListSectionProps extends SectionProps {} /** * A GridListSection represents a section within a GridList. */ -export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, (props: GridListSectionProps, ref: ForwardedRef, item: Node) => { +export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, (props: GridListSectionProps, ref: ForwardedRef, item: Node) => { let state = useContext(ListStateContext)!; let {CollectionBranch, isVirtualized} = useContext(CollectionRendererContext); let headingRef = useRef(null); - ref = useObjectRef(ref); + ref = useObjectRef(ref); let {rowHeaderProps, rowProps, rowGroupProps} = useGridListSection({ 'aria-label': props['aria-label'] ?? undefined, node: item, @@ -601,33 +600,35 @@ export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, delete DOMProps.id; return ( -
-
+ ); }); -const GridListHeaderContext = createContext | null>(null); -export const GridListHeader = /*#__PURE__*/ createLeafComponent(HeaderNode, function Header(props: HTMLAttributes, ref: ForwardedRef) { - [props, ref] = useContextProps(props, ref, HeaderContext); - let rowHeaderProps = useContext(GridListHeaderContext); +export const GridListHeaderContext = createContext, HTMLDivElement>>({}); +const GridListHeaderInnerContext = createContext | null>(null); + +export const GridListHeader = /*#__PURE__*/ createLeafComponent(HeaderNode, function Header(props: HTMLAttributes, ref: ForwardedRef) { + [props, ref] = useContextProps(props, ref, GridListHeaderContext); + let rowHeaderProps = useContext(GridListHeaderInnerContext); return ( -
+
{props.children}
-
+ ); }); diff --git a/packages/react-aria-components/src/index.ts b/packages/react-aria-components/src/index.ts index 36cf3e61b43..f5fac2b62b0 100644 --- a/packages/react-aria-components/src/index.ts +++ b/packages/react-aria-components/src/index.ts @@ -39,7 +39,7 @@ export {DropZone, DropZoneContext} from './DropZone'; export {FieldError, FieldErrorContext} from './FieldError'; export {FileTrigger} from './FileTrigger'; export {Form, FormContext} from './Form'; -export {GridListLoadMoreItem, GridList, GridListItem, GridListContext, GridListHeader, GridListSection} from './GridList'; +export {GridListLoadMoreItem, GridList, GridListItem, GridListContext, GridListHeader, GridListHeaderContext, GridListSection} from './GridList'; export {Group, GroupContext} from './Group'; export {Header, HeaderContext} from './Header'; export {Heading} from './Heading'; diff --git a/packages/react-aria-components/test/GridList.test.js b/packages/react-aria-components/test/GridList.test.js index 367e8957c5e..db20692c46b 100644 --- a/packages/react-aria-components/test/GridList.test.js +++ b/packages/react-aria-components/test/GridList.test.js @@ -507,7 +507,7 @@ describe('GridList', () => { jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 100); jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 300); - let {getAllByRole} = render( + let {getAllByRole, getByRole} = render( @@ -524,6 +524,10 @@ describe('GridList', () => { ); + let grid = getByRole('grid'); + expect(grid).toHaveAttribute('aria-rowcount'); + expect(grid.getAttribute('aria-rowcount')).toEqual('15'); + let rows = getAllByRole('row'); expect(rows).toHaveLength(15); expect(rows.map(r => r.textContent)).toEqual(['Section 0', 'Section 0, Item 0', 'Section 0, Item 1', 'Section 1', 'Section 1, Item 0', 'Section 1, Item 1', 'Section 2', 'Section 2, Item 0', 'Section 2, Item 1', 'Section 3', 'Section 3, Item 0', 'Section 3, Item 1', 'Section 4', 'Section 4, Item 0', 'Section 4, Item 1']); @@ -538,7 +542,7 @@ describe('GridList', () => { jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 100); jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 300); - let {getAllByRole} = render( + let {getAllByRole, getByRole} = render( Home @@ -563,6 +567,10 @@ describe('GridList', () => { ); + let grid = getByRole('grid'); + expect(grid).toHaveAttribute('aria-rowcount'); + expect(grid.getAttribute('aria-rowcount')).toEqual('12'); + let rows = getAllByRole('row'); expect(rows).toHaveLength(12); expect(rows.map(r => r.textContent)).toEqual(['Home', 'School', 'Pets', 'Cat', 'Dog', 'Vanilla', 'Chocolate', 'City Hall', 'Pharmacy', 'Plants', 'Sunflower', 'Daffodil']); From ca4369d254fdf171444234059ac9b9160e7ea7cf Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:37:55 -0700 Subject: [PATCH 07/15] update useGridListItem with lastChildKey --- .../gridlist/src/useGridListItem.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 61bf9636546..ac46f5d7141 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -278,25 +278,27 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt // }); // } - let sumOfNodes = (node: RSNode): number => { + let sumOfNodes = (node: CollectionNode): number => { if (node.prevKey === null) { if (node.type === 'section') { - return [...state.collection.getChildren!(node.key)].length; + let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null + return lastChild ? lastChild.index + 1 : 0; } else if (node.type === 'item') { return 1; } return 0; } - let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) : null; + let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode : null; if (parentNode && parentNode.type === 'section') { return sumOfNodes(parentNode); } - let prevNode = state.collection.getItem(node.prevKey!); + let prevNode = state.collection.getItem(node.prevKey!) as CollectionNode; if (prevNode) { if (node.type === 'section') { - return sumOfNodes(prevNode) + [...state.collection.getChildren!(node.key)].length; + let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null + return lastChild ? sumOfNodes(prevNode) + lastChild.index + 1 : 0; } return sumOfNodes(prevNode) + 1; @@ -321,7 +323,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt if (isVirtualized) { let {collection} = state; let nodes = [...collection]; - // TODO: refactor ListCollection to store an absolute index of a node's position? + // TODO: refactor BaseCollection to store an absolute index of a node's position? if (nodes.find(node => node.type === 'section')) { let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode : null; let isInSection = parentNode && parentNode.type === 'section'; @@ -332,10 +334,10 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt if (parentNode!.prevKey) { rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - diff; } else { - rowProps['aria-rowindex'] = [...state.collection.getChildren!(parentNode!.key)].length - diff; + rowProps['aria-rowindex'] = lastChild ? lastChild.index - diff + 1 : 0; } } else { - rowProps['aria-rowindex'] = sumOfNodes(node); + rowProps['aria-rowindex'] = sumOfNodes(node as CollectionNode); } } else { rowProps['aria-rowindex'] = node.index + 1; From 75c3c4f119c1d11554e065d26867549ad99a6134 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:54:07 -0700 Subject: [PATCH 08/15] fix lint --- packages/@react-aria/gridlist/src/useGridListItem.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index ac46f5d7141..33c01e4c556 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -281,7 +281,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let sumOfNodes = (node: CollectionNode): number => { if (node.prevKey === null) { if (node.type === 'section') { - let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null + let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null; return lastChild ? lastChild.index + 1 : 0; } else if (node.type === 'item') { return 1; @@ -297,7 +297,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let prevNode = state.collection.getItem(node.prevKey!) as CollectionNode; if (prevNode) { if (node.type === 'section') { - let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null + let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null; return lastChild ? sumOfNodes(prevNode) + lastChild.index + 1 : 0; } From 22c37850a7c80a2003527ad8046c5c196c522810 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:56:34 -0700 Subject: [PATCH 09/15] small fixes from review --- packages/@react-aria/collections/src/BaseCollection.ts | 2 +- packages/@react-aria/gridlist/src/useGridListItem.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/collections/src/BaseCollection.ts b/packages/@react-aria/collections/src/BaseCollection.ts index 5ec2c353f53..b7a7a4dc1bd 100644 --- a/packages/@react-aria/collections/src/BaseCollection.ts +++ b/packages/@react-aria/collections/src/BaseCollection.ts @@ -254,7 +254,7 @@ export class BaseCollection implements ICollection> { throw new Error('Cannot add a node to a frozen collection'); } - if (node.type === 'item' && this.keyMap.get(node.key) == null || node.type === 'header' && this.keyMap.get(node.key) == null) { + if ((node.type === 'item' || node.type === 'header') && this.keyMap.get(node.key) == null) { this.itemCount++; } diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 33c01e4c556..3fd63efdc0d 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -324,17 +324,17 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let {collection} = state; let nodes = [...collection]; // TODO: refactor BaseCollection to store an absolute index of a node's position? - if (nodes.find(node => node.type === 'section')) { + if (nodes.some(node => node.type === 'section')) { let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode : null; let isInSection = parentNode && parentNode.type === 'section'; let lastChildKey = parentNode?.lastChildKey; if (isInSection && lastChildKey) { let lastChild = state.collection.getItem(lastChildKey); - let diff = lastChild ? lastChild.index - node.index : 0; + let delta = lastChild ? lastChild.index - node.index : 0; if (parentNode!.prevKey) { - rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - diff; + rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - delta; } else { - rowProps['aria-rowindex'] = lastChild ? lastChild.index - diff + 1 : 0; + rowProps['aria-rowindex'] = lastChild ? lastChild.index - delta + 1 : 0; } } else { rowProps['aria-rowindex'] = sumOfNodes(node as CollectionNode); From 50b732203b4096bff1aa645200186824b3a77800 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:59:25 -0700 Subject: [PATCH 10/15] type stuff --- packages/@react-aria/gridlist/src/useGridListItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 3fd63efdc0d..98d631d1724 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -331,7 +331,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt if (isInSection && lastChildKey) { let lastChild = state.collection.getItem(lastChildKey); let delta = lastChild ? lastChild.index - node.index : 0; - if (parentNode!.prevKey) { + if (parentNode && parentNode.prevKey) { rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - delta; } else { rowProps['aria-rowindex'] = lastChild ? lastChild.index - delta + 1 : 0; From 2c5afbab7ebd885d061f8e7b8556fde7b2255346 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:32:01 -0700 Subject: [PATCH 11/15] consolidate code for sum of rows --- .../gridlist/src/useGridListItem.ts | 32 +++++++++++-------- .../gridlist/src/useGridListSection.ts | 30 ++++++----------- .../react-aria-components/src/GridList.tsx | 3 ++ 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 98d631d1724..64e57adf1cf 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -279,29 +279,21 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt // } let sumOfNodes = (node: CollectionNode): number => { + // If prevKey is null, then this is the first node in the collection so get number of row(s) if (node.prevKey === null) { - if (node.type === 'section') { - let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null; - return lastChild ? lastChild.index + 1 : 0; - } else if (node.type === 'item') { - return 1; - } - return 0; + return getNumberOfRows(node, state); } + // If the node is an item inside of a section, get number of rows in the current section let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode : null; if (parentNode && parentNode.type === 'section') { return sumOfNodes(parentNode); } + // Otherwise, if the node is a section or item outside of a section, recursively call to get the current sum + get the number of row(s) let prevNode = state.collection.getItem(node.prevKey!) as CollectionNode; if (prevNode) { - if (node.type === 'section') { - let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null; - return lastChild ? sumOfNodes(prevNode) + lastChild.index + 1 : 0; - } - - return sumOfNodes(prevNode) + 1; + return sumOfNodes(prevNode) + getNumberOfRows(node, state); } return 0; @@ -334,7 +326,8 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt if (parentNode && parentNode.prevKey) { rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - delta; } else { - rowProps['aria-rowindex'] = lastChild ? lastChild.index - delta + 1 : 0; + // If the item is within a section but the section is the first node in the collection + rowProps['aria-rowindex'] = node.index + 1; } } else { rowProps['aria-rowindex'] = sumOfNodes(node as CollectionNode); @@ -371,3 +364,14 @@ function last(walker: TreeWalker) { } while (last); return next; } + +export function getNumberOfRows(node: CollectionNode, state: ListState | TreeState) { + if (node.type === 'section') { + // Use the index of the last child to determine the number of nodes in the section + let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null; + return lastChild ? lastChild.index + 1 : 0; + } else if (node.type === 'item') { + return 1; + } + return 0; +} diff --git a/packages/@react-aria/gridlist/src/useGridListSection.ts b/packages/@react-aria/gridlist/src/useGridListSection.ts index 813ea8eda98..4ef3fdd431b 100644 --- a/packages/@react-aria/gridlist/src/useGridListSection.ts +++ b/packages/@react-aria/gridlist/src/useGridListSection.ts @@ -12,6 +12,7 @@ import {CollectionNode} from '@react-aria/collections'; import {DOMAttributes, RefObject, Node as RSNode} from '@react-types/shared'; +import {getNumberOfRows} from './useGridListItem'; import type {ListState} from '@react-stately/list'; import {useLabels, useSlotId} from '@react-aria/utils'; @@ -50,31 +51,18 @@ export function useGridListSection(props: AriaGridListSectionProps, state: Li }); let rowIndex; - let sumOfNodes = (node: RSNode): number => { + let sumOfNodes = (node: CollectionNode): number => { + // If prevKey is null, then this is the first node in the collection if (node.prevKey === null) { - let lastChildKey = (node as CollectionNode).lastChildKey; - if (node.type === 'section' && lastChildKey) { - let lastChild = state.collection.getItem(lastChildKey); - return lastChild ? lastChild.index + 1 : 0; - } else if (node.type === 'item') { - return 1; - } - return 0; + return getNumberOfRows(node, state); } - let prevNode = state.collection.getItem(node.prevKey!); + // Otherwise, if the node is a section or item outside of a section, recursively call to get the current sum + get the number of row(s) + let prevNode = state.collection.getItem(node.prevKey!) as CollectionNode; if (prevNode) { - if (node.type === 'item') { - return sumOfNodes(prevNode) + 1; - } - - let lastChildKey = (node as CollectionNode).lastChildKey; - if (lastChildKey) { - let lastChild = state.collection.getItem(lastChildKey); - return lastChild ? sumOfNodes(prevNode) + lastChild.index + 1 : sumOfNodes(prevNode); - } + return sumOfNodes(prevNode) + getNumberOfRows(node, state); } - + return 0; }; @@ -82,7 +70,7 @@ export function useGridListSection(props: AriaGridListSectionProps, state: Li if (node.prevKey) { let prevNode = state.collection.getItem(node.prevKey); if (prevNode) { - rowIndex = sumOfNodes(prevNode) + 1; + rowIndex = sumOfNodes(prevNode as CollectionNode) + 1; } } else { rowIndex = 1; diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index cbf0d6abb93..fc69c68c829 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -297,6 +297,9 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext); let ref = useObjectRef(forwardedRef); let {isVirtualized} = useContext(CollectionRendererContext); + + // supply isInSection info here by checking if node has parentNode which has type === 'section' ? + let {rowProps, gridCellProps, descriptionProps, ...states} = useGridListItem( { node: item, From f3d5ef29dc48107f03249d1a783412b6857a87c3 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:37:51 -0700 Subject: [PATCH 12/15] remove comment --- packages/react-aria-components/src/GridList.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index fc69c68c829..cbf0d6abb93 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -297,9 +297,6 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext); let ref = useObjectRef(forwardedRef); let {isVirtualized} = useContext(CollectionRendererContext); - - // supply isInSection info here by checking if node has parentNode which has type === 'section' ? - let {rowProps, gridCellProps, descriptionProps, ...states} = useGridListItem( { node: item, From 63097a6cc2e03527216e2c52d386b39ea5ff5066 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:25:15 -0700 Subject: [PATCH 13/15] fix parcel transformer issue --- packages/@react-aria/gridlist/src/useGridListItem.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 64e57adf1cf..f5bac312818 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -324,7 +324,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let lastChild = state.collection.getItem(lastChildKey); let delta = lastChild ? lastChild.index - node.index : 0; if (parentNode && parentNode.prevKey) { - rowProps['aria-rowindex'] = sumOfNodes(parentNode!) - delta; + rowProps['aria-rowindex'] = sumOfNodes(parentNode) - delta; } else { // If the item is within a section but the section is the first node in the collection rowProps['aria-rowindex'] = node.index + 1; @@ -365,10 +365,11 @@ function last(walker: TreeWalker) { return next; } -export function getNumberOfRows(node: CollectionNode, state: ListState | TreeState) { +export function getNumberOfRows(node: RSNode, state: ListState | TreeState) { if (node.type === 'section') { // Use the index of the last child to determine the number of nodes in the section - let lastChild = node.lastChildKey ? state.collection.getItem(node.lastChildKey) : null; + let currentNode = node as CollectionNode + let lastChild = currentNode.lastChildKey ? state.collection.getItem(currentNode.lastChildKey) : null; return lastChild ? lastChild.index + 1 : 0; } else if (node.type === 'item') { return 1; From 1d0adefa8f55597dae082f96827f4946438708b8 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:31:57 -0700 Subject: [PATCH 14/15] fix lint --- packages/@react-aria/gridlist/src/useGridListItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index f5bac312818..0c59badd95e 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -368,7 +368,7 @@ function last(walker: TreeWalker) { export function getNumberOfRows(node: RSNode, state: ListState | TreeState) { if (node.type === 'section') { // Use the index of the last child to determine the number of nodes in the section - let currentNode = node as CollectionNode + let currentNode = node as CollectionNode; let lastChild = currentNode.lastChildKey ? state.collection.getItem(currentNode.lastChildKey) : null; return lastChild ? lastChild.index + 1 : 0; } else if (node.type === 'item') { From e6e76d3092253f3d19be941440f89e74cec8ce35 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:42:22 -0700 Subject: [PATCH 15/15] save iterations checking for sections --- packages/@react-aria/gridlist/src/useGridList.ts | 4 +++- packages/@react-aria/gridlist/src/useGridListItem.ts | 6 ++---- packages/@react-aria/gridlist/src/utils.ts | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridList.ts b/packages/@react-aria/gridlist/src/useGridList.ts index 055602c8146..905e9eaa198 100644 --- a/packages/@react-aria/gridlist/src/useGridList.ts +++ b/packages/@react-aria/gridlist/src/useGridList.ts @@ -145,7 +145,9 @@ export function useGridList(props: AriaGridListOptions, state: ListState node.type === 'section')}); let descriptionProps = useHighlightSelectionDescription({ selectionManager: state.selectionManager, diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 0c59badd95e..ffb4d7c1dc4 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -68,7 +68,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt // let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/gridlist'); let {direction} = useLocale(); - let {onAction, linkBehavior, keyboardNavigationBehavior, shouldSelectOnPressUp} = listMap.get(state)!; + let {onAction, linkBehavior, keyboardNavigationBehavior, shouldSelectOnPressUp, hasSection} = listMap.get(state)!; let descriptionId = useSlotId(); // We need to track the key of the item at the time it was last focused so that we force @@ -313,10 +313,8 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt }); if (isVirtualized) { - let {collection} = state; - let nodes = [...collection]; // TODO: refactor BaseCollection to store an absolute index of a node's position? - if (nodes.some(node => node.type === 'section')) { + if (hasSection) { let parentNode = node.parentKey ? state.collection.getItem(node.parentKey) as CollectionNode : null; let isInSection = parentNode && parentNode.type === 'section'; let lastChildKey = parentNode?.lastChildKey; diff --git a/packages/@react-aria/gridlist/src/utils.ts b/packages/@react-aria/gridlist/src/utils.ts index 9d1ab97a95d..0a625ce3777 100644 --- a/packages/@react-aria/gridlist/src/utils.ts +++ b/packages/@react-aria/gridlist/src/utils.ts @@ -18,7 +18,8 @@ interface ListMapShared { onAction?: (key: Key) => void, linkBehavior?: 'action' | 'selection' | 'override', keyboardNavigationBehavior: 'arrow' | 'tab', - shouldSelectOnPressUp?: boolean + shouldSelectOnPressUp?: boolean, + hasSection?: boolean } // Used to share: