@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/components/Combobox/Combobox.js +12 -8
  3. package/dist/cjs/components/Combobox/Combobox.js.map +1 -1
  4. package/dist/cjs/components/Combobox/Listbox.js +13 -2
  5. package/dist/cjs/components/Combobox/Listbox.js.map +1 -1
  6. package/dist/cjs/components/Combobox/useComboboxHighlight.js +8 -2
  7. package/dist/cjs/components/Combobox/useComboboxHighlight.js.map +1 -1
  8. package/dist/cjs/components/Combobox/useComboboxOptions.js +6 -4
  9. package/dist/cjs/components/Combobox/useComboboxOptions.js.map +1 -1
  10. package/dist/cjs/components/Combobox/utils.js +1 -0
  11. package/dist/cjs/components/Combobox/utils.js.map +1 -1
  12. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  13. package/dist/esm/components/Combobox/Combobox.js +12 -8
  14. package/dist/esm/components/Combobox/Combobox.js.map +1 -1
  15. package/dist/esm/components/Combobox/Listbox.js +13 -2
  16. package/dist/esm/components/Combobox/Listbox.js.map +1 -1
  17. package/dist/esm/components/Combobox/types.js.map +1 -1
  18. package/dist/esm/components/Combobox/useComboboxHighlight.js +8 -2
  19. package/dist/esm/components/Combobox/useComboboxHighlight.js.map +1 -1
  20. package/dist/esm/components/Combobox/useComboboxOptions.js +6 -4
  21. package/dist/esm/components/Combobox/useComboboxOptions.js.map +1 -1
  22. package/dist/esm/components/Combobox/utils.js +1 -0
  23. package/dist/esm/components/Combobox/utils.js.map +1 -1
  24. package/dist/types/src/components/Combobox/Listbox.d.ts +2 -1
  25. package/dist/types/src/components/Combobox/types.d.ts +2 -0
  26. package/dist/types/src/components/Combobox/useComboboxHighlight.d.ts +1 -1
  27. package/dist/types/src/components/Combobox/useComboboxOptions.d.ts +3 -2
  28. package/dist/types/src/components/Combobox/utils.d.ts +1 -1
  29. package/package.json +2 -2
  30. package/src/components/Combobox/Combobox.css +10 -3
  31. package/src/components/Combobox/Combobox.test.tsx +4 -1
  32. package/src/components/Combobox/Combobox.tsx +13 -6
  33. package/src/components/Combobox/Listbox.tsx +22 -11
  34. package/src/components/Combobox/types.tsx +2 -0
  35. package/src/components/Combobox/useComboboxHighlight.tsx +13 -3
  36. package/src/components/Combobox/useComboboxOptions.test.ts +68 -4
  37. package/src/components/Combobox/useComboboxOptions.ts +8 -4
  38. package/src/components/Combobox/utils.tsx +2 -1
  39. 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
- options: ComboboxOptions<T>;
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.map((option, index) => (
79
- <Option
80
- key={index}
81
- option={option}
82
- isHighlighted={highlightedIndex === index}
83
- onOptionSelect={onOptionSelect}
84
- optionComponent={optionComponent}
85
- id={getOptionId(option, 0, index)}
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(options.length ? 0 : -1);
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<ComboboxOptions<T>>(
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] &&
@@ -1,2 +0,0 @@
1
- declare const _default: import("turbowatch/dist/types").TurbowatchConfigurationInput;
2
- export default _default;