Skip to content

Commit f0f3c5a

Browse files
authored
[🐛][Select]: fix a bug that caused the group to be hidden incorrectly whensearchable={true} (#100)
* Add `getOptions` to context * Update useComboBox hook to fix the issue with search queries * Improve getOptions utility * Update version
1 parent de499aa commit f0f3c5a

File tree

6 files changed

+112
-63
lines changed

6 files changed

+112
-63
lines changed

lib/Select/Select.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import { SelectContext, type SelectContextValue } from "./context";
2020
import { Root as RootSlot } from "./slots";
2121
import {
22-
getOptions,
22+
getOptions as getOptionsUtil,
2323
noValueSelected,
2424
normalizeValues,
2525
useElementsRegistry,
@@ -415,6 +415,8 @@ const SelectBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
415415
closeList();
416416
}
417417

418+
const getOptions = () => getOptionsUtil(React.Children.toArray(children));
419+
418420
const context: SelectContextValue = {
419421
readOnly,
420422
disabled,
@@ -431,6 +433,7 @@ const SelectBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
431433
closeListAndMaintainFocus,
432434
setFilteredEntities,
433435
setActiveDescendant,
436+
getOptions,
434437
openList,
435438
closeList,
436439
toggleList,
@@ -488,9 +491,7 @@ const SelectBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
488491
if (selectedValues.length === 0) return null;
489492

490493
const renderOptions = () => {
491-
const disabledOptions = getOptions(
492-
React.Children.toArray(children),
493-
).filter(o => o.disabled);
494+
const disabledOptions = getOptions().filter(o => o.disabled);
494495

495496
const isOptionDisabled = (optionValue: string) =>
496497
disabledOptions.some(o => o.value === optionValue);
@@ -538,7 +539,7 @@ const SelectBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
538539
<div
539540
{...otherProps}
540541
// @ts-expect-error React hasn't added `inert` yet
541-
inert={disabled || readOnly ? "" : undefined}
542+
inert={disabled ? "" : undefined}
542543
style={style}
543544
id={id}
544545
ref={handleRootRef}

lib/Select/components/Controller/Controller.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ const ControllerBase = (props: Props, ref: React.Ref<HTMLInputElement>) => {
8383
searchable: ctx?.searchable ?? false,
8484
onInputChange: onChange,
8585
onKeyDown,
86-
getListItems: () => {
86+
getOptionsInfo: ctx?.getOptions ?? (() => []),
87+
getOptionElements: () => {
8788
const listId = ctx?.elementsRegistry.getElementId("list");
8889
const listNode = document.getElementById(listId ?? "");
8990

lib/Select/components/Controller/utils.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import {
1010
useIsomorphicLayoutEffect,
1111
useOnChange,
1212
} from "../../../utils";
13+
import type { SelectContextValue } from "../../context";
1314

1415
type Props<T extends HTMLElement> = {
1516
disabled: boolean;
1617
readOnly: boolean;
1718
autoFocus: boolean;
1819
searchable: boolean;
19-
activeDescendant: HTMLElement | null;
2020
listOpenState: boolean;
21+
activeDescendant: SelectContextValue["activeDescendant"];
2122
onClick?: React.MouseEventHandler<T>;
2223
onBlur?: React.FocusEventHandler<T>;
2324
onFocus?: React.FocusEventHandler<T>;
@@ -26,10 +27,15 @@ type Props<T extends HTMLElement> = {
2627
onEscapeKeyDown?: React.KeyboardEventHandler<T>;
2728
onBackspaceKeyDown?: React.KeyboardEventHandler<T>;
2829
onInputChange?: React.ChangeEventHandler<HTMLInputElement>;
29-
getListItems: () => HTMLElement[];
30-
onFilteredEntities: (entities: null | string[]) => void;
30+
getOptionElements: () => HTMLElement[];
31+
getOptionsInfo: SelectContextValue["getOptions"];
32+
onFilteredEntities: (
33+
entities: SelectContextValue["filteredEntities"],
34+
) => void;
3135
onListOpenChange: (nextListOpenState: boolean) => void;
32-
onActiveDescendantChange: (nextActiveDescendant: HTMLElement | null) => void;
36+
onActiveDescendantChange: (
37+
nextActiveDescendant: SelectContextValue["activeDescendant"],
38+
) => void;
3339
};
3440

3541
export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
@@ -42,7 +48,8 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
4248
onPrintableKeyDown,
4349
onEscapeKeyDown,
4450
onBackspaceKeyDown,
45-
getListItems,
51+
getOptionElements,
52+
getOptionsInfo,
4653
onActiveDescendantChange,
4754
onListOpenChange,
4855
onFilteredEntities,
@@ -65,7 +72,7 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
6572

6673
const jumpToChar = useJumpToChar({
6774
activeDescendantElement: activeDescendant,
68-
getListItems,
75+
getListItems: getOptionElements,
6976
onActiveDescendantElementChange: onActiveDescendantChange,
7077
});
7178

@@ -140,12 +147,14 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
140147
});
141148

142149
const handleKeyDown = useEventCallback<React.KeyboardEvent<T>>(event => {
143-
if (disabled || readOnly || !isMounted()) {
150+
if (disabled || !isMounted()) {
144151
event.preventDefault();
145152

146153
return;
147154
}
148155

156+
if (readOnly) return;
157+
149158
const getAvailableItem = (
150159
items: (HTMLElement | null)[],
151160
idx: number,
@@ -195,15 +204,15 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
195204
onListOpenChange(true);
196205
});
197206

198-
const items = getListItems();
207+
const items = getOptionElements();
199208
const nextActive = getInitialAvailableItem(items, true);
200209

201210
onActiveDescendantChange(nextActive);
202211

203212
break;
204213
}
205214

206-
const items = getListItems();
215+
const items = getOptionElements();
207216
let nextActive: HTMLElement | null = null;
208217

209218
if (activeDescendant) {
@@ -219,22 +228,24 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
219228
}
220229

221230
case SystemKeys.UP: {
231+
if (readOnly) return;
232+
222233
event.preventDefault();
223234

224235
if (!listOpenState) {
225236
flushSync(() => {
226237
onListOpenChange(true);
227238
});
228239

229-
const items = getListItems();
240+
const items = getOptionElements();
230241
const nextActive = getInitialAvailableItem(items, true);
231242

232243
onActiveDescendantChange(nextActive);
233244

234245
break;
235246
}
236247

237-
const items = getListItems();
248+
const items = getOptionElements();
238249
let nextActive: HTMLElement | null = null;
239250

240251
if (activeDescendant) {
@@ -257,7 +268,7 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
257268

258269
if (!listOpenState) break;
259270

260-
const items = getListItems();
271+
const items = getOptionElements();
261272
const nextActive = getAvailableItem(items, 0, true);
262273

263274
onActiveDescendantChange(nextActive);
@@ -270,7 +281,7 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
270281

271282
if (!listOpenState) break;
272283

273-
const items = getListItems();
284+
const items = getOptionElements();
274285
const nextActive = getAvailableItem(items, items.length - 1, false);
275286

276287
onActiveDescendantChange(nextActive);
@@ -280,6 +291,7 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
280291

281292
case SystemKeys.ESCAPE: {
282293
event.preventDefault();
294+
283295
onEscapeKeyDown?.(event);
284296

285297
break;
@@ -353,15 +365,15 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
353365

354366
const query = target.value;
355367

356-
const items = getListItems();
368+
const options = getOptionsInfo();
357369

358-
const entities = items
359-
.filter(item => {
360-
const text = item.textContent?.toLowerCase() ?? "";
370+
const entities = options
371+
.filter(option => {
372+
const text = option.valueLabel.toLowerCase();
361373

362374
return text.includes(query.toLowerCase());
363375
})
364-
.map(item => item.getAttribute("data-entity") ?? "");
376+
.map(option => option.value);
365377

366378
onFilteredEntities(entities);
367379
onInputChange?.(event);

lib/Select/context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from "react";
22
import { type LabelInfo } from "../internals";
3+
import type { PickAsMandatory } from "../types";
34
import type { RegisteredElementsKeys } from "./Select";
5+
import type { OptionProps } from "./components";
46
import type { ElementsRegistry } from "./utils";
57

68
type ContextValue = {
@@ -19,6 +21,9 @@ type ContextValue = {
1921
closeListAndMaintainFocus: () => void;
2022
setActiveDescendant: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
2123
setFilteredEntities: React.Dispatch<React.SetStateAction<null | string[]>>;
24+
getOptions: () => Array<
25+
PickAsMandatory<OptionProps, "disabled" | "value" | "valueLabel">
26+
>;
2227
openList: () => void;
2328
closeList: () => void;
2429
toggleList: () => void;

lib/Select/utils.ts

Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { PickAsMandatory } from "../types";
33
import {
44
Controller,
55
EmptyStatement,
6+
List,
67
Option,
78
Trigger,
89
type OptionProps,
@@ -25,44 +26,73 @@ export const noValueSelected = (value: string | string[] | undefined) =>
2526

2627
export const getOptions = (
2728
childArray: Array<Exclude<React.ReactNode, boolean | null | undefined>>,
28-
): Array<PickAsMandatory<OptionProps, "disabled" | "value" | "valueLabel">> => {
29-
return childArray.reduce(
30-
(result, child) => {
31-
if (!React.isValidElement(child)) return result;
32-
33-
if (
34-
child.type === EmptyStatement ||
35-
child.type === Controller ||
36-
child.type === Trigger
37-
) {
38-
return result;
39-
}
40-
41-
if (child.type === Option) {
42-
const { disabled, value, valueLabel } = (
43-
child as React.ReactElement<OptionProps>
44-
).props;
45-
46-
result.push({ disabled: disabled ?? false, value, valueLabel });
47-
48-
return result;
49-
}
50-
51-
if (!("children" in child.props)) return result;
52-
53-
const options = getOptions(
54-
React.Children.toArray(
55-
(child as React.ReactElement<{ children: React.ReactNode }>).props
56-
.children,
57-
),
58-
);
59-
60-
return [...result, ...options];
61-
},
62-
[] as Array<
63-
PickAsMandatory<OptionProps, "disabled" | "value" | "valueLabel">
64-
>,
65-
);
29+
) => {
30+
let isListFound = false;
31+
32+
const recurse = (
33+
childArray: Array<Exclude<React.ReactNode, boolean | null | undefined>>,
34+
isInList = false,
35+
): Array<
36+
PickAsMandatory<OptionProps, "disabled" | "value" | "valueLabel">
37+
> => {
38+
return childArray.reduce(
39+
(result, child) => {
40+
if (!React.isValidElement(child)) return result;
41+
42+
if (child.type === List) {
43+
isListFound = true;
44+
45+
const options = recurse(
46+
React.Children.toArray(
47+
(child as React.ReactElement<{ children: React.ReactNode }>).props
48+
.children,
49+
),
50+
true,
51+
);
52+
53+
return [...result, ...options];
54+
}
55+
56+
if (child.type === Option) {
57+
if (!isInList) return result;
58+
59+
const { disabled, value, valueLabel } = (
60+
child as React.ReactElement<OptionProps>
61+
).props;
62+
63+
result.push({ disabled: disabled ?? false, value, valueLabel });
64+
65+
return result;
66+
}
67+
68+
if (
69+
child.type === EmptyStatement ||
70+
child.type === Controller ||
71+
child.type === Trigger
72+
) {
73+
return result;
74+
}
75+
76+
if (!("children" in child.props)) return result;
77+
if (isListFound && !isInList) return result;
78+
79+
const options = recurse(
80+
React.Children.toArray(
81+
(child as React.ReactElement<{ children: React.ReactNode }>).props
82+
.children,
83+
),
84+
isInList,
85+
);
86+
87+
return [...result, ...options];
88+
},
89+
[] as Array<
90+
PickAsMandatory<OptionProps, "disabled" | "value" | "valueLabel">
91+
>,
92+
);
93+
};
94+
95+
return recurse(childArray);
6696
};
6797

6898
type Registry<Key extends string> = Map<Key, string>;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@styleless-ui/react",
3-
"version": "1.0.0-rc.7",
3+
"version": "1.0.0-rc.8",
44
"description": "Completely unstyled, headless and accessible React UI components.",
55
"author": "mimshins <mostafa.sh.coderino@gmail.com>",
66
"license": "MIT",

0 commit comments

Comments
 (0)