@noya-app/noya-designsystem 0.0.2

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 (73) hide show
  1. package/.turbo/turbo-build.log +17 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +1 -0
  4. package/dist/index.d.mts +1178 -0
  5. package/dist/index.d.ts +1178 -0
  6. package/dist/index.js +15651 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.mjs +15684 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/package.json +49 -0
  11. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +41 -0
  12. package/src/__tests__/fuzzyScorer.test.ts +85 -0
  13. package/src/components/ActivityIndicator.tsx +44 -0
  14. package/src/components/Avatar.tsx +64 -0
  15. package/src/components/Button.tsx +209 -0
  16. package/src/components/Chip.tsx +214 -0
  17. package/src/components/ContextMenu.tsx +224 -0
  18. package/src/components/Dialog.tsx +143 -0
  19. package/src/components/Divider.tsx +40 -0
  20. package/src/components/DropdownMenu.tsx +208 -0
  21. package/src/components/FillInputField.tsx +43 -0
  22. package/src/components/FillPreviewBackground.tsx +132 -0
  23. package/src/components/GradientPicker.tsx +88 -0
  24. package/src/components/Grid.tsx +54 -0
  25. package/src/components/GridView.tsx +468 -0
  26. package/src/components/IconButton.tsx +46 -0
  27. package/src/components/InputField.tsx +568 -0
  28. package/src/components/InputFieldWithCompletions.tsx +477 -0
  29. package/src/components/Label.tsx +62 -0
  30. package/src/components/LabeledElementView.tsx +185 -0
  31. package/src/components/ListView.tsx +950 -0
  32. package/src/components/Popover.tsx +113 -0
  33. package/src/components/Progress.tsx +57 -0
  34. package/src/components/RadioGroup.tsx +163 -0
  35. package/src/components/ScrollArea.tsx +70 -0
  36. package/src/components/Select.tsx +152 -0
  37. package/src/components/Slider.tsx +82 -0
  38. package/src/components/Sortable.tsx +296 -0
  39. package/src/components/Spacer.tsx +29 -0
  40. package/src/components/Stack.tsx +142 -0
  41. package/src/components/Switch.tsx +67 -0
  42. package/src/components/Text.tsx +164 -0
  43. package/src/components/Toast.tsx +86 -0
  44. package/src/components/Tooltip.tsx +43 -0
  45. package/src/components/TreeView.tsx +85 -0
  46. package/src/components/internal/Menu.tsx +222 -0
  47. package/src/components/internal/TextInput.tsx +296 -0
  48. package/src/components/internal/__tests__/TextInput.test.tsx +144 -0
  49. package/src/contexts/DesignSystemConfiguration.tsx +57 -0
  50. package/src/contexts/DialogContext.tsx +190 -0
  51. package/src/contexts/GlobalInputBlurContext.tsx +61 -0
  52. package/src/contexts/ImageDataContext.tsx +44 -0
  53. package/src/hooks/__tests__/mergeEventHandlers.test.ts +47 -0
  54. package/src/hooks/mergeEventHandlers.ts +55 -0
  55. package/src/hooks/useHover.ts +173 -0
  56. package/src/hooks/useObjectURL.ts +22 -0
  57. package/src/hooks/usePlatform.ts +13 -0
  58. package/src/index.tsx +77 -0
  59. package/src/mediaQuery.ts +13 -0
  60. package/src/theme/dark.ts +37 -0
  61. package/src/theme/index.ts +17 -0
  62. package/src/theme/light.ts +178 -0
  63. package/src/utils/breakpoints.ts +45 -0
  64. package/src/utils/completions.ts +21 -0
  65. package/src/utils/createSectionedMenu.ts +38 -0
  66. package/src/utils/fuzzyScorer.ts +105 -0
  67. package/src/utils/getGradientBackground.tsx +33 -0
  68. package/src/utils/handleNudge.ts +30 -0
  69. package/src/utils/mouseEvent.ts +7 -0
  70. package/src/utils/sketchColor.ts +34 -0
  71. package/src/utils/sketchPattern.ts +29 -0
  72. package/src/utils/withSeparatorElements.ts +31 -0
  73. package/tsconfig.json +3 -0
@@ -0,0 +1,296 @@
1
+ import { composeRefs } from "@radix-ui/react-compose-refs";
2
+ import React, {
3
+ AriaRole,
4
+ FocusEventHandler,
5
+ ForwardedRef,
6
+ forwardRef,
7
+ InputHTMLAttributes,
8
+ KeyboardEventHandler,
9
+ MouseEventHandler,
10
+ PointerEventHandler,
11
+ useCallback,
12
+ useEffect,
13
+ useLayoutEffect,
14
+ useRef,
15
+ useState,
16
+ } from "react";
17
+ import { useGlobalInputBlurListener } from "../../contexts/GlobalInputBlurContext";
18
+
19
+ type Props = {
20
+ id?: string;
21
+ style?: any;
22
+ className?: string;
23
+ type?: "text" | "search";
24
+ disabled?: boolean;
25
+ value: string;
26
+ placeholder?: string;
27
+ role?: AriaRole;
28
+ name?: HTMLInputElement["name"];
29
+ onKeyDown?: KeyboardEventHandler;
30
+ onClick?: MouseEventHandler;
31
+ onDoubleClick?: MouseEventHandler;
32
+ onPointerDown?: PointerEventHandler;
33
+ onFocusCapture?: FocusEventHandler;
34
+ onFocusChange?: (isFocused: boolean) => void;
35
+ onBlur?: FocusEventHandler;
36
+ } & Pick<
37
+ InputHTMLAttributes<HTMLInputElement>,
38
+ "autoComplete" | "autoCapitalize" | "autoCorrect" | "spellCheck" | "autoFocus"
39
+ >;
40
+
41
+ type ReadOnlyProps = Props & {
42
+ readOnly: true;
43
+ };
44
+
45
+ const ReadOnlyTextInput = forwardRef(function ReadOnlyTextInput(
46
+ { onKeyDown, onFocusChange, value, ...rest }: ReadOnlyProps,
47
+ forwardedRef: ForwardedRef<HTMLInputElement>
48
+ ) {
49
+ return (
50
+ <input
51
+ ref={forwardedRef}
52
+ {...rest}
53
+ value={value}
54
+ onKeyDown={onKeyDown}
55
+ readOnly
56
+ />
57
+ );
58
+ });
59
+
60
+ type ControlledProps = Props & {
61
+ onChange: (value: string) => void;
62
+ };
63
+
64
+ const ControlledTextInput = forwardRef(function ControlledTextInput(
65
+ {
66
+ onKeyDown,
67
+ value,
68
+ onChange,
69
+ onFocusChange,
70
+ onBlur,
71
+ onFocusCapture,
72
+ ...rest
73
+ }: ControlledProps,
74
+ forwardedRef: ForwardedRef<HTMLInputElement>
75
+ ) {
76
+ const handleBlur = useCallback(
77
+ (event: React.FocusEvent<HTMLInputElement>) => {
78
+ onBlur?.(event);
79
+
80
+ onFocusChange?.(false);
81
+ },
82
+ [onBlur, onFocusChange]
83
+ );
84
+
85
+ const handleFocusCapture = useCallback(
86
+ (event: React.FocusEvent<HTMLInputElement>) => {
87
+ onFocusCapture?.(event);
88
+
89
+ onFocusChange?.(true);
90
+ },
91
+ [onFocusCapture, onFocusChange]
92
+ );
93
+
94
+ return (
95
+ <input
96
+ ref={forwardedRef}
97
+ {...rest}
98
+ value={value}
99
+ onKeyDown={onKeyDown}
100
+ onChange={useCallback(
101
+ (event: React.ChangeEvent<HTMLInputElement>) =>
102
+ onChange(event.target.value),
103
+ [onChange]
104
+ )}
105
+ onBlur={handleBlur}
106
+ onFocusCapture={handleFocusCapture}
107
+ />
108
+ );
109
+ });
110
+
111
+ type SubmittableProps = Props & {
112
+ onSubmit: (value: string) => void;
113
+ allowSubmittingWithSameValue?: boolean;
114
+ submitAutomaticallyAfterDelay?: number;
115
+ };
116
+
117
+ const SubmittableTextInput = forwardRef(function SubmittableTextInput(
118
+ {
119
+ onKeyDown,
120
+ value,
121
+ onSubmit,
122
+ onBlur,
123
+ onFocusChange,
124
+ onFocusCapture,
125
+ allowSubmittingWithSameValue = false,
126
+ submitAutomaticallyAfterDelay,
127
+ ...rest
128
+ }: SubmittableProps,
129
+ forwardedRef: ForwardedRef<HTMLInputElement>
130
+ ) {
131
+ const ref = React.useRef<HTMLInputElement>(null);
132
+
133
+ const latestValue = useRef(value);
134
+ latestValue.current = value;
135
+
136
+ // Track whether the user has edited the input since the last submission.
137
+ // We use this to support automatically submitting the input after a delay.
138
+ const userDidEdit = useRef(false);
139
+
140
+ const isSubmitTriggeredByEscapeKey = useRef(false);
141
+
142
+ const [internalValue, setInternalValue] = useState("");
143
+
144
+ useLayoutEffect(() => {
145
+ setInternalValue(value);
146
+ }, [value]);
147
+
148
+ const handleSubmit = useCallback(() => {
149
+ // If this submission was triggered with Escape, we attempt to submit the original value.
150
+ // This will only actually call `onSubmit` if `allowSubmittingWithSameValue` is also true.
151
+ const submissionValue = isSubmitTriggeredByEscapeKey.current
152
+ ? value
153
+ : internalValue;
154
+
155
+ isSubmitTriggeredByEscapeKey.current = false;
156
+
157
+ if (submissionValue === value && !allowSubmittingWithSameValue) return;
158
+
159
+ onSubmit(submissionValue);
160
+
161
+ userDidEdit.current = false;
162
+
163
+ setInternalValue(latestValue.current);
164
+ }, [allowSubmittingWithSameValue, value, internalValue, onSubmit]);
165
+
166
+ useGlobalInputBlurListener(
167
+ useCallback(() => {
168
+ if (ref.current !== document.activeElement) return;
169
+
170
+ handleSubmit();
171
+ }, [handleSubmit])
172
+ );
173
+
174
+ const handleKeyDown = useCallback(
175
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
176
+ if (event.key === "Enter") {
177
+ handleSubmit();
178
+
179
+ event.preventDefault();
180
+ event.stopPropagation();
181
+ } else if (event.key === "Escape") {
182
+ isSubmitTriggeredByEscapeKey.current = true;
183
+
184
+ ref.current?.blur();
185
+ } else {
186
+ onKeyDown?.(event);
187
+ }
188
+ },
189
+ [onKeyDown, handleSubmit]
190
+ );
191
+
192
+ const handleChange = useCallback(
193
+ (event: React.ChangeEvent<HTMLInputElement>) => {
194
+ userDidEdit.current = true;
195
+
196
+ setInternalValue(event.target.value);
197
+ },
198
+ []
199
+ );
200
+
201
+ const handleBlur = useCallback(
202
+ (event: React.FocusEvent<HTMLInputElement>) => {
203
+ onBlur?.(event);
204
+
205
+ onFocusChange?.(false);
206
+
207
+ handleSubmit();
208
+ },
209
+ [onBlur, onFocusChange, handleSubmit]
210
+ );
211
+
212
+ const handleFocusCapture = useCallback(
213
+ (event: React.FocusEvent<HTMLInputElement>) => {
214
+ onFocusCapture?.(event);
215
+
216
+ onFocusChange?.(true);
217
+ },
218
+ [onFocusCapture, onFocusChange]
219
+ );
220
+
221
+ const autoSubmitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
222
+ null
223
+ );
224
+ const latestHandleSubmit = useRef(handleSubmit);
225
+ latestHandleSubmit.current = handleSubmit;
226
+
227
+ useEffect(() => {
228
+ if (submitAutomaticallyAfterDelay) {
229
+ if (autoSubmitTimeoutRef.current) {
230
+ clearTimeout(autoSubmitTimeoutRef.current);
231
+ }
232
+
233
+ autoSubmitTimeoutRef.current = setTimeout(() => {
234
+ if (userDidEdit.current === false) return;
235
+
236
+ latestHandleSubmit.current();
237
+ }, submitAutomaticallyAfterDelay);
238
+ }
239
+
240
+ // Clear the timeout when the component is unmounted or if the value changes
241
+ return () => {
242
+ if (autoSubmitTimeoutRef.current) {
243
+ clearTimeout(autoSubmitTimeoutRef.current);
244
+ }
245
+ };
246
+ // Track `internalValue` to explicitly reset the timer
247
+ }, [internalValue, submitAutomaticallyAfterDelay]);
248
+
249
+ return (
250
+ <input
251
+ ref={composeRefs(ref, forwardedRef)}
252
+ {...rest}
253
+ value={internalValue}
254
+ onKeyDown={handleKeyDown}
255
+ onChange={handleChange}
256
+ onBlur={handleBlur}
257
+ onFocusCapture={handleFocusCapture}
258
+ />
259
+ );
260
+ });
261
+
262
+ export type TextInputProps = ReadOnlyProps | ControlledProps | SubmittableProps;
263
+
264
+ /**
265
+ * This component shouldn't be used directly. Instead use the InputField components.
266
+ */
267
+ export default forwardRef(function TextInput(
268
+ props: TextInputProps,
269
+ forwardedRef: ForwardedRef<HTMLInputElement>
270
+ ) {
271
+ const commonProps: TextInputProps = {
272
+ onPointerDown: useCallback(
273
+ (event: React.PointerEvent) => event.stopPropagation(),
274
+ []
275
+ ),
276
+ onClick: useCallback(
277
+ (event: React.MouseEvent) => event.stopPropagation(),
278
+ []
279
+ ),
280
+ autoComplete: "off",
281
+ autoCapitalize: "off",
282
+ autoCorrect: "off",
283
+ spellCheck: false,
284
+ type: "text",
285
+ disabled: false,
286
+ ...props,
287
+ };
288
+
289
+ if ("readOnly" in commonProps) {
290
+ return <ReadOnlyTextInput ref={forwardedRef} {...commonProps} readOnly />;
291
+ } else if ("onChange" in commonProps) {
292
+ return <ControlledTextInput ref={forwardedRef} {...commonProps} />;
293
+ } else {
294
+ return <SubmittableTextInput ref={forwardedRef} {...commonProps} />;
295
+ }
296
+ });
@@ -0,0 +1,144 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import TextInput from '../TextInput';
4
+
5
+ describe('submittable text input', () => {
6
+ test('change and blur', () => {
7
+ let text = '';
8
+ const handleSubmit = (value: string) => {
9
+ text = value;
10
+ };
11
+
12
+ const { container, rerender } = render(
13
+ <TextInput value={text} onSubmit={handleSubmit} />,
14
+ );
15
+
16
+ const input = container.querySelector('input')!;
17
+ fireEvent.change(input, { target: { value: 'abc' } });
18
+ fireEvent.blur(input);
19
+
20
+ expect(text).toEqual('abc');
21
+ expect(input.value).toEqual('');
22
+
23
+ rerender(<TextInput value={text} onSubmit={handleSubmit} />);
24
+
25
+ expect(input.value).toEqual('abc');
26
+ });
27
+
28
+ test('change and blur, reset', () => {
29
+ const handleSubmit = (value: string) => {};
30
+
31
+ const { container, rerender } = render(
32
+ <TextInput value="" onSubmit={handleSubmit} />,
33
+ );
34
+
35
+ const input = container.querySelector('input')!;
36
+ fireEvent.change(input, { target: { value: 'abc' } });
37
+ fireEvent.blur(input);
38
+
39
+ expect(input.value).toEqual('');
40
+
41
+ rerender(<TextInput value="def" onSubmit={handleSubmit} />);
42
+
43
+ expect(input.value).toEqual('def');
44
+ });
45
+
46
+ test('submit without changing', () => {
47
+ const handleSubmit = jest.fn();
48
+
49
+ const { container } = render(
50
+ <TextInput value="" onSubmit={handleSubmit} />,
51
+ );
52
+
53
+ const input = container.querySelector('input')!;
54
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
55
+
56
+ expect(handleSubmit).not.toBeCalled();
57
+ });
58
+
59
+ test('change and submit', () => {
60
+ let text = '';
61
+ const handleSubmit = (value: string) => {
62
+ text = value;
63
+ };
64
+
65
+ const { container, rerender } = render(
66
+ <TextInput value={text} onSubmit={handleSubmit} />,
67
+ );
68
+
69
+ const input = container.querySelector('input')!;
70
+ fireEvent.change(input, { target: { value: 'abc' } });
71
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
72
+
73
+ expect(text).toEqual('abc');
74
+ expect(input.value).toEqual('');
75
+
76
+ rerender(<TextInput value={text} onSubmit={handleSubmit} />);
77
+
78
+ expect(input.value).toEqual('abc');
79
+
80
+ fireEvent.change(input, { target: { value: 'abcdef' } });
81
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
82
+
83
+ expect(text).toEqual('abcdef');
84
+ expect(input.value).toEqual('abc');
85
+
86
+ rerender(<TextInput value={text} onSubmit={handleSubmit} />);
87
+
88
+ expect(input.value).toEqual('abcdef');
89
+ });
90
+
91
+ test('change and submit, reset', () => {
92
+ const handleSubmit = (value: string) => {};
93
+
94
+ const { container, rerender } = render(
95
+ <TextInput value="" onSubmit={handleSubmit} />,
96
+ );
97
+
98
+ const input = container.querySelector('input')!;
99
+ fireEvent.change(input, { target: { value: 'abc' } });
100
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
101
+
102
+ expect(input.value).toEqual('');
103
+
104
+ rerender(<TextInput value="" onSubmit={handleSubmit} />);
105
+
106
+ expect(input.value).toEqual('');
107
+ });
108
+
109
+ test('submit only once', () => {
110
+ const handleSubmit = jest.fn();
111
+
112
+ const { container } = render(
113
+ <TextInput value="" onSubmit={handleSubmit} />,
114
+ );
115
+
116
+ const input = container.querySelector('input')!;
117
+ fireEvent.change(input, { target: { value: 'abc' } });
118
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
119
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
120
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
121
+
122
+ expect(handleSubmit).toBeCalledTimes(1);
123
+ });
124
+
125
+ test('submit multiple times', () => {
126
+ const handleSubmit = jest.fn();
127
+
128
+ const { container } = render(
129
+ <TextInput
130
+ value=""
131
+ onSubmit={handleSubmit}
132
+ allowSubmittingWithSameValue
133
+ />,
134
+ );
135
+
136
+ const input = container.querySelector('input')!;
137
+ fireEvent.change(input, { target: { value: 'abc' } });
138
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
139
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
140
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
141
+
142
+ expect(handleSubmit).toBeCalledTimes(3);
143
+ });
144
+ });
@@ -0,0 +1,57 @@
1
+ import { PlatformName } from '@noya-app/noya-keymap';
2
+ import React, {
3
+ createContext,
4
+ memo,
5
+ ReactNode,
6
+ useContext,
7
+ useMemo,
8
+ } from 'react';
9
+ import { ThemeProvider, useTheme } from 'styled-components';
10
+ import { ToastProvider } from '../components/Toast';
11
+ import { Theme } from '../theme';
12
+ import { DialogProvider } from './DialogContext';
13
+
14
+ export type DesignSystemConfigurationContextValue = {
15
+ platform: PlatformName;
16
+ };
17
+
18
+ const DesignSystemConfigurationContext = createContext<
19
+ DesignSystemConfigurationContextValue | undefined
20
+ >(undefined);
21
+
22
+ export const DesignSystemConfigurationProvider = memo(
23
+ function DesignSystemConfigurationProvider({
24
+ children,
25
+ theme,
26
+ platform,
27
+ }: {
28
+ children: ReactNode;
29
+ theme: Theme;
30
+ } & DesignSystemConfigurationContextValue) {
31
+ const contextValue = useMemo(() => ({ platform }), [platform]);
32
+
33
+ return (
34
+ <DesignSystemConfigurationContext.Provider value={contextValue}>
35
+ <ThemeProvider theme={theme}>
36
+ <DialogProvider>
37
+ <ToastProvider>{children}</ToastProvider>
38
+ </DialogProvider>
39
+ </ThemeProvider>
40
+ </DesignSystemConfigurationContext.Provider>
41
+ );
42
+ },
43
+ );
44
+
45
+ export function useDesignSystemConfiguration(): DesignSystemConfigurationContextValue {
46
+ const value = useContext(DesignSystemConfigurationContext);
47
+
48
+ if (!value) {
49
+ throw new Error('Missing DesignSystemConfigurationProvider');
50
+ }
51
+
52
+ return value;
53
+ }
54
+
55
+ export function useDesignSystemTheme() {
56
+ return useTheme();
57
+ }
@@ -0,0 +1,190 @@
1
+ import React, {
2
+ createContext,
3
+ ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import styled from "styled-components";
11
+ import { Button } from "../components/Button";
12
+ import { Dialog, IDialog } from "../components/Dialog";
13
+ import { InputField } from "../components/InputField";
14
+ import { Spacer } from "../components/Spacer";
15
+
16
+ export const Row = styled.div(({ theme }) => ({
17
+ flex: "1",
18
+ display: "flex",
19
+ flexDirection: "row",
20
+ alignItems: "center",
21
+ }));
22
+
23
+ function createDeferredPromise<T>() {
24
+ let resolve: (value: T) => void = () => {};
25
+ let reject: (value: any) => void = () => {};
26
+
27
+ const promise = new Promise<T>((res, rej) => {
28
+ resolve = res;
29
+ reject = rej;
30
+ });
31
+
32
+ return { promise, resolve, reject };
33
+ }
34
+
35
+ export type DialogContextValue = {
36
+ openInputDialog(
37
+ title: string,
38
+ inputValue?: string
39
+ ): Promise<string | undefined>;
40
+
41
+ containsElement(element: HTMLElement): boolean;
42
+ };
43
+
44
+ const DialogContext = createContext<DialogContextValue | undefined>(undefined);
45
+
46
+ export const DialogProvider = function DialogProvider({
47
+ children,
48
+ }: {
49
+ children: ReactNode;
50
+ }) {
51
+ const [contents, setContents] = useState<
52
+ | {
53
+ title: string;
54
+ inputValue: string;
55
+ resolve: (value: string | undefined) => void;
56
+ }
57
+ | undefined
58
+ >();
59
+
60
+ const isOpen = !!contents;
61
+
62
+ const close = useCallback(() => {
63
+ if (!contents) return;
64
+
65
+ contents.resolve(undefined);
66
+ }, [contents]);
67
+
68
+ const submit = useCallback(() => {
69
+ if (!contents || !contents.inputValue) return;
70
+
71
+ contents.resolve(contents.inputValue);
72
+ setContents(undefined);
73
+ }, [contents]);
74
+
75
+ const open: DialogContextValue["openInputDialog"] = useCallback(
76
+ (title, inputValue = "") => {
77
+ const { promise, resolve } = createDeferredPromise<string | undefined>();
78
+
79
+ setContents({
80
+ title,
81
+ inputValue,
82
+ resolve: (value) => {
83
+ resolve(value);
84
+ setContents(undefined);
85
+ },
86
+ });
87
+
88
+ return promise;
89
+ },
90
+ []
91
+ );
92
+
93
+ const handleKeyDown = useCallback(
94
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
95
+ if (event.key !== "Enter") return;
96
+
97
+ event.stopPropagation();
98
+ event.preventDefault();
99
+
100
+ submit();
101
+ },
102
+ [submit]
103
+ );
104
+
105
+ const inputRef = useRef<HTMLInputElement>(null);
106
+ const dialogRef = useRef<IDialog>(null);
107
+
108
+ const containsElement = useCallback((element: HTMLElement) => {
109
+ if (!dialogRef.current) return false;
110
+
111
+ return dialogRef.current.containsElement(element);
112
+ }, []);
113
+
114
+ return (
115
+ <>
116
+ <DialogContext.Provider
117
+ value={useMemo(
118
+ () => ({ openInputDialog: open, containsElement }),
119
+ [containsElement, open]
120
+ )}
121
+ >
122
+ {children}
123
+ </DialogContext.Provider>
124
+ <Dialog
125
+ ref={dialogRef}
126
+ title={contents?.title}
127
+ open={isOpen}
128
+ onOpenChange={useCallback(
129
+ (isOpen: boolean) => {
130
+ if (!isOpen) {
131
+ close();
132
+ }
133
+ },
134
+ [close]
135
+ )}
136
+ onOpenAutoFocus={useCallback((event: Event) => {
137
+ event.stopPropagation();
138
+ event.preventDefault();
139
+
140
+ inputRef.current?.focus();
141
+ }, [])}
142
+ >
143
+ <InputField.Root>
144
+ <InputField.Input
145
+ ref={inputRef}
146
+ value={contents?.inputValue ?? ""}
147
+ onChange={(value) => {
148
+ setContents((contents) =>
149
+ contents
150
+ ? {
151
+ ...contents,
152
+ inputValue: value,
153
+ }
154
+ : undefined
155
+ );
156
+ }}
157
+ onKeyDown={handleKeyDown}
158
+ />
159
+ </InputField.Root>
160
+ <Spacer.Vertical size={20} />
161
+ <Row>
162
+ <Spacer.Horizontal />
163
+ <Button onClick={close}>Cancel</Button>
164
+ <Spacer.Horizontal size={16} />
165
+ <Button disabled={!contents?.inputValue} onClick={submit}>
166
+ Submit
167
+ </Button>
168
+ </Row>
169
+ </Dialog>
170
+ </>
171
+ );
172
+ };
173
+
174
+ function useDialog(): DialogContextValue {
175
+ const value = useContext(DialogContext);
176
+
177
+ if (!value) {
178
+ throw new Error("Missing DialogProvider");
179
+ }
180
+
181
+ return value;
182
+ }
183
+
184
+ export function useOpenInputDialog() {
185
+ return useDialog().openInputDialog;
186
+ }
187
+
188
+ export function useDialogContainsElement() {
189
+ return useDialog().containsElement;
190
+ }