@jobber/hooks 2.0.3-dar.45 → 2.0.3-dar.47

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 (44) hide show
  1. package/package.json +4 -3
  2. package/src/index.ts +10 -0
  3. package/src/useAssert/index.ts +1 -0
  4. package/src/useAssert/useAssert.stories.mdx +32 -0
  5. package/src/useAssert/useAssert.tsx +19 -0
  6. package/src/useCollectionQuery/index.ts +1 -0
  7. package/src/useCollectionQuery/mdxUtils.ts +190 -0
  8. package/src/useCollectionQuery/test-utilities/index.ts +3 -0
  9. package/src/useCollectionQuery/test-utilities/mocks.tsx +147 -0
  10. package/src/useCollectionQuery/test-utilities/queries.ts +95 -0
  11. package/src/useCollectionQuery/test-utilities/utils.ts +3 -0
  12. package/src/useCollectionQuery/uniqueEdges.tsx +26 -0
  13. package/src/useCollectionQuery/uniqueNodes.tsx +12 -0
  14. package/src/useCollectionQuery/useCollectionQuery.stories.mdx +129 -0
  15. package/src/useCollectionQuery/useCollectionQuery.test.tsx +419 -0
  16. package/src/useCollectionQuery/useCollectionQuery.ts +359 -0
  17. package/src/useFocusTrap/index.ts +1 -0
  18. package/src/useFocusTrap/useFocusTrap.stories.mdx +49 -0
  19. package/src/useFocusTrap/useFocusTrap.test.tsx +66 -0
  20. package/src/useFocusTrap/useFocusTrap.ts +64 -0
  21. package/src/useFormState/index.ts +1 -0
  22. package/src/useFormState/useFormState.stories.mdx +70 -0
  23. package/src/useFormState/useFormState.ts +10 -0
  24. package/src/useIsMounted/index.ts +1 -0
  25. package/src/useIsMounted/useIsMounted.stories.mdx +59 -0
  26. package/src/useIsMounted/useIsMounted.test.tsx +18 -0
  27. package/src/useIsMounted/useIsMounted.ts +30 -0
  28. package/src/useLiveAnnounce/index.ts +1 -0
  29. package/src/useLiveAnnounce/useLiveAnnounce.stories.mdx +38 -0
  30. package/src/useLiveAnnounce/useLiveAnnounce.test.tsx +55 -0
  31. package/src/useLiveAnnounce/useLiveAnnounce.tsx +47 -0
  32. package/src/useOnKeyDown/index.ts +1 -0
  33. package/src/useOnKeyDown/useOnKeyDown.stories.mdx +67 -0
  34. package/src/useOnKeyDown/useOnKeyDown.test.tsx +31 -0
  35. package/src/useOnKeyDown/useOnKeyDown.ts +52 -0
  36. package/src/usePasswordStrength/index.ts +1 -0
  37. package/src/usePasswordStrength/usePasswordStrength.stories.mdx +51 -0
  38. package/src/usePasswordStrength/usePasswordStrength.ts +21 -0
  39. package/src/useRefocusOnActivator/index.ts +1 -0
  40. package/src/useRefocusOnActivator/useRefocusOnActivator.stories.mdx +39 -0
  41. package/src/useRefocusOnActivator/useRefocusOnActivator.ts +26 -0
  42. package/src/useResizeObserver/index.ts +1 -0
  43. package/src/useResizeObserver/useResizeObserver.stories.mdx +134 -0
  44. package/src/useResizeObserver/useResizeObserver.ts +78 -0
@@ -0,0 +1,18 @@
1
+ import { renderHook } from "@testing-library/react-hooks";
2
+ import { useIsMounted } from "./useIsMounted";
3
+
4
+ it("should return true when the component is currently mounted", () => {
5
+ const { result } = renderHook(() => useIsMounted());
6
+ const isMounted = result.current;
7
+
8
+ expect(isMounted.current).toBe(true);
9
+ });
10
+
11
+ it("should return false when the component is unmounted", () => {
12
+ const { result, unmount } = renderHook(() => useIsMounted());
13
+ const isMounted = result.current;
14
+
15
+ unmount();
16
+
17
+ expect(isMounted.current).toBe(false);
18
+ });
@@ -0,0 +1,30 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Why does this work?
5
+ *
6
+ * The following is from the react docs:
7
+ * [The return function from `useLayoutEffect`] is the optional cleanup mechanism for effects.
8
+ * Every effect may return a function that cleans up after it.
9
+ *
10
+ * When exactly does React clean up an effect? React performs the cleanup when the component unmounts.
11
+ * The cleanup for useLayoutEffect is called after component unmounts and before before browser painting
12
+ * the screen
13
+ *
14
+ * What does that mean for us? When this hook is initially loaded, we then trigger a `useLayoutEffect` that
15
+ * sets the isMounted to true right after the component is mounted.
16
+ * When the component unmounts, it calls the cleanup function that sets `isMounted` to false.
17
+ * This `useLayoutEffect` hook will only be run once.
18
+ */
19
+ export function useIsMounted(): { current: boolean } {
20
+ const isMounted = useRef(false);
21
+
22
+ useLayoutEffect(() => {
23
+ isMounted.current = true;
24
+ return () => {
25
+ isMounted.current = false;
26
+ };
27
+ }, []);
28
+
29
+ return isMounted;
30
+ }
@@ -0,0 +1 @@
1
+ export { useLiveAnnounce } from "./useLiveAnnounce";
@@ -0,0 +1,38 @@
1
+ import { Canvas, Meta, Story } from "@storybook/addon-docs";
2
+ import { Button } from "@jobber/components/Button";
3
+ import * as hooks from ".";
4
+
5
+ <Meta title="Hooks/useLiveAnnounce" />
6
+
7
+ # useLiveAnnounce
8
+
9
+ Announce a message through the screen reader whenever a user does an action like
10
+ deletion or addition.
11
+
12
+ ```ts
13
+ import { useLiveAnnounce } from "@jobber/hooks";
14
+ ```
15
+
16
+ Before you try this example, turn on Voice Over (Mac), or a Windows equivalent.
17
+ You can turn on Voice Over by holding `Command ⌘` and press the fingerprint key
18
+ 3 times (fast).
19
+
20
+ <Canvas>
21
+ <Story name="useLiveAnnounce">
22
+ {() => {
23
+ const { liveAnnounce } = hooks.useLiveAnnounce();
24
+ return (
25
+ <>
26
+ <Button
27
+ label="Delete"
28
+ onClick={() => liveAnnounce("You have clicked the Delete button")}
29
+ />{" "}
30
+ <Button
31
+ label="Add"
32
+ onClick={() => liveAnnounce("You have clicked the Add button")}
33
+ />
34
+ </>
35
+ );
36
+ }}
37
+ </Story>
38
+ </Canvas>
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ import { act, render, screen, waitFor } from "@testing-library/react";
3
+ import { useLiveAnnounce } from ".";
4
+
5
+ function setupHook() {
6
+ const returnVal: ReturnType<typeof useLiveAnnounce> = {
7
+ liveAnnounce: jest.fn,
8
+ };
9
+
10
+ function TestComponent() {
11
+ Object.assign(returnVal, useLiveAnnounce());
12
+ return <></>;
13
+ }
14
+
15
+ const { rerender } = render(<TestComponent />);
16
+ return { ...returnVal, rerenderComponent: () => rerender(<TestComponent />) };
17
+ }
18
+
19
+ it("should render a div to announce", async () => {
20
+ const { liveAnnounce } = setupHook();
21
+ const message = "Huzzah";
22
+ act(() => liveAnnounce(message));
23
+
24
+ await waitFor(() => {
25
+ const expectedElement = screen.queryByRole("status");
26
+ expect(expectedElement).toBeInTheDocument();
27
+ expect(expectedElement?.textContent).toBe(message);
28
+ expect(expectedElement).toHaveAttribute("role", "status");
29
+ expect(expectedElement).toHaveAttribute("aria-atomic", "true");
30
+ expect(expectedElement).toHaveAttribute("aria-live", "assertive");
31
+ });
32
+ });
33
+
34
+ it("should not render the announced div", async () => {
35
+ setupHook();
36
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
37
+ });
38
+
39
+ it("should only have 1 div to announce a message on a single instance of the hook", async () => {
40
+ const { liveAnnounce } = setupHook();
41
+ const firstMessage = "I am first";
42
+ const secondMessage = "I am second";
43
+
44
+ act(() => liveAnnounce(firstMessage));
45
+ await waitFor(() => {
46
+ expect(screen.queryAllByRole("status")).toHaveLength(1);
47
+ expect(screen.getByRole("status").textContent).toBe(firstMessage);
48
+ });
49
+
50
+ act(() => liveAnnounce(secondMessage));
51
+ await waitFor(() => {
52
+ expect(screen.queryAllByRole("status")).toHaveLength(1);
53
+ expect(screen.getByRole("status").textContent).toBe(secondMessage);
54
+ });
55
+ });
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Announce a message on voice over whenever you do an action. This is
5
+ * especially helpful when you have an action that adds or deletes an element
6
+ * from the screen.
7
+ */
8
+ export function useLiveAnnounce() {
9
+ const [announcedMessage, setAnnouncedMessage] = useState("");
10
+
11
+ useEffect(() => {
12
+ let target: HTMLElement;
13
+
14
+ if (announcedMessage) {
15
+ target = createAnnouncedElement();
16
+ setTimeout(() => target.append(announcedMessage), 100);
17
+ }
18
+
19
+ return () => target?.remove();
20
+ }, [announcedMessage]);
21
+
22
+ return {
23
+ liveAnnounce: (message: string) => {
24
+ setAnnouncedMessage(message);
25
+ },
26
+ };
27
+ }
28
+
29
+ // eslint-disable-next-line max-statements
30
+ function createAnnouncedElement() {
31
+ const el = document.createElement("div");
32
+
33
+ el.style.position = "absolute";
34
+ el.style.width = "1px";
35
+ el.style.height = "1px";
36
+ el.style.overflow = "hidden";
37
+ el.style.clipPath = " inset(100%)";
38
+ el.style.whiteSpace = " nowrap";
39
+ el.style.top = "0";
40
+ el.setAttribute("role", "status");
41
+ el.setAttribute("aria-atomic", "true");
42
+ el.setAttribute("aria-live", "assertive");
43
+
44
+ document.body.appendChild(el);
45
+
46
+ return el;
47
+ }
@@ -0,0 +1 @@
1
+ export { useOnKeyDown } from "./useOnKeyDown";
@@ -0,0 +1,67 @@
1
+ import { Canvas, Meta, Story } from "@storybook/addon-docs";
2
+ import { useState } from "react";
3
+ import { Text } from "@jobber/components/Text";
4
+ import { Card } from "@jobber/components/Card";
5
+ import { Content } from "@jobber/components/Content";
6
+ import * as hooks from ".";
7
+
8
+ <Meta title="Hooks/useOnKeyDown" />
9
+
10
+ # useOnKeyDown
11
+
12
+ `useOnKeyDown` is a simple hook that adds the `keydown` event handler when the
13
+ component is mounted and removed when unmounted.
14
+
15
+ `useOnKeyDown` should **only** be used when building keyboard shortcuts in a
16
+ component.
17
+
18
+ ```tsx
19
+ import { useOnKeyDown } from "@jobber/hooks";
20
+ ```
21
+
22
+ You can specify a list of keys to watch including a key modifier with the event
23
+ handler.
24
+
25
+ <Canvas>
26
+ <Story name="useOnKeyDown">
27
+ {() => {
28
+ const initialListText = "";
29
+ const [listText, setListText] = useState(initialListText);
30
+ const initialModifierText = "Press escape to clear this text";
31
+ const [modifierText, setModifierText] = useState(initialModifierText);
32
+ hooks.useOnKeyDown(
33
+ e => {
34
+ setListText("You pressed '" + e.key + "'");
35
+ },
36
+ ["Shift", "Enter"]
37
+ );
38
+ hooks.useOnKeyDown(
39
+ () => setModifierText("Removed. Press Control + z to undo."),
40
+ "Escape"
41
+ );
42
+ hooks.useOnKeyDown(() => setModifierText(initialModifierText), {
43
+ key: "z",
44
+ ctrlKey: true,
45
+ });
46
+ return (
47
+ <Content>
48
+ <Card title="Shift or Enter Example">
49
+ <Content>
50
+ Press shift or enter.
51
+ <pre>{listText}</pre>
52
+ </Content>
53
+ </Card>
54
+ <Card title="With a Key modifier">
55
+ <Content>
56
+ <Text>
57
+ A key can have a modifier. In this case, we show how to
58
+ implement a ctrl+z workflow.
59
+ </Text>
60
+ <pre>{modifierText}</pre>
61
+ </Content>
62
+ </Card>
63
+ </Content>
64
+ );
65
+ }}
66
+ </Story>
67
+ </Canvas>
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { fireEvent, render } from "@testing-library/react";
3
+ import { useOnKeyDown } from ".";
4
+
5
+ test("fires the method when the key is pressed", () => {
6
+ const keypressCallback = jest.fn();
7
+ const { container } = render(<TestComponent callback={keypressCallback} />);
8
+
9
+ expect(keypressCallback).toHaveBeenCalledTimes(0);
10
+
11
+ fireEvent(
12
+ container,
13
+ new KeyboardEvent("keydown", {
14
+ key: "Enter",
15
+ bubbles: true,
16
+ cancelable: false,
17
+ }),
18
+ );
19
+
20
+ expect(keypressCallback).toHaveBeenCalledTimes(1);
21
+ });
22
+
23
+ interface TestComponentProps {
24
+ callback(): void;
25
+ }
26
+
27
+ function TestComponent({ callback }: TestComponentProps) {
28
+ useOnKeyDown(callback, "Enter");
29
+
30
+ return <>Look at me!</>;
31
+ }
@@ -0,0 +1,52 @@
1
+ import useEventListener from "@use-it/event-listener";
2
+ import { XOR } from "ts-xor";
3
+
4
+ type SimpleKeyComparator = KeyboardEvent["key"];
5
+
6
+ interface VerboseKeyComparator {
7
+ readonly key: SimpleKeyComparator;
8
+ readonly shiftKey?: boolean;
9
+ readonly ctrlKey?: boolean;
10
+ readonly altKey?: boolean;
11
+ readonly metaKey?: boolean;
12
+ readonly [index: string]: boolean | string | undefined;
13
+ }
14
+
15
+ type KeyComparator = XOR<VerboseKeyComparator, SimpleKeyComparator>;
16
+
17
+ export function useOnKeyDown(
18
+ callback: (event: KeyboardEvent) => void,
19
+ keys: KeyComparator[] | KeyComparator,
20
+ ) {
21
+ useEventListener("keydown", handler);
22
+
23
+ function handler(event: KeyboardEvent) {
24
+ const keyboardEvent = event as unknown as VerboseKeyComparator;
25
+ if (typeof keys === "string" && keyboardEvent.key === keys) {
26
+ callback(event);
27
+ return;
28
+ }
29
+
30
+ if (
31
+ Array.isArray(keys) &&
32
+ keys.some(item => {
33
+ if (typeof item === "string") return keyboardEvent.key === item;
34
+ return Object.keys(item).every(
35
+ index => keyboardEvent[index] === item[index],
36
+ );
37
+ })
38
+ ) {
39
+ callback(event);
40
+ return;
41
+ }
42
+
43
+ if (
44
+ !Array.isArray(keys) &&
45
+ typeof keys !== "string" &&
46
+ Object.keys(keys).every(index => keyboardEvent[index] === keys[index])
47
+ ) {
48
+ callback(event);
49
+ return;
50
+ }
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ export { usePasswordStrength } from "./usePasswordStrength";
@@ -0,0 +1,51 @@
1
+ import { Canvas, Meta, Story } from "@storybook/addon-docs";
2
+ import { useState } from "react";
3
+ import { Content } from "@jobber/components/Content";
4
+ import { DataDump } from "@jobber/components/DataDump";
5
+ import { InputText } from "@jobber/components/InputText";
6
+ import * as hooks from ".";
7
+
8
+ <Meta title="Hooks/usePasswordStrength" />
9
+
10
+ # usePasswordStrength
11
+
12
+ `usePasswordStrength` is a hook used to calculate the strength of a password
13
+ using the [zxcvbn](https://github.com/dropbox/zxcvbn) package. You can use it
14
+ as-is or pass a dictionary of common passwords which should be treated as
15
+ insecure.
16
+
17
+ ```tsx
18
+ import { usePasswordStrength } from "@jobber/hooks";
19
+ ```
20
+
21
+ <Canvas>
22
+ <Story name="usePasswordStrength">
23
+ {() => {
24
+ const [password, setPassword] = useState("atlantis_is_a_strong_password");
25
+ const resultWithoutDict = hooks.usePasswordStrength(password);
26
+ const resultWithDict = hooks.usePasswordStrength(password, [
27
+ "atlantis",
28
+ "atlantis_is_a_strong_password",
29
+ ]);
30
+ return (
31
+ <Content>
32
+ <InputText
33
+ placeholder="Password"
34
+ defaultValue="atlantis_is_a_strong_password"
35
+ onChange={setPassword}
36
+ />
37
+ <DataDump
38
+ label="Password Strength (with Dictionary)"
39
+ data={resultWithDict}
40
+ defaultOpen
41
+ />
42
+ <DataDump
43
+ label="Password Strength (without Dictionary)"
44
+ data={resultWithoutDict}
45
+ defaultOpen
46
+ />
47
+ </Content>
48
+ );
49
+ }}
50
+ </Story>
51
+ </Canvas>
@@ -0,0 +1,21 @@
1
+ import { useMemo } from "react";
2
+ import calculateStrength from "zxcvbn";
3
+
4
+ export function usePasswordStrength(password: string, dictionary?: string[]) {
5
+ const {
6
+ guesses,
7
+ score,
8
+ feedback: { warning, suggestions },
9
+ crack_times_display: { offline_fast_hashing_1e10_per_second: timeToCrack },
10
+ } = useMemo(
11
+ () => calculateStrength(password, dictionary),
12
+ [password, dictionary],
13
+ );
14
+ return {
15
+ guesses,
16
+ score,
17
+ warning,
18
+ suggestions,
19
+ timeToCrack,
20
+ };
21
+ }
@@ -0,0 +1 @@
1
+ export { useRefocusOnActivator } from "./useRefocusOnActivator";
@@ -0,0 +1,39 @@
1
+ import { Canvas, Meta, Story } from "@storybook/addon-docs";
2
+ import { useState } from "react";
3
+ import { Button } from "@jobber/components/Button";
4
+ import { Content } from "@jobber/components/Content";
5
+ import { Card } from "@jobber/components/Card";
6
+ import * as hooks from ".";
7
+
8
+ <Meta title="Hooks/useRefocusOnActivator" />
9
+
10
+ # useRefocusOnActivator
11
+
12
+ Improves the keyboard accessibility on modals and/or popovers since the HTML
13
+ element focus returns to the one that opens it.
14
+
15
+ ```tsx
16
+ import { useRefocusOnActivator } from "@jobber/hooks";
17
+ ```
18
+
19
+ <Canvas>
20
+ <Story name="useRefocusOnActivator">
21
+ {() => {
22
+ const [open, setOpen] = useState(false);
23
+ hooks.useRefocusOnActivator(open);
24
+ return (
25
+ <Content>
26
+ <Button label="Click me" onClick={() => setOpen(true)} />
27
+ {open && (
28
+ <Card onClick={() => setOpen(false)}>
29
+ <Content>
30
+ Huzzah! Click me to hide me and watch me return the focus on the
31
+ button
32
+ </Content>
33
+ </Card>
34
+ )}
35
+ </Content>
36
+ );
37
+ }}
38
+ </Story>
39
+ </Canvas>
@@ -0,0 +1,26 @@
1
+ import { useEffect } from "react";
2
+
3
+ /**
4
+ * Brings back the focus to the element that opened an overlaid element once
5
+ * said overlaid element is dismissed.
6
+ *
7
+ * @param active - Determines if it should focus or not
8
+ */
9
+ export function useRefocusOnActivator(active: boolean) {
10
+ useEffect(() => {
11
+ let activator: Element | null | undefined;
12
+
13
+ if (active && !activator) {
14
+ activator = document.activeElement;
15
+ }
16
+
17
+ return () => {
18
+ if (active) {
19
+ if (activator instanceof HTMLElement) {
20
+ activator.focus();
21
+ }
22
+ activator = undefined;
23
+ }
24
+ };
25
+ }, [active]);
26
+ }
@@ -0,0 +1 @@
1
+ export * from "./useResizeObserver";
@@ -0,0 +1,134 @@
1
+ import { isValidElement } from "react";
2
+ import { Canvas, Meta, Source, Story } from "@storybook/addon-docs";
3
+ import { ExampleWithHooks } from "mdxUtils/ExampleWithHooks";
4
+ import { Text } from "@jobber/components/Text";
5
+ import { Card } from "@jobber/components/Card";
6
+ import { Content } from "@jobber/components/Content";
7
+ import * as hooks from ".";
8
+
9
+ <Meta title="Hooks/useResizeObserver" />
10
+
11
+ # useResizeObserver
12
+
13
+ `useResizeObserver` is a hook that will allow for responsive styling of
14
+ components based on their size, instead of the browser size.
15
+
16
+ ```tsx
17
+ import { useResizeObserver } from "@jobber/hooks";
18
+ ```
19
+
20
+ <Canvas withToolbar>
21
+ <Story name="useResizeObserver">
22
+ {() => {
23
+ const { Breakpoints } = hooks;
24
+ const [ref, { width, exactWidth, exactHeight, height }] =
25
+ hooks.useResizeObserver();
26
+ return (
27
+ <div ref={ref}>
28
+ <Card title={`Check out my àçƈéñŦ`} accent={getAccent()}>
29
+ <Content>
30
+ <Text>Width Step: {width}</Text>
31
+ <Text>Height Step: {height}</Text>
32
+ <Text>Exact Width: {exactWidth}</Text>
33
+ <Text>Exact Height: {exactHeight}</Text>
34
+ </Content>
35
+ </Card>
36
+ </div>
37
+ );
38
+ function getAccent() {
39
+ if (exactWidth < Breakpoints.smaller) return "red";
40
+ if (width < Breakpoints.small) return "orange";
41
+ if (width < Breakpoints.base) return "yellow";
42
+ if (width < Breakpoints.large) return "green";
43
+ if (width < Breakpoints.larger) return "lightBlue";
44
+ return "purple";
45
+ }
46
+ }}
47
+ </Story>
48
+ </Canvas>
49
+
50
+ ## Typing
51
+
52
+ When using `useResizeObserver` in a component, you will need to pass it a type
53
+ to represent the `ref`. In the example below, we pass it the type of
54
+ `HTMLDivElement`.
55
+
56
+ ```tsx
57
+ const [ref, { width, height }] = useResizeObserver<HTMLDivElement>();
58
+ return <div ref={ref}>...</div>;
59
+ ```
60
+
61
+ ## Breakpoints
62
+
63
+ `useResizeObserver` exports an object of `Breakpoints`. This can be used for
64
+ high level components such as `Page`
65
+
66
+ ```tsx
67
+ import { Breakpoints } from "@jobber/hooks";
68
+ ```
69
+
70
+ ### The `Breakpoints` object:
71
+
72
+ <Source language="json" code={JSON.stringify(hooks.Breakpoints)} />
73
+
74
+ ## Custom Sizes
75
+
76
+ `useResizeObserver` can take a custom `widths` or `heights` object if you do not
77
+ want to use `Breakpoints`.
78
+
79
+ <Canvas withToolbar>
80
+ <ExampleWithHooks>
81
+ {() => {
82
+ const customWidths = {
83
+ small: 480,
84
+ medium: 640,
85
+ large: 768,
86
+ };
87
+ const [ref, { width, exactWidth }] = hooks.useResizeObserver({
88
+ widths: customWidths,
89
+ });
90
+ return (
91
+ <div ref={ref}>
92
+ <Card title={`Width: ${getCurrentWidth()}`} accent={getAccent()}>
93
+ <Content>
94
+ <Text>Width Step: {width}</Text>
95
+ <Text>Exact Width: {exactWidth}</Text>
96
+ </Content>
97
+ </Card>
98
+ </div>
99
+ );
100
+ function getAccent() {
101
+ if (width < customWidths.medium) return "red";
102
+ if (width < customWidths.large) return "green";
103
+ return "indigo";
104
+ }
105
+ function getCurrentWidth() {
106
+ return Object.keys(customWidths).find(
107
+ key => customWidths[key] === width
108
+ );
109
+ }
110
+ }}
111
+ </ExampleWithHooks>
112
+ </Canvas>
113
+
114
+ ## Testing with Jest
115
+
116
+ Use `jest.mock` to set your desired window attributes.
117
+
118
+ ```tsx
119
+ jest.mock("@jobber/hooks", () => {
120
+ return {
121
+ useResizeObserver: () => [
122
+ { current: undefined },
123
+ { width: 1000, height: 100 },
124
+ ],
125
+ Breakpoints: {
126
+ base: 640,
127
+ small: 500,
128
+ smaller: 265,
129
+ large: 750,
130
+ larger: 1024,
131
+ },
132
+ };
133
+ });
134
+ ```