Skip to content

Commit d8b53b7

Browse files
[docs][Autocomplete] Update virtualization example to use react-window v2 (#47054)
Co-authored-by: Zeeshan Tamboli <[email protected]>
1 parent f7da376 commit d8b53b7

File tree

10 files changed

+411
-245
lines changed

10 files changed

+411
-245
lines changed

docs/data/joy/components/autocomplete/Virtualize.js

Lines changed: 81 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as React from 'react';
22
import PropTypes from 'prop-types';
3-
import { FixedSizeList } from 'react-window';
3+
import { List } from 'react-window';
44
import { Popper } from '@mui/base/Popper';
55
import Autocomplete from '@mui/joy/Autocomplete';
6-
import AutocompleteListbox from '@mui/joy/AutocompleteListbox';
76
import AutocompleteOption from '@mui/joy/AutocompleteOption';
87
import FormControl from '@mui/joy/FormControl';
98
import FormLabel from '@mui/joy/FormLabel';
109
import ListSubheader from '@mui/joy/ListSubheader';
10+
import AutocompleteListbox from '@mui/joy/AutocompleteListbox';
1111

1212
const LISTBOX_PADDING = 6; // px
1313

@@ -16,7 +16,7 @@ function renderRow(props) {
1616
const dataSet = data[index];
1717
const inlineStyle = {
1818
...style,
19-
top: style.top + LISTBOX_PADDING,
19+
top: (style.top ?? 0) + LISTBOX_PADDING,
2020
};
2121

2222
if (dataSet.hasOwnProperty('group')) {
@@ -34,37 +34,26 @@ function renderRow(props) {
3434
);
3535
}
3636

37-
const OuterElementContext = React.createContext({});
38-
39-
const OuterElementType = React.forwardRef((props, ref) => {
40-
const outerProps = React.useContext(OuterElementContext);
41-
return (
42-
<AutocompleteListbox
43-
{...props}
44-
{...outerProps}
45-
component="div"
46-
ref={ref}
47-
sx={{
48-
'& ul': {
49-
padding: 0,
50-
margin: 0,
51-
flexShrink: 0,
52-
},
53-
}}
54-
/>
55-
);
56-
});
57-
5837
// Adapter for react-window
59-
6038
const ListboxComponent = React.forwardRef(function ListboxComponent(props, ref) {
61-
const { children, anchorEl, open, modifiers, ...other } = props;
39+
const { children, anchorEl, open, modifiers, internalListRef, ...other } = props;
6240
const itemData = [];
41+
const optionIndexMap = {};
42+
43+
if (children && Array.isArray(children) && children[0]) {
44+
children[0].forEach((item) => {
45+
if (item) {
46+
itemData.push(item);
47+
itemData.push(...(item.children || []));
48+
}
49+
});
50+
}
6351

64-
children[0].forEach((item) => {
65-
if (item) {
66-
itemData.push(item);
67-
itemData.push(...(item.children || []));
52+
// Build the index map after flattening
53+
itemData.forEach((item, index) => {
54+
if (Array.isArray(item) && item[1]) {
55+
// Option item: [props, optionValue]
56+
optionIndexMap[item[1]] = index;
6857
}
6958
});
7059

@@ -73,27 +62,53 @@ const ListboxComponent = React.forwardRef(function ListboxComponent(props, ref)
7362

7463
return (
7564
<Popper ref={ref} anchorEl={anchorEl} open={open} modifiers={modifiers}>
76-
<OuterElementContext.Provider value={other}>
77-
<FixedSizeList
78-
itemData={itemData}
79-
height={itemSize * 8}
80-
width="100%"
81-
outerElementType={OuterElementType}
82-
innerElementType="ul"
83-
itemSize={itemSize}
65+
<AutocompleteListbox
66+
{...other}
67+
component="div"
68+
sx={{
69+
'& ul': {
70+
padding: 0,
71+
margin: 0,
72+
flexShrink: 0,
73+
},
74+
maxHeight: '100%',
75+
}}
76+
>
77+
<List
78+
listRef={(api) => {
79+
// Store both the API and the map in the ref
80+
if (internalListRef) {
81+
internalListRef.current = { api, optionIndexMap };
82+
}
83+
}}
84+
rowCount={itemCount}
85+
rowHeight={itemSize}
86+
rowComponent={renderRow}
87+
rowProps={{ data: itemData }}
88+
style={{
89+
height: itemSize * 8,
90+
width: '100%',
91+
}}
8492
overscanCount={5}
85-
itemCount={itemCount}
86-
>
87-
{renderRow}
88-
</FixedSizeList>
89-
</OuterElementContext.Provider>
93+
tagName="ul"
94+
/>
95+
</AutocompleteListbox>
9096
</Popper>
9197
);
9298
});
9399

94100
ListboxComponent.propTypes = {
95101
anchorEl: PropTypes.any.isRequired,
96102
children: PropTypes.node,
103+
internalListRef: PropTypes.shape({
104+
current: PropTypes.shape({
105+
api: PropTypes.shape({
106+
element: PropTypes.object,
107+
scrollToRow: PropTypes.func.isRequired,
108+
}),
109+
optionIndexMap: PropTypes.object.isRequired,
110+
}).isRequired,
111+
}).isRequired,
97112
modifiers: PropTypes.array.isRequired,
98113
open: PropTypes.bool.isRequired,
99114
};
@@ -115,6 +130,23 @@ const OPTIONS = Array.from(new Array(10000))
115130
.sort((a, b) => a.toUpperCase().localeCompare(b.toUpperCase()));
116131

117132
export default function Virtualize() {
133+
// Ref to store both the List API and the option index map
134+
const internalListRef = React.useRef({
135+
api: null,
136+
optionIndexMap: {},
137+
});
138+
139+
// Handle keyboard navigation by scrolling to highlighted option
140+
const handleHighlightChange = (event, option) => {
141+
if (option && internalListRef.current) {
142+
const { api, optionIndexMap } = internalListRef.current;
143+
const index = optionIndexMap[option];
144+
if (index !== undefined && api) {
145+
api.scrollToRow({ index, align: 'auto' });
146+
}
147+
}
148+
};
149+
118150
return (
119151
<FormControl id="virtualize-demo">
120152
<FormLabel>10,000 options</FormLabel>
@@ -125,11 +157,17 @@ export default function Virtualize() {
125157
slots={{
126158
listbox: ListboxComponent,
127159
}}
160+
slotProps={{
161+
listbox: {
162+
internalListRef,
163+
},
164+
}}
128165
options={OPTIONS}
129166
groupBy={(option) => option[0].toUpperCase()}
130167
renderOption={(props, option) => [props, option]}
131168
// TODO: Post React 18 update - validate this conversion, look like a hidden bug
132169
renderGroup={(params) => params}
170+
onHighlightChange={handleHighlightChange}
133171
/>
134172
</FormControl>
135173
);

docs/data/joy/components/autocomplete/Virtualize.tsx

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import * as React from 'react';
2-
import { FixedSizeList, ListChildComponentProps } from 'react-window';
2+
import { List, RowComponentProps, ListImperativeAPI } from 'react-window';
33
import { Popper } from '@mui/base/Popper';
44
import Autocomplete from '@mui/joy/Autocomplete';
5-
import AutocompleteListbox from '@mui/joy/AutocompleteListbox';
65
import AutocompleteOption from '@mui/joy/AutocompleteOption';
76
import FormControl from '@mui/joy/FormControl';
87
import FormLabel from '@mui/joy/FormLabel';
98
import ListSubheader from '@mui/joy/ListSubheader';
9+
import AutocompleteListbox, {
10+
AutocompleteListboxProps,
11+
} from '@mui/joy/AutocompleteListbox';
1012

1113
const LISTBOX_PADDING = 6; // px
1214

13-
function renderRow(props: ListChildComponentProps) {
15+
function renderRow(props: RowComponentProps & { data: any }) {
1416
const { data, index, style } = props;
1517
const dataSet = data[index];
1618
const inlineStyle = {
1719
...style,
18-
top: (style.top as number) + LISTBOX_PADDING,
20+
top: ((style.top as number) ?? 0) + LISTBOX_PADDING,
1921
};
2022

2123
if (dataSet.hasOwnProperty('group')) {
@@ -33,44 +35,40 @@ function renderRow(props: ListChildComponentProps) {
3335
);
3436
}
3537

36-
const OuterElementContext = React.createContext({});
37-
38-
const OuterElementType = React.forwardRef<HTMLDivElement>((props, ref) => {
39-
const outerProps = React.useContext(OuterElementContext);
40-
return (
41-
<AutocompleteListbox
42-
{...props}
43-
{...outerProps}
44-
component="div"
45-
ref={ref}
46-
sx={{
47-
'& ul': {
48-
padding: 0,
49-
margin: 0,
50-
flexShrink: 0,
51-
},
52-
}}
53-
/>
54-
);
55-
});
56-
5738
// Adapter for react-window
5839
const ListboxComponent = React.forwardRef<
5940
HTMLDivElement,
6041
{
6142
anchorEl: any;
6243
open: boolean;
6344
modifiers: any[];
64-
} & React.HTMLAttributes<HTMLElement>
45+
internalListRef: React.MutableRefObject<{
46+
api: ListImperativeAPI | null;
47+
optionIndexMap: Record<string, number>;
48+
}>;
49+
} & React.HTMLAttributes<HTMLElement> &
50+
AutocompleteListboxProps
6551
>(function ListboxComponent(props, ref) {
66-
const { children, anchorEl, open, modifiers, ...other } = props;
52+
const { children, anchorEl, open, modifiers, internalListRef, ...other } = props;
6753
const itemData: Array<any> = [];
68-
(
69-
children as [Array<{ children: Array<React.ReactElement<any>> | undefined }>]
70-
)[0].forEach((item) => {
71-
if (item) {
72-
itemData.push(item);
73-
itemData.push(...(item.children || []));
54+
const optionIndexMap: Record<string, number> = {};
55+
56+
if (children && Array.isArray(children) && children[0]) {
57+
(
58+
children as [Array<{ children: Array<React.ReactElement<any>> | undefined }>]
59+
)[0].forEach((item) => {
60+
if (item) {
61+
itemData.push(item);
62+
itemData.push(...(item.children || []));
63+
}
64+
});
65+
}
66+
67+
// Build the index map after flattening
68+
itemData.forEach((item, index) => {
69+
if (Array.isArray(item) && item[1]) {
70+
// Option item: [props, optionValue]
71+
optionIndexMap[item[1]] = index;
7472
}
7573
});
7674

@@ -79,20 +77,37 @@ const ListboxComponent = React.forwardRef<
7977

8078
return (
8179
<Popper ref={ref} anchorEl={anchorEl} open={open} modifiers={modifiers}>
82-
<OuterElementContext.Provider value={other}>
83-
<FixedSizeList
84-
itemData={itemData}
85-
height={itemSize * 8}
86-
width="100%"
87-
outerElementType={OuterElementType}
88-
innerElementType="ul"
89-
itemSize={itemSize}
80+
<AutocompleteListbox
81+
{...other}
82+
component="div"
83+
sx={{
84+
'& ul': {
85+
padding: 0,
86+
margin: 0,
87+
flexShrink: 0,
88+
},
89+
maxHeight: '100%',
90+
}}
91+
>
92+
<List
93+
listRef={(api) => {
94+
// Store both the API and the map in the ref
95+
if (internalListRef) {
96+
internalListRef.current = { api, optionIndexMap };
97+
}
98+
}}
99+
rowCount={itemCount}
100+
rowHeight={itemSize}
101+
rowComponent={renderRow}
102+
rowProps={{ data: itemData }}
103+
style={{
104+
height: itemSize * 8,
105+
width: '100%',
106+
}}
90107
overscanCount={5}
91-
itemCount={itemCount}
92-
>
93-
{renderRow}
94-
</FixedSizeList>
95-
</OuterElementContext.Provider>
108+
tagName="ul"
109+
/>
110+
</AutocompleteListbox>
96111
</Popper>
97112
);
98113
});
@@ -114,6 +129,29 @@ const OPTIONS = Array.from(new Array(10000))
114129
.sort((a, b) => a.toUpperCase().localeCompare(b.toUpperCase()));
115130

116131
export default function Virtualize() {
132+
// Ref to store both the List API and the option index map
133+
const internalListRef = React.useRef<{
134+
api: ListImperativeAPI | null;
135+
optionIndexMap: Record<string, number>;
136+
}>({
137+
api: null,
138+
optionIndexMap: {},
139+
});
140+
141+
// Handle keyboard navigation by scrolling to highlighted option
142+
const handleHighlightChange = (
143+
event: React.SyntheticEvent,
144+
option: string | null,
145+
) => {
146+
if (option && internalListRef.current) {
147+
const { api, optionIndexMap } = internalListRef.current;
148+
const index = optionIndexMap[option];
149+
if (index !== undefined && api) {
150+
api.scrollToRow({ index, align: 'auto' });
151+
}
152+
}
153+
};
154+
117155
return (
118156
<FormControl id="virtualize-demo">
119157
<FormLabel>10,000 options</FormLabel>
@@ -124,11 +162,17 @@ export default function Virtualize() {
124162
slots={{
125163
listbox: ListboxComponent,
126164
}}
165+
slotProps={{
166+
listbox: {
167+
internalListRef,
168+
} as any,
169+
}}
127170
options={OPTIONS}
128171
groupBy={(option) => option[0].toUpperCase()}
129172
renderOption={(props, option) => [props, option] as React.ReactNode}
130173
// TODO: Post React 18 update - validate this conversion, look like a hidden bug
131174
renderGroup={(params) => params as unknown as React.ReactNode}
175+
onHighlightChange={handleHighlightChange}
132176
/>
133177
</FormControl>
134178
);

0 commit comments

Comments
 (0)