@rovula/ui 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/bundle.css +204 -26
- package/dist/cjs/bundle.js +675 -675
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Badge/Badge.d.ts +40 -0
- package/dist/cjs/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/cjs/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/cjs/types/components/Badge/index.d.ts +2 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.stories.d.ts +16 -0
- package/dist/cjs/types/index.d.ts +3 -1
- package/dist/cjs/types/patterns/menu/Menu.d.ts +72 -0
- package/dist/cjs/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +18 -10
- package/dist/cjs/types/utils/mergeRefs.d.ts +20 -0
- package/dist/components/ActionButton/ActionButton.styles.js +9 -1
- package/dist/components/Avatar/Avatar.styles.js +2 -2
- package/dist/components/Badge/Badge.js +36 -0
- package/dist/components/Badge/Badge.stories.js +51 -0
- package/dist/components/Badge/Badge.styles.js +62 -0
- package/dist/components/Badge/index.js +2 -0
- package/dist/components/Dropdown/Dropdown.js +54 -163
- package/dist/components/Dropdown/Dropdown.stories.js +29 -0
- package/dist/components/DropdownMenu/DropdownMenu.js +24 -11
- package/dist/components/DropdownMenu/DropdownMenu.stories.js +54 -10
- package/dist/components/TextInput/TextInput.js +9 -4
- package/dist/esm/bundle.css +204 -26
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Badge/Badge.d.ts +40 -0
- package/dist/esm/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/esm/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/esm/types/components/Badge/index.d.ts +2 -0
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.stories.d.ts +16 -0
- package/dist/esm/types/index.d.ts +3 -1
- package/dist/esm/types/patterns/menu/Menu.d.ts +72 -0
- package/dist/esm/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +18 -10
- package/dist/esm/types/utils/mergeRefs.d.ts +20 -0
- package/dist/index.d.ts +118 -73
- package/dist/index.js +2 -1
- package/dist/patterns/menu/Menu.js +95 -0
- package/dist/patterns/menu/Menu.stories.js +611 -0
- package/dist/src/theme/global.css +393 -43
- package/dist/utils/mergeRefs.js +42 -0
- package/package.json +1 -1
- package/src/components/ActionButton/ActionButton.styles.ts +9 -1
- package/src/components/Avatar/Avatar.styles.ts +2 -2
- package/src/components/Badge/Badge.stories.tsx +128 -0
- package/src/components/Badge/Badge.styles.ts +70 -0
- package/src/components/Badge/Badge.tsx +103 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +170 -1
- package/src/components/Dropdown/Dropdown.tsx +186 -276
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +1050 -113
- package/src/components/DropdownMenu/DropdownMenu.tsx +117 -56
- package/src/components/TextInput/TextInput.tsx +42 -32
- package/src/index.ts +3 -1
- package/src/patterns/menu/Menu.stories.tsx +1100 -0
- package/src/patterns/menu/Menu.tsx +286 -0
- package/src/theme/presets/colors.js +14 -0
- package/src/theme/themes/variable-mapping.css +30 -0
- package/src/theme/themes/variable.css +37 -6
- package/src/theme/themes/xspector/baseline.css +0 -1
- package/src/theme/tokens/baseline.css +2 -1
- package/src/theme/tokens/components/badge.css +54 -0
- package/src/theme/tokens/components/dropdown-menu.css +15 -4
- package/src/utils/mergeRefs.ts +46 -0
- package/dist/cjs/types/components/Menu/Menu.d.ts +0 -65
- package/dist/cjs/types/components/Menu/helpers.d.ts +0 -19
- package/dist/cjs/types/components/Menu/index.d.ts +0 -4
- package/dist/components/Menu/Menu.js +0 -64
- package/dist/components/Menu/Menu.stories.js +0 -406
- package/dist/components/Menu/helpers.js +0 -28
- package/dist/components/Menu/index.js +0 -3
- package/dist/esm/types/components/Menu/Menu.d.ts +0 -65
- package/dist/esm/types/components/Menu/helpers.d.ts +0 -19
- package/dist/esm/types/components/Menu/index.d.ts +0 -4
- package/src/components/Menu/Menu.stories.tsx +0 -586
- package/src/components/Menu/Menu.tsx +0 -235
- package/src/components/Menu/helpers.ts +0 -45
- package/src/components/Menu/index.ts +0 -7
- package/src/theme/themes/xspector/components/dropdown-menu.css +0 -28
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import React, {
|
|
2
|
-
CSSProperties,
|
|
3
4
|
ReactNode,
|
|
4
5
|
forwardRef,
|
|
5
6
|
useCallback,
|
|
@@ -10,13 +11,31 @@ import React, {
|
|
|
10
11
|
useState,
|
|
11
12
|
Fragment,
|
|
12
13
|
} from "react";
|
|
13
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
Combobox,
|
|
16
|
+
ComboboxInput,
|
|
17
|
+
ComboboxOptions,
|
|
18
|
+
ComboboxOption,
|
|
19
|
+
} from "@headlessui/react";
|
|
14
20
|
import TextInput, { InputProps } from "../TextInput/TextInput";
|
|
15
21
|
import { customInputVariant, dropdownIconVariant } from "./Dropdown.styles";
|
|
16
|
-
import { Menu, MenuItemType, MenuOption } from "../Menu/Menu";
|
|
17
|
-
|
|
18
|
-
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
19
22
|
import { cn } from "@/utils/cn";
|
|
23
|
+
import Icon from "../Icon/Icon";
|
|
24
|
+
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Shared menu item styles (used by Dropdown items + renderOptions consumers)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
export const menuItemBaseStyles = cn(
|
|
30
|
+
"relative flex gap-1 cursor-pointer select-none box-border items-center py-4 pl-4 pr-8 typography-subtitle4 outline-none transition-colors",
|
|
31
|
+
"bg-[var(--dropdown-menu-default-bg)] text-[var(--dropdown-menu-default-text)]",
|
|
32
|
+
"active:opacity-75",
|
|
33
|
+
"hover:bg-[var(--dropdown-menu-hover-bg)] hover:text-[var(--dropdown-menu-hover-text)]",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Types
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
20
39
|
|
|
21
40
|
type RenderLabelCallbackArg = {
|
|
22
41
|
value: string;
|
|
@@ -37,7 +56,6 @@ export type DropdownProps = {
|
|
|
37
56
|
size?: "sm" | "md" | "lg";
|
|
38
57
|
rounded?: "none" | "normal" | "full";
|
|
39
58
|
variant?: "flat" | "outline" | "underline";
|
|
40
|
-
defaultMenuItemType?: MenuOption["type"];
|
|
41
59
|
helperText?: string;
|
|
42
60
|
errorMessage?: string;
|
|
43
61
|
filterMode?: boolean;
|
|
@@ -45,6 +63,7 @@ export type DropdownProps = {
|
|
|
45
63
|
disabled?: boolean;
|
|
46
64
|
error?: boolean;
|
|
47
65
|
required?: boolean;
|
|
66
|
+
/** @deprecated no longer needed */
|
|
48
67
|
modal?: boolean;
|
|
49
68
|
className?: string;
|
|
50
69
|
optionContainerClassName?: string;
|
|
@@ -59,11 +78,13 @@ export type DropdownProps = {
|
|
|
59
78
|
optionsFiltered: Options[];
|
|
60
79
|
selectedOption: Options | null | undefined;
|
|
61
80
|
onClick: (option: Options) => void;
|
|
62
|
-
style?: CSSProperties;
|
|
63
|
-
dropdownRef?: React.RefObject<HTMLUListElement>;
|
|
64
81
|
}) => ReactNode;
|
|
65
82
|
} & Omit<InputProps, "value" | "onSelect">;
|
|
66
83
|
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Dropdown
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
67
88
|
const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
68
89
|
(
|
|
69
90
|
{
|
|
@@ -74,7 +95,6 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
74
95
|
size = "md",
|
|
75
96
|
rounded = "normal",
|
|
76
97
|
variant = "outline",
|
|
77
|
-
defaultMenuItemType = "checkbox",
|
|
78
98
|
helperText,
|
|
79
99
|
errorMessage,
|
|
80
100
|
fullwidth = true,
|
|
@@ -82,7 +102,7 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
82
102
|
error = false,
|
|
83
103
|
filterMode = false,
|
|
84
104
|
required = true,
|
|
85
|
-
modal
|
|
105
|
+
modal: _modal,
|
|
86
106
|
onChangeText,
|
|
87
107
|
onSelect,
|
|
88
108
|
renderOptions: customRenderOptions,
|
|
@@ -96,305 +116,195 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
96
116
|
) => {
|
|
97
117
|
const _id = id || `${label}-select`;
|
|
98
118
|
|
|
99
|
-
const [
|
|
100
|
-
const [
|
|
101
|
-
Options | null | undefined
|
|
102
|
-
>(null);
|
|
103
|
-
const [textValue, setTextValue] = useState("");
|
|
104
|
-
const keyCode = useRef("");
|
|
119
|
+
const [selectedOption, setSelectedOption] = useState<Options | null>(null);
|
|
120
|
+
const [query, setQuery] = useState("");
|
|
105
121
|
|
|
106
|
-
const dropdownRef = useRef<HTMLUListElement>(null);
|
|
107
122
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
108
|
-
const [dropdownStyles, setDropdownStyles] = useState<CSSProperties>({});
|
|
109
|
-
const [isAbove, setIsAbove] = useState(false);
|
|
110
|
-
const [isInsideDialog, setIsInsideDialog] = useState(false);
|
|
111
123
|
|
|
112
|
-
|
|
124
|
+
// Expose the inner <input> element via forwardRef
|
|
125
|
+
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
|
|
113
126
|
|
|
127
|
+
// Sync external value prop
|
|
114
128
|
useEffect(() => {
|
|
115
|
-
setSelectedOption(value);
|
|
116
|
-
setTextValue(value?.label ?? "");
|
|
129
|
+
setSelectedOption(value ?? null);
|
|
117
130
|
}, [value]);
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
node = node.parentElement;
|
|
128
|
-
}
|
|
129
|
-
setIsInsideDialog(false);
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
const handleOnChangeText = useCallback(
|
|
133
|
-
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
134
|
-
onChangeText?.(event);
|
|
135
|
-
setTextValue(event.target.value);
|
|
132
|
+
const optionsFiltered = useMemo(() => {
|
|
133
|
+
if (!filterMode || !query) return options;
|
|
134
|
+
return options.filter((opt) =>
|
|
135
|
+
opt.label.toLowerCase().includes(query.toLowerCase()),
|
|
136
|
+
);
|
|
137
|
+
}, [options, filterMode, query]);
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
const handleSelect = useCallback(
|
|
140
|
+
(option: Options | null) => {
|
|
141
|
+
setSelectedOption(option);
|
|
142
|
+
setQuery("");
|
|
143
|
+
if (option) {
|
|
144
|
+
onSelect?.(option);
|
|
145
|
+
// After selection Headless UI keeps focus on the input (via rAF refocus).
|
|
146
|
+
// Blur after that rAF so the next click triggers onFocus → immediate open.
|
|
147
|
+
setTimeout(() => inputRef.current?.blur(), 0);
|
|
139
148
|
}
|
|
140
149
|
},
|
|
141
|
-
[
|
|
150
|
+
[onSelect],
|
|
142
151
|
);
|
|
143
152
|
|
|
144
|
-
const
|
|
145
|
-
(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
onSelect?.(option);
|
|
149
|
-
setIsFocused(false);
|
|
153
|
+
const handleInputChange = useCallback(
|
|
154
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
155
|
+
if (filterMode) setQuery(e.target.value);
|
|
156
|
+
onChangeText?.(e);
|
|
150
157
|
},
|
|
151
|
-
[
|
|
158
|
+
[filterMode, onChangeText],
|
|
152
159
|
);
|
|
153
160
|
|
|
154
|
-
const
|
|
155
|
-
return options.filter(
|
|
156
|
-
(option) =>
|
|
157
|
-
!filterMode ||
|
|
158
|
-
option.label?.toLowerCase().includes(textValue?.toLowerCase()),
|
|
159
|
-
);
|
|
160
|
-
}, [options, filterMode, textValue]);
|
|
161
|
-
|
|
162
|
-
const usePortal = isInsideDialog ? false : modal;
|
|
163
|
-
|
|
164
|
-
const updateDropdownPosition = useCallback(() => {
|
|
165
|
-
if (inputRef.current && dropdownRef.current) {
|
|
166
|
-
const rect = inputRef.current.getBoundingClientRect();
|
|
167
|
-
const dropdownHeight = dropdownRef.current.offsetHeight;
|
|
168
|
-
const spaceBelow = window.innerHeight - rect.bottom;
|
|
169
|
-
const spaceAbove = rect.top;
|
|
170
|
-
|
|
171
|
-
const shouldOpenAbove =
|
|
172
|
-
spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
|
|
173
|
-
setIsAbove(shouldOpenAbove);
|
|
174
|
-
|
|
175
|
-
if (usePortal) {
|
|
176
|
-
setDropdownStyles({
|
|
177
|
-
position: "absolute",
|
|
178
|
-
top: shouldOpenAbove
|
|
179
|
-
? `${rect.top - dropdownHeight}px`
|
|
180
|
-
: `${rect.bottom}px`,
|
|
181
|
-
left: `${rect.left}px`,
|
|
182
|
-
width: `${rect.width}px`,
|
|
183
|
-
zIndex: 9999,
|
|
184
|
-
});
|
|
185
|
-
} else {
|
|
186
|
-
setDropdownStyles({
|
|
187
|
-
position: "absolute",
|
|
188
|
-
top: shouldOpenAbove ? `-${dropdownHeight}px` : "100%",
|
|
189
|
-
left: "0",
|
|
190
|
-
width: "100%",
|
|
191
|
-
zIndex: 9999,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}, [modal, isInsideDialog, usePortal]);
|
|
196
|
-
|
|
197
|
-
useEffect(() => {
|
|
198
|
-
if (isFocused) {
|
|
199
|
-
updateDropdownPosition();
|
|
200
|
-
window.addEventListener("resize", updateDropdownPosition);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return () => {
|
|
204
|
-
window.removeEventListener("resize", updateDropdownPosition);
|
|
205
|
-
};
|
|
206
|
-
}, [isFocused, updateDropdownPosition]);
|
|
207
|
-
|
|
208
|
-
const renderOptions = () => {
|
|
161
|
+
const renderOptionList = () => {
|
|
209
162
|
if (customRenderOptions) {
|
|
210
163
|
return customRenderOptions({
|
|
211
164
|
optionsFiltered,
|
|
212
165
|
selectedOption,
|
|
213
|
-
onClick:
|
|
214
|
-
style: dropdownStyles,
|
|
215
|
-
dropdownRef,
|
|
166
|
+
onClick: handleSelect,
|
|
216
167
|
});
|
|
217
168
|
}
|
|
218
169
|
|
|
219
|
-
|
|
220
|
-
|
|
170
|
+
if (optionsFiltered.length === 0) {
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
className={cn(
|
|
174
|
+
"px-4 py-14 text-center text-input-text",
|
|
175
|
+
optionNotFoundItemClassName,
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
Not found
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
221
182
|
|
|
222
|
-
|
|
183
|
+
return optionsFiltered.map((option) => {
|
|
223
184
|
if (option.renderLabel) {
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
selectedOption?.value === option.value,
|
|
240
|
-
},
|
|
241
|
-
optionItemClassName,
|
|
242
|
-
),
|
|
243
|
-
})}
|
|
244
|
-
</Fragment>
|
|
245
|
-
),
|
|
246
|
-
};
|
|
185
|
+
return (
|
|
186
|
+
<Fragment key={option.value}>
|
|
187
|
+
{option.renderLabel({
|
|
188
|
+
value: option.value,
|
|
189
|
+
label: option.label,
|
|
190
|
+
handleOnClick: () => handleSelect(option),
|
|
191
|
+
className: cn(
|
|
192
|
+
menuItemBaseStyles,
|
|
193
|
+
selectedOption?.value === option.value &&
|
|
194
|
+
"bg-[var(--dropdown-menu-selected-bg)] text-[var(--dropdown-menu-selected-text)]",
|
|
195
|
+
optionItemClassName,
|
|
196
|
+
),
|
|
197
|
+
})}
|
|
198
|
+
</Fragment>
|
|
199
|
+
);
|
|
247
200
|
}
|
|
248
201
|
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
type: "custom",
|
|
263
|
-
render: () => (
|
|
264
|
-
<div
|
|
265
|
-
key="not-found"
|
|
266
|
-
className={cn(
|
|
267
|
-
"px-4 py-14 text-center text-input-text",
|
|
268
|
-
optionNotFoundItemClassName,
|
|
269
|
-
)}
|
|
270
|
-
>
|
|
271
|
-
Not found
|
|
272
|
-
</div>
|
|
273
|
-
),
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return (
|
|
278
|
-
<Menu
|
|
279
|
-
ref={dropdownRef as any}
|
|
280
|
-
items={finalMenuItems}
|
|
281
|
-
selectedValues={selectedOption?.value ? [selectedOption.value] : []}
|
|
282
|
-
onSelect={(value) => {
|
|
283
|
-
const option = optionsFiltered.find((opt) => opt.value === value);
|
|
284
|
-
if (option) {
|
|
285
|
-
handleOptionClick(option);
|
|
202
|
+
return (
|
|
203
|
+
<ComboboxOption
|
|
204
|
+
key={option.value}
|
|
205
|
+
value={option}
|
|
206
|
+
className={({ focus, selected }) =>
|
|
207
|
+
cn(
|
|
208
|
+
menuItemBaseStyles,
|
|
209
|
+
(selected || selectedOption?.value === option.value) &&
|
|
210
|
+
"bg-[var(--dropdown-menu-selected-bg)] text-[var(--dropdown-menu-selected-text)]",
|
|
211
|
+
focus &&
|
|
212
|
+
"bg-[var(--dropdown-menu-hover-bg)] text-[var(--dropdown-menu-hover-text)]",
|
|
213
|
+
optionItemClassName,
|
|
214
|
+
)
|
|
286
215
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
[props?.onFocus],
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
const clearMismatchValue = useCallback(
|
|
307
|
-
(e: React.FocusEvent<HTMLInputElement, Element>) => {
|
|
308
|
-
const matchSelectedValue = optionsFiltered.find(
|
|
309
|
-
(opt) =>
|
|
310
|
-
opt.value === e.target?.value || opt.label === e.target?.value,
|
|
216
|
+
>
|
|
217
|
+
{({ selected }) => (
|
|
218
|
+
<>
|
|
219
|
+
<span className="shrink-0 size-4 flex items-center justify-center">
|
|
220
|
+
{(selected || selectedOption?.value === option.value) && (
|
|
221
|
+
<Icon
|
|
222
|
+
type="heroicons"
|
|
223
|
+
name="check"
|
|
224
|
+
className="size-4 text-[var(--dropdown-menu-selected-text)]"
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</span>
|
|
228
|
+
{option.label}
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
231
|
+
</ComboboxOption>
|
|
311
232
|
);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
let option = matchSelectedValue || {
|
|
315
|
-
value: "",
|
|
316
|
-
label: "",
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
if (!isMatchSelectedValue && textValue) {
|
|
320
|
-
option = {
|
|
321
|
-
value: "",
|
|
322
|
-
label: "",
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (keyCode.current === "Enter") {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
setSelectedOption(option);
|
|
331
|
-
setTextValue(option.label);
|
|
332
|
-
onSelect?.(option);
|
|
333
|
-
},
|
|
334
|
-
[optionsFiltered, textValue],
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
const handleOnBlur = useCallback(
|
|
338
|
-
(e: React.FocusEvent<HTMLInputElement, Element>) => {
|
|
339
|
-
setTimeout(() => setIsFocused(false), 200);
|
|
340
|
-
clearMismatchValue(e);
|
|
341
|
-
props?.onBlur?.(e);
|
|
342
|
-
},
|
|
343
|
-
[props?.onBlur, clearMismatchValue],
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
const handleOnKeyDown = useCallback(
|
|
347
|
-
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
348
|
-
keyCode.current = e.code;
|
|
349
|
-
props?.onKeyDown?.(e);
|
|
350
|
-
},
|
|
351
|
-
[props?.onKeyDown],
|
|
352
|
-
);
|
|
233
|
+
});
|
|
234
|
+
};
|
|
353
235
|
|
|
354
236
|
return (
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
237
|
+
<Combobox
|
|
238
|
+
value={selectedOption}
|
|
239
|
+
onChange={handleSelect}
|
|
240
|
+
immediate
|
|
241
|
+
by="value"
|
|
242
|
+
disabled={disabled}
|
|
243
|
+
>
|
|
244
|
+
{({ open }) => (
|
|
245
|
+
<div className={cn("relative", fullwidth && "w-full")}>
|
|
246
|
+
{/*
|
|
247
|
+
* ComboboxInput as={TextInput}:
|
|
248
|
+
* Now that TextInput uses mergeRefs(externalRef, internalRef) on <input>,
|
|
249
|
+
* Headless UI receives a direct HTMLInputElement ref — no imperative handle
|
|
250
|
+
* wrapper — so it can attach its event listeners correctly.
|
|
251
|
+
*/}
|
|
252
|
+
<ComboboxInput
|
|
253
|
+
as={TextInput}
|
|
254
|
+
ref={inputRef}
|
|
255
|
+
hasClearIcon={false}
|
|
256
|
+
endIcon={
|
|
257
|
+
<ChevronDownIcon
|
|
258
|
+
className={dropdownIconVariant({ isFocus: open })}
|
|
259
|
+
/>
|
|
260
|
+
}
|
|
261
|
+
label={label}
|
|
262
|
+
placeholder=" "
|
|
263
|
+
autoComplete="off"
|
|
264
|
+
rounded={rounded}
|
|
265
|
+
variant={variant}
|
|
266
|
+
helperText={helperText}
|
|
267
|
+
errorMessage={errorMessage}
|
|
268
|
+
fullwidth={fullwidth}
|
|
269
|
+
error={error}
|
|
270
|
+
required={required}
|
|
271
|
+
id={_id}
|
|
272
|
+
disabled={disabled}
|
|
273
|
+
size={size}
|
|
274
|
+
className={
|
|
275
|
+
segmentedInput ? customInputVariant({ size }) : undefined
|
|
276
|
+
}
|
|
277
|
+
displayValue={(opt: unknown) =>
|
|
278
|
+
(opt as Options | null)?.label ?? ""
|
|
279
|
+
}
|
|
280
|
+
readOnly={!filterMode}
|
|
281
|
+
onChange={handleInputChange}
|
|
282
|
+
{...props}
|
|
361
283
|
/>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
className={segmentedInput ? customInputVariant({ size }) : undefined}
|
|
383
|
-
onFocus={handleOnFocus}
|
|
384
|
-
onBlur={handleOnBlur}
|
|
385
|
-
onKeyDown={handleOnKeyDown}
|
|
386
|
-
/>
|
|
387
|
-
{isFocused &&
|
|
388
|
-
(usePortal ? (
|
|
389
|
-
<Portal.Root container={document.body}>
|
|
390
|
-
{renderOptions()}
|
|
391
|
-
</Portal.Root>
|
|
392
|
-
) : (
|
|
393
|
-
renderOptions()
|
|
394
|
-
))}
|
|
395
|
-
</div>
|
|
284
|
+
|
|
285
|
+
{/*
|
|
286
|
+
* No anchor = no forced portal — content stays inside Dialog DOM.
|
|
287
|
+
* Radix Dialog won't see option clicks as "outside" → Dialog won't close.
|
|
288
|
+
* z-[51] beats the Dialog overlay (z-50) for items that visually overflow.
|
|
289
|
+
*/}
|
|
290
|
+
<ComboboxOptions
|
|
291
|
+
className={cn(
|
|
292
|
+
"absolute top-full left-0 w-full -mt-3 z-[51]",
|
|
293
|
+
"min-w-[154px] max-h-60 overflow-y-auto",
|
|
294
|
+
"rounded-md bg-modal-dropdown-surface border border-bg-stroke3 text-text-g-contrast-high",
|
|
295
|
+
optionContainerClassName,
|
|
296
|
+
)}
|
|
297
|
+
style={{ boxShadow: "var(--dropdown-menu-shadow)" }}
|
|
298
|
+
>
|
|
299
|
+
{renderOptionList()}
|
|
300
|
+
</ComboboxOptions>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</Combobox>
|
|
396
304
|
);
|
|
397
305
|
},
|
|
398
306
|
);
|
|
399
307
|
|
|
308
|
+
Dropdown.displayName = "Dropdown";
|
|
309
|
+
|
|
400
310
|
export default Dropdown;
|