@noya-app/noya-designsystem 0.1.44 → 0.1.46

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 (51) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +1194 -878
  5. package/dist/index.d.ts +1194 -878
  6. package/dist/index.js +11432 -10219
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +11556 -10360
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -4
  11. package/src/__tests__/combobox.test.ts +137 -89
  12. package/src/components/AnimatePresence.tsx +10 -1
  13. package/src/components/Avatar.tsx +2 -0
  14. package/src/components/Breadcrumbs.tsx +29 -0
  15. package/src/components/Checkbox.tsx +6 -1
  16. package/src/components/Collection.tsx +74 -0
  17. package/src/components/Combobox.tsx +75 -56
  18. package/src/components/ComboboxMenu.tsx +61 -28
  19. package/src/components/CommandPalette.tsx +69 -0
  20. package/src/components/ContextMenu.tsx +33 -134
  21. package/src/components/DropdownMenu.tsx +46 -155
  22. package/src/components/EditableText.tsx +203 -0
  23. package/src/components/Fade.tsx +62 -0
  24. package/src/components/Grid.tsx +243 -0
  25. package/src/components/GridView.tsx +86 -96
  26. package/src/components/InputField.tsx +109 -133
  27. package/src/components/Label.tsx +81 -7
  28. package/src/components/LabeledField.tsx +112 -0
  29. package/src/components/List.tsx +268 -0
  30. package/src/components/ListView.tsx +65 -61
  31. package/src/components/Popover.tsx +12 -1
  32. package/src/components/SearchCompletionMenu.tsx +210 -0
  33. package/src/components/SegmentedControl.tsx +12 -6
  34. package/src/components/SelectMenu.tsx +110 -126
  35. package/src/components/Slider.tsx +18 -26
  36. package/src/components/Text.tsx +1 -0
  37. package/src/components/TextArea.tsx +4 -1
  38. package/src/components/Toolbar.tsx +9 -4
  39. package/src/components/TreeView.tsx +4 -0
  40. package/src/components/WorkspaceLayout.tsx +64 -20
  41. package/src/components/internal/Menu.tsx +107 -18
  42. package/src/components/internal/MenuViewport.tsx +152 -0
  43. package/src/components/internal/SelectItem.tsx +111 -0
  44. package/src/hooks/useIndent.ts +12 -0
  45. package/src/hooks/useLabel.ts +51 -0
  46. package/src/hooks/usePreservePanelSize.tsx +50 -19
  47. package/src/index.tsx +15 -6
  48. package/src/utils/combobox.ts +116 -101
  49. package/src/utils/createSectionedMenu.ts +11 -14
  50. package/src/utils/fuzzyScorer.ts +11 -8
  51. package/src/utils/selection.ts +48 -0
@@ -1,19 +1,13 @@
1
- import { CheckIcon, ChevronRightIcon } from "@noya-app/noya-icons";
2
1
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
2
  import { memoGeneric } from "@noya-app/react-utils";
4
3
  import * as RadixContextMenu from "@radix-ui/react-context-menu";
5
4
  import React, { ReactNode, useCallback, useMemo } from "react";
6
- import { renderIcon } from "./Icons";
7
- import { Spacer } from "./Spacer";
8
5
  import {
9
- CHECKBOX_RIGHT_INSET,
10
- CHECKBOX_WIDTH,
11
- KeyboardShortcut,
12
6
  MenuItem,
13
- SEPARATOR_ITEM,
14
7
  getKeyboardShortcutsForMenuItems,
15
8
  styles,
16
9
  } from "./internal/Menu";
10
+ import { MenuViewport } from "./internal/MenuViewport";
17
11
 
18
12
  export interface MenuItemProps<T extends string> {
19
13
  value?: T;
@@ -27,90 +21,17 @@ export interface MenuItemProps<T extends string> {
27
21
  items?: MenuItem<T>[];
28
22
  }
29
23
 
30
- const ContextMenuItem = memoGeneric(function ContextMenuItem<T extends string>({
31
- value,
32
- children,
33
- onSelect,
34
- checked,
35
- disabled,
36
- indented,
37
- icon,
38
- items,
39
- shortcut,
40
- }: MenuItemProps<T>) {
41
- // The pointer event within the context menu will bubble outside of the
42
- // context menu unless we stop it here.
43
- const handlePointerDown = useCallback(
44
- (event: React.PointerEvent) => event.stopPropagation(),
45
- []
46
- );
47
-
48
- const handleSelectItem = useCallback(() => {
49
- if (!value) return;
50
-
51
- onSelect(value);
52
- }, [onSelect, value]);
53
-
54
- if (checked) {
55
- return (
56
- <RadixContextMenu.CheckboxItem
57
- checked={checked}
58
- disabled={disabled}
59
- onSelect={handleSelectItem}
60
- className={styles.itemStyle({ disabled })}
61
- >
62
- <RadixContextMenu.ItemIndicator className={styles.itemIndicatorStyle}>
63
- <CheckIcon />
64
- </RadixContextMenu.ItemIndicator>
65
- {children}
66
- </RadixContextMenu.CheckboxItem>
67
- );
68
- }
69
-
70
- const element = (
71
- <RadixContextMenu.Item
72
- className={styles.itemStyle({ disabled })}
73
- disabled={disabled}
74
- onSelect={handleSelectItem}
75
- onPointerDown={handlePointerDown}
76
- >
77
- {indented && (
78
- <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
79
- )}
80
- {icon && (
81
- <>
82
- {renderIcon(icon)}
83
- <Spacer.Horizontal size={8} />
84
- </>
85
- )}
86
- {children}
87
- {shortcut && (
88
- <>
89
- <Spacer.Horizontal />
90
- <Spacer.Horizontal size={24} />
91
- <KeyboardShortcut shortcut={shortcut} />
92
- </>
93
- )}
94
- {items && items.length > 0 && (
95
- <>
96
- <Spacer.Horizontal />
97
- <Spacer.Horizontal size={16} />
98
- <ChevronRightIcon />
99
- </>
100
- )}
101
- </RadixContextMenu.Item>
102
- );
103
-
104
- if (items && items.length > 0) {
105
- return (
106
- <ContextMenuRoot isNested items={items} onSelect={onSelect}>
107
- {element}
108
- </ContextMenuRoot>
109
- );
110
- } else {
111
- return element;
112
- }
113
- });
24
+ const Components = {
25
+ Separator: RadixContextMenu.Separator,
26
+ ItemText: React.Fragment,
27
+ Item: RadixContextMenu.Item,
28
+ CheckboxItem: RadixContextMenu.CheckboxItem,
29
+ ItemIndicator: RadixContextMenu.ItemIndicator,
30
+ SubTrigger: RadixContextMenu.SubTrigger,
31
+ SubContent: RadixContextMenu.SubContent,
32
+ Sub: RadixContextMenu.Sub,
33
+ Portal: RadixContextMenu.Portal,
34
+ };
114
35
 
115
36
  /* ----------------------------------------------------------------------------
116
37
  * Root
@@ -120,7 +41,6 @@ export interface MenuProps<T extends string> {
120
41
  children: ReactNode;
121
42
  items: MenuItem<T>[];
122
43
  onSelect: (value: T) => void;
123
- isNested?: boolean;
124
44
  shouldBindKeyboardShortcuts?: boolean;
125
45
  onOpenChange?: (open: boolean) => void;
126
46
  }
@@ -129,20 +49,15 @@ function ContextMenuRoot<T extends string>({
129
49
  items,
130
50
  children,
131
51
  onSelect,
132
- isNested,
133
52
  shouldBindKeyboardShortcuts,
134
53
  onOpenChange,
135
54
  }: MenuProps<T>) {
136
- const hasCheckedItem = items.some(
137
- (item) => item !== SEPARATOR_ITEM && item.checked
138
- );
139
-
140
55
  const keymap = useMemo(
141
56
  () =>
142
- isNested || shouldBindKeyboardShortcuts === false
57
+ shouldBindKeyboardShortcuts === false
143
58
  ? {}
144
59
  : getKeyboardShortcutsForMenuItems(items, onSelect),
145
- [isNested, items, onSelect, shouldBindKeyboardShortcuts]
60
+ [items, onSelect, shouldBindKeyboardShortcuts]
146
61
  );
147
62
 
148
63
  useKeyboardShortcuts(keymap);
@@ -155,46 +70,30 @@ function ContextMenuRoot<T extends string>({
155
70
  event.preventDefault();
156
71
  }, []);
157
72
 
158
- const RootComponent = isNested ? RadixContextMenu.Sub : RadixContextMenu.Root;
159
- const TriggerComponent = isNested
160
- ? RadixContextMenu.SubTrigger
161
- : RadixContextMenu.Trigger;
162
- const ContentComponent: typeof RadixContextMenu.Content = isNested
163
- ? RadixContextMenu.SubContent
164
- : RadixContextMenu.Content;
73
+ const handleSelectItem = useCallback(
74
+ (value: T) => {
75
+ if (!value) return;
76
+
77
+ onSelect(value);
78
+ },
79
+ [onSelect]
80
+ );
165
81
 
166
82
  return (
167
- <RootComponent onOpenChange={onOpenChange}>
168
- <TriggerComponent asChild onPointerDown={onPointerDown}>
83
+ <RadixContextMenu.Root onOpenChange={onOpenChange}>
84
+ <RadixContextMenu.Trigger asChild onPointerDown={onPointerDown}>
169
85
  {children}
170
- </TriggerComponent>
86
+ </RadixContextMenu.Trigger>
171
87
  <RadixContextMenu.Portal>
172
- <ContentComponent className={styles.contentStyle}>
173
- {items.map((item, index) =>
174
- item === SEPARATOR_ITEM ? (
175
- <RadixContextMenu.Separator
176
- key={index}
177
- className={styles.separatorStyle}
178
- />
179
- ) : (
180
- <ContextMenuItem
181
- key={item.value ?? index}
182
- value={item.value}
183
- indented={hasCheckedItem}
184
- checked={item.checked ?? false}
185
- disabled={item.disabled ?? false}
186
- icon={item.icon}
187
- onSelect={onSelect}
188
- items={item.items}
189
- shortcut={item.shortcut}
190
- >
191
- {item.title}
192
- </ContextMenuItem>
193
- )
194
- )}
195
- </ContentComponent>
88
+ <RadixContextMenu.Content className={styles.contentStyle}>
89
+ <MenuViewport
90
+ items={items}
91
+ Components={Components}
92
+ onSelect={handleSelectItem}
93
+ />
94
+ </RadixContextMenu.Content>
196
95
  </RadixContextMenu.Portal>
197
- </RootComponent>
96
+ </RadixContextMenu.Root>
198
97
  );
199
98
  }
200
99
 
@@ -1,4 +1,3 @@
1
- import { CheckIcon, ChevronRightIcon } from "@noya-app/noya-icons";
2
1
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
2
  import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
4
3
  import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
@@ -6,132 +5,45 @@ import React, {
6
5
  ComponentProps,
7
6
  ForwardedRef,
8
7
  SyntheticEvent,
9
- useCallback,
10
8
  useMemo,
11
9
  } from "react";
12
- import { MenuItemProps, MenuProps } from "./ContextMenu";
13
- import { renderIcon } from "./Icons";
14
- import { Spacer } from "./Spacer";
15
- import {
16
- CHECKBOX_RIGHT_INSET,
17
- CHECKBOX_WIDTH,
18
- KeyboardShortcut,
19
- SEPARATOR_ITEM,
20
- getKeyboardShortcutsForMenuItems,
21
- styles,
22
- } from "./internal/Menu";
23
-
24
- const DropdownMenuItem = memoGeneric(function ContextMenuItem<
25
- T extends string,
26
- >({
27
- value,
28
- children,
29
- onSelect,
30
- checked,
31
- disabled,
32
- indented,
33
- icon,
34
- items,
35
- shortcut,
36
- }: MenuItemProps<T>) {
37
- const handleSelectItem = useCallback(() => {
38
- if (!value) return;
39
-
40
- onSelect(value);
41
- }, [onSelect, value]);
42
-
43
- if (checked) {
44
- return (
45
- <RadixDropdownMenu.CheckboxItem
46
- checked={checked}
47
- disabled={disabled}
48
- onSelect={handleSelectItem}
49
- className={styles.itemStyle({ disabled })}
50
- >
51
- <RadixDropdownMenu.ItemIndicator className={styles.itemIndicatorStyle}>
52
- <CheckIcon />
53
- </RadixDropdownMenu.ItemIndicator>
54
- {icon && (
55
- <>
56
- {renderIcon(icon)}
57
- <Spacer.Horizontal size={8} />
58
- </>
59
- )}
60
- {children}
61
- </RadixDropdownMenu.CheckboxItem>
62
- );
63
- }
64
-
65
- const element = (
66
- <RadixDropdownMenu.Item
67
- disabled={disabled}
68
- onSelect={handleSelectItem}
69
- className={styles.itemStyle({ disabled })}
70
- >
71
- {indented && (
72
- <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
73
- )}
74
- {icon && (
75
- <>
76
- {renderIcon(icon)}
77
- <Spacer.Horizontal size={8} />
78
- </>
79
- )}
80
- {children}
81
- {shortcut && (
82
- <>
83
- <Spacer.Horizontal />
84
- <Spacer.Horizontal size={24} />
85
- <KeyboardShortcut shortcut={shortcut} />
86
- </>
87
- )}
88
- {items && items.length > 0 && (
89
- <>
90
- <Spacer.Horizontal />
91
- <Spacer.Horizontal size={16} />
92
- <ChevronRightIcon />
93
- </>
94
- )}
95
- </RadixDropdownMenu.Item>
96
- );
97
-
98
- if (items && items.length > 0) {
99
- return (
100
- <DropdownMenuRoot
101
- isNested
102
- items={items}
103
- onSelect={onSelect}
104
- alignOffset={-5}
105
- >
106
- {element}
107
- </DropdownMenuRoot>
108
- );
109
- } else {
110
- return element;
111
- }
112
- });
10
+ import { MenuProps } from "./ContextMenu";
11
+ import { getKeyboardShortcutsForMenuItems, styles } from "./internal/Menu";
12
+ import { MenuViewport } from "./internal/MenuViewport";
113
13
 
114
14
  /* ----------------------------------------------------------------------------
115
15
  * Root
116
16
  * ------------------------------------------------------------------------- */
117
17
 
118
- type DropdownRootProps<T extends string> = MenuProps<T> & {
18
+ export type DropdownRootProps<T extends string> = MenuProps<T> & {
119
19
  open?: boolean;
120
20
  onCloseAutoFocus?: React.EventHandler<SyntheticEvent<unknown>>;
121
21
  emptyState?: React.ReactNode;
22
+ contentStyle?: React.CSSProperties;
122
23
  } & Pick<
123
24
  ComponentProps<typeof RadixDropdownMenu.Content>,
124
25
  "side" | "sideOffset" | "align" | "alignOffset" | "collisionPadding"
125
26
  >;
126
27
 
28
+ const Components = {
29
+ Separator: RadixDropdownMenu.Separator,
30
+ ItemText: React.Fragment,
31
+ Item: RadixDropdownMenu.Item,
32
+ CheckboxItem: RadixDropdownMenu.CheckboxItem,
33
+ ItemIndicator: RadixDropdownMenu.ItemIndicator,
34
+ SubTrigger: RadixDropdownMenu.SubTrigger,
35
+ SubContent: RadixDropdownMenu.SubContent,
36
+ Sub: RadixDropdownMenu.Sub,
37
+ Portal: RadixDropdownMenu.Portal,
38
+ };
39
+
127
40
  const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
128
41
  T extends string,
129
- >(
130
- {
42
+ >(props: DropdownRootProps<T>, forwardedRef: ForwardedRef<HTMLElement>) {
43
+ const {
131
44
  items,
132
45
  children,
133
46
  onSelect,
134
- isNested,
135
47
  shouldBindKeyboardShortcuts,
136
48
  onOpenChange,
137
49
  open,
@@ -142,77 +54,56 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
142
54
  alignOffset,
143
55
  collisionPadding = 8,
144
56
  emptyState,
145
- }: DropdownRootProps<T>,
146
- forwardedRef: ForwardedRef<HTMLElement>
147
- ) {
148
- const hasCheckedItem = items.some(
149
- (item) => item !== SEPARATOR_ITEM && item.checked
150
- );
57
+ contentStyle,
58
+ } = props;
151
59
 
152
60
  const keymap = useMemo(
153
61
  () =>
154
- isNested || shouldBindKeyboardShortcuts === false
62
+ shouldBindKeyboardShortcuts === false
155
63
  ? {}
156
64
  : getKeyboardShortcutsForMenuItems(items, onSelect),
157
- [isNested, items, onSelect, shouldBindKeyboardShortcuts]
65
+ [items, onSelect, shouldBindKeyboardShortcuts]
158
66
  );
159
67
 
160
68
  useKeyboardShortcuts(keymap);
161
69
 
162
- const RootComponent = isNested
163
- ? RadixDropdownMenu.Sub
164
- : RadixDropdownMenu.Root;
165
- const TriggerComponent = isNested
166
- ? RadixDropdownMenu.SubTrigger
167
- : RadixDropdownMenu.Trigger;
168
- const ContentComponent = isNested
169
- ? RadixDropdownMenu.SubContent
170
- : RadixDropdownMenu.Content;
171
-
172
- const contentStyle = useMemo(() => ({ zIndex: 1000 }), []);
70
+ const contentStyles = useMemo(
71
+ () => ({
72
+ maxHeight: "500px",
73
+ overflow: "auto",
74
+ ...contentStyle,
75
+ }),
76
+ [contentStyle]
77
+ );
173
78
 
174
79
  return (
175
- <RootComponent onOpenChange={onOpenChange} open={open}>
176
- <TriggerComponent ref={forwardedRef as ForwardedRef<any>} asChild>
80
+ <RadixDropdownMenu.Root onOpenChange={onOpenChange} open={open}>
81
+ <RadixDropdownMenu.Trigger
82
+ ref={forwardedRef as ForwardedRef<any>}
83
+ asChild
84
+ >
177
85
  {children}
178
- </TriggerComponent>
86
+ </RadixDropdownMenu.Trigger>
179
87
  <RadixDropdownMenu.Portal>
180
- <ContentComponent
88
+ <RadixDropdownMenu.Content
181
89
  className={styles.contentStyle}
182
90
  side={side}
183
91
  sideOffset={sideOffset}
184
92
  align={align}
185
93
  alignOffset={alignOffset}
186
- style={contentStyle}
94
+ style={contentStyles}
187
95
  collisionPadding={collisionPadding}
188
96
  {...({ onCloseAutoFocus } as any)}
189
97
  >
190
98
  {items.length === 0 ? emptyState : null}
191
- {items.map((item, index) =>
192
- item === SEPARATOR_ITEM ? (
193
- <RadixDropdownMenu.Separator
194
- key={index}
195
- className={styles.separatorStyle}
196
- />
197
- ) : (
198
- <DropdownMenuItem
199
- key={item.value ?? index}
200
- value={item.value}
201
- indented={hasCheckedItem}
202
- checked={item.checked ?? false}
203
- disabled={item.disabled ?? false}
204
- icon={item.icon}
205
- onSelect={onSelect}
206
- items={item.items}
207
- shortcut={item.shortcut}
208
- >
209
- {item.title}
210
- </DropdownMenuItem>
211
- )
212
- )}
213
- </ContentComponent>
99
+ <MenuViewport
100
+ items={items}
101
+ Components={Components}
102
+ onSelect={onSelect}
103
+ />
104
+ </RadixDropdownMenu.Content>
214
105
  </RadixDropdownMenu.Portal>
215
- </RootComponent>
106
+ </RadixDropdownMenu.Root>
216
107
  );
217
108
  });
218
109
 
@@ -0,0 +1,203 @@
1
+ import { cx, InputField } from "@noya-app/noya-designsystem";
2
+ import React, {
3
+ forwardRef,
4
+ memo,
5
+ useCallback,
6
+ useEffect,
7
+ useImperativeHandle,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { BreadcrumbText } from "./Breadcrumbs";
12
+
13
+ type RenderPreviewProps = {
14
+ children: string;
15
+ onClick: () => void;
16
+ ref: React.Ref<HTMLElement & HTMLAnchorElement & HTMLSpanElement>;
17
+ tabIndex: number;
18
+ onKeyDown: (e: React.KeyboardEvent) => void;
19
+ style?: React.CSSProperties;
20
+ className?: string;
21
+ };
22
+
23
+ type Props = {
24
+ value: string;
25
+ children?: (props: RenderPreviewProps) => React.ReactNode;
26
+ placeholder?: string;
27
+ onChange?: (value: string) => void;
28
+ onSubmit?: (value: string) => void;
29
+ style?: React.CSSProperties;
30
+ textStyle?: React.CSSProperties;
31
+ className?: string;
32
+ id?: string;
33
+ disabled?: boolean;
34
+ focused?: boolean;
35
+ textClassName?: string;
36
+ readOnly?: boolean;
37
+ onBlur?: () => void;
38
+ /**
39
+ * If true, will render an unfocused input. This should only be used for visual regression tests.
40
+ */
41
+ testFocused?: boolean;
42
+ };
43
+
44
+ export interface EditableTextRef {
45
+ startEditing: () => void;
46
+ }
47
+
48
+ const defaultRenderPreview = (props: RenderPreviewProps) => (
49
+ <BreadcrumbText {...props} />
50
+ );
51
+
52
+ export const EditableText = memo(
53
+ forwardRef(function EditableText(
54
+ {
55
+ value,
56
+ onChange,
57
+ onSubmit,
58
+ focused: focusedProp,
59
+ disabled,
60
+ style,
61
+ textStyle,
62
+ className,
63
+ textClassName,
64
+ placeholder,
65
+ children = defaultRenderPreview,
66
+ readOnly,
67
+ onBlur,
68
+ testFocused = false,
69
+ ...props
70
+ }: Props,
71
+ ref: React.ForwardedRef<EditableTextRef>
72
+ ) {
73
+ const [inputValue, setInputValue] = useState(value);
74
+
75
+ useEffect(() => {
76
+ setInputValue(value);
77
+ }, [value]);
78
+
79
+ const [focusedInternal, setFocused] = useState(false);
80
+ const focused = focusedProp ?? focusedInternal;
81
+ const inputRef = useRef<HTMLInputElement>(null);
82
+ const previewRef = useRef<HTMLElement & HTMLAnchorElement>(null);
83
+
84
+ const ignoreNextBlur = useRef(false);
85
+
86
+ const handleKeyDown = useCallback(
87
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
88
+ if (event.key === "Escape") {
89
+ setInputValue(value);
90
+ ignoreNextBlur.current = true;
91
+ }
92
+
93
+ // Enter and Esc both blur the input
94
+ if (event.key === "Enter" || event.key === "Escape") {
95
+ inputRef.current?.blur();
96
+ }
97
+
98
+ if (event.key === "Escape") {
99
+ previewRef.current?.focus();
100
+ }
101
+
102
+ // If tab or shift+tab is pressed, let bubble
103
+ if (event.key === "Tab") {
104
+ return;
105
+ }
106
+
107
+ event.stopPropagation();
108
+ },
109
+ [value]
110
+ );
111
+
112
+ useImperativeHandle(ref, () => ({
113
+ startEditing: () => {
114
+ setFocused(true);
115
+ },
116
+ }));
117
+
118
+ function focusInput() {
119
+ inputRef.current?.focus();
120
+ inputRef.current?.setSelectionRange(0, inputRef.current.value.length);
121
+ }
122
+
123
+ useEffect(() => {
124
+ if (testFocused) return;
125
+ if (focused) {
126
+ // Use a timeout to focus the input after the next render.
127
+ // The input isn't available even though `focused` was already set to true
128
+ // must be a delay of 1ms or more to ensure any radix ui components have finished any focus behaviors
129
+ setTimeout(focusInput, 1);
130
+ }
131
+ }, [focused, testFocused]);
132
+
133
+ const displayValue = inputValue || placeholder || "";
134
+
135
+ const preview = children({
136
+ children: displayValue,
137
+ onClick: () => setFocused(true),
138
+ ref: previewRef,
139
+ tabIndex: 0,
140
+ onKeyDown: (e: React.KeyboardEvent) => {
141
+ if (e.key === "Enter" || e.key === " ") {
142
+ setFocused(true);
143
+ }
144
+ },
145
+ style: textStyle,
146
+ className: textClassName,
147
+ });
148
+
149
+ const input = (
150
+ <div className="flex absolute inset-0 -mx-1.5 -my-1">
151
+ <InputField.Root onFocusChange={setFocused}>
152
+ <InputField.Input
153
+ ref={inputRef}
154
+ style={{
155
+ lineHeight: "15px", // Match breadcrumb height
156
+ ...textStyle,
157
+ }}
158
+ className={textClassName}
159
+ {...props}
160
+ value={inputValue}
161
+ placeholder={placeholder}
162
+ onKeyDown={handleKeyDown}
163
+ onBlur={() => {
164
+ setFocused(false);
165
+
166
+ if (ignoreNextBlur.current) {
167
+ ignoreNextBlur.current = false;
168
+ return;
169
+ }
170
+
171
+ onSubmit?.(inputValue);
172
+ onBlur?.();
173
+ }}
174
+ {...(onChange
175
+ ? {
176
+ onChange: (value) => {
177
+ setInputValue(value);
178
+ onChange?.(value);
179
+ },
180
+ }
181
+ : {
182
+ onSubmit: (value) => {
183
+ setInputValue(value);
184
+ onSubmit?.(value);
185
+ setFocused(false);
186
+ },
187
+ onChange: (value) => {
188
+ setInputValue(value);
189
+ },
190
+ })}
191
+ />
192
+ </InputField.Root>
193
+ </div>
194
+ );
195
+
196
+ return (
197
+ <div className={cx("flex relative", className)} style={style}>
198
+ {preview}
199
+ {focused && !readOnly && input}
200
+ </div>
201
+ );
202
+ })
203
+ );