@simplybusiness/mobius 8.0.1 → 9.0.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cjs/index.js +4773 -0
  3. package/dist/cjs/index.js.map +7 -0
  4. package/dist/cjs/meta.json +4948 -0
  5. package/dist/esm/index.js +542 -694
  6. package/dist/esm/index.js.map +4 -4
  7. package/dist/esm/meta.json +300 -532
  8. package/dist/esm/tsconfig.build.tsbuildinfo +1 -1
  9. package/dist/types/src/hooks/index.d.ts +0 -7
  10. package/dist/types/src/utils/htmlDialogPolyfill.d.ts +1 -0
  11. package/dist/types/src/utils/index.d.ts +0 -1
  12. package/dist/types/src/utils/mockMatchMedia.d.ts +1 -0
  13. package/dist/types/vitest.config.d.ts +2 -0
  14. package/package.json +14 -18
  15. package/src/components/Accordion/Accordion.stories.tsx +1 -1
  16. package/src/components/Accordion/Accordion.test.tsx +12 -12
  17. package/src/components/Accordion/Accordion.tsx +1 -1
  18. package/src/components/Accordion/AccordionList.stories.tsx +1 -1
  19. package/src/components/Accordion/AccordionList.test.tsx +6 -6
  20. package/src/components/AddressLookup/AddressLookup.stories.tsx +1 -1
  21. package/src/components/AddressLookup/AddressLookup.test.tsx +19 -20
  22. package/src/components/AddressLookup/LoqateAddressLookupService.test.tsx +7 -6
  23. package/src/components/Alert/Alert.stories.tsx +1 -1
  24. package/src/components/Box/Box.stories.tsx +1 -1
  25. package/src/components/Breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  26. package/src/components/Button/Button.stories.tsx +3 -4
  27. package/src/components/Button/Button.test.tsx +4 -4
  28. package/src/components/Checkbox/Checkbox.stories.tsx +1 -1
  29. package/src/components/Checkbox/Checkbox.test.tsx +2 -2
  30. package/src/components/Checkbox/CheckboxGroup.stories.tsx +1 -1
  31. package/src/components/Checkbox/CheckboxGroup.test.tsx +5 -5
  32. package/src/components/Combobox/Combobox.stories.tsx +1 -1
  33. package/src/components/Combobox/Combobox.test.tsx +67 -78
  34. package/src/components/Combobox/Combobox.tsx +2 -1
  35. package/src/components/Combobox/useComboboxOptions.test.ts +30 -30
  36. package/src/components/Combobox/useComboboxOptions.ts +1 -1
  37. package/src/components/Container/Container.stories.tsx +1 -1
  38. package/src/components/DateField/DateField.stories.tsx +1 -1
  39. package/src/components/DateField/DateField.test.tsx +1 -1
  40. package/src/components/Divider/Divider.stories.tsx +1 -1
  41. package/src/components/Drawer/Drawer.stories.tsx +1 -1
  42. package/src/components/Drawer/Drawer.test.tsx +6 -6
  43. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -10
  44. package/src/components/DropdownMenu/DropdownMenu.test.tsx +1 -1
  45. package/src/components/ErrorMessage/ErrorMessage.stories.tsx +1 -1
  46. package/src/components/ExpandableText/ExpandableText.test.tsx +14 -14
  47. package/src/components/Fieldset/Fieldset.stories.tsx +1 -1
  48. package/src/components/Flex/Flex.stories.tsx +1 -1
  49. package/src/components/Grid/Grid.stories.tsx +4 -7
  50. package/src/components/Icon/Icon.stories.tsx +1 -1
  51. package/src/components/Image/Image.stories.tsx +1 -1
  52. package/src/components/Label/Label.stories.tsx +1 -1
  53. package/src/components/Link/Link.stories.tsx +1 -1
  54. package/src/components/Link/Link.test.tsx +1 -1
  55. package/src/components/LinkButton/LinkButton.stories.tsx +1 -1
  56. package/src/components/LinkButton/LinkButton.test.tsx +2 -2
  57. package/src/components/List/List.stories.tsx +1 -1
  58. package/src/components/LoadingIndicator/LoadingIndicator.stories.tsx +1 -1
  59. package/src/components/Logo/Logo.stories.tsx +1 -1
  60. package/src/components/Modal/Modal.stories.tsx +1 -1
  61. package/src/components/Modal/Modal.test.tsx +6 -6
  62. package/src/components/NumberField/NumberField.stories.tsx +1 -1
  63. package/src/components/NumberField/NumberField.test.tsx +5 -5
  64. package/src/components/PasswordField/PasswordField.stories.tsx +1 -1
  65. package/src/components/Popover/Popover.stories.tsx +4 -8
  66. package/src/components/Popover/Popover.test.tsx +4 -4
  67. package/src/components/Popover/Popover.tsx +1 -1
  68. package/src/components/Progress/Progress.stories.tsx +1 -1
  69. package/src/components/Radio/Radio.stories.tsx +1 -1
  70. package/src/components/Radio/Radio.test.tsx +9 -9
  71. package/src/components/SVG/SVG.stories.tsx +1 -1
  72. package/src/components/Segment/Segment.stories.tsx +1 -1
  73. package/src/components/Select/Select.stories.tsx +1 -1
  74. package/src/components/Select/Select.test.tsx +1 -1
  75. package/src/components/Slider/Slider.stories.tsx +1 -1
  76. package/src/components/Slider/Slider.test.tsx +6 -6
  77. package/src/components/Slider/helpers.test.ts +1 -1
  78. package/src/components/Stack/Stack.stories.tsx +1 -1
  79. package/src/components/Switch/Switch.stories.tsx +1 -1
  80. package/src/components/Switch/Switch.test.tsx +1 -1
  81. package/src/components/Table/Table.stories.tsx +1 -1
  82. package/src/components/Text/Text.stories.tsx +1 -1
  83. package/src/components/TextArea/TextArea.stories.tsx +1 -1
  84. package/src/components/TextArea/TextArea.test.tsx +3 -3
  85. package/src/components/TextField/TextField.stories.tsx +1 -1
  86. package/src/components/TextOrHTML/TextOrHTML.stories.tsx +1 -1
  87. package/src/components/Title/Title.stories.tsx +1 -1
  88. package/src/components/Toast/Toast.stories.tsx +1 -1
  89. package/src/components/Toast/Toast.test.tsx +6 -6
  90. package/src/components/Trust/Trust.stories.tsx +1 -1
  91. package/src/components/VisuallyHidden/VisuallyHidden.stories.tsx +1 -1
  92. package/src/hooks/index.tsx +0 -7
  93. package/src/hooks/useBreakpoint/useBreakpoint.ssr.test.tsx +18 -0
  94. package/src/hooks/useBreakpoint/useBreakpoint.stories.tsx +1 -1
  95. package/src/hooks/useBreakpoint/useBreakpoint.test.tsx +65 -5
  96. package/src/hooks/useBreakpoint/useBreakpoint.tsx +25 -39
  97. package/src/hooks/useButton/useButton.test.tsx +4 -4
  98. package/src/hooks/useDialog/useDialog.ts +1 -1
  99. package/src/hooks/useLabel/useLabel.test.tsx +1 -1
  100. package/src/hooks/useTextField/useTextField.test.tsx +4 -4
  101. package/src/public-whitelist.test.ts +1 -0
  102. package/src/utils/delay.test.ts +4 -4
  103. package/src/utils/{jestHTMLDialogPolyfill.ts → htmlDialogPolyfill.ts} +5 -5
  104. package/src/utils/index.ts +0 -1
  105. package/src/utils/mockMatchMedia.ts +16 -0
  106. package/dist/types/src/hooks/useBodyScrollLock/index.d.ts +0 -1
  107. package/dist/types/src/hooks/useBodyScrollLock/useBodyScrollLock.d.ts +0 -3
  108. package/dist/types/src/hooks/useDebouncedValue/index.d.ts +0 -1
  109. package/dist/types/src/hooks/useDebouncedValue/useDebouncedValue.d.ts +0 -1
  110. package/dist/types/src/hooks/useOnClickOutside/index.d.ts +0 -1
  111. package/dist/types/src/hooks/useOnClickOutside/useOnClickOutside.d.ts +0 -2
  112. package/dist/types/src/hooks/useOnUnmount/index.d.ts +0 -1
  113. package/dist/types/src/hooks/useOnUnmount/useOnUnmount.d.ts +0 -1
  114. package/dist/types/src/hooks/usePrefersReducedMotion/index.d.ts +0 -1
  115. package/dist/types/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.d.ts +0 -1
  116. package/dist/types/src/hooks/useRenderCount/index.d.ts +0 -1
  117. package/dist/types/src/hooks/useRenderCount/useRenderCount.d.ts +0 -1
  118. package/dist/types/src/hooks/useWindowEvent/index.d.ts +0 -1
  119. package/dist/types/src/hooks/useWindowEvent/useWindowEvent.d.ts +0 -1
  120. package/dist/types/src/utils/jestHTMLDialogPolyfill.d.ts +0 -1
  121. package/dist/types/src/utils/jestMockMatchMedia.d.ts +0 -1
  122. package/src/hooks/useBodyScrollLock/index.ts +0 -1
  123. package/src/hooks/useBodyScrollLock/useBodyScrollLock.test.ts +0 -34
  124. package/src/hooks/useBodyScrollLock/useBodyScrollLock.ts +0 -30
  125. package/src/hooks/useDebouncedValue/index.tsx +0 -1
  126. package/src/hooks/useDebouncedValue/useDebouncedValue.test.tsx +0 -62
  127. package/src/hooks/useDebouncedValue/useDebouncedValue.tsx +0 -25
  128. package/src/hooks/useOnClickOutside/index.tsx +0 -1
  129. package/src/hooks/useOnClickOutside/useOnClickOutside.test.tsx +0 -189
  130. package/src/hooks/useOnClickOutside/useOnClickOutside.tsx +0 -44
  131. package/src/hooks/useOnUnmount/index.tsx +0 -1
  132. package/src/hooks/useOnUnmount/useOnUnmount.test.tsx +0 -37
  133. package/src/hooks/useOnUnmount/useOnUnmount.tsx +0 -8
  134. package/src/hooks/usePrefersReducedMotion/index.tsx +0 -1
  135. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.test.tsx +0 -48
  136. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.tsx +0 -22
  137. package/src/hooks/useRenderCount/index.ts +0 -1
  138. package/src/hooks/useRenderCount/useRenderCount.test.ts +0 -26
  139. package/src/hooks/useRenderCount/useRenderCount.ts +0 -9
  140. package/src/hooks/useWindowEvent/index.tsx +0 -1
  141. package/src/hooks/useWindowEvent/useWindowEvent.test.tsx +0 -188
  142. package/src/hooks/useWindowEvent/useWindowEvent.tsx +0 -41
  143. package/src/utils/jestMockMatchMedia.ts +0 -16
@@ -59,7 +59,7 @@ describe("Slider helpers", () => {
59
59
 
60
60
  describe("useUnwrappedHandler", () => {
61
61
  it("should call the handler with the unwrapped value", () => {
62
- const handler = jest.fn();
62
+ const handler = vi.fn();
63
63
  const { result } = renderHook(() => useUnwrappedHandler(handler));
64
64
  result.current([1]);
65
65
  expect(handler).toHaveBeenCalledWith(1);
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import type { StackProps } from ".";
3
3
  import { Stack } from ".";
4
4
  import { excludeControls } from "../../utils";
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
3
  import { Switch as Control, type SwitchProps } from ".";
4
4
  import { excludeControls } from "../../utils";
5
5
 
@@ -2,7 +2,7 @@ import { render, screen, fireEvent } from "@testing-library/react";
2
2
  import { Switch } from ".";
3
3
 
4
4
  describe("Switch", () => {
5
- const mockOnChange = jest.fn();
5
+ const mockOnChange = vi.fn();
6
6
 
7
7
  const props = {
8
8
  checked: false,
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import type { TableProps } from ".";
3
3
  import { Table } from ".";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { excludeControls } from "../../utils";
3
3
  import { Box } from "../Box";
4
4
  import type { TextProps } from "./Text";
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { excludeControls } from "../../utils";
3
3
  import { StoryContainer } from "../../utils/StoryContainer";
4
4
  import type { TextAreaProps } from "./TextArea";
@@ -11,7 +11,7 @@ describe("TextArea", () => {
11
11
  });
12
12
 
13
13
  it("should call onChange with the updated value when the user types", async () => {
14
- const callback = jest.fn();
14
+ const callback = vi.fn();
15
15
  const { getByLabelText } = render(
16
16
  <TextArea label="First name" onChange={callback} />,
17
17
  );
@@ -24,8 +24,8 @@ describe("TextArea", () => {
24
24
  });
25
25
 
26
26
  it("focuses correctly via keyboard", async () => {
27
- const callbackFirstName = jest.fn();
28
- const callbackLastName = jest.fn();
27
+ const callbackFirstName = vi.fn();
28
+ const callbackLastName = vi.fn();
29
29
  render(
30
30
  <>
31
31
  <TextArea label="First name" onChange={callbackFirstName} />
@@ -1,5 +1,5 @@
1
1
  import { search } from "@simplybusiness/icons";
2
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
3
  import { useRef } from "react";
4
4
  import { excludeControls } from "../../utils";
5
5
  import { StoryContainer } from "../../utils/StoryContainer";
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { excludeControls } from "../../utils";
3
3
  import { Box } from "../Box";
4
4
  import type { TextOrHTMLProps } from "./TextOrHTML";
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import type { TitleProps } from "./Title";
3
3
  import { Title } from "./Title";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { useEffect } from "react";
3
3
  import { Button } from "../Button";
4
4
  import { Flex } from "../Flex";
@@ -4,8 +4,8 @@ import { Toaster, toast } from ".";
4
4
 
5
5
  // Mock setPointerCapture which JSDOM doesn't support (used by Sonner)
6
6
  beforeAll(() => {
7
- Element.prototype.setPointerCapture = jest.fn();
8
- Element.prototype.releasePointerCapture = jest.fn();
7
+ Element.prototype.setPointerCapture = vi.fn();
8
+ Element.prototype.releasePointerCapture = vi.fn();
9
9
  });
10
10
 
11
11
  describe("Toast", () => {
@@ -86,7 +86,7 @@ describe("Toast", () => {
86
86
  render(<Toaster />);
87
87
 
88
88
  toast.info("Action toast", {
89
- action: { label: "Undo", onClick: jest.fn() },
89
+ action: { label: "Undo", onClick: vi.fn() },
90
90
  });
91
91
 
92
92
  await waitFor(() => {
@@ -108,7 +108,7 @@ describe("Toast", () => {
108
108
 
109
109
  it("calls action onClick and dismisses toast when action button is clicked", async () => {
110
110
  const user = userEvent.setup();
111
- const onClickMock = jest.fn();
111
+ const onClickMock = vi.fn();
112
112
  render(<Toaster />);
113
113
 
114
114
  toast.info("Action toast", {
@@ -130,7 +130,7 @@ describe("Toast", () => {
130
130
 
131
131
  it("calls cancel onClick and dismisses toast when cancel button is clicked", async () => {
132
132
  const user = userEvent.setup();
133
- const onClickMock = jest.fn();
133
+ const onClickMock = vi.fn();
134
134
  render(<Toaster />);
135
135
 
136
136
  toast.info("Cancel toast", {
@@ -168,7 +168,7 @@ describe("Toast", () => {
168
168
 
169
169
  it("calls onDismiss callback when toast is dismissed via close button", async () => {
170
170
  const user = userEvent.setup();
171
- const onDismissMock = jest.fn();
171
+ const onDismissMock = vi.fn();
172
172
  render(<Toaster closeButton />);
173
173
 
174
174
  toast.info("Dismissable toast", { onDismiss: onDismissMock });
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { Trust, type TrustProps } from "..";
3
3
  import { excludeControls } from "../../utils";
4
4
  import { TrustpilotProvider } from "./TrustpilotProvider";
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import type { VisuallyHiddenProps } from ".";
3
3
  import { VisuallyHidden } from ".";
4
4
 
@@ -1,14 +1,7 @@
1
- export * from "./useBodyScrollLock";
2
1
  export * from "./useBreakpoint";
3
2
  export * from "./useButton";
4
- export * from "./useDebouncedValue";
5
3
  export * from "./useDialog";
6
4
  export * from "./useDialogPolyfill";
7
5
  export * from "./useLabel";
8
- export * from "./useOnClickOutside";
9
- export * from "./useOnUnmount";
10
- export * from "./usePrefersReducedMotion";
11
- export * from "./useRenderCount";
12
6
  export * from "./useTextField";
13
7
  export * from "./useValidationClasses";
14
- export * from "./useWindowEvent";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { renderToString } from "react-dom/server";
5
+ import { DEFAULT_BREAKPOINTS, useBreakpoint } from ".";
6
+
7
+ function TestComponent() {
8
+ const { breakpoint } = useBreakpoint();
9
+ return <div data-testid="breakpoint">{breakpoint.size}</div>;
10
+ }
11
+
12
+ describe("useBreakpoint SSR", () => {
13
+ it("renders the default breakpoint on the server", () => {
14
+ const view = renderToString(<TestComponent />);
15
+
16
+ expect(view).toContain(DEFAULT_BREAKPOINTS[0].size);
17
+ });
18
+ });
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react-webpack5";
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { useBreakpoint } from "./useBreakpoint";
3
3
 
4
4
  type StoryType = StoryObj<typeof useBreakpoint>;
@@ -1,4 +1,6 @@
1
- import { render, renderHook, screen } from "@testing-library/react";
1
+ import { act, render, renderHook, screen } from "@testing-library/react";
2
+ import { hydrateRoot } from "react-dom/client";
3
+ import { renderToString } from "react-dom/server";
2
4
  import type { BreakpointsType } from ".";
3
5
  import { DEFAULT_BREAKPOINTS, useBreakpoint } from ".";
4
6
 
@@ -38,6 +40,7 @@ describe("useBreakpoint", () => {
38
40
 
39
41
  describe("given no breakpoints are provided through a Context", () => {
40
42
  it("returns default breakpoint", () => {
43
+ setWindowWidth(300);
41
44
  render(<TestComponent />);
42
45
 
43
46
  expect(screen.getByText(DEFAULT_BREAKPOINTS[0].size)).toBeInTheDocument();
@@ -133,7 +136,7 @@ describe("useBreakpoint", () => {
133
136
  ] as const;
134
137
 
135
138
  describe.each(table)("given window width %s", (value, expected) => {
136
- it.each(expected)(
139
+ it.each([...expected])(
137
140
  "it returns %s for breakpoint %s",
138
141
  (expectedResult, breakpoint) => {
139
142
  setWindowWidth(value as number);
@@ -216,7 +219,7 @@ describe("useBreakpoint", () => {
216
219
  ] as const;
217
220
 
218
221
  describe.each(table)("given window width %s", (value, expected) => {
219
- it.each(expected)(
222
+ it.each([...expected])(
220
223
  "it returns %s for breakpoint %s",
221
224
  (expectedResult, breakpoint) => {
222
225
  setWindowWidth(value as number);
@@ -334,7 +337,7 @@ describe("useBreakpoint", () => {
334
337
  ] as const;
335
338
 
336
339
  describe.each(table)("given window width %s", (value, expected) => {
337
- it.each(expected)(
340
+ it.each([...expected])(
338
341
  "it returns %s for breakpoint %s",
339
342
  (expectedResult, breakpoint) => {
340
343
  setWindowWidth(value as number);
@@ -413,7 +416,7 @@ describe("useBreakpoint", () => {
413
416
  ] as const;
414
417
 
415
418
  describe.each(table)("given window width %s", (value, expected) => {
416
- it.each(expected)(
419
+ it.each([...expected])(
417
420
  "it returns %s for breakpoint %s",
418
421
  (expectedResult, breakpoint) => {
419
422
  setWindowWidth(value as number);
@@ -425,4 +428,61 @@ describe("useBreakpoint", () => {
425
428
  });
426
429
  });
427
430
  });
431
+
432
+ describe("resize after mount", () => {
433
+ it("updates the breakpoint after the debounce delay", () => {
434
+ vi.useFakeTimers();
435
+ setWindowWidth(300);
436
+
437
+ const { result } = renderHook(() => useBreakpoint());
438
+ expect(result.current.breakpoint.size).toBe("xs");
439
+
440
+ act(() => {
441
+ Reflect.set(window, "innerWidth", 1400);
442
+ window.dispatchEvent(new Event("resize"));
443
+ });
444
+
445
+ // Before debounce settles, still xs
446
+ expect(result.current.breakpoint.size).toBe("xs");
447
+
448
+ act(() => {
449
+ vi.advanceTimersByTime(200);
450
+ });
451
+
452
+ expect(result.current.breakpoint.size).toBe("xxl");
453
+ vi.useRealTimers();
454
+ });
455
+ });
456
+
457
+ describe("hydration", () => {
458
+ it("does not produce hydration mismatches", () => {
459
+ // Window is xxl — deliberately different from default xs breakpoint
460
+ setWindowWidth(1400);
461
+
462
+ const view = renderToString(<TestComponent />);
463
+ // Server should render the default (xs) regardless of window width
464
+ expect(view).toContain("xs");
465
+
466
+ const container = document.createElement("div");
467
+ const parsed = new DOMParser().parseFromString(view, "text/html");
468
+ container.append(...parsed.body.childNodes);
469
+ document.body.appendChild(container);
470
+
471
+ const errorSpy = vi.spyOn(console, "error");
472
+
473
+ act(() => {
474
+ hydrateRoot(container, <TestComponent />);
475
+ });
476
+
477
+ const hydrationErrors = errorSpy.mock.calls.filter(
478
+ ([firstArg]) =>
479
+ typeof firstArg === "string" &&
480
+ firstArg.toLowerCase().includes("hydrat"),
481
+ );
482
+ expect(hydrationErrors).toHaveLength(0);
483
+
484
+ errorSpy.mockRestore();
485
+ document.body.removeChild(container);
486
+ });
487
+ });
428
488
  });
@@ -1,7 +1,11 @@
1
1
  "use client";
2
2
 
3
- import debounce from "lodash.debounce";
4
- import { useCallback, useEffect, useState } from "react";
3
+ import {
4
+ useDebouncedValue,
5
+ useIsClient,
6
+ useWindowEvent,
7
+ } from "@simplybusiness/mobius-hooks";
8
+ import { useCallback, useMemo, useState } from "react";
5
9
  import type { SizeType } from "../../types";
6
10
 
7
11
  export type Breakpoint = {
@@ -25,6 +29,8 @@ export const DEFAULT_BREAKPOINTS = [
25
29
  { size: "xxl", value: 1320 },
26
30
  ] as BreakpointsType;
27
31
 
32
+ const PASSIVE: AddEventListenerOptions = { passive: true };
33
+
28
34
  const getBreakpoint = (breakpoints: BreakpointsType, windowWidth: number) => {
29
35
  // When breakpoint size and windowWidth are a match
30
36
  // The addition of 1px ensures the right breakpoint
@@ -44,46 +50,30 @@ const getBreakpoint = (breakpoints: BreakpointsType, windowWidth: number) => {
44
50
  const useBreakpoint = (
45
51
  customBreakpoints?: BreakpointsType,
46
52
  ): UseBreakpointType => {
47
- const window: Window = globalThis?.window || undefined;
48
- const isClientSide = typeof window !== "undefined";
53
+ const isClientSide = useIsClient();
49
54
  const breakpoints = customBreakpoints || DEFAULT_BREAKPOINTS;
50
55
  const defaultBreakpoint = breakpoints[0];
56
+
51
57
  const [windowWidth, setWindowWidth] = useState<number>(
52
- isClientSide ? window.innerWidth : defaultBreakpoint.value,
58
+ typeof globalThis?.window !== "undefined"
59
+ ? window.innerWidth
60
+ : defaultBreakpoint.value,
53
61
  );
54
- const [currentBreakpoint, setCurrentBreakpoint] =
55
- useState<Breakpoint>(defaultBreakpoint);
56
- const handleResize = () => {
57
- setWindowWidth(window.innerWidth);
58
- };
59
-
60
- useEffect(() => {
61
- if (!windowWidth || breakpoints.length === 0) {
62
- setCurrentBreakpoint(breakpoints[0]);
63
- return;
64
- }
65
62
 
66
- const newBreakpoint = getBreakpoint(breakpoints, windowWidth);
63
+ useWindowEvent("resize", () => setWindowWidth(window.innerWidth), PASSIVE);
67
64
 
68
- // Don't update state if value is the same
69
- if (currentBreakpoint.size === newBreakpoint?.size) return;
65
+ const debouncedWidth = useDebouncedValue(windowWidth, 200);
70
66
 
71
- setCurrentBreakpoint(newBreakpoint);
72
- }, [currentBreakpoint, windowWidth, breakpoints]);
73
-
74
- useEffect(() => {
75
- window.addEventListener("resize", debounce(handleResize, 200), {
76
- passive: true,
77
- });
78
-
79
- return () => window.removeEventListener("resize", handleResize);
80
- // eslint-disable-next-line react-hooks/exhaustive-deps
81
- }, [window]);
67
+ // Gate on isClientSide so up()/down() also return default-based values
68
+ // during SSR and hydration, matching the server render.
69
+ const currentBreakpoint = useMemo(
70
+ () =>
71
+ !isClientSide || breakpoints.length === 0
72
+ ? defaultBreakpoint
73
+ : getBreakpoint(breakpoints, debouncedWidth),
74
+ [isClientSide, breakpoints, defaultBreakpoint, debouncedWidth],
75
+ );
82
76
 
83
- /**
84
- * up(breakpointSize: string) => boolean
85
- * Returns true if the current screen width >= breakpoint width
86
- */
87
77
  const up = useCallback(
88
78
  (size: SizeType) => {
89
79
  const sizeIndex = breakpoints.findIndex(item => item.size === size);
@@ -99,10 +89,6 @@ const useBreakpoint = (
99
89
  [currentBreakpoint, breakpoints],
100
90
  );
101
91
 
102
- /**
103
- * down(breakpointSize: string) => boolean
104
- * Returns true if the current screen width <= breakpoint width
105
- */
106
92
  const down = useCallback(
107
93
  (size: SizeType) => {
108
94
  const sizeIndex = breakpoints.findIndex(item => item.size === size);
@@ -119,7 +105,7 @@ const useBreakpoint = (
119
105
  );
120
106
 
121
107
  return {
122
- breakpoint: isClientSide ? currentBreakpoint : defaultBreakpoint,
108
+ breakpoint: currentBreakpoint,
123
109
  up,
124
110
  down,
125
111
  };
@@ -144,14 +144,14 @@ describe("useButton", () => {
144
144
 
145
145
  describe("onClick", () => {
146
146
  it("should call onClick when buttonProps onClick is called", () => {
147
- const onClick = jest.fn();
147
+ const onClick = vi.fn();
148
148
  const { result } = renderHook(() => useButton({ onClick }));
149
149
  result.current.buttonProps.onClick(mockClickEvent);
150
150
  expect(onClick).toHaveBeenCalled();
151
151
  });
152
152
 
153
153
  it("should not call onClick when buttonProps onClick is called and isDisabled is true", () => {
154
- const onClick = jest.fn();
154
+ const onClick = vi.fn();
155
155
  const { result } = renderHook(() =>
156
156
  useButton({ onClick, isDisabled: true }),
157
157
  );
@@ -162,14 +162,14 @@ describe("useButton", () => {
162
162
 
163
163
  describe("onPress", () => {
164
164
  it("should call onPress when buttonProps onClick is called", () => {
165
- const onPress = jest.fn();
165
+ const onPress = vi.fn();
166
166
  const { result } = renderHook(() => useButton({ onPress }));
167
167
  result.current.buttonProps.onClick(mockClickEvent);
168
168
  expect(onPress).toHaveBeenCalled();
169
169
  });
170
170
 
171
171
  it("should not call onPress when buttonProps onClick is called and isDisabled is true", () => {
172
- const onPress = jest.fn();
172
+ const onPress = vi.fn();
173
173
  const { result } = renderHook(() =>
174
174
  useButton({ onPress, isDisabled: true }),
175
175
  );
@@ -1,7 +1,7 @@
1
1
  import type { MutableRefObject, SyntheticEvent } from "react";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import { supportsDialog } from "../../utils";
4
- import { useBodyScrollLock } from "../useBodyScrollLock";
4
+ import { useBodyScrollLock } from "@simplybusiness/mobius-hooks";
5
5
  import { useDialogPolyfill } from "../useDialogPolyfill";
6
6
 
7
7
  export type TransitionProps = {
@@ -7,7 +7,7 @@ describe("useLabel", () => {
7
7
  });
8
8
 
9
9
  it("should warn if no label or aria-label or aria-labelledby is provided", () => {
10
- jest.spyOn(console, "warn").mockImplementation(() => {});
10
+ vi.spyOn(console, "warn").mockImplementation(() => {});
11
11
  renderHook(() => useLabel({}));
12
12
  expect(console.warn).toHaveBeenCalled();
13
13
  });
@@ -45,7 +45,7 @@ describe("useTextField", () => {
45
45
 
46
46
  describe("controlled value", () => {
47
47
  it("should set the value to the value prop", () => {
48
- const handleChange = jest.fn();
48
+ const handleChange = vi.fn();
49
49
  render(
50
50
  <WrapperComponent
51
51
  label={TEST_LABEL}
@@ -57,7 +57,7 @@ describe("useTextField", () => {
57
57
  });
58
58
 
59
59
  it("should call onChange with event", async () => {
60
- const handleChange = jest.fn();
60
+ const handleChange = vi.fn();
61
61
  render(
62
62
  <WrapperComponent
63
63
  label={TEST_LABEL}
@@ -83,7 +83,7 @@ describe("useTextField", () => {
83
83
  });
84
84
 
85
85
  it("should not call onChange when isDisabled is true", async () => {
86
- const handleChange = jest.fn();
86
+ const handleChange = vi.fn();
87
87
  render(
88
88
  <WrapperComponent
89
89
  label={TEST_LABEL}
@@ -105,7 +105,7 @@ describe("useTextField", () => {
105
105
  });
106
106
 
107
107
  it("should not call onChange", async () => {
108
- const handleChange = jest.fn();
108
+ const handleChange = vi.fn();
109
109
  render(
110
110
  <WrapperComponent
111
111
  label={TEST_LABEL}
@@ -8,6 +8,7 @@ const exec = promisify(_exec);
8
8
 
9
9
  // Publishing packages to the public registry requires approval from appsec
10
10
  const PUBLIC_PACKAGE_WHITELIST = [
11
+ "@simplybusiness/mobius-hooks",
11
12
  "@simplybusiness/icons",
12
13
  "@simplybusiness/mobius-datepicker",
13
14
  "@simplybusiness/mobius",
@@ -2,16 +2,16 @@ import { delay } from "./delay";
2
2
 
3
3
  describe("delay", () => {
4
4
  beforeEach(() => {
5
- jest.useFakeTimers();
5
+ vi.useFakeTimers();
6
6
  });
7
7
 
8
8
  afterEach(() => {
9
- jest.useRealTimers();
9
+ vi.useRealTimers();
10
10
  });
11
11
 
12
12
  it("should resolve after specified delay", async () => {
13
13
  const promise = delay(1000);
14
- jest.advanceTimersByTime(1000);
14
+ vi.advanceTimersByTime(1000);
15
15
  await promise;
16
16
  });
17
17
 
@@ -27,7 +27,7 @@ describe("delay", () => {
27
27
 
28
28
  it("should not resolve before the specified time", async () => {
29
29
  const promise = delay(1000);
30
- jest.advanceTimersByTime(500);
30
+ vi.advanceTimersByTime(500);
31
31
 
32
32
  const resolved = await Promise.race([promise, Promise.resolve("early")]);
33
33
 
@@ -1,21 +1,21 @@
1
1
  // Workaround for lack of `<dialog>` support in jsdom
2
2
  // Workaround from: https://github.com/jsdom/jsdom/issues/3294#issuecomment-1268330372
3
3
  // Fix: https://github.com/jsdom/jsdom/pull/3403
4
- // This can be removed once `<dialog>` support is introduced in Jest via jsdom
5
- export const jestHTMLDialogPolyfill = () => {
6
- HTMLDialogElement.prototype.show = jest.fn(function mock(
4
+ // This can be removed once `<dialog>` support is introduced in jsdom
5
+ export const htmlDialogPolyfill = () => {
6
+ HTMLDialogElement.prototype.show = vi.fn(function mock(
7
7
  this: HTMLDialogElement,
8
8
  ) {
9
9
  this.open = true;
10
10
  });
11
11
 
12
- HTMLDialogElement.prototype.showModal = jest.fn(function mock(
12
+ HTMLDialogElement.prototype.showModal = vi.fn(function mock(
13
13
  this: HTMLDialogElement,
14
14
  ) {
15
15
  this.open = true;
16
16
  });
17
17
 
18
- HTMLDialogElement.prototype.close = jest.fn(function mock(
18
+ HTMLDialogElement.prototype.close = vi.fn(function mock(
19
19
  this: HTMLDialogElement,
20
20
  ) {
21
21
  this.open = false;
@@ -3,7 +3,6 @@ export * from "./delay";
3
3
  export * from "./excludeControls";
4
4
  export * from "./filterUndefinedProps";
5
5
  export * from "./getSpacingValue";
6
- export * from "./jestHTMLDialogPolyfill";
7
6
  export * from "./mergeRefs";
8
7
  export * from "./polyfill-tests";
9
8
  export * from "./sizeClasses";
@@ -0,0 +1,16 @@
1
+ export const mockMatchMedia = (matches: boolean) => {
2
+ Object.defineProperty(window, "matchMedia", {
3
+ writable: true,
4
+ configurable: true,
5
+ value: vi.fn().mockImplementation(query => ({
6
+ matches,
7
+ media: query,
8
+ onchange: null,
9
+ addListener: vi.fn(),
10
+ removeListener: vi.fn(),
11
+ addEventListener: vi.fn(),
12
+ removeEventListener: vi.fn(),
13
+ dispatchEvent: vi.fn(),
14
+ })),
15
+ });
16
+ };
@@ -1 +0,0 @@
1
- export * from "./useBodyScrollLock";
@@ -1,3 +0,0 @@
1
- export declare function useBodyScrollLock({ enabled, }?: {
2
- enabled?: boolean;
3
- }): void;
@@ -1 +0,0 @@
1
- export * from "./useDebouncedValue";
@@ -1 +0,0 @@
1
- export declare function useDebouncedValue<T>(value: T, delay?: number): T;
@@ -1 +0,0 @@
1
- export * from "./useOnClickOutside";
@@ -1,2 +0,0 @@
1
- import type { RefObject } from "react";
2
- export declare const useOnClickOutside: (ref: RefObject<HTMLElement | null>, handler: (event: MouseEvent | TouchEvent | KeyboardEvent) => void) => void;
@@ -1 +0,0 @@
1
- export * from "./useOnUnmount";
@@ -1 +0,0 @@
1
- export declare function useOnUnmount(callback: () => void): void;
@@ -1 +0,0 @@
1
- export * from "./usePrefersReducedMotion";
@@ -1 +0,0 @@
1
- export declare function usePrefersReducedMotion(): boolean;
@@ -1 +0,0 @@
1
- export * from "./useRenderCount";