Skip to content

Commit 4a9c464

Browse files
committed
Combobox items deprecated in favor of options
1 parent 4894416 commit 4a9c464

File tree

2 files changed

+47
-39
lines changed

2 files changed

+47
-39
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@near-pagoda/ui",
3-
"version": "3.1.6",
3+
"version": "3.1.7",
44
"description": "A React component library that implements the official NEAR design system.",
55
"license": "MIT",
66
"repository": {

src/components/Combobox.tsx

+46-38
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Input } from './Input';
1919
import { SvgIcon } from './SvgIcon';
2020
import { Text } from './Text';
2121

22-
export type ComboboxItem = {
22+
export type ComboboxOption = {
2323
group?: string;
2424
hidden?: boolean;
2525
label?: string;
@@ -34,7 +34,10 @@ type Props = {
3434
error?: string;
3535
icon?: ReactElement;
3636
infoText?: string;
37-
items: ComboboxItem[];
37+
/**
38+
* @deprecated please use "options" prop instead
39+
*/
40+
items: ComboboxOption[];
3841
label: string;
3942
maxDropdownHeight?: string;
4043
name: string;
@@ -43,6 +46,7 @@ type Props = {
4346
onChange: (value: string | null) => any;
4447
onCreateItem?: (inputText: string) => any;
4548
onEditItem?: (selectedItemValue: string) => any;
49+
options: ComboboxOption[];
4650
placeholder?: string;
4751
style?: CSSProperties;
4852
success?: string;
@@ -51,33 +55,37 @@ type Props = {
5155

5256
export const Combobox = forwardRef<HTMLInputElement, Props>(
5357
({ allowCustomInput, allowNone, noneLabel, ...props }, ref) => {
54-
const noneItem = useMemo(() => {
55-
const item: ComboboxItem = { label: noneLabel || 'None', value: '__NONE__' };
56-
return item;
58+
if (props.items) {
59+
props.options = props.items;
60+
}
61+
62+
const noneOption = useMemo(() => {
63+
const option: ComboboxOption = { label: noneLabel || 'None', value: '__NONE__' };
64+
return option;
5765
}, [noneLabel]);
5866

5967
const inputValue = useRef<string>('');
6068
const internalCurrentValue = useRef<string | null | undefined>(undefined);
6169
const internalCurrentValueBeforeFocus = useRef<string | null | undefined>(undefined);
6270
const onBlurTimeout = useRef<NodeJS.Timeout>();
63-
const [filteredItems, setFilteredItems] = useState(allowNone ? [noneItem, ...props.items] : props.items);
64-
const defaultSelectedItem = props.items.find((item) => item.value === props.value);
65-
const forceOverrideClosed = allowCustomInput && filteredItems.length === 0;
66-
const debouncedItems = useDebouncedValue(props.items, 25); // This debounce avoids race condition where prop.items updates before props.value
71+
const [filteredOptions, setFilteredItems] = useState(allowNone ? [noneOption, ...props.options] : props.options);
72+
const defaultSelectedItem = props.options.find((o) => o.value === props.value);
73+
const forceOverrideClosed = allowCustomInput && filteredOptions.length === 0;
74+
const debouncedOptions = useDebouncedValue(props.options, 25); // This debounce avoids race condition where prop.items updates before props.value
6775

6876
const { selectItem, setInputValue, ...combobox } = useCombobox({
6977
id: props.name,
7078
defaultSelectedItem,
71-
items: filteredItems,
79+
items: filteredOptions,
7280
itemToString(item) {
7381
return item ? (item.label ?? item.value.toString()) : '';
7482
},
7583
onInputValueChange(event) {
7684
const query = event.inputValue?.toLowerCase() ?? '';
77-
const items = allowNone ? [noneItem, ...props.items] : props.items;
78-
const results = items.filter((item) => {
79-
if (item.hidden) return false;
80-
const label = (item.label ?? item.value).toString().toLowerCase();
85+
const options = allowNone ? [noneOption, ...props.options] : props.options;
86+
const results = options.filter((o) => {
87+
if (o.hidden) return false;
88+
const label = (o.label ?? o.value).toString().toLowerCase();
8189
return label.includes(query);
8290
});
8391
setFilteredItems(results);
@@ -142,17 +150,17 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
142150
const comboboxInputRef = (combobox.getInputProps() as any).ref; // This value actually exists, the types are wrong
143151

144152
useEffect(() => {
145-
const items = allowNone ? [noneItem, ...props.items] : props.items;
146-
setFilteredItems(items.filter((item) => !item.hidden));
147-
}, [allowNone, noneItem, props.items]);
153+
const options = allowNone ? [noneOption, ...props.options] : props.options;
154+
setFilteredItems(options.filter((o) => !o.hidden));
155+
}, [allowNone, noneOption, props.options]);
148156

149157
useEffect(() => {
150-
const selected = debouncedItems.find((item) => item.value === props.value);
158+
const selected = debouncedOptions.find((o) => o.value === props.value);
151159

152160
if (props.value !== internalCurrentValue.current) {
153161
if (props.value === null && allowNone) {
154162
internalCurrentValue.current = null;
155-
selectItem(noneItem);
163+
selectItem(noneOption);
156164
} else {
157165
if (selected) {
158166
internalCurrentValue.current = selected.value;
@@ -166,14 +174,14 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
166174
}
167175
}
168176
} else if (props.value !== null) {
169-
setInputValue(selected?.label ?? selected?.value.toString() ?? ''); // In the case of the selected item's label being updated, we need to update the input
177+
setInputValue(selected?.label ?? selected?.value.toString() ?? ''); // In the case of the selected option's label being updated, we need to update the input
170178
}
171-
}, [allowCustomInput, allowNone, noneItem, selectItem, props.value, debouncedItems, setInputValue]);
179+
}, [allowCustomInput, allowNone, noneOption, selectItem, props.value, debouncedOptions, setInputValue]);
172180

173-
const shouldRenderGroupLabel = (item: ComboboxItem, index: number) => {
174-
const previousItem = filteredItems[Math.max(0, index - 1)];
175-
if (item.group && index === 0) return true;
176-
return item.group && item.group !== previousItem?.group;
181+
const shouldRenderGroupLabel = (option: ComboboxOption, index: number) => {
182+
const previousItem = filteredOptions[Math.max(0, index - 1)];
183+
if (option.group && index === 0) return true;
184+
return option.group && option.group !== previousItem?.group;
177185
};
178186

179187
return (
@@ -211,35 +219,35 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
211219
</li>
212220
)}
213221

214-
{filteredItems.map((item, index) => (
215-
<Fragment key={item.value}>
216-
{shouldRenderGroupLabel(item, index) && (
222+
{filteredOptions.map((option, index) => (
223+
<Fragment key={option.value}>
224+
{shouldRenderGroupLabel(option, index) && (
217225
<li className={s.dropdownGroupLabel}>
218-
<Text as="h5">{item.group}</Text>
226+
<Text as="h5">{option.group}</Text>
219227
</li>
220228
)}
221229

222230
<li
223231
className={s.dropdownItem}
224232
data-highlighted={combobox.highlightedIndex === index}
225-
data-selected={combobox.selectedItem?.value === item.value}
226-
{...combobox.getItemProps({ item, index })}
233+
data-selected={combobox.selectedItem?.value === option.value}
234+
{...combobox.getItemProps({ item: option, index })}
227235
>
228-
{combobox.selectedItem?.value === item.value ? (
236+
{combobox.selectedItem?.value === option.value ? (
229237
<SvgIcon icon={<CheckCircle weight="duotone" />} color="green-9" />
230238
) : (
231239
<SvgIcon icon={<Circle weight="duotone" />} color="sand-10" />
232240
)}
233241

234-
{item.label ?? item.value}
242+
{option.label ?? option.value}
235243
</li>
236244
</Fragment>
237245
))}
238246

239-
{filteredItems.length === 0 && (
247+
{filteredOptions.length === 0 && (
240248
<li className={s.content}>
241249
<Text size="text-s">
242-
{props.items.length === 0
250+
{props.options.length === 0
243251
? 'No available options.'
244252
: 'No matching options. Try a different search?'}
245253
</Text>
@@ -257,13 +265,13 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
257265
);
258266
Combobox.displayName = 'Combobox';
259267

260-
export function useComboboxItemMapper<T extends unknown[]>(
268+
export function useComboboxOptionMapper<T extends unknown[]>(
261269
array: T | undefined,
262-
mapItem: (item: T[number]) => ComboboxItem | ComboboxItem[] | null,
270+
mapper: (item: T[number]) => ComboboxOption | ComboboxOption[] | null,
263271
dependencies?: unknown[],
264272
) {
265273
const options = useMemo(() => {
266-
return (array?.flatMap(mapItem) ?? []).filter((value) => !!value) as ComboboxItem[];
274+
return (array?.flatMap(mapper) ?? []).filter((value) => !!value) as ComboboxOption[];
267275
// eslint-disable-next-line react-hooks/exhaustive-deps
268276
}, [array, ...(dependencies ?? [])]);
269277

0 commit comments

Comments
 (0)