Remove sticky headers in shared components grouped list (#32908)
* fix: use a flat list instead of grouped virtuoso for the grouped list * test: update virtualized list * test: update snapshots
This commit is contained in:
+51
-104
@@ -8283,16 +8283,18 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
style="position: sticky; top: 0px; width: 100%; z-index: 1; margin-top: 0px;"
|
||||
data-viewport-type="element"
|
||||
style="height: 100%; position: absolute; top: 0px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
data-testid="virtuoso-top-item-list"
|
||||
data-testid="virtuoso-item-list"
|
||||
style="box-sizing: border-box; margin-top: 0px; padding-bottom: 3432px; padding-top: 0px;"
|
||||
>
|
||||
<div
|
||||
data-index="0"
|
||||
data-item-index="0"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none; position: sticky; z-index: 1;"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
@@ -8334,20 +8336,9 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-viewport-type="element"
|
||||
style="height: 100%; position: absolute; top: 0px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
data-testid="virtuoso-item-list"
|
||||
style="box-sizing: border-box; margin-top: 0px; padding-bottom: 3432px; padding-top: 52px;"
|
||||
>
|
||||
<div
|
||||
data-index="1"
|
||||
data-item-group-index="0"
|
||||
data-item-index="0"
|
||||
data-item-index="1"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -8501,8 +8492,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="2"
|
||||
data-item-group-index="0"
|
||||
data-item-index="1"
|
||||
data-item-index="2"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -8629,8 +8619,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="3"
|
||||
data-item-group-index="0"
|
||||
data-item-index="2"
|
||||
data-item-index="3"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -8763,8 +8752,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="4"
|
||||
data-item-group-index="0"
|
||||
data-item-index="3"
|
||||
data-item-index="4"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -8891,8 +8879,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="5"
|
||||
data-item-group-index="0"
|
||||
data-item-index="4"
|
||||
data-item-index="5"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9025,8 +9012,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="6"
|
||||
data-item-group-index="0"
|
||||
data-item-index="5"
|
||||
data-item-index="6"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9176,8 +9162,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="7"
|
||||
data-item-group-index="0"
|
||||
data-item-index="6"
|
||||
data-item-index="7"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9310,8 +9295,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="8"
|
||||
data-item-group-index="0"
|
||||
data-item-index="7"
|
||||
data-item-index="8"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9438,8 +9422,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="9"
|
||||
data-item-group-index="0"
|
||||
data-item-index="8"
|
||||
data-item-index="9"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9572,8 +9555,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="10"
|
||||
data-item-group-index="0"
|
||||
data-item-index="9"
|
||||
data-item-index="10"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9700,8 +9682,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="11"
|
||||
data-item-group-index="0"
|
||||
data-item-index="10"
|
||||
data-item-index="11"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9857,8 +9838,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="12"
|
||||
data-item-group-index="0"
|
||||
data-item-index="11"
|
||||
data-item-index="12"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -9985,8 +9965,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="13"
|
||||
data-item-group-index="0"
|
||||
data-item-index="12"
|
||||
data-item-index="13"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -10119,8 +10098,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="14"
|
||||
data-item-group-index="0"
|
||||
data-item-index="13"
|
||||
data-item-index="14"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -10247,8 +10225,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="15"
|
||||
data-item-group-index="0"
|
||||
data-item-index="14"
|
||||
data-item-index="15"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -10381,8 +10358,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="16"
|
||||
data-item-group-index="0"
|
||||
data-item-index="15"
|
||||
data-item-index="16"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -10532,8 +10508,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="17"
|
||||
data-item-group-index="0"
|
||||
data-item-index="16"
|
||||
data-item-index="17"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -10666,8 +10641,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="18"
|
||||
data-item-group-index="0"
|
||||
data-item-index="17"
|
||||
data-item-index="18"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -10794,8 +10768,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="19"
|
||||
data-item-group-index="0"
|
||||
data-item-index="18"
|
||||
data-item-index="19"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -10928,8 +10901,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="20"
|
||||
data-item-group-index="0"
|
||||
data-item-index="19"
|
||||
data-item-index="20"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -11056,8 +11028,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="21"
|
||||
data-item-group-index="0"
|
||||
data-item-index="20"
|
||||
data-item-index="21"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -11213,8 +11184,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="22"
|
||||
data-item-group-index="0"
|
||||
data-item-index="21"
|
||||
data-item-index="22"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -11341,8 +11311,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="23"
|
||||
data-item-group-index="0"
|
||||
data-item-index="22"
|
||||
data-item-index="23"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -11475,9 +11444,9 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="24"
|
||||
data-item-index="1"
|
||||
data-item-index="24"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none; position: sticky; z-index: 1;"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
@@ -11521,8 +11490,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="25"
|
||||
data-item-group-index="1"
|
||||
data-item-index="23"
|
||||
data-item-index="25"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -11649,8 +11617,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="26"
|
||||
data-item-group-index="1"
|
||||
data-item-index="24"
|
||||
data-item-index="26"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -11783,8 +11750,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="27"
|
||||
data-item-group-index="1"
|
||||
data-item-index="25"
|
||||
data-item-index="27"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -11934,8 +11900,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="28"
|
||||
data-item-group-index="1"
|
||||
data-item-index="26"
|
||||
data-item-index="28"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -12068,8 +12033,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="29"
|
||||
data-item-group-index="1"
|
||||
data-item-index="27"
|
||||
data-item-index="29"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -12196,8 +12160,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="30"
|
||||
data-item-group-index="1"
|
||||
data-item-index="28"
|
||||
data-item-index="30"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -12330,8 +12293,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="31"
|
||||
data-item-group-index="1"
|
||||
data-item-index="29"
|
||||
data-item-index="31"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -12458,8 +12420,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="32"
|
||||
data-item-group-index="1"
|
||||
data-item-index="30"
|
||||
data-item-index="32"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -12615,8 +12576,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="33"
|
||||
data-item-group-index="1"
|
||||
data-item-index="31"
|
||||
data-item-index="33"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -12743,8 +12703,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="34"
|
||||
data-item-group-index="1"
|
||||
data-item-index="32"
|
||||
data-item-index="34"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -12877,8 +12836,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="35"
|
||||
data-item-group-index="1"
|
||||
data-item-index="33"
|
||||
data-item-index="35"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -13005,8 +12963,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="36"
|
||||
data-item-group-index="1"
|
||||
data-item-index="34"
|
||||
data-item-index="36"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -13626,16 +13583,18 @@ exports[`<RoomListView /> > renders SmallSectionList story 1`] = `
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
style="position: sticky; top: 0px; width: 100%; z-index: 1; margin-top: 0px;"
|
||||
data-viewport-type="element"
|
||||
style="height: 100%; position: absolute; top: 0px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
data-testid="virtuoso-top-item-list"
|
||||
data-testid="virtuoso-item-list"
|
||||
style="box-sizing: border-box; margin-top: 0px; padding-bottom: 0px; padding-top: 0px;"
|
||||
>
|
||||
<div
|
||||
data-index="0"
|
||||
data-item-index="0"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none; position: sticky; z-index: 1;"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
@@ -13677,20 +13636,9 @@ exports[`<RoomListView /> > renders SmallSectionList story 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-viewport-type="element"
|
||||
style="height: 100%; position: absolute; top: 0px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
data-testid="virtuoso-item-list"
|
||||
style="box-sizing: border-box; margin-top: 0px; padding-bottom: 0px; padding-top: 52px;"
|
||||
>
|
||||
<div
|
||||
data-index="1"
|
||||
data-item-group-index="0"
|
||||
data-item-index="0"
|
||||
data-item-index="1"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -13844,8 +13792,7 @@ exports[`<RoomListView /> > renders SmallSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="2"
|
||||
data-item-group-index="0"
|
||||
data-item-index="1"
|
||||
data-item-index="2"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
@@ -13972,9 +13919,9 @@ exports[`<RoomListView /> > renders SmallSectionList story 1`] = `
|
||||
</div>
|
||||
<div
|
||||
data-index="3"
|
||||
data-item-index="1"
|
||||
data-item-index="3"
|
||||
data-known-size="52"
|
||||
style="overflow-anchor: none; position: sticky; z-index: 1;"
|
||||
style="overflow-anchor: none;"
|
||||
>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
|
||||
+41
-50
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import React, { type JSX, useCallback, useMemo } from "react";
|
||||
import { GroupedVirtuoso } from "react-virtuoso";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
|
||||
import { useVirtualizedList, type VirtualizedListContext, type VirtualizedListProps } from "../virtualized-list";
|
||||
|
||||
@@ -102,7 +102,7 @@ export interface GroupedVirtualizedListProps<Header, Item, Context> extends Omit
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic grouped virtualized list component built on top of react-virtuoso's GroupedVirtuoso.
|
||||
* A generic grouped virtualized list component built on top of react-virtuoso's Virtuoso.
|
||||
* Provides keyboard navigation (including group headers) and virtualized rendering for
|
||||
* performance with large lists.
|
||||
*
|
||||
@@ -129,10 +129,8 @@ export function GroupedVirtualizedList<Header, Item, Context>(
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const groupCounts = useMemo(() => groups.map((group) => group.items.length), [groups]);
|
||||
const items = useMemo(() => groups.flatMap((group) => group.items), [groups]);
|
||||
|
||||
// Build a flat navigation array interleaving group headers with items.
|
||||
// Build a flat array interleaving group headers with items.
|
||||
// Each entry is either { header } or { item }.
|
||||
const flatEntries = useMemo(
|
||||
() =>
|
||||
groups.flatMap<NavigationEntry<Header, Item>>((group) => [
|
||||
@@ -142,34 +140,12 @@ export function GroupedVirtualizedList<Header, Item, Context>(
|
||||
[groups],
|
||||
);
|
||||
|
||||
// Build both index-mapping functions in a single pass over the flat entries.
|
||||
// mapScrollIndex: flat index → GroupedVirtuoso item index (headers map to their
|
||||
// first item so scrollIntoView makes the sticky header visible).
|
||||
// mapRangeIndex: GroupedVirtuoso item index → flat index (translates visible-range
|
||||
// indices back so the hook's PageUp/PageDown and focus-restore logic works).
|
||||
const { mapScrollIndex, mapRangeIndex } = useMemo(() => {
|
||||
// Map each flat index to the corresponding virtuoso item index.
|
||||
// Headers map to the first item of their group so scrollIntoView shows the sticky header.
|
||||
const flatIndexToVirtuosoIndex: number[] = [];
|
||||
|
||||
// Map the Item index (from virtuoso) to their position in the flat list
|
||||
const virtuosoIndexToFlatIndex: number[] = [];
|
||||
let virtuosoIndex = 0;
|
||||
|
||||
for (let i = 0; i < flatEntries.length; i++) {
|
||||
flatIndexToVirtuosoIndex.push(virtuosoIndex);
|
||||
|
||||
if ("item" in flatEntries[i]) {
|
||||
virtuosoIndexToFlatIndex.push(i);
|
||||
virtuosoIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mapScrollIndex: (flatIndex: number): number => flatIndexToVirtuosoIndex[flatIndex] ?? 0,
|
||||
mapRangeIndex: (virtuosoIndex: number): number => virtuosoIndexToFlatIndex[virtuosoIndex] ?? 0,
|
||||
};
|
||||
}, [flatEntries]);
|
||||
// Pre-compute a lookup from flat index to group index.
|
||||
// Each group contributes 1 header + N items, all mapped to the same group index.
|
||||
const flatIndexToGroupIndex = useMemo(
|
||||
() => groups.flatMap((group, groupIdx) => new Array(1 + group.items.length).fill(groupIdx)),
|
||||
[groups],
|
||||
);
|
||||
|
||||
// Wrap getItemKey: dispatch to getHeaderKey or getItemKey based on entry type
|
||||
const wrappedGetEntryKey = useCallback(
|
||||
@@ -178,7 +154,7 @@ export function GroupedVirtualizedList<Header, Item, Context>(
|
||||
[getHeaderKey, getItemKey],
|
||||
);
|
||||
|
||||
// Wrap isItemFocusable: headers use isHeaderFocusable (default: always true), items use isItemFocusable
|
||||
// Wrap isItemFocusable: headers use isGroupHeaderFocusable, items use isItemFocusable
|
||||
const wrappedIsEntryFocusable = useCallback(
|
||||
(entry: NavigationEntry<Header, Item>): boolean =>
|
||||
"header" in entry ? isGroupHeaderFocusable(entry.header) : isItemFocusable(entry.item),
|
||||
@@ -194,8 +170,6 @@ export function GroupedVirtualizedList<Header, Item, Context>(
|
||||
items: flatEntries,
|
||||
isItemFocusable: wrappedIsEntryFocusable,
|
||||
getItemKey: wrappedGetEntryKey,
|
||||
mapScrollIndex,
|
||||
mapRangeIndex,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -215,27 +189,44 @@ export function GroupedVirtualizedList<Header, Item, Context>(
|
||||
[onFocusForGetItemComponent],
|
||||
);
|
||||
|
||||
const getItemComponentInternal = useCallback(
|
||||
(index: number, groupIndex: number, _item: unknown, context: VirtualizedListContext<Context>): JSX.Element =>
|
||||
getItemComponent(index, items[index], context, onFocusForItem, groupIndex),
|
||||
[items, getItemComponent, onFocusForItem],
|
||||
);
|
||||
// Unified item renderer that dispatches to group header or item component
|
||||
// based on the entry type at the given flat index.
|
||||
const itemContent = useCallback(
|
||||
(
|
||||
flatIndex: number,
|
||||
_entry: NavigationEntry<Header, Item>,
|
||||
context: VirtualizedListContext<Context>,
|
||||
): JSX.Element => {
|
||||
const entry = flatEntries[flatIndex];
|
||||
const groupIndex = flatIndexToGroupIndex[flatIndex];
|
||||
|
||||
const getGroupHeaderComponentInternal = useCallback(
|
||||
(groupIndex: number, context: VirtualizedListContext<Context>): JSX.Element =>
|
||||
getGroupHeaderComponent(groupIndex, groups[groupIndex].header, context, onFocusForHeader),
|
||||
[getGroupHeaderComponent, onFocusForHeader, groups],
|
||||
if ("header" in entry) {
|
||||
return getGroupHeaderComponent(groupIndex, entry.header, context, onFocusForHeader);
|
||||
}
|
||||
|
||||
// Item index in the flattened (non-header) items array:
|
||||
// flatIndex minus the number of headers before it (groupIndex + 1).
|
||||
const itemIndex = flatIndex - (groupIndex + 1);
|
||||
return getItemComponent(itemIndex, entry.item, context, onFocusForItem, groupIndex);
|
||||
},
|
||||
[
|
||||
flatEntries,
|
||||
flatIndexToGroupIndex,
|
||||
getGroupHeaderComponent,
|
||||
getItemComponent,
|
||||
onFocusForItem,
|
||||
onFocusForHeader,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<GroupedVirtuoso
|
||||
<Virtuoso
|
||||
// note that either the container of direct children must be focusable to be axe
|
||||
// compliant, so we leave tabIndex as the default so the container can be focused
|
||||
// (virtuoso wraps the children inside another couple of elements so setting it
|
||||
// on those doesn't seem to work, unfortunately)
|
||||
groupCounts={groupCounts}
|
||||
itemContent={getItemComponentInternal}
|
||||
groupContent={getGroupHeaderComponentInternal}
|
||||
itemContent={itemContent}
|
||||
data={flatEntries}
|
||||
{...virtuosoProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -379,12 +379,13 @@ describe.each<ListTestVariant>([flatVariant, groupedVariant])("$name", (variant)
|
||||
// Then press PageUp to jump up by viewport size
|
||||
fireEvent.keyDown(container, { code: "PageUp" });
|
||||
|
||||
// Verify focus moved up
|
||||
const items = container.querySelectorAll(".mx_item");
|
||||
// PageUp should move back to the first item since we only have 4 items
|
||||
expectTabIndex(items[0], "0");
|
||||
const lastIndex = items.length - 1;
|
||||
expectTabIndex(items[lastIndex], "-1");
|
||||
// Verify focus moved up – use the variant's navigable selector so
|
||||
// group headers (which are also navigable) are included.
|
||||
const allNav = container.querySelectorAll(variant.navigableSelector);
|
||||
// PageUp should move back to the first navigable element since we only have a few items
|
||||
expectTabIndex(allNav[0], "0");
|
||||
const lastIndex = allNav.length - 1;
|
||||
expectTabIndex(allNav[lastIndex], "-1");
|
||||
});
|
||||
|
||||
it("should not handle keyboard navigation when modifier keys are pressed", () => {
|
||||
|
||||
Reference in New Issue
Block a user