@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.
- package/package.json +4 -3
- package/src/index.ts +10 -0
- package/src/useAssert/index.ts +1 -0
- package/src/useAssert/useAssert.stories.mdx +32 -0
- package/src/useAssert/useAssert.tsx +19 -0
- package/src/useCollectionQuery/index.ts +1 -0
- package/src/useCollectionQuery/mdxUtils.ts +190 -0
- package/src/useCollectionQuery/test-utilities/index.ts +3 -0
- package/src/useCollectionQuery/test-utilities/mocks.tsx +147 -0
- package/src/useCollectionQuery/test-utilities/queries.ts +95 -0
- package/src/useCollectionQuery/test-utilities/utils.ts +3 -0
- package/src/useCollectionQuery/uniqueEdges.tsx +26 -0
- package/src/useCollectionQuery/uniqueNodes.tsx +12 -0
- package/src/useCollectionQuery/useCollectionQuery.stories.mdx +129 -0
- package/src/useCollectionQuery/useCollectionQuery.test.tsx +419 -0
- package/src/useCollectionQuery/useCollectionQuery.ts +359 -0
- package/src/useFocusTrap/index.ts +1 -0
- package/src/useFocusTrap/useFocusTrap.stories.mdx +49 -0
- package/src/useFocusTrap/useFocusTrap.test.tsx +66 -0
- package/src/useFocusTrap/useFocusTrap.ts +64 -0
- package/src/useFormState/index.ts +1 -0
- package/src/useFormState/useFormState.stories.mdx +70 -0
- package/src/useFormState/useFormState.ts +10 -0
- package/src/useIsMounted/index.ts +1 -0
- package/src/useIsMounted/useIsMounted.stories.mdx +59 -0
- package/src/useIsMounted/useIsMounted.test.tsx +18 -0
- package/src/useIsMounted/useIsMounted.ts +30 -0
- package/src/useLiveAnnounce/index.ts +1 -0
- package/src/useLiveAnnounce/useLiveAnnounce.stories.mdx +38 -0
- package/src/useLiveAnnounce/useLiveAnnounce.test.tsx +55 -0
- package/src/useLiveAnnounce/useLiveAnnounce.tsx +47 -0
- package/src/useOnKeyDown/index.ts +1 -0
- package/src/useOnKeyDown/useOnKeyDown.stories.mdx +67 -0
- package/src/useOnKeyDown/useOnKeyDown.test.tsx +31 -0
- package/src/useOnKeyDown/useOnKeyDown.ts +52 -0
- package/src/usePasswordStrength/index.ts +1 -0
- package/src/usePasswordStrength/usePasswordStrength.stories.mdx +51 -0
- package/src/usePasswordStrength/usePasswordStrength.ts +21 -0
- package/src/useRefocusOnActivator/index.ts +1 -0
- package/src/useRefocusOnActivator/useRefocusOnActivator.stories.mdx +39 -0
- package/src/useRefocusOnActivator/useRefocusOnActivator.ts +26 -0
- package/src/useResizeObserver/index.ts +1 -0
- package/src/useResizeObserver/useResizeObserver.stories.mdx +134 -0
- 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
|
+
```
|