@simplybusiness/mobius 8.0.2 → 9.0.1
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 +46 -0
- package/dist/cjs/index.js +577 -733
- package/dist/cjs/index.js.map +4 -4
- package/dist/cjs/meta.json +295 -520
- package/dist/esm/index.js +542 -694
- package/dist/esm/index.js.map +4 -4
- package/dist/esm/meta.json +300 -532
- package/dist/esm/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/hooks/index.d.ts +0 -7
- package/dist/types/src/utils/htmlDialogPolyfill.d.ts +1 -0
- package/dist/types/src/utils/index.d.ts +0 -1
- package/dist/types/src/utils/mockMatchMedia.d.ts +1 -0
- package/dist/types/vitest.config.d.ts +2 -0
- package/package.json +18 -24
- package/src/components/Accordion/Accordion.stories.tsx +1 -1
- package/src/components/Accordion/Accordion.test.tsx +12 -12
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/Accordion/AccordionList.stories.tsx +1 -1
- package/src/components/Accordion/AccordionList.test.tsx +6 -6
- package/src/components/AddressLookup/AddressLookup.stories.tsx +1 -1
- package/src/components/AddressLookup/AddressLookup.test.tsx +19 -20
- package/src/components/AddressLookup/LoqateAddressLookupService.test.tsx +7 -6
- package/src/components/Alert/Alert.stories.tsx +1 -1
- package/src/components/Box/Box.stories.tsx +1 -1
- package/src/components/Breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/Button/Button.stories.tsx +3 -4
- package/src/components/Button/Button.test.tsx +4 -4
- package/src/components/Checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/Checkbox/Checkbox.test.tsx +2 -2
- package/src/components/Checkbox/CheckboxGroup.stories.tsx +1 -1
- package/src/components/Checkbox/CheckboxGroup.test.tsx +5 -5
- package/src/components/Combobox/Combobox.stories.tsx +1 -1
- package/src/components/Combobox/Combobox.test.tsx +67 -78
- package/src/components/Combobox/Combobox.tsx +2 -1
- package/src/components/Combobox/useComboboxOptions.test.ts +30 -30
- package/src/components/Combobox/useComboboxOptions.ts +1 -1
- package/src/components/Container/Container.stories.tsx +1 -1
- package/src/components/DateField/DateField.stories.tsx +1 -1
- package/src/components/DateField/DateField.test.tsx +1 -1
- package/src/components/Divider/Divider.stories.tsx +1 -1
- package/src/components/Drawer/Drawer.stories.tsx +1 -1
- package/src/components/Drawer/Drawer.test.tsx +6 -6
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -10
- package/src/components/DropdownMenu/DropdownMenu.test.tsx +1 -1
- package/src/components/ErrorMessage/ErrorMessage.stories.tsx +1 -1
- package/src/components/ExpandableText/ExpandableText.test.tsx +14 -14
- package/src/components/Fieldset/Fieldset.stories.tsx +1 -1
- package/src/components/Flex/Flex.stories.tsx +1 -1
- package/src/components/Grid/Grid.stories.tsx +4 -7
- package/src/components/Icon/Icon.stories.tsx +1 -1
- package/src/components/Image/Image.stories.tsx +1 -1
- package/src/components/Label/Label.stories.tsx +1 -1
- package/src/components/Link/Link.stories.tsx +1 -1
- package/src/components/Link/Link.test.tsx +1 -1
- package/src/components/LinkButton/LinkButton.stories.tsx +1 -1
- package/src/components/LinkButton/LinkButton.test.tsx +2 -2
- package/src/components/List/List.stories.tsx +1 -1
- package/src/components/LoadingIndicator/LoadingIndicator.stories.tsx +1 -1
- package/src/components/Logo/Logo.stories.tsx +1 -1
- package/src/components/Modal/Modal.stories.tsx +1 -1
- package/src/components/Modal/Modal.test.tsx +6 -6
- package/src/components/NumberField/NumberField.stories.tsx +1 -1
- package/src/components/NumberField/NumberField.test.tsx +5 -5
- package/src/components/PasswordField/PasswordField.stories.tsx +1 -1
- package/src/components/Popover/Popover.stories.tsx +4 -8
- package/src/components/Popover/Popover.test.tsx +4 -4
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/Progress/Progress.stories.tsx +1 -1
- package/src/components/Radio/Radio.stories.tsx +1 -1
- package/src/components/Radio/Radio.test.tsx +9 -9
- package/src/components/SVG/SVG.stories.tsx +1 -1
- package/src/components/Segment/Segment.stories.tsx +1 -1
- package/src/components/Select/Select.stories.tsx +1 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Slider/Slider.stories.tsx +1 -1
- package/src/components/Slider/Slider.test.tsx +6 -6
- package/src/components/Slider/helpers.test.ts +1 -1
- package/src/components/Stack/Stack.stories.tsx +1 -1
- package/src/components/Switch/Switch.stories.tsx +1 -1
- package/src/components/Switch/Switch.test.tsx +1 -1
- package/src/components/Table/Table.stories.tsx +1 -1
- package/src/components/Text/Text.stories.tsx +1 -1
- package/src/components/TextArea/TextArea.stories.tsx +1 -1
- package/src/components/TextArea/TextArea.test.tsx +3 -3
- package/src/components/TextField/TextField.stories.tsx +1 -1
- package/src/components/TextOrHTML/TextOrHTML.stories.tsx +1 -1
- package/src/components/Title/Title.stories.tsx +1 -1
- package/src/components/Toast/Toast.stories.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +6 -6
- package/src/components/Trust/Trust.stories.tsx +1 -1
- package/src/components/VisuallyHidden/VisuallyHidden.stories.tsx +1 -1
- package/src/hooks/index.tsx +0 -7
- package/src/hooks/useBreakpoint/useBreakpoint.ssr.test.tsx +18 -0
- package/src/hooks/useBreakpoint/useBreakpoint.stories.tsx +1 -1
- package/src/hooks/useBreakpoint/useBreakpoint.test.tsx +57 -5
- package/src/hooks/useBreakpoint/useBreakpoint.tsx +25 -39
- package/src/hooks/useButton/useButton.test.tsx +4 -4
- package/src/hooks/useDialog/useDialog.ts +1 -1
- package/src/hooks/useLabel/useLabel.test.tsx +1 -1
- package/src/hooks/useTextField/useTextField.test.tsx +4 -4
- package/src/public-whitelist.test.ts +1 -0
- package/src/utils/delay.test.ts +4 -4
- package/src/utils/{jestHTMLDialogPolyfill.ts → htmlDialogPolyfill.ts} +5 -5
- package/src/utils/index.ts +0 -1
- package/src/utils/mockMatchMedia.ts +16 -0
- package/dist/types/src/hooks/useBodyScrollLock/index.d.ts +0 -1
- package/dist/types/src/hooks/useBodyScrollLock/useBodyScrollLock.d.ts +0 -3
- package/dist/types/src/hooks/useDebouncedValue/index.d.ts +0 -1
- package/dist/types/src/hooks/useDebouncedValue/useDebouncedValue.d.ts +0 -1
- package/dist/types/src/hooks/useOnClickOutside/index.d.ts +0 -1
- package/dist/types/src/hooks/useOnClickOutside/useOnClickOutside.d.ts +0 -2
- package/dist/types/src/hooks/useOnUnmount/index.d.ts +0 -1
- package/dist/types/src/hooks/useOnUnmount/useOnUnmount.d.ts +0 -1
- package/dist/types/src/hooks/usePrefersReducedMotion/index.d.ts +0 -1
- package/dist/types/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.d.ts +0 -1
- package/dist/types/src/hooks/useRenderCount/index.d.ts +0 -1
- package/dist/types/src/hooks/useRenderCount/useRenderCount.d.ts +0 -1
- package/dist/types/src/hooks/useWindowEvent/index.d.ts +0 -1
- package/dist/types/src/hooks/useWindowEvent/useWindowEvent.d.ts +0 -1
- package/dist/types/src/utils/jestHTMLDialogPolyfill.d.ts +0 -1
- package/dist/types/src/utils/jestMockMatchMedia.d.ts +0 -1
- package/src/hooks/useBodyScrollLock/index.ts +0 -1
- package/src/hooks/useBodyScrollLock/useBodyScrollLock.test.ts +0 -34
- package/src/hooks/useBodyScrollLock/useBodyScrollLock.ts +0 -30
- package/src/hooks/useDebouncedValue/index.tsx +0 -1
- package/src/hooks/useDebouncedValue/useDebouncedValue.test.tsx +0 -62
- package/src/hooks/useDebouncedValue/useDebouncedValue.tsx +0 -25
- package/src/hooks/useOnClickOutside/index.tsx +0 -1
- package/src/hooks/useOnClickOutside/useOnClickOutside.test.tsx +0 -189
- package/src/hooks/useOnClickOutside/useOnClickOutside.tsx +0 -44
- package/src/hooks/useOnUnmount/index.tsx +0 -1
- package/src/hooks/useOnUnmount/useOnUnmount.test.tsx +0 -37
- package/src/hooks/useOnUnmount/useOnUnmount.tsx +0 -8
- package/src/hooks/usePrefersReducedMotion/index.tsx +0 -1
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.test.tsx +0 -48
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.tsx +0 -22
- package/src/hooks/useRenderCount/index.ts +0 -1
- package/src/hooks/useRenderCount/useRenderCount.test.ts +0 -26
- package/src/hooks/useRenderCount/useRenderCount.ts +0 -9
- package/src/hooks/useWindowEvent/index.tsx +0 -1
- package/src/hooks/useWindowEvent/useWindowEvent.test.tsx +0 -188
- package/src/hooks/useWindowEvent/useWindowEvent.tsx +0 -41
- package/src/utils/jestMockMatchMedia.ts +0 -16
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useWindowEvent";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function useWindowEvent<EventType extends keyof WindowEventMap>(type: EventType, listener: (this: Window, ev: WindowEventMap[EventType]) => unknown, options?: AddEventListenerOptions): void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const jestHTMLDialogPolyfill: () => void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const jestMockMatchMedia: (matches: boolean) => void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useBodyScrollLock";
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { renderHook } from "@testing-library/react";
|
|
2
|
-
import { useBodyScrollLock } from "./useBodyScrollLock";
|
|
3
|
-
|
|
4
|
-
describe("useBodyScrollLock", () => {
|
|
5
|
-
it("should be defined", () => {
|
|
6
|
-
expect(useBodyScrollLock).toBeDefined();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("should not throw", () => {
|
|
10
|
-
expect(() => renderHook(() => useBodyScrollLock())).not.toThrow();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("should disable scroll by default", () => {
|
|
14
|
-
renderHook(() => useBodyScrollLock());
|
|
15
|
-
expect(document.body).toHaveStyle({ overflow: "hidden" });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("should disable scroll when enabled is true", () => {
|
|
19
|
-
renderHook(() => useBodyScrollLock({ enabled: true }));
|
|
20
|
-
expect(document.body).toHaveStyle({ overflow: "hidden" });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("should enable scroll when enabled is false", () => {
|
|
24
|
-
renderHook(() => useBodyScrollLock({ enabled: false }));
|
|
25
|
-
expect(document.body).toHaveStyle({ overflow: "" });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("should enable scroll on unmount", () => {
|
|
29
|
-
const { unmount } = renderHook(() => useBodyScrollLock());
|
|
30
|
-
expect(document.body).toHaveStyle({ overflow: "hidden" });
|
|
31
|
-
unmount();
|
|
32
|
-
expect(document.body).toHaveStyle({ overflow: "" });
|
|
33
|
-
});
|
|
34
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
export function useBodyScrollLock({
|
|
4
|
-
enabled = true,
|
|
5
|
-
}: { enabled?: boolean } = {}) {
|
|
6
|
-
function disableScrollLock() {
|
|
7
|
-
document.body.style.removeProperty("overflow");
|
|
8
|
-
document.body.style.removeProperty("scrollbar-gutter");
|
|
9
|
-
document.documentElement.style.removeProperty("scrollbar-gutter");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function enableScrollLock() {
|
|
13
|
-
document.body.style.overflow = "hidden";
|
|
14
|
-
// Prevent content jumping due to scrollbar disappearing
|
|
15
|
-
document.body.style.scrollbarGutter = "stable";
|
|
16
|
-
document.documentElement.style.scrollbarGutter = "stable";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (enabled) {
|
|
21
|
-
enableScrollLock();
|
|
22
|
-
} else {
|
|
23
|
-
disableScrollLock();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return () => {
|
|
27
|
-
disableScrollLock();
|
|
28
|
-
};
|
|
29
|
-
}, [enabled]);
|
|
30
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useDebouncedValue";
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { renderHook, act } from "@testing-library/react";
|
|
2
|
-
import { useDebouncedValue } from "./useDebouncedValue";
|
|
3
|
-
|
|
4
|
-
describe("useDebouncedValue", () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
jest.useFakeTimers();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
jest.useRealTimers();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("returns the initial value immediately when delay is 0", () => {
|
|
14
|
-
const { result } = renderHook(() => useDebouncedValue("initial", 0));
|
|
15
|
-
expect(result.current).toBe("initial");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("returns the previous value until delay has elapsed", () => {
|
|
19
|
-
const { result, rerender } = renderHook(
|
|
20
|
-
({ value }) => useDebouncedValue(value, 1000),
|
|
21
|
-
{ initialProps: { value: "initial" } },
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
expect(result.current).toBe("initial");
|
|
25
|
-
|
|
26
|
-
rerender({ value: "updated" });
|
|
27
|
-
expect(result.current).toBe("initial");
|
|
28
|
-
|
|
29
|
-
act(() => {
|
|
30
|
-
jest.advanceTimersByTime(999);
|
|
31
|
-
});
|
|
32
|
-
expect(result.current).toBe("initial");
|
|
33
|
-
|
|
34
|
-
act(() => {
|
|
35
|
-
jest.advanceTimersByTime(1);
|
|
36
|
-
});
|
|
37
|
-
expect(result.current).toBe("updated");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("cancels previous timeout when value changes", () => {
|
|
41
|
-
const { result, rerender } = renderHook(
|
|
42
|
-
({ value }) => useDebouncedValue(value, 1000),
|
|
43
|
-
{ initialProps: { value: "initial" } },
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
rerender({ value: "updated1" });
|
|
47
|
-
act(() => {
|
|
48
|
-
jest.advanceTimersByTime(500);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
rerender({ value: "updated2" });
|
|
52
|
-
act(() => {
|
|
53
|
-
jest.advanceTimersByTime(500);
|
|
54
|
-
});
|
|
55
|
-
expect(result.current).toBe("initial");
|
|
56
|
-
|
|
57
|
-
act(() => {
|
|
58
|
-
jest.advanceTimersByTime(500);
|
|
59
|
-
});
|
|
60
|
-
expect(result.current).toBe("updated2");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
export function useDebouncedValue<T>(value: T, delay: number = 0): T {
|
|
4
|
-
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
5
|
-
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
let timeout: NodeJS.Timeout | null = null;
|
|
8
|
-
if (delay === 0) {
|
|
9
|
-
setDebouncedValue(value);
|
|
10
|
-
} else {
|
|
11
|
-
timeout = setTimeout(() => {
|
|
12
|
-
setDebouncedValue(value);
|
|
13
|
-
}, delay);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return () => {
|
|
17
|
-
if (timeout) {
|
|
18
|
-
clearTimeout(timeout);
|
|
19
|
-
timeout = null;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
}, [value, delay]);
|
|
23
|
-
|
|
24
|
-
return debouncedValue;
|
|
25
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useOnClickOutside";
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { renderHook } from "@testing-library/react";
|
|
2
|
-
import { useRef } from "react";
|
|
3
|
-
import { useOnClickOutside } from "./useOnClickOutside";
|
|
4
|
-
|
|
5
|
-
describe("useOnClickOutside", () => {
|
|
6
|
-
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
7
|
-
const removeEventListenerSpy = jest.spyOn(document, "removeEventListener");
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
addEventListenerSpy.mockClear();
|
|
11
|
-
removeEventListenerSpy.mockClear();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterAll(() => {
|
|
15
|
-
addEventListenerSpy.mockRestore();
|
|
16
|
-
removeEventListenerSpy.mockRestore();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("adds event listeners on mount", () => {
|
|
20
|
-
const handler = jest.fn();
|
|
21
|
-
const { result } = renderHook(() => {
|
|
22
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
23
|
-
useOnClickOutside(ref, handler);
|
|
24
|
-
return ref;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
expect(result.current).toBeDefined();
|
|
28
|
-
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
29
|
-
"mousedown",
|
|
30
|
-
expect.any(Function),
|
|
31
|
-
);
|
|
32
|
-
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
33
|
-
"touchstart",
|
|
34
|
-
expect.any(Function),
|
|
35
|
-
);
|
|
36
|
-
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
37
|
-
"keydown",
|
|
38
|
-
expect.any(Function),
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("removes event listeners on unmount", () => {
|
|
43
|
-
const handler = jest.fn();
|
|
44
|
-
const { unmount } = renderHook(() => {
|
|
45
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
46
|
-
useOnClickOutside(ref, handler);
|
|
47
|
-
return ref;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
removeEventListenerSpy.mockClear();
|
|
51
|
-
unmount();
|
|
52
|
-
|
|
53
|
-
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
54
|
-
"mousedown",
|
|
55
|
-
expect.any(Function),
|
|
56
|
-
);
|
|
57
|
-
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
58
|
-
"touchstart",
|
|
59
|
-
expect.any(Function),
|
|
60
|
-
);
|
|
61
|
-
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
62
|
-
"keydown",
|
|
63
|
-
expect.any(Function),
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("calls handler when clicking outside the ref element", () => {
|
|
68
|
-
const handler = jest.fn();
|
|
69
|
-
const div = document.createElement("div");
|
|
70
|
-
document.body.appendChild(div);
|
|
71
|
-
|
|
72
|
-
renderHook(() => {
|
|
73
|
-
const ref = useRef<HTMLDivElement>(div);
|
|
74
|
-
useOnClickOutside(ref, handler);
|
|
75
|
-
return ref;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Click outside the element
|
|
79
|
-
const outsideElement = document.createElement("span");
|
|
80
|
-
document.body.appendChild(outsideElement);
|
|
81
|
-
|
|
82
|
-
const event = new MouseEvent("mousedown", { bubbles: true });
|
|
83
|
-
outsideElement.dispatchEvent(event);
|
|
84
|
-
|
|
85
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
86
|
-
|
|
87
|
-
document.body.removeChild(div);
|
|
88
|
-
document.body.removeChild(outsideElement);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("does not call handler when clicking inside the ref element", () => {
|
|
92
|
-
const handler = jest.fn();
|
|
93
|
-
const div = document.createElement("div");
|
|
94
|
-
const child = document.createElement("span");
|
|
95
|
-
div.appendChild(child);
|
|
96
|
-
document.body.appendChild(div);
|
|
97
|
-
|
|
98
|
-
renderHook(() => {
|
|
99
|
-
const ref = useRef<HTMLDivElement>(div);
|
|
100
|
-
useOnClickOutside(ref, handler);
|
|
101
|
-
return ref;
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const event = new MouseEvent("mousedown", { bubbles: true });
|
|
105
|
-
child.dispatchEvent(event);
|
|
106
|
-
|
|
107
|
-
expect(handler).not.toHaveBeenCalled();
|
|
108
|
-
|
|
109
|
-
document.body.removeChild(div);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("calls handler when Escape key is pressed", () => {
|
|
113
|
-
const handler = jest.fn();
|
|
114
|
-
const div = document.createElement("div");
|
|
115
|
-
document.body.appendChild(div);
|
|
116
|
-
|
|
117
|
-
renderHook(() => {
|
|
118
|
-
const ref = useRef<HTMLDivElement>(div);
|
|
119
|
-
useOnClickOutside(ref, handler);
|
|
120
|
-
return ref;
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const event = new KeyboardEvent("keydown", {
|
|
124
|
-
key: "Escape",
|
|
125
|
-
bubbles: true,
|
|
126
|
-
});
|
|
127
|
-
document.dispatchEvent(event);
|
|
128
|
-
|
|
129
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
130
|
-
|
|
131
|
-
document.body.removeChild(div);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("does not call handler for non-Escape key presses", () => {
|
|
135
|
-
const handler = jest.fn();
|
|
136
|
-
const div = document.createElement("div");
|
|
137
|
-
document.body.appendChild(div);
|
|
138
|
-
|
|
139
|
-
renderHook(() => {
|
|
140
|
-
const ref = useRef<HTMLDivElement>(div);
|
|
141
|
-
useOnClickOutside(ref, handler);
|
|
142
|
-
return ref;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const event = new KeyboardEvent("keydown", { key: "Enter", bubbles: true });
|
|
146
|
-
document.dispatchEvent(event);
|
|
147
|
-
|
|
148
|
-
expect(handler).not.toHaveBeenCalled();
|
|
149
|
-
|
|
150
|
-
document.body.removeChild(div);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("uses the latest handler without re-subscribing", () => {
|
|
154
|
-
const handler1 = jest.fn();
|
|
155
|
-
const handler2 = jest.fn();
|
|
156
|
-
const div = document.createElement("div");
|
|
157
|
-
document.body.appendChild(div);
|
|
158
|
-
|
|
159
|
-
const { rerender } = renderHook(
|
|
160
|
-
({ handler }) => {
|
|
161
|
-
const ref = useRef<HTMLDivElement>(div);
|
|
162
|
-
useOnClickOutside(ref, handler);
|
|
163
|
-
return ref;
|
|
164
|
-
},
|
|
165
|
-
{ initialProps: { handler: handler1 } },
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// Clear the spy to only count new subscriptions
|
|
169
|
-
addEventListenerSpy.mockClear();
|
|
170
|
-
|
|
171
|
-
// Rerender with new handler
|
|
172
|
-
rerender({ handler: handler2 });
|
|
173
|
-
|
|
174
|
-
// Should not re-subscribe (useEffectEvent handles this)
|
|
175
|
-
expect(addEventListenerSpy).not.toHaveBeenCalled();
|
|
176
|
-
|
|
177
|
-
// Trigger event - should call the new handler
|
|
178
|
-
const event = new KeyboardEvent("keydown", {
|
|
179
|
-
key: "Escape",
|
|
180
|
-
bubbles: true,
|
|
181
|
-
});
|
|
182
|
-
document.dispatchEvent(event);
|
|
183
|
-
|
|
184
|
-
expect(handler1).not.toHaveBeenCalled();
|
|
185
|
-
expect(handler2).toHaveBeenCalledTimes(1);
|
|
186
|
-
|
|
187
|
-
document.body.removeChild(div);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { RefObject } from "react";
|
|
4
|
-
import { useEffect, useEffectEvent } from "react";
|
|
5
|
-
|
|
6
|
-
export const useOnClickOutside = (
|
|
7
|
-
ref: RefObject<HTMLElement | null>,
|
|
8
|
-
handler: (event: MouseEvent | TouchEvent | KeyboardEvent) => void,
|
|
9
|
-
) => {
|
|
10
|
-
// useEffectEvent creates a stable reference to the handler
|
|
11
|
-
// that always calls the latest version
|
|
12
|
-
const stableHandler = useEffectEvent(handler);
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
const listener = (event: MouseEvent | TouchEvent) => {
|
|
16
|
-
// Do nothing if clicking ref's element or descendent elements
|
|
17
|
-
if (!ref.current || ref.current.contains(event.target as HTMLElement)) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
event.preventDefault();
|
|
22
|
-
event.stopPropagation();
|
|
23
|
-
stableHandler(event);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const handleEscapeKeyPress = (event: KeyboardEvent) => {
|
|
27
|
-
if (event.key === "Escape") {
|
|
28
|
-
event.preventDefault();
|
|
29
|
-
event.stopPropagation();
|
|
30
|
-
stableHandler(event);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
document.addEventListener("mousedown", listener);
|
|
35
|
-
document.addEventListener("touchstart", listener);
|
|
36
|
-
document.addEventListener("keydown", handleEscapeKeyPress);
|
|
37
|
-
|
|
38
|
-
return () => {
|
|
39
|
-
document.removeEventListener("mousedown", listener);
|
|
40
|
-
document.removeEventListener("touchstart", listener);
|
|
41
|
-
document.removeEventListener("keydown", handleEscapeKeyPress);
|
|
42
|
-
};
|
|
43
|
-
}, [ref]);
|
|
44
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useOnUnmount";
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { renderHook } from "@testing-library/react";
|
|
2
|
-
import { useOnUnmount } from "./useOnUnmount";
|
|
3
|
-
|
|
4
|
-
describe("useOnUnmount", () => {
|
|
5
|
-
it("should call the callback on unmount", () => {
|
|
6
|
-
const callback = jest.fn();
|
|
7
|
-
const { unmount } = renderHook(() => useOnUnmount(callback));
|
|
8
|
-
|
|
9
|
-
expect(callback).not.toHaveBeenCalled();
|
|
10
|
-
|
|
11
|
-
unmount();
|
|
12
|
-
|
|
13
|
-
expect(callback).toHaveBeenCalled();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("should only use the latest callback", () => {
|
|
17
|
-
const firstCallback = jest.fn();
|
|
18
|
-
const secondCallback = jest.fn();
|
|
19
|
-
|
|
20
|
-
const { rerender, unmount } = renderHook(
|
|
21
|
-
({ callback }) => useOnUnmount(callback),
|
|
22
|
-
{
|
|
23
|
-
initialProps: { callback: firstCallback },
|
|
24
|
-
},
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
// Change the callback
|
|
28
|
-
rerender({ callback: secondCallback });
|
|
29
|
-
|
|
30
|
-
// Unmount the component
|
|
31
|
-
unmount();
|
|
32
|
-
|
|
33
|
-
// Ensure the second callback is called, not the first one
|
|
34
|
-
expect(firstCallback).not.toHaveBeenCalled();
|
|
35
|
-
expect(secondCallback).toHaveBeenCalled();
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { useEffect, useEffectEvent } from "react";
|
|
2
|
-
|
|
3
|
-
export function useOnUnmount(callback: () => void) {
|
|
4
|
-
// useEffectEvent creates a stable reference that always calls the latest callback
|
|
5
|
-
const stableCallback = useEffectEvent(callback);
|
|
6
|
-
|
|
7
|
-
useEffect(() => () => stableCallback(), []);
|
|
8
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./usePrefersReducedMotion";
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { renderHook } from "@testing-library/react";
|
|
2
|
-
import { usePrefersReducedMotion } from "./usePrefersReducedMotion";
|
|
3
|
-
|
|
4
|
-
function mockMatchMedia(matches: boolean) {
|
|
5
|
-
window.matchMedia = jest.fn().mockReturnValue({
|
|
6
|
-
matches,
|
|
7
|
-
addEventListener: jest.fn(),
|
|
8
|
-
removeEventListener: jest.fn(),
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe("usePrefersReducedMotion", () => {
|
|
13
|
-
it("should return a boolean", () => {
|
|
14
|
-
mockMatchMedia(false);
|
|
15
|
-
|
|
16
|
-
const { result } = renderHook(() => usePrefersReducedMotion());
|
|
17
|
-
expect(typeof result.current).toBe("boolean");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("should return false by default", () => {
|
|
21
|
-
mockMatchMedia(false);
|
|
22
|
-
|
|
23
|
-
const { result } = renderHook(() => usePrefersReducedMotion());
|
|
24
|
-
expect(result.current).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("should return true when the user prefers reduced motion", () => {
|
|
28
|
-
mockMatchMedia(true);
|
|
29
|
-
|
|
30
|
-
const { result } = renderHook(() => usePrefersReducedMotion());
|
|
31
|
-
expect(result.current).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("should return false when the user does not prefer reduced motion", () => {
|
|
35
|
-
mockMatchMedia(false);
|
|
36
|
-
|
|
37
|
-
const { result } = renderHook(() => usePrefersReducedMotion());
|
|
38
|
-
expect(result.current).toBe(false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// TODO: These tests need better mocking to work
|
|
42
|
-
it.todo(
|
|
43
|
-
"should return true when the user prefers reduced motion after changing",
|
|
44
|
-
);
|
|
45
|
-
it.todo(
|
|
46
|
-
"should return false when the user does not prefer reduced motion after changing",
|
|
47
|
-
);
|
|
48
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
export function usePrefersReducedMotion(): boolean {
|
|
4
|
-
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
5
|
-
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
8
|
-
setPrefersReducedMotion(mediaQuery.matches);
|
|
9
|
-
|
|
10
|
-
const listener = (event: MediaQueryListEvent) => {
|
|
11
|
-
setPrefersReducedMotion(event.matches);
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
mediaQuery.addEventListener("change", listener);
|
|
15
|
-
|
|
16
|
-
return () => {
|
|
17
|
-
mediaQuery.removeEventListener("change", listener);
|
|
18
|
-
};
|
|
19
|
-
}, []);
|
|
20
|
-
|
|
21
|
-
return prefersReducedMotion;
|
|
22
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useRenderCount";
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { renderHook } from "@testing-library/react";
|
|
2
|
-
import { useRenderCount } from "./useRenderCount";
|
|
3
|
-
|
|
4
|
-
describe("useRenderCount", () => {
|
|
5
|
-
it("should return 1 on initial render", () => {
|
|
6
|
-
const { result } = renderHook(() => useRenderCount());
|
|
7
|
-
expect(result.current).toBe(1);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("should increment the count on re-render", () => {
|
|
11
|
-
const { result, rerender } = renderHook(() => useRenderCount());
|
|
12
|
-
expect(result.current).toBe(1);
|
|
13
|
-
rerender();
|
|
14
|
-
expect(result.current).toBe(2);
|
|
15
|
-
rerender();
|
|
16
|
-
expect(result.current).toBe(3);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("should maintain the count across multiple renders", () => {
|
|
20
|
-
const { result, rerender } = renderHook(() => useRenderCount());
|
|
21
|
-
rerender();
|
|
22
|
-
rerender();
|
|
23
|
-
rerender();
|
|
24
|
-
expect(result.current).toBe(4);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useWindowEvent";
|