@simplybusiness/mobius 5.24.0 → 5.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/components/Combobox/Combobox.js +12 -8
- package/dist/cjs/components/Combobox/Combobox.js.map +1 -1
- package/dist/cjs/components/Combobox/Listbox.js +13 -2
- package/dist/cjs/components/Combobox/Listbox.js.map +1 -1
- package/dist/cjs/components/Combobox/useComboboxHighlight.js +8 -2
- package/dist/cjs/components/Combobox/useComboboxHighlight.js.map +1 -1
- package/dist/cjs/components/Combobox/useComboboxOptions.js +6 -4
- package/dist/cjs/components/Combobox/useComboboxOptions.js.map +1 -1
- package/dist/cjs/components/Combobox/utils.js +1 -0
- package/dist/cjs/components/Combobox/utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/components/Combobox/Combobox.js +12 -8
- package/dist/esm/components/Combobox/Combobox.js.map +1 -1
- package/dist/esm/components/Combobox/Listbox.js +13 -2
- package/dist/esm/components/Combobox/Listbox.js.map +1 -1
- package/dist/esm/components/Combobox/types.js.map +1 -1
- package/dist/esm/components/Combobox/useComboboxHighlight.js +8 -2
- package/dist/esm/components/Combobox/useComboboxHighlight.js.map +1 -1
- package/dist/esm/components/Combobox/useComboboxOptions.js +6 -4
- package/dist/esm/components/Combobox/useComboboxOptions.js.map +1 -1
- package/dist/esm/components/Combobox/utils.js +1 -0
- package/dist/esm/components/Combobox/utils.js.map +1 -1
- package/dist/types/src/components/Combobox/Listbox.d.ts +2 -1
- package/dist/types/src/components/Combobox/types.d.ts +2 -0
- package/dist/types/src/components/Combobox/useComboboxHighlight.d.ts +1 -1
- package/dist/types/src/components/Combobox/useComboboxOptions.d.ts +3 -2
- package/dist/types/src/components/Combobox/utils.d.ts +1 -1
- package/package.json +2 -2
- package/src/components/Combobox/Combobox.css +10 -3
- package/src/components/Combobox/Combobox.test.tsx +4 -1
- package/src/components/Combobox/Combobox.tsx +13 -6
- package/src/components/Combobox/Listbox.tsx +22 -11
- package/src/components/Combobox/types.tsx +2 -0
- package/src/components/Combobox/useComboboxHighlight.tsx +13 -3
- package/src/components/Combobox/useComboboxOptions.test.ts +68 -4
- package/src/components/Combobox/useComboboxOptions.ts +8 -4
- package/src/components/Combobox/utils.tsx +2 -1
- package/dist/types/turbowatch.d.ts +0 -2
|
@@ -9,7 +9,8 @@ import { isOptionGroup } from "./utils";
|
|
|
9
9
|
|
|
10
10
|
export type ListboxProps<T extends ComboboxOption> = {
|
|
11
11
|
id: string;
|
|
12
|
-
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
options: ComboboxOptions<T> | undefined;
|
|
13
14
|
highlightedIndex: number;
|
|
14
15
|
highlightedGroupIndex: number;
|
|
15
16
|
onOptionSelect: (option: T) => void;
|
|
@@ -43,6 +44,14 @@ export const Listbox = <T extends ComboboxOption>({
|
|
|
43
44
|
: `${id}-option-${index}`;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
if (options && options.length === 0) {
|
|
48
|
+
return (
|
|
49
|
+
<div role="listbox" id={id} className={classes}>
|
|
50
|
+
<div className="mobius-combobox__no-options">No options</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
return (
|
|
47
56
|
<div role="listbox" id={id} className={classes}>
|
|
48
57
|
{isOptionGroup(options)
|
|
@@ -75,16 +84,18 @@ export const Listbox = <T extends ComboboxOption>({
|
|
|
75
84
|
))}
|
|
76
85
|
</ul>
|
|
77
86
|
))
|
|
78
|
-
: options
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
: typeof options !== "undefined"
|
|
88
|
+
? options.map((option, index) => (
|
|
89
|
+
<Option
|
|
90
|
+
key={index}
|
|
91
|
+
option={option}
|
|
92
|
+
isHighlighted={highlightedIndex === index}
|
|
93
|
+
onOptionSelect={onOptionSelect}
|
|
94
|
+
optionComponent={optionComponent}
|
|
95
|
+
id={getOptionId(option, 0, index)}
|
|
96
|
+
/>
|
|
97
|
+
))
|
|
98
|
+
: null}
|
|
88
99
|
</div>
|
|
89
100
|
);
|
|
90
101
|
};
|
|
@@ -25,6 +25,7 @@ export type ComboboxSyncProps<T extends ComboboxOption = ComboboxOption> =
|
|
|
25
25
|
asyncOptions?: never;
|
|
26
26
|
delay?: never;
|
|
27
27
|
minSearchLength?: never;
|
|
28
|
+
onSearched?: never;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export type ComboboxAsyncProps<T extends ComboboxOption = ComboboxOption> =
|
|
@@ -39,6 +40,7 @@ export type ComboboxAsyncProps<T extends ComboboxOption = ComboboxOption> =
|
|
|
39
40
|
inputValue: string,
|
|
40
41
|
options?: { signal?: AbortSignal },
|
|
41
42
|
) => Promise<ComboboxOptions<T>>);
|
|
43
|
+
onSearched?: (searchTerm: string) => void;
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export type ComboboxProps<T extends ComboboxOption = ComboboxOption> =
|
|
@@ -2,15 +2,19 @@ import { useState } from "react";
|
|
|
2
2
|
import type { ComboboxOptions } from "./types";
|
|
3
3
|
import { isOptionGroup } from "./utils";
|
|
4
4
|
|
|
5
|
-
export function useComboboxHighlight(options: ComboboxOptions) {
|
|
5
|
+
export function useComboboxHighlight(options: ComboboxOptions | undefined) {
|
|
6
6
|
const [highlightedIndex, setHighlightedIndex] = useState(
|
|
7
|
-
options.length ? 0 : -1,
|
|
7
|
+
options && options.length ? 0 : -1,
|
|
8
8
|
);
|
|
9
9
|
const [highlightedGroupIndex, setHighlightedGroupIndex] = useState(0);
|
|
10
10
|
|
|
11
11
|
function highlightNextOption() {
|
|
12
12
|
const isGroup = isOptionGroup(options);
|
|
13
13
|
|
|
14
|
+
if (!options) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
if (isGroup) {
|
|
15
19
|
const group = options[highlightedGroupIndex];
|
|
16
20
|
if (highlightedIndex === group.options.length - 1) {
|
|
@@ -61,6 +65,10 @@ export function useComboboxHighlight(options: ComboboxOptions) {
|
|
|
61
65
|
function highlightLastOption() {
|
|
62
66
|
const isGroup = isOptionGroup(options);
|
|
63
67
|
|
|
68
|
+
if (!options) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
if (isGroup) {
|
|
65
73
|
const lastGroupIndex = options.length - 1;
|
|
66
74
|
const lastGroup = options[lastGroupIndex];
|
|
@@ -72,7 +80,9 @@ export function useComboboxHighlight(options: ComboboxOptions) {
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
const clearHighlight = () => {
|
|
75
|
-
setHighlightedIndex(
|
|
83
|
+
setHighlightedIndex(
|
|
84
|
+
typeof options === "undefined" || options.length ? 0 : -1,
|
|
85
|
+
);
|
|
76
86
|
setHighlightedGroupIndex(0);
|
|
77
87
|
};
|
|
78
88
|
|
|
@@ -143,7 +143,7 @@ describe("useComboboxOptions", () => {
|
|
|
143
143
|
expect(result.current).toStrictEqual({
|
|
144
144
|
updateFilteredOptions: expect.any(Function),
|
|
145
145
|
error: null,
|
|
146
|
-
filteredOptions:
|
|
146
|
+
filteredOptions: undefined,
|
|
147
147
|
isLoading: false,
|
|
148
148
|
isError: false,
|
|
149
149
|
});
|
|
@@ -227,11 +227,75 @@ describe("useComboboxOptions", () => {
|
|
|
227
227
|
expect(result.current).toStrictEqual({
|
|
228
228
|
updateFilteredOptions: expect.any(Function),
|
|
229
229
|
error: null,
|
|
230
|
-
filteredOptions:
|
|
230
|
+
filteredOptions: undefined,
|
|
231
231
|
isLoading: false,
|
|
232
232
|
isError: false,
|
|
233
233
|
});
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
it("should call onSearched when async options have been fetched", async () => {
|
|
237
|
+
const onSearched = jest.fn();
|
|
238
|
+
const asyncOptions = jest.fn().mockResolvedValue([
|
|
239
|
+
{ label: "Async Option 1", value: "1" },
|
|
240
|
+
{ label: "Async Option 2", value: "2" },
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
renderHook(() =>
|
|
244
|
+
useComboboxOptions({
|
|
245
|
+
asyncOptions,
|
|
246
|
+
inputValue: "Async",
|
|
247
|
+
onSearched,
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Wait for the async function to resolve
|
|
252
|
+
await act(() => Promise.resolve());
|
|
253
|
+
|
|
254
|
+
expect(onSearched).toHaveBeenCalledWith("Async");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should not call onSearched when input length is less than minSearchLength", async () => {
|
|
258
|
+
const onSearched = jest.fn();
|
|
259
|
+
const asyncOptions = jest.fn().mockResolvedValue([
|
|
260
|
+
{ label: "Async Option 1", value: "1" },
|
|
261
|
+
{ label: "Async Option 2", value: "2" },
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
renderHook(() =>
|
|
265
|
+
useComboboxOptions({
|
|
266
|
+
asyncOptions,
|
|
267
|
+
inputValue: "As",
|
|
268
|
+
minSearchLength: 3,
|
|
269
|
+
onSearched,
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Wait for the async function to resolve
|
|
274
|
+
await act(() => Promise.resolve());
|
|
275
|
+
|
|
276
|
+
expect(onSearched).not.toHaveBeenCalled();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should not call onSearched for synchronous options", async () => {
|
|
280
|
+
const onSearched = jest.fn();
|
|
281
|
+
const options = [
|
|
282
|
+
{ label: "Option 1", value: "1" },
|
|
283
|
+
{ label: "Option 2", value: "2" },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
renderHook(() =>
|
|
287
|
+
useComboboxOptions({
|
|
288
|
+
options,
|
|
289
|
+
inputValue: "Option",
|
|
290
|
+
onSearched,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Wait for any potential async operations
|
|
295
|
+
await act(() => Promise.resolve());
|
|
296
|
+
|
|
297
|
+
expect(onSearched).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
235
299
|
});
|
|
236
300
|
|
|
237
301
|
describe("loading states", () => {
|
|
@@ -331,7 +395,7 @@ describe("useComboboxOptions", () => {
|
|
|
331
395
|
expect(result.current).toStrictEqual({
|
|
332
396
|
updateFilteredOptions: expect.any(Function),
|
|
333
397
|
error: new Error("Fetch error"),
|
|
334
|
-
filteredOptions:
|
|
398
|
+
filteredOptions: undefined,
|
|
335
399
|
isLoading: false,
|
|
336
400
|
isError: true,
|
|
337
401
|
});
|
|
@@ -391,7 +455,7 @@ describe("useComboboxOptions", () => {
|
|
|
391
455
|
expect(result.current).toStrictEqual({
|
|
392
456
|
updateFilteredOptions: expect.any(Function),
|
|
393
457
|
error: new Error("Fetch error"),
|
|
394
|
-
filteredOptions:
|
|
458
|
+
filteredOptions: undefined,
|
|
395
459
|
isLoading: false,
|
|
396
460
|
isError: true,
|
|
397
461
|
});
|
|
@@ -9,6 +9,7 @@ export type UseComboboxOptionsProps<T extends ComboboxOption> = Pick<
|
|
|
9
9
|
> & {
|
|
10
10
|
skipNextDebounceRef?: React.MutableRefObject<boolean>;
|
|
11
11
|
inputValue?: string;
|
|
12
|
+
onSearched?: (searchTerm: string) => void;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export function useComboboxOptions<T extends ComboboxOption>({
|
|
@@ -18,10 +19,11 @@ export function useComboboxOptions<T extends ComboboxOption>({
|
|
|
18
19
|
minSearchLength = 3,
|
|
19
20
|
inputValue = "",
|
|
20
21
|
skipNextDebounceRef,
|
|
22
|
+
onSearched,
|
|
21
23
|
}: UseComboboxOptionsProps<T>) {
|
|
22
|
-
const [filteredOptions, setFilteredOptions] = useState<
|
|
23
|
-
|
|
24
|
-
);
|
|
24
|
+
const [filteredOptions, setFilteredOptions] = useState<
|
|
25
|
+
ComboboxOptions<T> | undefined
|
|
26
|
+
>(undefined);
|
|
25
27
|
const debouncedInputValue = useDebouncedValue(
|
|
26
28
|
inputValue,
|
|
27
29
|
// Don't debounce synchronous options
|
|
@@ -40,11 +42,12 @@ export function useComboboxOptions<T extends ComboboxOption>({
|
|
|
40
42
|
try {
|
|
41
43
|
if (asyncOptions) {
|
|
42
44
|
if (debouncedInputValue.length < minSearchLength) {
|
|
43
|
-
setFilteredOptions(
|
|
45
|
+
setFilteredOptions(undefined);
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
48
|
const result = await asyncOptions(debouncedInputValue, { signal });
|
|
47
49
|
setFilteredOptions(result);
|
|
50
|
+
onSearched?.(debouncedInputValue);
|
|
48
51
|
} else {
|
|
49
52
|
// @ts-expect-error options is erroneously typed as possibly undefined
|
|
50
53
|
setFilteredOptions(filterOptions(options, debouncedInputValue));
|
|
@@ -76,6 +79,7 @@ export function useComboboxOptions<T extends ComboboxOption>({
|
|
|
76
79
|
delay,
|
|
77
80
|
minSearchLength,
|
|
78
81
|
skipNextDebounceRef,
|
|
82
|
+
onSearched,
|
|
79
83
|
]);
|
|
80
84
|
|
|
81
85
|
function updateFilteredOptions(newOptions: Promise<ComboboxOptions<T>>) {
|
|
@@ -6,8 +6,9 @@ import type {
|
|
|
6
6
|
|
|
7
7
|
// FIXME: This might be better handled with Zod
|
|
8
8
|
export function isOptionGroup<T extends ComboboxOption>(
|
|
9
|
-
options: ComboboxOptions<T
|
|
9
|
+
options: ComboboxOptions<T> | undefined,
|
|
10
10
|
): options is ComboboxOptionGroup<T>[] {
|
|
11
|
+
if (!options) return false;
|
|
11
12
|
return (
|
|
12
13
|
typeof options[0] === "object" &&
|
|
13
14
|
"options" in options[0] &&
|