Skip to content

Commit d83aa0f

Browse files
committed
fix: automatic combobox options virtualization
1 parent 8228a8e commit d83aa0f

File tree

3 files changed

+35
-95
lines changed

3 files changed

+35
-95
lines changed

docs/stories/03-inputs/TimePicker.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const Size = {
9090

9191
export const Steps = {
9292
args: {
93-
step: 60,
93+
step: 1,
9494
},
9595
name: 'Steps',
9696
} satisfies Story;

packages/design-system/src/components/Combobox/Combobox.tsx

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,29 +66,6 @@ interface ComboboxProps
6666
*/
6767
size?: 'S' | 'M';
6868
startIcon?: React.ReactNode;
69-
/**
70-
* Enable virtualization for large lists
71-
* @default false
72-
*/
73-
virtualized?: boolean;
74-
/**
75-
* Estimated size of each item for virtualization
76-
* @default 40
77-
*/
78-
estimatedItemSize?: number;
79-
/**
80-
* Number of items to render outside visible area
81-
* @default 5
82-
*/
83-
overscan?: number;
84-
/**
85-
* For lazy virtualization: total item count
86-
*/
87-
virtualItemCount?: number;
88-
/**
89-
* For lazy virtualization: function to render item by index
90-
*/
91-
renderVirtualItem?: (index: number) => React.ReactNode;
9269
}
9370

9471
type ComboboxInputElement = HTMLInputElement;
@@ -123,11 +100,6 @@ const Combobox = React.forwardRef<ComboboxInputElement, ComboboxProps>(
123100
onChange,
124101
onClear,
125102
onCreateOption,
126-
virtualized = false,
127-
estimatedItemSize = 40,
128-
overscan = 5,
129-
virtualItemCount,
130-
renderVirtualItem,
131103
onFilterValueChange,
132104
onInputChange,
133105
onTextValueChange,
@@ -221,6 +193,24 @@ const Combobox = React.forwardRef<ComboboxInputElement, ComboboxProps>(
221193
const generatedIntersectionId = useId();
222194
const intersectionId = `intersection-${stripReactIdOfColon(generatedIntersectionId)}`;
223195

196+
// Convert children to array for virtualization
197+
const childrenArray = React.useMemo(() => {
198+
return React.Children.toArray(children);
199+
}, [children]);
200+
201+
const childrenCount = childrenArray.length;
202+
203+
// Automatically use virtualization for large lists
204+
const shouldVirtualize = childrenCount > 100;
205+
206+
// Lazy render function for automatic virtualization
207+
const renderVirtualItem = React.useCallback(
208+
(index: number) => {
209+
return childrenArray[index];
210+
},
211+
[childrenArray],
212+
);
213+
224214
useIntersection(viewportRef, handleReachEnd, {
225215
selectorToWatch: `#${intersectionId}`,
226216
/**
@@ -301,15 +291,13 @@ const Combobox = React.forwardRef<ComboboxInputElement, ComboboxProps>(
301291
<Content sideOffset={4}>
302292
<ComboboxPrimitive.Viewport ref={viewportRef}>
303293
<ScrollAreaCombobox>
304-
{virtualized ? (
294+
{shouldVirtualize ? (
305295
<VirtualizedList
306-
estimatedItemSize={estimatedItemSize}
307-
overscan={overscan}
308-
itemCount={virtualItemCount}
296+
estimatedItemSize={40}
297+
overscan={10}
298+
itemCount={childrenCount}
309299
renderItem={renderVirtualItem}
310-
>
311-
{children}
312-
</VirtualizedList>
300+
/>
313301
) : (
314302
children
315303
)}

packages/design-system/src/components/TimePicker/TimePicker.tsx

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -74,43 +74,13 @@ export const TimePicker = React.forwardRef<ComboboxInputElement, TimePickerProps
7474
return separator;
7575
}, [formatter]);
7676

77-
// Enable virtualization for small steps that generate many options
78-
// step=1 -> 1440 options, step=5 -> 288 options
79-
const shouldVirtualize = step <= 5;
80-
81-
// Increase overscan for smoother scrolling with many items
82-
const overscanCount = step === 1 ? 15 : 10;
83-
84-
// For virtualized lists, generate options lazily to avoid creating 1440 React elements upfront
85-
// For non-virtualized lists, generate all options as before
77+
// Generate all time options - Combobox will automatically virtualize if > 100
8678
const timeOptions = React.useMemo(() => {
87-
if (!shouldVirtualize) {
88-
// Non-virtualized: generate all options as before
89-
const stepCount = 60 / step;
90-
return [...Array(24).keys()].flatMap((hour) =>
91-
[...Array(stepCount).keys()].map((minuteStep) =>
92-
formatter.format(new Date(0, 0, 0, hour, minuteStep * step)),
93-
),
94-
);
95-
}
96-
97-
// Virtualized: return empty array and generate on-demand
98-
return [];
99-
}, [step, formatter, shouldVirtualize]);
100-
101-
// Generate time option on demand for virtualized lists
102-
const getTimeOption = React.useCallback(
103-
(index: number) => {
104-
const stepCount = 60 / step;
105-
const hour = Math.floor(index / stepCount);
106-
const minuteStep = index % stepCount;
107-
return formatter.format(new Date(0, 0, 0, hour, minuteStep * step));
108-
},
109-
[step, formatter],
110-
);
111-
112-
// Calculate total count for virtualized lists
113-
const totalTimeCount = (60 / step) * 24;
79+
const stepCount = 60 / step;
80+
return [...Array(24).keys()].flatMap((hour) =>
81+
[...Array(stepCount).keys()].map((minuteStep) => formatter.format(new Date(0, 0, 0, hour, minuteStep * step))),
82+
);
83+
}, [step, formatter]);
11484

11585
const handleTextValueChange = (string?: string) => {
11686
if (!string || isNotAlphabeticalCharacter(string)) {
@@ -167,19 +137,6 @@ export const TimePicker = React.forwardRef<ComboboxInputElement, TimePickerProps
167137
const escapedSeparator = escapeForRegex(separator);
168138
const pattern = `\\d{2}${escapedSeparator}\\d{2}`;
169139

170-
// Render function for lazy virtualization
171-
const renderTimeOption = React.useCallback(
172-
(index: number) => {
173-
const time = getTimeOption(index);
174-
return (
175-
<ComboboxOption key={time} value={time}>
176-
{time}
177-
</ComboboxOption>
178-
);
179-
},
180-
[getTimeOption],
181-
);
182-
183140
return (
184141
<TimePickerCombobox
185142
{...restProps}
@@ -196,17 +153,12 @@ export const TimePicker = React.forwardRef<ComboboxInputElement, TimePickerProps
196153
textValue={textValue}
197154
onTextValueChange={handleTextValueChange}
198155
onBlur={handleBlur}
199-
virtualized={shouldVirtualize}
200-
overscan={overscanCount}
201-
virtualItemCount={shouldVirtualize ? totalTimeCount : undefined}
202-
renderVirtualItem={shouldVirtualize ? renderTimeOption : undefined}
203156
>
204-
{!shouldVirtualize &&
205-
timeOptions.map((time) => (
206-
<ComboboxOption key={time} value={time}>
207-
{time}
208-
</ComboboxOption>
209-
))}
157+
{timeOptions.map((time) => (
158+
<ComboboxOption key={time} value={time}>
159+
{time}
160+
</ComboboxOption>
161+
))}
210162
</TimePickerCombobox>
211163
);
212164
},

0 commit comments

Comments
 (0)