@k8o/arte-odyssey 1.1.0 → 1.3.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.
@@ -18,7 +18,7 @@ const Code = ({ children }) => {
18
18
  "span",
19
19
  {
20
20
  "aria-label": `Color: ${colorInfo.color}`,
21
- className: "inline-block h-3 w-3 flex-shrink-0 rounded-sm border border-gray-300",
21
+ className: "inline-block h-3 w-3 shrink-0 rounded-sm border border-border-base",
22
22
  role: "img",
23
23
  style: { backgroundColor: colorInfo.color }
24
24
  }
@@ -0,0 +1,42 @@
1
+ import type { ChangeEventHandler, FC, PropsWithChildren, ReactElement } from 'react';
2
+ type AcceptedFile = {
3
+ file: File;
4
+ id: string;
5
+ };
6
+ type FileFieldContext = {
7
+ isDisabled: boolean;
8
+ isInvalid: boolean;
9
+ acceptedFiles: AcceptedFile[];
10
+ onFileDelete: (id: string) => void;
11
+ openFilePicker: () => void;
12
+ };
13
+ declare const FileFieldContext: import("react").Context<FileFieldContext | null>;
14
+ export declare const FileFieldProvider: import("react").Context<FileFieldContext | null>;
15
+ export declare const FileField: {
16
+ readonly Root: ({ children, id, name, describedbyId, isDisabled, isInvalid, isRequired, accept, multiple, maxFiles, onChange, webkitDirectory, }: PropsWithChildren<{
17
+ id?: string;
18
+ name?: string;
19
+ describedbyId?: string | undefined;
20
+ isDisabled?: boolean;
21
+ isInvalid?: boolean;
22
+ isRequired?: boolean;
23
+ accept?: string;
24
+ multiple?: boolean;
25
+ maxFiles?: number;
26
+ defaultValue?: File[];
27
+ onChange?: ChangeEventHandler<HTMLInputElement>;
28
+ webkitDirectory?: boolean;
29
+ }>) => import("react/jsx-runtime").JSX.Element;
30
+ readonly Trigger: FC<{
31
+ renderItem: (props: {
32
+ onClick: () => void;
33
+ disabled: boolean;
34
+ invalid: boolean;
35
+ }) => ReactElement;
36
+ }>;
37
+ readonly ItemList: FC<{
38
+ showWebkitRelativePath?: boolean;
39
+ clearable?: boolean;
40
+ }>;
41
+ };
42
+ export {};
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ createContext,
5
+ use,
6
+ useCallback,
7
+ useId,
8
+ useMemo,
9
+ useRef,
10
+ useState
11
+ } from "react";
12
+ import { uuidV4 } from "../../../helpers/uuid-v4";
13
+ import { IconButton } from "../../icon-button";
14
+ import { CloseIcon } from "../../icons";
15
+ const FileFieldContext = createContext(null);
16
+ const FileFieldProvider = FileFieldContext;
17
+ const useFileFieldContext = () => {
18
+ const fileField = use(FileFieldContext);
19
+ if (!fileField) {
20
+ throw new Error("useFileFieldContext must be used within a FileField.Root");
21
+ }
22
+ return fileField;
23
+ };
24
+ const Root = ({
25
+ children,
26
+ id,
27
+ name,
28
+ describedbyId,
29
+ isDisabled = false,
30
+ isInvalid = false,
31
+ isRequired = false,
32
+ accept,
33
+ multiple = false,
34
+ maxFiles,
35
+ onChange,
36
+ webkitDirectory = false
37
+ }) => {
38
+ const generatedId = useId();
39
+ const inputRef = useRef(null);
40
+ const [acceptedFiles, setAcceptedFiles] = useState([]);
41
+ const onFilesChange = useCallback(
42
+ (event) => {
43
+ onChange?.(event);
44
+ const files = Array.from(event.target.files ?? []);
45
+ const newFiles = files.map((file) => ({ file, id: uuidV4() }));
46
+ const updatedFiles = multiple || webkitDirectory ? [...acceptedFiles, ...newFiles].slice(
47
+ 0,
48
+ maxFiles ?? Number.POSITIVE_INFINITY
49
+ ) : newFiles.slice(0, 1);
50
+ setAcceptedFiles(updatedFiles);
51
+ },
52
+ [acceptedFiles, multiple, maxFiles, onChange, webkitDirectory]
53
+ );
54
+ const onFileDelete = useCallback(
55
+ (fileId) => {
56
+ const updatedFiles = acceptedFiles.filter((f) => f.id !== fileId);
57
+ setAcceptedFiles(updatedFiles);
58
+ if (inputRef.current && onChange) {
59
+ const dataTransfer = new DataTransfer();
60
+ for (const { file } of updatedFiles) {
61
+ dataTransfer.items.add(file);
62
+ }
63
+ inputRef.current.files = dataTransfer.files;
64
+ const event = new Event("change", { bubbles: true });
65
+ Object.defineProperty(event, "target", {
66
+ writable: false,
67
+ value: inputRef.current
68
+ });
69
+ onChange(event);
70
+ }
71
+ },
72
+ [acceptedFiles, onChange]
73
+ );
74
+ const openFilePicker = useCallback(() => {
75
+ inputRef.current?.click();
76
+ }, []);
77
+ const contextValue = useMemo(
78
+ () => ({
79
+ isDisabled,
80
+ isInvalid,
81
+ acceptedFiles,
82
+ onFileDelete,
83
+ openFilePicker
84
+ }),
85
+ [isDisabled, isInvalid, acceptedFiles, onFileDelete, openFilePicker]
86
+ );
87
+ return /* @__PURE__ */ jsxs(FileFieldProvider, { value: contextValue, children: [
88
+ /* @__PURE__ */ jsx(
89
+ "input",
90
+ {
91
+ accept,
92
+ "aria-describedby": describedbyId,
93
+ "aria-invalid": isInvalid,
94
+ className: "sr-only",
95
+ disabled: isDisabled,
96
+ id: id ?? generatedId,
97
+ multiple,
98
+ name,
99
+ onChange: onFilesChange,
100
+ ref: inputRef,
101
+ required: isRequired,
102
+ type: "file",
103
+ webkitdirectory: webkitDirectory ? "true" : void 0
104
+ }
105
+ ),
106
+ children
107
+ ] });
108
+ };
109
+ const Trigger = ({ renderItem }) => {
110
+ const context = useFileFieldContext();
111
+ return renderItem({
112
+ onClick: context.openFilePicker,
113
+ disabled: context.isDisabled,
114
+ invalid: context.isInvalid
115
+ });
116
+ };
117
+ const ItemList = ({ showWebkitRelativePath, clearable }) => {
118
+ const { acceptedFiles, onFileDelete } = useFileFieldContext();
119
+ if (acceptedFiles.length === 0) {
120
+ return null;
121
+ }
122
+ return /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-2", children: acceptedFiles.map((acceptedFile) => {
123
+ const { file, id } = acceptedFile;
124
+ const onDelete = () => onFileDelete(id);
125
+ const sizeInKB = (file.size / 1024).toFixed(2);
126
+ return /* @__PURE__ */ jsxs(
127
+ "li",
128
+ {
129
+ className: "flex items-center justify-between rounded-md border border-border-base bg-bg-base px-3 py-2",
130
+ children: [
131
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
132
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-fg-base text-sm", children: showWebkitRelativePath ? file.webkitRelativePath : file.name }),
133
+ /* @__PURE__ */ jsxs("span", { className: "text-fg-mute text-xs", children: [
134
+ sizeInKB,
135
+ " KB"
136
+ ] })
137
+ ] }),
138
+ clearable && /* @__PURE__ */ jsx(IconButton, { label: "\u30D5\u30A1\u30A4\u30EB\u3092\u524A\u9664", onClick: onDelete, children: /* @__PURE__ */ jsx(CloseIcon, { size: "sm" }) })
139
+ ]
140
+ },
141
+ id
142
+ );
143
+ }) });
144
+ };
145
+ const FileField = {
146
+ Root,
147
+ Trigger,
148
+ ItemList
149
+ };
150
+ export {
151
+ FileField,
152
+ FileFieldProvider
153
+ };
@@ -0,0 +1,137 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Button } from "../../button";
3
+ import { FileField } from "./file-field";
4
+ const meta = {
5
+ title: "components/form/file-field",
6
+ component: FileField.Root,
7
+ args: {
8
+ id: "filefield"
9
+ },
10
+ render: (args) => {
11
+ return /* @__PURE__ */ jsxs(FileField.Root, { ...args, children: [
12
+ /* @__PURE__ */ jsx(
13
+ FileField.Trigger,
14
+ {
15
+ renderItem: ({ disabled, onClick }) => /* @__PURE__ */ jsx(Button, { disabled, onClick, children: "\u30D5\u30A1\u30A4\u30EB\u3092\u9078\u629E" })
16
+ }
17
+ ),
18
+ /* @__PURE__ */ jsx(FileField.ItemList, {})
19
+ ] });
20
+ },
21
+ parameters: {
22
+ a11y: {
23
+ options: {
24
+ rules: {
25
+ // FileField単体ではラベルを付随しない
26
+ "label-title-only": { enabled: false },
27
+ label: { enabled: false }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ };
33
+ var file_field_stories_default = meta;
34
+ const Default = {
35
+ args: {
36
+ isDisabled: false,
37
+ isInvalid: false,
38
+ isRequired: false
39
+ }
40
+ };
41
+ const Multiple = {
42
+ args: {
43
+ isDisabled: false,
44
+ isInvalid: false,
45
+ isRequired: false,
46
+ multiple: true
47
+ }
48
+ };
49
+ const MaxFiles = {
50
+ args: {
51
+ isDisabled: false,
52
+ isInvalid: false,
53
+ isRequired: false,
54
+ multiple: true,
55
+ maxFiles: 3
56
+ }
57
+ };
58
+ const ImageOnly = {
59
+ args: {
60
+ isDisabled: false,
61
+ isInvalid: false,
62
+ isRequired: false,
63
+ accept: "image/*"
64
+ }
65
+ };
66
+ const WebkitDirectory = {
67
+ args: {
68
+ isDisabled: false,
69
+ isInvalid: false,
70
+ isRequired: false,
71
+ webkitDirectory: true
72
+ }
73
+ };
74
+ const HasClearButton = {
75
+ args: {
76
+ isDisabled: false,
77
+ isInvalid: false,
78
+ isRequired: false,
79
+ multiple: true
80
+ },
81
+ render: (args) => {
82
+ return /* @__PURE__ */ jsxs(FileField.Root, { ...args, children: [
83
+ /* @__PURE__ */ jsx(
84
+ FileField.Trigger,
85
+ {
86
+ renderItem: ({ disabled, onClick }) => /* @__PURE__ */ jsx(Button, { disabled, onClick, children: "\u30D5\u30A1\u30A4\u30EB\u3092\u8FFD\u52A0" })
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx(FileField.ItemList, { clearable: true })
90
+ ] });
91
+ }
92
+ };
93
+ const ShowWebkitRelativePath = {
94
+ args: {
95
+ isDisabled: false,
96
+ isInvalid: false,
97
+ isRequired: false,
98
+ webkitDirectory: true
99
+ },
100
+ render: (args) => {
101
+ return /* @__PURE__ */ jsxs(FileField.Root, { ...args, children: [
102
+ /* @__PURE__ */ jsx(
103
+ FileField.Trigger,
104
+ {
105
+ renderItem: ({ disabled, onClick }) => /* @__PURE__ */ jsx(Button, { disabled, onClick, variant: "outlined", children: "\u30D5\u30A1\u30A4\u30EB\u3092\u9078\u629E" })
106
+ }
107
+ ),
108
+ /* @__PURE__ */ jsx(FileField.ItemList, { showWebkitRelativePath: true })
109
+ ] });
110
+ }
111
+ };
112
+ const OnlyTrigger = {
113
+ args: {
114
+ isDisabled: false,
115
+ isInvalid: false,
116
+ isRequired: false
117
+ },
118
+ render: (args) => {
119
+ return /* @__PURE__ */ jsx(FileField.Root, { ...args, children: /* @__PURE__ */ jsx(
120
+ FileField.Trigger,
121
+ {
122
+ renderItem: ({ disabled, onClick }) => /* @__PURE__ */ jsx(Button, { disabled, onClick, variant: "outlined", children: "\u30D5\u30A1\u30A4\u30EB\u3092\u9078\u629E" })
123
+ }
124
+ ) });
125
+ }
126
+ };
127
+ export {
128
+ Default,
129
+ HasClearButton,
130
+ ImageOnly,
131
+ MaxFiles,
132
+ Multiple,
133
+ OnlyTrigger,
134
+ ShowWebkitRelativePath,
135
+ WebkitDirectory,
136
+ file_field_stories_default as default
137
+ };
@@ -0,0 +1 @@
1
+ export * from './file-field';
@@ -0,0 +1 @@
1
+ export * from "./file-field";
@@ -13,7 +13,7 @@ const IconButton = ({
13
13
  {
14
14
  "aria-label": props.role ? label : void 0,
15
15
  className: cn(
16
- "inline-flex rounded-full bg-transparent",
16
+ "inline-flex cursor-pointer rounded-full bg-transparent",
17
17
  "hover:bg-bg-subtle",
18
18
  "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info active:bg-bg-emphasize",
19
19
  bg === "base" && "bg-bg-base/90",
@@ -12,6 +12,7 @@ export * from './dropdown-menu';
12
12
  export * from './error-boundary';
13
13
  export * from './form/autocomplete';
14
14
  export * from './form/checkbox';
15
+ export * from './form/file-field';
15
16
  export * from './form/form-control';
16
17
  export * from './form/number-field';
17
18
  export * from './form/radio';
@@ -12,6 +12,7 @@ export * from "./dropdown-menu";
12
12
  export * from "./error-boundary";
13
13
  export * from "./form/autocomplete";
14
14
  export * from "./form/checkbox";
15
+ export * from "./form/file-field";
15
16
  export * from "./form/form-control";
16
17
  export * from "./form/number-field";
17
18
  export * from "./form/radio";
@@ -79,7 +79,8 @@ function findAllColors(text) {
79
79
  while (index !== -1) {
80
80
  const beforeChar = index > 0 ? lowerText[index - 1] ?? " " : " ";
81
81
  const afterChar = index + color.length < lowerText.length ? lowerText[index + color.length] ?? " " : " ";
82
- if (/\s|;|,|:/.test(beforeChar) && /\s|;|,|$|\)|]|}/.test(afterChar)) {
82
+ const isWordBoundary = !(/[a-zA-Z0-9]/.test(beforeChar) || /[a-zA-Z0-9]/.test(afterChar));
83
+ if (isWordBoundary) {
83
84
  results.push({
84
85
  color,
85
86
  start: index,
@@ -202,6 +203,18 @@ if (import.meta.vitest) {
202
203
  const result = findAllColors("red");
203
204
  expect(result).toEqual([{ color: "red", start: 0, end: 3 }]);
204
205
  });
206
+ it("\u4ED6\u306E\u5358\u8A9E\u306E\u4E00\u90E8\u3068\u3057\u3066\u542B\u307E\u308C\u308B\u8272\u540D\u3092\u691C\u51FA\u3057\u306A\u3044", () => {
207
+ const result = findAllColors("reduce");
208
+ expect(result).toEqual([]);
209
+ });
210
+ it("\u4ED6\u306E\u5358\u8A9E\u306E\u4E00\u90E8\u3068\u3057\u3066\u542B\u307E\u308C\u308B\u8272\u540D\u3092\u691C\u51FA\u3057\u306A\u3044\uFF08\u8907\u6570\u30D1\u30BF\u30FC\u30F3\uFF09", () => {
211
+ const result = findAllColors("blueberry, greenfield, redirection");
212
+ expect(result).toEqual([]);
213
+ });
214
+ it("\u5358\u8A9E\u306E\u4E00\u90E8\u3067\u306A\u3044\u8272\u540D\u306E\u307F\u3092\u691C\u51FA\u3059\u308B", () => {
215
+ const result = findAllColors("reduce color: red; blueberry");
216
+ expect(result).toEqual([{ color: "red", start: 14, end: 17 }]);
217
+ });
205
218
  });
206
219
  });
207
220
  }
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { userEvent } from "@vitest/browser/context";
2
+ import { userEvent } from "vitest/browser";
3
3
  import { render } from "vitest-browser-react";
4
4
  import { useClickAway } from ".";
5
5
  const OutsideClicker = ({ callback }) => {
@@ -12,7 +12,7 @@ const OutsideClicker = ({ callback }) => {
12
12
  describe("useClickAway", () => {
13
13
  it("\u9818\u57DF\u5916\u3092\u89E6\u308B\u3068callback\u304C\u547C\u3073\u51FA\u3055\u308C\u308B", async () => {
14
14
  const fn = vi.fn();
15
- const { getByText } = render(/* @__PURE__ */ jsx(OutsideClicker, { callback: fn }));
15
+ const { getByText } = await render(/* @__PURE__ */ jsx(OutsideClicker, { callback: fn }));
16
16
  const element = getByText("Element");
17
17
  const outsideElement = getByText("Outside");
18
18
  expect(fn).not.toHaveBeenCalled();
@@ -12,7 +12,7 @@ describe("useClipboard", () => {
12
12
  writeText: writeTextMockFn
13
13
  }
14
14
  });
15
- const { result } = renderHook(() => useClipboard());
15
+ const { result } = await renderHook(() => useClipboard());
16
16
  await result.current.writeClipboard(writeText);
17
17
  expect(writeTextMockFn).toBeCalledWith(writeText);
18
18
  expect(navigator.clipboard.writeText).toHaveBeenCalledOnce();
@@ -24,7 +24,7 @@ describe("useClipboard", () => {
24
24
  readText: readTextMockFn
25
25
  }
26
26
  });
27
- const { result } = renderHook(() => useClipboard());
27
+ const { result } = await renderHook(() => useClipboard());
28
28
  await result.current.readClipboard();
29
29
  expect(readTextMockFn).toHaveBeenCalledOnce();
30
30
  });
@@ -12,12 +12,12 @@ describe("useHash", () => {
12
12
  vi.unstubAllGlobals();
13
13
  window.location.hash = realHash;
14
14
  });
15
- it("\u73FE\u5728\u306Ehash\u5024\u3092\u53D6\u5F97\u3067\u304D\u308B", () => {
16
- const { result } = renderHook(() => useHash());
15
+ it("\u73FE\u5728\u306Ehash\u5024\u3092\u53D6\u5F97\u3067\u304D\u308B", async () => {
16
+ const { result } = await renderHook(() => useHash());
17
17
  expect(result.current).toBe("test");
18
18
  });
19
- it("hash\u5024\u304C\u5909\u66F4\u3055\u308C\u305F\u3068\u304D\u306B\u66F4\u65B0\u3055\u308C\u308B", () => {
20
- const { result, act } = renderHook(() => useHash());
19
+ it("hash\u5024\u304C\u5909\u66F4\u3055\u308C\u305F\u3068\u304D\u306B\u66F4\u65B0\u3055\u308C\u308B", async () => {
20
+ const { result, act } = await renderHook(() => useHash());
21
21
  act(() => {
22
22
  window.location.hash = "#changed";
23
23
  window.dispatchEvent(new Event("hashchange"));
@@ -25,7 +25,7 @@ describe("useHash", () => {
25
25
  expect(result.current).toBe("changed");
26
26
  });
27
27
  it("pushState\u3067hash\u5024\u304C\u5909\u66F4\u3055\u308C\u305F\u3068\u304D\u306B\u66F4\u65B0\u3055\u308C\u308B", async () => {
28
- const { result, act } = renderHook(() => useHash());
28
+ const { result, act } = await renderHook(() => useHash());
29
29
  act(() => {
30
30
  window.history.pushState({}, "", "/#pushed");
31
31
  });
@@ -34,7 +34,7 @@ describe("useHash", () => {
34
34
  });
35
35
  });
36
36
  it("replaceState\u3067hash\u5024\u304C\u5909\u66F4\u3055\u308C\u305F\u3068\u304D\u306B\u66F4\u65B0\u3055\u308C\u308B", async () => {
37
- const { result, act } = renderHook(() => useHash());
37
+ const { result, act } = await renderHook(() => useHash());
38
38
  act(() => {
39
39
  window.history.replaceState({}, "", "/#replaced");
40
40
  });
@@ -4,7 +4,9 @@ export * from './clipboard';
4
4
  export * from './hash';
5
5
  export * from './interval';
6
6
  export * from './local-storage';
7
+ export * from './resize';
7
8
  export * from './scroll-direction';
8
9
  export * from './step';
9
10
  export * from './timeout';
11
+ export * from './window-resize';
10
12
  export * from './window-size';
@@ -4,7 +4,9 @@ export * from "./clipboard";
4
4
  export * from "./hash";
5
5
  export * from "./interval";
6
6
  export * from "./local-storage";
7
+ export * from "./resize";
7
8
  export * from "./scroll-direction";
8
9
  export * from "./step";
9
10
  export * from "./timeout";
11
+ export * from "./window-resize";
10
12
  export * from "./window-size";
@@ -1,28 +1,28 @@
1
1
  import { renderHook } from "vitest-browser-react";
2
2
  import { useInterval } from ".";
3
3
  describe("useInterval", () => {
4
- it("\u6307\u5B9A\u6642\u9593\u3054\u3068\u306B\u5B9F\u884C\u3055\u308C\u308B", () => {
4
+ it("\u6307\u5B9A\u6642\u9593\u3054\u3068\u306B\u5B9F\u884C\u3055\u308C\u308B", async () => {
5
5
  const fn = vi.fn();
6
6
  vi.useFakeTimers();
7
- renderHook(() => {
7
+ await renderHook(() => {
8
8
  useInterval(fn, 1e3);
9
9
  });
10
10
  vi.advanceTimersByTime(2e3);
11
11
  expect(fn).toHaveBeenCalledTimes(2);
12
12
  });
13
- it("\u6307\u5B9A\u6642\u9593\u3092\u904E\u304E\u306A\u3044\u3068\u5B9F\u884C\u3055\u308C\u306A\u3044", () => {
13
+ it("\u6307\u5B9A\u6642\u9593\u3092\u904E\u304E\u306A\u3044\u3068\u5B9F\u884C\u3055\u308C\u306A\u3044", async () => {
14
14
  const fn = vi.fn();
15
15
  vi.useFakeTimers();
16
- renderHook(() => {
16
+ await renderHook(() => {
17
17
  useInterval(fn, 1e3);
18
18
  });
19
19
  vi.advanceTimersByTime(10);
20
20
  expect(fn).not.toHaveBeenCalled();
21
21
  });
22
- it("\u30A2\u30F3\u30DE\u30A6\u30F3\u30C8\u5F8C\u306F\u5B9F\u884C\u3055\u308C\u306A\u3044", () => {
22
+ it("\u30A2\u30F3\u30DE\u30A6\u30F3\u30C8\u5F8C\u306F\u5B9F\u884C\u3055\u308C\u306A\u3044", async () => {
23
23
  const fn = vi.fn();
24
24
  vi.useFakeTimers();
25
- const { unmount } = renderHook(() => {
25
+ const { unmount } = await renderHook(() => {
26
26
  useInterval(fn, 1e3);
27
27
  });
28
28
  unmount();
@@ -9,17 +9,21 @@ describe("useLocalStorage", () => {
9
9
  afterAll(() => {
10
10
  consoleErrorMock.mockReset();
11
11
  });
12
- it("localStorage\u306B\u5024\u304C\u306A\u3051\u308C\u3070\u521D\u671F\u5024\u3092\u8FD4\u3059", () => {
13
- const { result } = renderHook(() => useLocalStorage(key, "defaultValue"));
12
+ it("localStorage\u306B\u5024\u304C\u306A\u3051\u308C\u3070\u521D\u671F\u5024\u3092\u8FD4\u3059", async () => {
13
+ const { result } = await renderHook(
14
+ () => useLocalStorage(key, "defaultValue")
15
+ );
14
16
  expect(result.current[0]).toBe("defaultValue");
15
17
  });
16
- it("localStorage\u306B\u5024\u304C\u5B58\u5728\u3042\u308C\u3070\u305D\u306E\u5024\u3092\u8FD4\u3059", () => {
18
+ it("localStorage\u306B\u5024\u304C\u5B58\u5728\u3042\u308C\u3070\u305D\u306E\u5024\u3092\u8FD4\u3059", async () => {
17
19
  localStorage.setItem(key, JSON.stringify("storedValue"));
18
- const { result } = renderHook(() => useLocalStorage(key, "defaultValue"));
20
+ const { result } = await renderHook(
21
+ () => useLocalStorage(key, "defaultValue")
22
+ );
19
23
  expect(result.current[0]).toBe("storedValue");
20
24
  });
21
- it("\u66F4\u65B0\u51E6\u7406\u3067\u306FlocalStorage\u3068state\u306E\u4E21\u65B9\u3092\u66F4\u65B0\u3059\u308B", () => {
22
- const { result, act } = renderHook(
25
+ it("\u66F4\u65B0\u51E6\u7406\u3067\u306FlocalStorage\u3068state\u306E\u4E21\u65B9\u3092\u66F4\u65B0\u3059\u308B", async () => {
26
+ const { result, act } = await renderHook(
23
27
  () => useLocalStorage(key, "defaultValue")
24
28
  );
25
29
  act(() => {
@@ -28,9 +32,9 @@ describe("useLocalStorage", () => {
28
32
  expect(localStorage.getItem(key)).toBe(JSON.stringify("newValue"));
29
33
  expect(result.current[0]).toBe("newValue");
30
34
  });
31
- it("\u524A\u9664\u51E6\u7406\u3067\u306FlocalStorage\u306F\u5024\u3092\u524A\u9664\u3055\u308C\u3001state\u306F\u521D\u671F\u5024\u306B\u306A\u308B", () => {
35
+ it("\u524A\u9664\u51E6\u7406\u3067\u306FlocalStorage\u306F\u5024\u3092\u524A\u9664\u3055\u308C\u3001state\u306F\u521D\u671F\u5024\u306B\u306A\u308B", async () => {
32
36
  localStorage.setItem(key, JSON.stringify("storedValue"));
33
- const { result, act } = renderHook(
37
+ const { result, act } = await renderHook(
34
38
  () => useLocalStorage(key, "defaultValue")
35
39
  );
36
40
  act(() => {
@@ -39,8 +43,8 @@ describe("useLocalStorage", () => {
39
43
  expect(localStorage.getItem(key)).toBeNull();
40
44
  expect(result.current[0]).toBe("defaultValue");
41
45
  });
42
- it("null\u3067\u66F4\u65B0\u3057\u305F\u5834\u5408\u306Fremove\u3068\u540C\u3058\u7D50\u679C\u306B\u306A\u308B", () => {
43
- const { result, act } = renderHook(
46
+ it("null\u3067\u66F4\u65B0\u3057\u305F\u5834\u5408\u306Fremove\u3068\u540C\u3058\u7D50\u679C\u306B\u306A\u308B", async () => {
47
+ const { result, act } = await renderHook(
44
48
  () => useLocalStorage(key, {
45
49
  lang: ["ja", "en"]
46
50
  })
@@ -51,8 +55,8 @@ describe("useLocalStorage", () => {
51
55
  expect(localStorage.getItem(key)).toBeNull();
52
56
  expect(result.current[0]).toEqual({ lang: ["ja", "en"] });
53
57
  });
54
- it("storage\u30A4\u30D9\u30F3\u30C8\u306E\u767A\u706B\u306B\u5FDC\u3058\u3066\u72B6state\u304C\u66F4\u65B0\u3055\u308C\u308B", () => {
55
- const { result, act } = renderHook(
58
+ it("storage\u30A4\u30D9\u30F3\u30C8\u306E\u767A\u706B\u306B\u5FDC\u3058\u3066\u72B6state\u304C\u66F4\u65B0\u3055\u308C\u308B", async () => {
59
+ const { result, act } = await renderHook(
56
60
  () => useLocalStorage(key, "defaultValue")
57
61
  );
58
62
  act(() => {
@@ -66,8 +70,8 @@ describe("useLocalStorage", () => {
66
70
  });
67
71
  expect(result.current[0]).toBe("updatedValue");
68
72
  });
69
- it("\u7570\u306A\u308B\u30AD\u30FC\u306Estorage\u30A4\u30D9\u30F3\u30C8\u306Fstate\u3092\u66F4\u65B0\u3057\u306A\u3044", () => {
70
- const { result, act } = renderHook(
73
+ it("\u7570\u306A\u308B\u30AD\u30FC\u306Estorage\u30A4\u30D9\u30F3\u30C8\u306Fstate\u3092\u66F4\u65B0\u3057\u306A\u3044", async () => {
74
+ const { result, act } = await renderHook(
71
75
  () => useLocalStorage(key, "defaultValue")
72
76
  );
73
77
  act(() => {
@@ -81,9 +85,11 @@ describe("useLocalStorage", () => {
81
85
  });
82
86
  expect(result.current[0]).toBe("defaultValue");
83
87
  });
84
- it("JSON\u3092\u30D1\u30FC\u30B9\u3067\u304D\u306A\u3044\u6642\u306F\u30A8\u30E9\u30FC\u3092\u5410\u3044\u3066\u521D\u671F\u5024\u3092\u8FD4\u3059", () => {
88
+ it("JSON\u3092\u30D1\u30FC\u30B9\u3067\u304D\u306A\u3044\u6642\u306F\u30A8\u30E9\u30FC\u3092\u5410\u3044\u3066\u521D\u671F\u5024\u3092\u8FD4\u3059", async () => {
85
89
  localStorage.setItem(key, "{invalidJSON");
86
- const { result } = renderHook(() => useLocalStorage(key, "defaultValue"));
90
+ const { result } = await renderHook(
91
+ () => useLocalStorage(key, "defaultValue")
92
+ );
87
93
  expect(result.current[0]).toBe("defaultValue");
88
94
  expect(consoleErrorMock).toHaveBeenCalledOnce();
89
95
  });
@@ -0,0 +1,7 @@
1
+ import { type RefObject } from 'react';
2
+ type Options = {
3
+ enabled?: boolean;
4
+ debounceMs?: number;
5
+ };
6
+ export declare const useResize: <T extends Element = HTMLElement>(callback: (entry: ResizeObserverEntry) => void, options?: Options) => RefObject<T | null>;
7
+ export {};
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ const useResize = (callback, options = {}) => {
4
+ const { enabled = true, debounceMs } = options;
5
+ const ref = useRef(null);
6
+ const timeoutRef = useRef(null);
7
+ useEffect(() => {
8
+ if (!enabled) return;
9
+ const element = ref.current;
10
+ if (!element) return;
11
+ const observer = new ResizeObserver((entries) => {
12
+ for (const entry of entries) {
13
+ if (debounceMs !== void 0) {
14
+ if (timeoutRef.current) {
15
+ clearTimeout(timeoutRef.current);
16
+ }
17
+ timeoutRef.current = setTimeout(() => {
18
+ callback(entry);
19
+ }, debounceMs);
20
+ } else {
21
+ callback(entry);
22
+ }
23
+ }
24
+ });
25
+ observer.observe(element);
26
+ return () => {
27
+ if (timeoutRef.current) {
28
+ clearTimeout(timeoutRef.current);
29
+ }
30
+ observer.disconnect();
31
+ };
32
+ }, [callback, enabled, debounceMs]);
33
+ return ref;
34
+ };
35
+ export {
36
+ useResize
37
+ };
@@ -0,0 +1,68 @@
1
+ import { renderHook } from "vitest-browser-react";
2
+ import { useResize } from ".";
3
+ describe("useResize", () => {
4
+ it("\u8981\u7D20\u306E\u30EA\u30B5\u30A4\u30BA\u6642\u306B\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u304C\u547C\u3070\u308C\u308B", async () => {
5
+ const callback = vi.fn();
6
+ const { result } = await renderHook(() => useResize(callback));
7
+ const element = document.createElement("div");
8
+ Object.defineProperty(result.current, "current", {
9
+ writable: true,
10
+ value: element
11
+ });
12
+ const observer = new ResizeObserver(callback);
13
+ observer.observe(element);
14
+ const contentRect = new DOMRectReadOnly(0, 0, 100, 100);
15
+ const entry = {
16
+ target: element,
17
+ contentRect,
18
+ borderBoxSize: [],
19
+ contentBoxSize: [],
20
+ devicePixelContentBoxSize: []
21
+ };
22
+ callback(entry);
23
+ expect(callback).toHaveBeenCalled();
24
+ observer.disconnect();
25
+ });
26
+ it("enabled=false\u306E\u5834\u5408\u306F\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u304C\u547C\u3070\u308C\u306A\u3044", async () => {
27
+ const callback = vi.fn();
28
+ const { result } = await renderHook(
29
+ () => useResize(callback, { enabled: false })
30
+ );
31
+ expect(result.current.current).toBeNull();
32
+ expect(callback).not.toHaveBeenCalled();
33
+ });
34
+ it("debounceMs\u6307\u5B9A\u6642\u306F\u6307\u5B9A\u6642\u9593\u5F8C\u306B\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u304C\u547C\u3070\u308C\u308B", async () => {
35
+ vi.useFakeTimers();
36
+ const callback = vi.fn();
37
+ const { result } = await renderHook(
38
+ () => useResize(callback, { debounceMs: 300 })
39
+ );
40
+ const element = document.createElement("div");
41
+ Object.defineProperty(result.current, "current", {
42
+ writable: true,
43
+ value: element
44
+ });
45
+ const observer = new ResizeObserver((entries) => {
46
+ for (const entry2 of entries) {
47
+ callback(entry2);
48
+ }
49
+ });
50
+ observer.observe(element);
51
+ const contentRect = new DOMRectReadOnly(0, 0, 100, 100);
52
+ const entry = {
53
+ target: element,
54
+ contentRect,
55
+ borderBoxSize: [],
56
+ contentBoxSize: [],
57
+ devicePixelContentBoxSize: []
58
+ };
59
+ callback(entry);
60
+ expect(callback).toHaveBeenCalledTimes(1);
61
+ vi.advanceTimersByTime(299);
62
+ expect(callback).toHaveBeenCalledTimes(1);
63
+ vi.advanceTimersByTime(1);
64
+ expect(callback).toHaveBeenCalledTimes(1);
65
+ observer.disconnect();
66
+ vi.useRealTimers();
67
+ });
68
+ });
@@ -11,29 +11,29 @@ describe("useScrollDirection", () => {
11
11
  value: 0
12
12
  });
13
13
  });
14
- it("\u521D\u671F\u72B6\u614B\u3067\u306Fx: right, y: up\u3092\u8FD4\u3059", () => {
15
- const { result } = renderHook(() => useScrollDirection());
14
+ it("\u521D\u671F\u72B6\u614B\u3067\u306Fx: right, y: up\u3092\u8FD4\u3059", async () => {
15
+ const { result } = await renderHook(() => useScrollDirection());
16
16
  expect(result.current).toEqual({ x: "right", y: "up" });
17
17
  });
18
18
  describe("Vertical scroll", () => {
19
- it("100px\u4EE5\u4E0A\u4E0B\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068y: down\u3092\u8FD4\u3059", () => {
20
- const { result, act } = renderHook(() => useScrollDirection());
19
+ it("100px\u4EE5\u4E0A\u4E0B\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068y: down\u3092\u8FD4\u3059", async () => {
20
+ const { result, act } = await renderHook(() => useScrollDirection());
21
21
  act(() => {
22
22
  Object.defineProperty(window, "scrollY", { value: 150 });
23
23
  window.dispatchEvent(new Event("scroll"));
24
24
  });
25
25
  expect(result.current.y).toBe("down");
26
26
  });
27
- it("100px\u672A\u6E80\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306Fy: up\u306E\u307E\u307E", () => {
28
- const { result, act } = renderHook(() => useScrollDirection());
27
+ it("100px\u672A\u6E80\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306Fy: up\u306E\u307E\u307E", async () => {
28
+ const { result, act } = await renderHook(() => useScrollDirection());
29
29
  act(() => {
30
30
  Object.defineProperty(window, "scrollY", { value: 50 });
31
31
  window.dispatchEvent(new Event("scroll"));
32
32
  });
33
33
  expect(result.current.y).toBe("up");
34
34
  });
35
- it("\u4E0A\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068y: up\u3092\u8FD4\u3059", () => {
36
- const { result, act } = renderHook(() => useScrollDirection());
35
+ it("\u4E0A\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068y: up\u3092\u8FD4\u3059", async () => {
36
+ const { result, act } = await renderHook(() => useScrollDirection());
37
37
  act(() => {
38
38
  Object.defineProperty(window, "scrollY", { value: 200 });
39
39
  window.dispatchEvent(new Event("scroll"));
@@ -47,24 +47,24 @@ describe("useScrollDirection", () => {
47
47
  });
48
48
  });
49
49
  describe("Horizontal scroll", () => {
50
- it("100px\u4EE5\u4E0A\u53F3\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068x: right\u3092\u8FD4\u3059", () => {
51
- const { result, act } = renderHook(() => useScrollDirection());
50
+ it("100px\u4EE5\u4E0A\u53F3\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068x: right\u3092\u8FD4\u3059", async () => {
51
+ const { result, act } = await renderHook(() => useScrollDirection());
52
52
  act(() => {
53
53
  Object.defineProperty(window, "scrollX", { value: 150 });
54
54
  window.dispatchEvent(new Event("scroll"));
55
55
  });
56
56
  expect(result.current.x).toBe("right");
57
57
  });
58
- it("100px\u672A\u6E80\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306Fx: right\u306E\u307E\u307E", () => {
59
- const { result, act } = renderHook(() => useScrollDirection());
58
+ it("100px\u672A\u6E80\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306Fx: right\u306E\u307E\u307E", async () => {
59
+ const { result, act } = await renderHook(() => useScrollDirection());
60
60
  act(() => {
61
61
  Object.defineProperty(window, "scrollX", { value: 50 });
62
62
  window.dispatchEvent(new Event("scroll"));
63
63
  });
64
64
  expect(result.current.x).toBe("right");
65
65
  });
66
- it("\u5DE6\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068x: left\u3092\u8FD4\u3059", () => {
67
- const { result, act } = renderHook(() => useScrollDirection());
66
+ it("\u5DE6\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u3068x: left\u3092\u8FD4\u3059", async () => {
67
+ const { result, act } = await renderHook(() => useScrollDirection());
68
68
  act(() => {
69
69
  Object.defineProperty(window, "scrollX", { value: 200 });
70
70
  window.dispatchEvent(new Event("scroll"));
@@ -78,8 +78,8 @@ describe("useScrollDirection", () => {
78
78
  });
79
79
  });
80
80
  describe("Combined scroll", () => {
81
- it("\u7E26\u6A2A\u540C\u6642\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3057\u305F\u5834\u5408\u3001\u4E21\u65B9\u5411\u3092\u6B63\u3057\u304F\u691C\u77E5\u3059\u308B", () => {
82
- const { result, act } = renderHook(() => useScrollDirection());
81
+ it("\u7E26\u6A2A\u540C\u6642\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3057\u305F\u5834\u5408\u3001\u4E21\u65B9\u5411\u3092\u6B63\u3057\u304F\u691C\u77E5\u3059\u308B", async () => {
82
+ const { result, act } = await renderHook(() => useScrollDirection());
83
83
  act(() => {
84
84
  Object.defineProperty(window, "scrollY", { value: 150 });
85
85
  Object.defineProperty(window, "scrollX", { value: 150 });
@@ -88,18 +88,18 @@ describe("useScrollDirection", () => {
88
88
  expect(result.current).toEqual({ x: "right", y: "down" });
89
89
  });
90
90
  });
91
- it("\u30A2\u30F3\u30DE\u30A6\u30F3\u30C8\u5F8C\u306F\u30A4\u30D9\u30F3\u30C8\u30EA\u30B9\u30CA\u30FC\u304C\u524A\u9664\u3055\u308C\u308B", () => {
91
+ it("\u30A2\u30F3\u30DE\u30A6\u30F3\u30C8\u5F8C\u306F\u30A4\u30D9\u30F3\u30C8\u30EA\u30B9\u30CA\u30FC\u304C\u524A\u9664\u3055\u308C\u308B", async () => {
92
92
  const removeEventListenerSpy = vi.spyOn(window, "removeEventListener");
93
- const { unmount } = renderHook(() => useScrollDirection());
93
+ const { unmount } = await renderHook(() => useScrollDirection());
94
94
  unmount();
95
95
  expect(removeEventListenerSpy).toHaveBeenCalledWith(
96
96
  "scroll",
97
97
  expect.any(Function)
98
98
  );
99
99
  });
100
- it("\u30B9\u30AF\u30ED\u30FC\u30EB\u30A4\u30D9\u30F3\u30C8\u304Cpassive: true\u3067\u767B\u9332\u3055\u308C\u308B", () => {
100
+ it("\u30B9\u30AF\u30ED\u30FC\u30EB\u30A4\u30D9\u30F3\u30C8\u304Cpassive: true\u3067\u767B\u9332\u3055\u308C\u308B", async () => {
101
101
  const addEventListenerSpy = vi.spyOn(window, "addEventListener");
102
- renderHook(() => useScrollDirection());
102
+ await renderHook(() => useScrollDirection());
103
103
  expect(addEventListenerSpy).toHaveBeenCalledWith(
104
104
  "scroll",
105
105
  expect.any(Function),
@@ -107,8 +107,8 @@ describe("useScrollDirection", () => {
107
107
  );
108
108
  });
109
109
  describe("Threshold parameter", () => {
110
- it("threshold\u304C100\u306E\u5834\u5408\u3001100px\u4EE5\u4E0B\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306F\u65B9\u5411\u304C\u5909\u308F\u3089\u306A\u3044", () => {
111
- const { result, act } = renderHook(() => useScrollDirection(100));
110
+ it("threshold\u304C100\u306E\u5834\u5408\u3001100px\u4EE5\u4E0B\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306F\u65B9\u5411\u304C\u5909\u308F\u3089\u306A\u3044", async () => {
111
+ const { result, act } = await renderHook(() => useScrollDirection(100));
112
112
  act(() => {
113
113
  Object.defineProperty(window, "scrollY", { value: 100 });
114
114
  window.dispatchEvent(new Event("scroll"));
@@ -120,8 +120,8 @@ describe("useScrollDirection", () => {
120
120
  });
121
121
  expect(result.current.x).toBe("right");
122
122
  });
123
- it("threshold\u304C100\u306E\u5834\u5408\u3001101px\u4EE5\u4E0A\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u65B9\u5411\u304C\u5909\u308F\u308B", () => {
124
- const { result, act } = renderHook(() => useScrollDirection(100));
123
+ it("threshold\u304C100\u306E\u5834\u5408\u3001101px\u4EE5\u4E0A\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u65B9\u5411\u304C\u5909\u308F\u308B", async () => {
124
+ const { result, act } = await renderHook(() => useScrollDirection(100));
125
125
  act(() => {
126
126
  Object.defineProperty(window, "scrollY", { value: 101 });
127
127
  window.dispatchEvent(new Event("scroll"));
@@ -133,32 +133,32 @@ describe("useScrollDirection", () => {
133
133
  });
134
134
  expect(result.current.x).toBe("right");
135
135
  });
136
- it("threshold\u304C10\u306E\u5834\u5408\u300110px\u4EE5\u4E0B\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306F\u65B9\u5411\u304C\u5909\u308F\u3089\u306A\u3044", () => {
137
- const { result, act } = renderHook(() => useScrollDirection(10));
136
+ it("threshold\u304C10\u306E\u5834\u5408\u300110px\u4EE5\u4E0B\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u306F\u65B9\u5411\u304C\u5909\u308F\u3089\u306A\u3044", async () => {
137
+ const { result, act } = await renderHook(() => useScrollDirection(10));
138
138
  act(() => {
139
139
  Object.defineProperty(window, "scrollY", { value: 10 });
140
140
  window.dispatchEvent(new Event("scroll"));
141
141
  });
142
142
  expect(result.current.y).toBe("up");
143
143
  });
144
- it("threshold\u304C10\u306E\u5834\u5408\u300111px\u4EE5\u4E0A\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u65B9\u5411\u304C\u5909\u308F\u308B", () => {
145
- const { result, act } = renderHook(() => useScrollDirection(10));
144
+ it("threshold\u304C10\u306E\u5834\u5408\u300111px\u4EE5\u4E0A\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u65B9\u5411\u304C\u5909\u308F\u308B", async () => {
145
+ const { result, act } = await renderHook(() => useScrollDirection(10));
146
146
  act(() => {
147
147
  Object.defineProperty(window, "scrollY", { value: 11 });
148
148
  window.dispatchEvent(new Event("scroll"));
149
149
  });
150
150
  expect(result.current.y).toBe("down");
151
151
  });
152
- it("threshold\u304C0\u306E\u5834\u5408\u30011px\u4EE5\u4E0A\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u65B9\u5411\u304C\u5909\u308F\u308B", () => {
153
- const { result, act } = renderHook(() => useScrollDirection(0));
152
+ it("threshold\u304C0\u306E\u5834\u5408\u30011px\u4EE5\u4E0A\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u65B9\u5411\u304C\u5909\u308F\u308B", async () => {
153
+ const { result, act } = await renderHook(() => useScrollDirection(0));
154
154
  act(() => {
155
155
  Object.defineProperty(window, "scrollY", { value: 1 });
156
156
  window.dispatchEvent(new Event("scroll"));
157
157
  });
158
158
  expect(result.current.y).toBe("down");
159
159
  });
160
- it("\u30C7\u30D5\u30A9\u30EB\u30C8\u5024\uFF08threshold\u672A\u6307\u5B9A\uFF09\u3067\u306F50px\u306E\u95BE\u5024\u304C\u9069\u7528\u3055\u308C\u308B", () => {
161
- const { result, act } = renderHook(() => useScrollDirection());
160
+ it("\u30C7\u30D5\u30A9\u30EB\u30C8\u5024\uFF08threshold\u672A\u6307\u5B9A\uFF09\u3067\u306F50px\u306E\u95BE\u5024\u304C\u9069\u7528\u3055\u308C\u308B", async () => {
161
+ const { result, act } = await renderHook(() => useScrollDirection());
162
162
  act(() => {
163
163
  Object.defineProperty(window, "scrollY", { value: 50 });
164
164
  window.dispatchEvent(new Event("scroll"));
@@ -1,19 +1,21 @@
1
- import { userEvent } from "@vitest/browser/context";
1
+ import { userEvent } from "vitest/browser";
2
2
  import { renderHook } from "vitest-browser-react";
3
3
  import { useStep } from ".";
4
4
  describe("useStep", () => {
5
- it("\u521D\u671F\u72B6\u614B", () => {
5
+ it("\u521D\u671F\u72B6\u614B", async () => {
6
6
  const initialCount = 1;
7
7
  const maxCount = 10;
8
- const { result } = renderHook(() => useStep({ initialCount, maxCount }));
8
+ const { result } = await renderHook(
9
+ () => useStep({ initialCount, maxCount })
10
+ );
9
11
  expect(result.current.count).toBe(initialCount);
10
12
  expect(result.current.isDisabledBack).toBeTruthy();
11
13
  expect(result.current.isDisabledNext).toBeFalsy();
12
14
  });
13
- it("next\u3067initialCount\u304B\u30891\u9032\u3080", () => {
15
+ it("next\u3067initialCount\u304B\u30891\u9032\u3080", async () => {
14
16
  const initialCount = 1;
15
17
  const maxCount = 10;
16
- const { result, act } = renderHook(
18
+ const { result, act } = await renderHook(
17
19
  () => useStep({ initialCount, maxCount })
18
20
  );
19
21
  act(() => {
@@ -23,10 +25,10 @@ describe("useStep", () => {
23
25
  expect(result.current.isDisabledBack).toBeFalsy();
24
26
  expect(result.current.isDisabledNext).toBeFalsy();
25
27
  });
26
- it("initialCount\u304B\u3089\u306Fback\u3067\u304D\u306A\u3044", () => {
28
+ it("initialCount\u304B\u3089\u306Fback\u3067\u304D\u306A\u3044", async () => {
27
29
  const initialCount = 1;
28
30
  const maxCount = 10;
29
- const { result, act } = renderHook(
31
+ const { result, act } = await renderHook(
30
32
  () => useStep({ initialCount, maxCount })
31
33
  );
32
34
  act(() => {
@@ -36,10 +38,10 @@ describe("useStep", () => {
36
38
  expect(result.current.isDisabledBack).toBeTruthy();
37
39
  expect(result.current.isDisabledNext).toBeFalsy();
38
40
  });
39
- it("maxCount\u307E\u3067\u9032\u3080", () => {
41
+ it("maxCount\u307E\u3067\u9032\u3080", async () => {
40
42
  const initialCount = 1;
41
43
  const maxCount = 3;
42
- const { result, act } = renderHook(
44
+ const { result, act } = await renderHook(
43
45
  () => useStep({ initialCount, maxCount })
44
46
  );
45
47
  act(() => {
@@ -50,10 +52,10 @@ describe("useStep", () => {
50
52
  expect(result.current.isDisabledBack).toBeFalsy();
51
53
  expect(result.current.isDisabledNext).toBeTruthy();
52
54
  });
53
- it("maxCount\u4EE5\u4E0A\u306F\u9032\u3081\u306A\u3044", () => {
55
+ it("maxCount\u4EE5\u4E0A\u306F\u9032\u3081\u306A\u3044", async () => {
54
56
  const initialCount = 1;
55
57
  const maxCount = 3;
56
- const { result, act } = renderHook(
58
+ const { result, act } = await renderHook(
57
59
  () => useStep({ initialCount, maxCount })
58
60
  );
59
61
  act(() => {
@@ -65,10 +67,10 @@ describe("useStep", () => {
65
67
  expect(result.current.isDisabledBack).toBeFalsy();
66
68
  expect(result.current.isDisabledNext).toBeTruthy();
67
69
  });
68
- it("next\u3068back\u3092\u7D44\u307F\u5408\u308F\u305B\u3066\u5229\u7528\u3067\u304D\u308B", () => {
70
+ it("next\u3068back\u3092\u7D44\u307F\u5408\u308F\u305B\u3066\u5229\u7528\u3067\u304D\u308B", async () => {
69
71
  const initialCount = 1;
70
72
  const maxCount = 3;
71
- const { result, act } = renderHook(
73
+ const { result, act } = await renderHook(
72
74
  () => useStep({ initialCount, maxCount })
73
75
  );
74
76
  act(() => {
@@ -82,7 +84,9 @@ describe("useStep", () => {
82
84
  it("\u5DE6\u53F3\u30AD\u30FC\u3067\u64CD\u4F5C\u3067\u304D\u308B", async () => {
83
85
  const initialCount = 1;
84
86
  const maxCount = 3;
85
- const { result } = renderHook(() => useStep({ initialCount, maxCount }));
87
+ const { result } = await renderHook(
88
+ () => useStep({ initialCount, maxCount })
89
+ );
86
90
  await userEvent.keyboard("{arrowright}");
87
91
  expect(result.current.count).toBe(initialCount + 1);
88
92
  await userEvent.keyboard("{arrowleft}");
@@ -1,28 +1,28 @@
1
1
  import { renderHook } from "vitest-browser-react";
2
2
  import { useTimeout } from ".";
3
3
  describe("useTimeout", () => {
4
- it("\u6307\u5B9A\u6642\u9593\u5F8C\u306B\u5B9F\u884C\u3055\u308C\u308B", () => {
4
+ it("\u6307\u5B9A\u6642\u9593\u5F8C\u306B\u5B9F\u884C\u3055\u308C\u308B", async () => {
5
5
  const fn = vi.fn();
6
6
  vi.useFakeTimers();
7
- renderHook(() => {
7
+ await renderHook(() => {
8
8
  useTimeout(fn, 1e3);
9
9
  });
10
10
  vi.advanceTimersByTime(1e3);
11
11
  expect(fn).toHaveBeenCalledOnce();
12
12
  });
13
- it("\u6307\u5B9A\u6642\u9593\u524D\u306B\u5B9F\u884C\u3055\u308C\u306A\u3044", () => {
13
+ it("\u6307\u5B9A\u6642\u9593\u524D\u306B\u5B9F\u884C\u3055\u308C\u306A\u3044", async () => {
14
14
  const fn = vi.fn();
15
15
  vi.useFakeTimers();
16
- renderHook(() => {
16
+ await renderHook(() => {
17
17
  useTimeout(fn, 1e3);
18
18
  });
19
19
  vi.advanceTimersByTime(10);
20
20
  expect(fn).not.toHaveBeenCalled();
21
21
  });
22
- it("\u6307\u5B9A\u6642\u9593\u524D\u306B\u30A2\u30F3\u30DE\u30A6\u30F3\u30C8\u3055\u308C\u306A\u3044\u5834\u5408\u306F\u5B9F\u884C\u3055\u308C\u306A\u3044", () => {
22
+ it("\u6307\u5B9A\u6642\u9593\u524D\u306B\u30A2\u30F3\u30DE\u30A6\u30F3\u30C8\u3055\u308C\u306A\u3044\u5834\u5408\u306F\u5B9F\u884C\u3055\u308C\u306A\u3044", async () => {
23
23
  const fn = vi.fn();
24
24
  vi.useFakeTimers();
25
- const { unmount } = renderHook(() => {
25
+ const { unmount } = await renderHook(() => {
26
26
  useTimeout(fn, 1e3);
27
27
  });
28
28
  unmount();
@@ -0,0 +1,10 @@
1
+ type Size = {
2
+ width: number;
3
+ height: number;
4
+ };
5
+ type Options = {
6
+ enabled?: boolean;
7
+ debounceMs?: number;
8
+ };
9
+ export declare const useWindowResize: (callback: (size: Size) => void, options?: Options) => void;
10
+ export {};
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ const useWindowResize = (callback, options = {}) => {
4
+ const { enabled = true, debounceMs } = options;
5
+ const timeoutRef = useRef(null);
6
+ useEffect(() => {
7
+ if (!enabled) return;
8
+ const handleResize = () => {
9
+ const size = {
10
+ width: window.innerWidth,
11
+ height: window.innerHeight
12
+ };
13
+ if (debounceMs !== void 0) {
14
+ if (timeoutRef.current) {
15
+ clearTimeout(timeoutRef.current);
16
+ }
17
+ timeoutRef.current = setTimeout(() => {
18
+ callback(size);
19
+ }, debounceMs);
20
+ } else {
21
+ callback(size);
22
+ }
23
+ };
24
+ window.addEventListener("resize", handleResize);
25
+ return () => {
26
+ if (timeoutRef.current) {
27
+ clearTimeout(timeoutRef.current);
28
+ }
29
+ window.removeEventListener("resize", handleResize);
30
+ };
31
+ }, [callback, enabled, debounceMs]);
32
+ };
33
+ export {
34
+ useWindowResize
35
+ };
@@ -0,0 +1,43 @@
1
+ import { renderHook } from "vitest-browser-react";
2
+ import { useWindowResize } from ".";
3
+ describe("useWindowResize", () => {
4
+ it("window\u30EA\u30B5\u30A4\u30BA\u6642\u306B\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u304C\u547C\u3070\u308C\u308B", async () => {
5
+ const resizedWindowSize = { width: 1e3, height: 1e3 };
6
+ const callback = vi.fn();
7
+ const { act } = await renderHook(() => useWindowResize(callback));
8
+ expect(callback).not.toHaveBeenCalled();
9
+ window.innerWidth = resizedWindowSize.width;
10
+ window.innerHeight = resizedWindowSize.height;
11
+ act(() => {
12
+ window.dispatchEvent(new Event("resize"));
13
+ });
14
+ expect(callback).toHaveBeenCalledWith(resizedWindowSize);
15
+ expect(callback).toHaveBeenCalledTimes(1);
16
+ });
17
+ it("enabled=false\u306E\u5834\u5408\u306F\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u304C\u547C\u3070\u308C\u306A\u3044", async () => {
18
+ const callback = vi.fn();
19
+ await renderHook(() => useWindowResize(callback, { enabled: false }));
20
+ expect(callback).not.toHaveBeenCalled();
21
+ });
22
+ it("debounceMs\u6307\u5B9A\u6642\u306F\u6307\u5B9A\u6642\u9593\u5F8C\u306B\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u304C\u547C\u3070\u308C\u308B", async () => {
23
+ vi.useFakeTimers();
24
+ const resizedWindowSize = { width: 1e3, height: 1e3 };
25
+ const callback = vi.fn();
26
+ const { act } = await renderHook(
27
+ () => useWindowResize(callback, { debounceMs: 300 })
28
+ );
29
+ expect(callback).not.toHaveBeenCalled();
30
+ window.innerWidth = resizedWindowSize.width;
31
+ window.innerHeight = resizedWindowSize.height;
32
+ act(() => {
33
+ window.dispatchEvent(new Event("resize"));
34
+ });
35
+ expect(callback).not.toHaveBeenCalled();
36
+ vi.advanceTimersByTime(299);
37
+ expect(callback).not.toHaveBeenCalled();
38
+ vi.advanceTimersByTime(1);
39
+ expect(callback).toHaveBeenCalledWith(resizedWindowSize);
40
+ expect(callback).toHaveBeenCalledTimes(1);
41
+ vi.useRealTimers();
42
+ });
43
+ });
@@ -1,12 +1,12 @@
1
1
  import { renderHook } from "vitest-browser-react";
2
2
  import { useWindowSize } from ".";
3
3
  describe("useWindowSize", () => {
4
- it("window\u30B5\u30A4\u30BA\u306E\u5909\u66F4\u306B\u5408\u308F\u305B\u3066\u73FE\u5728\u306Ewindow\u30B5\u30A4\u30BA\u3092\u53D6\u5F97\u3059\u308B", () => {
4
+ it("window\u30B5\u30A4\u30BA\u306E\u5909\u66F4\u306B\u5408\u308F\u305B\u3066\u73FE\u5728\u306Ewindow\u30B5\u30A4\u30BA\u3092\u53D6\u5F97\u3059\u308B", async () => {
5
5
  const initWindowSize = { width: 0, height: 0 };
6
6
  const resizedWindowSize = { width: 1e3, height: 1e3 };
7
7
  window.innerWidth = initWindowSize.width;
8
8
  window.innerHeight = initWindowSize.height;
9
- const { result, act } = renderHook(() => useWindowSize());
9
+ const { result, act } = await renderHook(() => useWindowSize());
10
10
  expect(result.current).toEqual(initWindowSize);
11
11
  window.innerWidth = resizedWindowSize.width;
12
12
  window.innerHeight = resizedWindowSize.height;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k8o/arte-odyssey",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "k8o's react ui library",
5
5
  "author": "k8o <kosakanoki@gmail.com>",
6
6
  "keywords": [
@@ -55,33 +55,33 @@
55
55
  "@floating-ui/react": "0.27.16",
56
56
  "baseline-status": "1.0.11",
57
57
  "clsx": "2.1.1",
58
- "esbuild": "0.25.10",
59
- "lucide-react": "0.545.0",
58
+ "esbuild": "0.25.12",
59
+ "lucide-react": "0.552.0",
60
60
  "motion": "12.23.24",
61
61
  "react-error-boundary": "6.0.0",
62
62
  "tailwind-merge": "3.3.1"
63
63
  },
64
64
  "devDependencies": {
65
- "@chromatic-com/storybook": "4.1.1",
66
- "@storybook/addon-a11y": "9.1.10",
67
- "@storybook/addon-docs": "9.1.10",
68
- "@storybook/addon-vitest": "9.1.10",
69
- "@storybook/react-vite": "9.1.10",
70
- "@tailwindcss/postcss": "4.1.14",
65
+ "@chromatic-com/storybook": "4.1.2",
66
+ "@storybook/addon-a11y": "10.0.2",
67
+ "@storybook/addon-docs": "10.0.2",
68
+ "@storybook/addon-vitest": "10.0.2",
69
+ "@storybook/react-vite": "10.0.2",
70
+ "@tailwindcss/postcss": "4.1.16",
71
71
  "@types/react": "19.2.2",
72
72
  "@types/react-dom": "19.2.2",
73
- "@vitejs/plugin-react-swc": "4.1.0",
74
- "@vitest/browser": "3.2.4",
75
- "@vitest/ui": "3.2.4",
73
+ "@vitejs/plugin-react-swc": "4.2.0",
74
+ "@vitest/browser-playwright": "4.0.6",
75
+ "@vitest/ui": "4.0.6",
76
76
  "postcss": "8.5.6",
77
77
  "react": "19.2.0",
78
78
  "react-dom": "19.2.0",
79
- "storybook": "9.1.10",
79
+ "storybook": "10.0.2",
80
80
  "storybook-addon-mock-date": "1.0.2",
81
- "tailwindcss": "4.1.14",
82
- "vite": "7.1.9",
83
- "vitest": "3.2.4",
84
- "vitest-browser-react": "1.0.1"
81
+ "tailwindcss": "4.1.16",
82
+ "vite": "7.1.12",
83
+ "vitest": "4.0.6",
84
+ "vitest-browser-react": "2.0.2"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "@types/react": ">=19.0.0",