@simplybusiness/mobius 9.1.2 → 9.2.0

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.
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode, RefAttributes } from "react";
2
2
  import type { DOMProps } from "../../types/dom";
3
3
  export type TextElementType = HTMLHeadingElement | HTMLParagraphElement;
4
- export type TextVariantType = "h1" | "h2" | "h3" | "h4" | "body" | "small" | "legal";
4
+ export type TextVariantType = "h1" | "h2" | "h3" | "h4" | "body" | "small" | "legal" | "title";
5
5
  export type ElementType = "h1" | "h2" | "h3" | "h4" | "p" | "span";
6
6
  export interface TextProps extends DOMProps, RefAttributes<TextElementType> {
7
7
  /** HTML element for the text */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@simplybusiness/mobius",
3
3
  "license": "UNLICENSED",
4
- "version": "9.1.2",
4
+ "version": "9.2.0",
5
5
  "description": "Core library of Mobius react components",
6
6
  "repository": {
7
7
  "type": "git",
@@ -95,6 +95,38 @@ describe("useComboboxOptions", () => {
95
95
  });
96
96
  });
97
97
 
98
+ describe("async options stability", () => {
99
+ it("should not re-fetch when asyncOptions reference changes but inputValue stays the same", async () => {
100
+ const asyncOptionsV1 = vi
101
+ .fn()
102
+ .mockResolvedValue([{ label: "Result", value: "1" }]);
103
+ const asyncOptionsV2 = vi
104
+ .fn()
105
+ .mockResolvedValue([{ label: "Result", value: "1" }]);
106
+
107
+ const { rerender } = renderHook(
108
+ ({ asyncOptions }) =>
109
+ useComboboxOptions({
110
+ asyncOptions,
111
+ inputValue: "hello",
112
+ }),
113
+ { initialProps: { asyncOptions: asyncOptionsV1 } },
114
+ );
115
+
116
+ await act(() => Promise.resolve());
117
+
118
+ expect(asyncOptionsV1).toHaveBeenCalledTimes(1);
119
+
120
+ // Simulate parent re-render providing a new asyncOptions reference
121
+ rerender({ asyncOptions: asyncOptionsV2 });
122
+
123
+ await act(() => Promise.resolve());
124
+
125
+ // Should NOT have triggered a new fetch since inputValue didn't change
126
+ expect(asyncOptionsV2).not.toHaveBeenCalled();
127
+ });
128
+ });
129
+
98
130
  describe("async options handling", () => {
99
131
  it("should handle async options", async () => {
100
132
  const asyncOptions = vi.fn().mockResolvedValue([
@@ -1,7 +1,7 @@
1
- import { useEffect, useState } from "react";
1
+ import { useDebouncedValue } from "@simplybusiness/mobius-hooks";
2
+ import { useEffect, useRef, useState } from "react";
2
3
  import type { ComboboxOption, ComboboxOptions, ComboboxProps } from "./types";
3
4
  import { filterOptions } from "./utils";
4
- import { useDebouncedValue } from "@simplybusiness/mobius-hooks";
5
5
 
6
6
  export type UseComboboxOptionsProps<T extends ComboboxOption> = Pick<
7
7
  ComboboxProps<T>,
@@ -32,6 +32,13 @@ export function useComboboxOptions<T extends ComboboxOption>({
32
32
  const [isLoading, setIsLoading] = useState(false);
33
33
  const [error, setError] = useState<Error | null>(null);
34
34
 
35
+ // Keep refs to latest callbacks so the fetch effect doesn't re-run when
36
+ // their references change (e.g. due to un-memoised props in parent)
37
+ const asyncOptionsRef = useRef(asyncOptions);
38
+ asyncOptionsRef.current = asyncOptions;
39
+ const onSearchedRef = useRef(onSearched);
40
+ onSearchedRef.current = onSearched;
41
+
35
42
  useEffect(() => {
36
43
  const controller = new AbortController();
37
44
  const { signal } = controller;
@@ -40,14 +47,16 @@ export function useComboboxOptions<T extends ComboboxOption>({
40
47
  setIsLoading(true);
41
48
  setError(null);
42
49
  try {
43
- if (asyncOptions) {
50
+ if (asyncOptionsRef.current) {
44
51
  if (debouncedInputValue.length < minSearchLength) {
45
52
  setFilteredOptions(undefined);
46
53
  return;
47
54
  }
48
- const result = await asyncOptions(debouncedInputValue, { signal });
55
+ const result = await asyncOptionsRef.current(debouncedInputValue, {
56
+ signal,
57
+ });
49
58
  setFilteredOptions(result);
50
- onSearched?.(debouncedInputValue);
59
+ onSearchedRef.current?.(debouncedInputValue);
51
60
  } else if (options) {
52
61
  setFilteredOptions(filterOptions(options, debouncedInputValue));
53
62
  } else {
@@ -76,11 +85,9 @@ export function useComboboxOptions<T extends ComboboxOption>({
76
85
  }, [
77
86
  debouncedInputValue,
78
87
  options,
79
- asyncOptions,
80
88
  delay,
81
89
  minSearchLength,
82
90
  skipNextDebounceRef,
83
- onSearched,
84
91
  ]);
85
92
 
86
93
  function updateFilteredOptions(newOptions: Promise<ComboboxOptions<T>>) {
@@ -88,6 +88,15 @@
88
88
  color: var(--color-text-medium);
89
89
  }
90
90
 
91
+ /* Title variant */
92
+ &:where(.--is-title) {
93
+ color: var(--color-text);
94
+ font-size: var(--font-size-small-title);
95
+ font-weight: var(--font-weight-bold);
96
+ line-height: var(--line-height-tight);
97
+ margin: var(--size-md) 0;
98
+ }
99
+
91
100
  /* Compact text */
92
101
  &:where(.--has-line-height-tight) {
93
102
  line-height: var(--line-height-tight);
@@ -111,6 +111,13 @@ export const VariantLegal: StoryType = {
111
111
  },
112
112
  };
113
113
 
114
+ export const VariantTitle: StoryType = {
115
+ render: (args: TextProps) => <Text {...args}>Variant title</Text>,
116
+ args: {
117
+ variant: "title",
118
+ },
119
+ };
120
+
114
121
  export const Themed: StoryType = {
115
122
  render: (args: TextProps) => <Text {...args}>Sample Text</Text>,
116
123
  args: {
@@ -10,7 +10,8 @@ export type TextVariantType =
10
10
  | "h4"
11
11
  | "body"
12
12
  | "small"
13
- | "legal";
13
+ | "legal"
14
+ | "title";
14
15
  export type ElementType = "h1" | "h2" | "h3" | "h4" | "p" | "span";
15
16
  export interface TextProps extends DOMProps, RefAttributes<TextElementType> {
16
17
  /** HTML element for the text */