@seedgrid/fe-components 2026.3.31-3 → 2026.3.31-5
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/inputs/SgAutocomplete.d.ts +1 -0
- package/dist/inputs/SgAutocomplete.d.ts.map +1 -1
- package/dist/inputs/SgAutocomplete.js +84 -2
- package/dist/inputs/SgCombobox.d.ts +1 -0
- package/dist/inputs/SgCombobox.d.ts.map +1 -1
- package/dist/inputs/SgCombobox.js +72 -15
- package/dist/sandbox.cjs +51 -51
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ export type SgAutocompleteProps<T = SgAutocompleteItem> = Omit<SgInputTextProps,
|
|
|
36
36
|
renderFooter?: (query: string, hasResults: boolean) => React.ReactNode;
|
|
37
37
|
renderEmpty?: (query: string) => React.ReactNode;
|
|
38
38
|
formatSelection?: (item: SgAutocompleteItem) => string;
|
|
39
|
+
renderValue?: (item: SgAutocompleteItem | null) => React.ReactNode;
|
|
39
40
|
itemTooltip?: (item: SgAutocompleteItem) => React.ReactNode;
|
|
40
41
|
} & RhfFieldProps;
|
|
41
42
|
export declare function SgAutocomplete<T = SgAutocompleteItem>(props: SgAutocompleteProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgAutocomplete.d.ts","sourceRoot":"","sources":["../../src/inputs/SgAutocomplete.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SgAutocomplete.d.ts","sourceRoot":"","sources":["../../src/inputs/SgAutocomplete.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,EAAwC,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAClF,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGnE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oBAAoB,CAAC,CAAC,GAAG,kBAAkB,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;AAEjG,MAAM,MAAM,mBAAmB,CAAC,CAAC,GAAG,kBAAkB,IAAI,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG;IACvG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,kBAAkB,CAAC;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9E,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IACvD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IACvE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IACjD,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,MAAM,CAAC;IACvD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,KAAK,KAAK,CAAC,SAAS,CAAC;IACnE,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,KAAK,CAAC,SAAS,CAAC;CAC7D,GAAG,aAAa,CAAC;AAQlB,wBAAgB,cAAc,CAAC,CAAC,GAAG,kBAAkB,EAAE,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,2CAyCnF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import { ChevronDown } from "lucide-react";
|
|
5
6
|
import { Controller } from "react-hook-form";
|
|
6
7
|
import { mergeRequiredRule, resolveFieldError } from "../rhf";
|
|
@@ -19,7 +20,7 @@ export function SgAutocomplete(props) {
|
|
|
19
20
|
return (_jsx(SgAutocompleteBase, { ...rest, name: name, register: register, rules: resolvedRules }));
|
|
20
21
|
}
|
|
21
22
|
function SgAutocompleteBase(props) {
|
|
22
|
-
const { value, onChange, source, mapItem, minLengthForSearch = 2, delay = 300, maxResult = 50, cacheEnabled = true, cacheTTL = 5 * 60 * 1000, showDropDownButton = false, openOnFocus = false, clearOnSelect = false, allowCustomValue = false, grouped, groupped, placeholderEmpty: placeholderEmptyProp, loadingText: loadingTextProp, onSelect, onSearch, onOpenChange, renderItem, renderGroupHeader, renderFooter, renderEmpty, formatSelection, itemTooltip, inputProps, iconButtons, enabled, readOnly, borderRadius, ...rest } = props;
|
|
23
|
+
const { value, onChange, source, mapItem, minLengthForSearch = 2, delay = 300, maxResult = 50, cacheEnabled = true, cacheTTL = 5 * 60 * 1000, showDropDownButton = false, openOnFocus = false, clearOnSelect = false, allowCustomValue = false, grouped, groupped, placeholderEmpty: placeholderEmptyProp, loadingText: loadingTextProp, onSelect, onSearch, onOpenChange, renderItem, renderGroupHeader, renderFooter, renderEmpty, formatSelection, renderValue, itemTooltip, inputProps, iconButtons, enabled, readOnly, borderRadius, ...rest } = props;
|
|
23
24
|
const i18n = useComponentsI18n();
|
|
24
25
|
const placeholderEmpty = placeholderEmptyProp ?? t(i18n, "components.autocomplete.empty");
|
|
25
26
|
const loadingText = loadingTextProp ?? t(i18n, "components.autocomplete.loading");
|
|
@@ -29,9 +30,13 @@ function SgAutocompleteBase(props) {
|
|
|
29
30
|
const [loading, setLoading] = React.useState(false);
|
|
30
31
|
const [open, setOpen] = React.useState(false);
|
|
31
32
|
const wrapperRef = React.useRef(null);
|
|
33
|
+
const inputRef = React.useRef(null);
|
|
34
|
+
const dropdownRef = React.useRef(null);
|
|
32
35
|
const ignoreBlurRef = React.useRef(false);
|
|
33
36
|
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
34
37
|
const [lastSelected, setLastSelected] = React.useState("");
|
|
38
|
+
const [selectedItem, setSelectedItem] = React.useState(null);
|
|
39
|
+
const [dropdownStyle, setDropdownStyle] = React.useState({});
|
|
35
40
|
const cacheRef = React.useRef(new Map());
|
|
36
41
|
const requestIdRef = React.useRef(0);
|
|
37
42
|
const openRef = React.useRef(false);
|
|
@@ -47,6 +52,18 @@ function SgAutocompleteBase(props) {
|
|
|
47
52
|
setInputValue(value);
|
|
48
53
|
setLastSelected(value);
|
|
49
54
|
}, [value]);
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (!value) {
|
|
57
|
+
setSelectedItem(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setSelectedItem((current) => {
|
|
61
|
+
if (current && (current.value === value || current.label === value)) {
|
|
62
|
+
return current;
|
|
63
|
+
}
|
|
64
|
+
return current;
|
|
65
|
+
});
|
|
66
|
+
}, [value]);
|
|
50
67
|
const toItem = React.useCallback((raw) => {
|
|
51
68
|
if (mapItem)
|
|
52
69
|
return mapItem(raw);
|
|
@@ -112,6 +129,7 @@ function SgAutocompleteBase(props) {
|
|
|
112
129
|
if (item.disabled)
|
|
113
130
|
return;
|
|
114
131
|
const selection = formatSelection ? formatSelection(item) : item.label;
|
|
132
|
+
setSelectedItem(item);
|
|
115
133
|
setInputValue(selection);
|
|
116
134
|
onChange?.(selection);
|
|
117
135
|
setLastSelected(selection);
|
|
@@ -124,6 +142,9 @@ function SgAutocompleteBase(props) {
|
|
|
124
142
|
}
|
|
125
143
|
};
|
|
126
144
|
const handleInputChange = (next) => {
|
|
145
|
+
if (selectedItem && next !== (formatSelection ? formatSelection(selectedItem) : selectedItem.label)) {
|
|
146
|
+
setSelectedItem(null);
|
|
147
|
+
}
|
|
127
148
|
setInputValue(next);
|
|
128
149
|
onChange?.(next);
|
|
129
150
|
if (next.length === 0) {
|
|
@@ -157,6 +178,10 @@ function SgAutocompleteBase(props) {
|
|
|
157
178
|
onOpenChange?.(false);
|
|
158
179
|
};
|
|
159
180
|
const handleFocus = () => {
|
|
181
|
+
if (renderValue && selectedItem) {
|
|
182
|
+
const selection = formatSelection ? formatSelection(selectedItem) : selectedItem.label;
|
|
183
|
+
setInputValue(selection);
|
|
184
|
+
}
|
|
160
185
|
if (openOnFocus) {
|
|
161
186
|
setOpen(true);
|
|
162
187
|
onOpenChange?.(true);
|
|
@@ -185,6 +210,21 @@ function SgAutocompleteBase(props) {
|
|
|
185
210
|
onOpenChange?.(false);
|
|
186
211
|
}
|
|
187
212
|
};
|
|
213
|
+
const syncDropdownPosition = React.useCallback(() => {
|
|
214
|
+
const anchor = wrapperRef.current?.querySelector("input") ?? inputRef.current ?? wrapperRef.current;
|
|
215
|
+
if (!anchor)
|
|
216
|
+
return;
|
|
217
|
+
const rect = anchor.getBoundingClientRect();
|
|
218
|
+
setDropdownStyle({
|
|
219
|
+
position: "fixed",
|
|
220
|
+
top: rect.bottom + 4,
|
|
221
|
+
left: rect.left,
|
|
222
|
+
minWidth: rect.width,
|
|
223
|
+
width: "max-content",
|
|
224
|
+
maxWidth: "min(32rem, calc(100vw - 24px))",
|
|
225
|
+
borderRadius: resolvedBorderRadius
|
|
226
|
+
});
|
|
227
|
+
}, [resolvedBorderRadius]);
|
|
188
228
|
const dropdownButton = showDropDownButton ? (_jsx("button", { type: "button", className: "text-foreground/60 hover:text-foreground", onMouseDown: (event) => {
|
|
189
229
|
event.preventDefault();
|
|
190
230
|
ignoreBlurRef.current = true;
|
|
@@ -236,13 +276,53 @@ function SgAutocompleteBase(props) {
|
|
|
236
276
|
}, onClick: () => selectItem(item), children: [renderItem ? renderItem(item, isActive) : item.label, itemTooltip ? (_jsx("div", { className: "pointer-events-none absolute left-full top-1/2 z-20 ml-2 -translate-y-1/2 rounded border border-border bg-[rgb(var(--sg-surface,var(--sg-bg)))] px-2 py-1 text-xs text-[rgb(var(--sg-text,var(--sg-fg)))] shadow-md opacity-0 transition-opacity group-hover:opacity-100", children: itemTooltip(item) })) : null] }, item.id));
|
|
237
277
|
});
|
|
238
278
|
};
|
|
279
|
+
React.useEffect(() => {
|
|
280
|
+
if (!open)
|
|
281
|
+
return;
|
|
282
|
+
const handleOutside = (event) => {
|
|
283
|
+
if (wrapperRef.current?.contains(event.target))
|
|
284
|
+
return;
|
|
285
|
+
if (dropdownRef.current?.contains(event.target))
|
|
286
|
+
return;
|
|
287
|
+
if (!allowCustomValue && lastSelected && inputValue !== lastSelected) {
|
|
288
|
+
setInputValue(lastSelected);
|
|
289
|
+
onChange?.(lastSelected);
|
|
290
|
+
}
|
|
291
|
+
setOpen(false);
|
|
292
|
+
onOpenChange?.(false);
|
|
293
|
+
};
|
|
294
|
+
document.addEventListener("mousedown", handleOutside);
|
|
295
|
+
return () => document.removeEventListener("mousedown", handleOutside);
|
|
296
|
+
}, [allowCustomValue, inputValue, lastSelected, onChange, onOpenChange, open]);
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
if (!open)
|
|
299
|
+
return;
|
|
300
|
+
syncDropdownPosition();
|
|
301
|
+
const handleLayoutChange = () => {
|
|
302
|
+
syncDropdownPosition();
|
|
303
|
+
};
|
|
304
|
+
window.addEventListener("resize", handleLayoutChange);
|
|
305
|
+
window.addEventListener("scroll", handleLayoutChange, true);
|
|
306
|
+
return () => {
|
|
307
|
+
window.removeEventListener("resize", handleLayoutChange);
|
|
308
|
+
window.removeEventListener("scroll", handleLayoutChange, true);
|
|
309
|
+
};
|
|
310
|
+
}, [open, syncDropdownPosition]);
|
|
239
311
|
return (_jsxs("div", { className: "relative", ref: wrapperRef, children: [_jsx(SgInputText, { ...rest, enabled: enabled, readOnly: readOnly, borderRadius: borderRadius, iconButtons: mergedIconButtons, inputProps: {
|
|
240
312
|
...inputProps,
|
|
313
|
+
ref: inputRef,
|
|
241
314
|
autoComplete: "off",
|
|
242
315
|
autoCorrect: "off",
|
|
243
316
|
autoCapitalize: "off",
|
|
244
317
|
spellCheck: false,
|
|
245
318
|
value: inputValue,
|
|
319
|
+
style: renderValue && selectedItem && !open
|
|
320
|
+
? {
|
|
321
|
+
...(inputProps?.style ?? {}),
|
|
322
|
+
color: "transparent",
|
|
323
|
+
textShadow: "none"
|
|
324
|
+
}
|
|
325
|
+
: inputProps?.style,
|
|
246
326
|
onChange: (event) => handleInputChange(event.currentTarget.value),
|
|
247
327
|
onBlur: (event) => {
|
|
248
328
|
inputProps?.onBlur?.(event);
|
|
@@ -256,5 +336,7 @@ function SgAutocompleteBase(props) {
|
|
|
256
336
|
inputProps?.onKeyDown?.(event);
|
|
257
337
|
handleKeyDown(event);
|
|
258
338
|
}
|
|
259
|
-
} }),
|
|
339
|
+
} }), renderValue && selectedItem && !open ? (_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-3 right-10 z-10 flex items-center overflow-hidden", children: renderValue(selectedItem) })) : null, open && !(enabled === false || readOnly) && typeof document !== "undefined"
|
|
340
|
+
? createPortal(_jsxs("div", { ref: dropdownRef, className: "z-[1100] overflow-hidden rounded-md border border-border bg-[rgb(var(--sg-surface,var(--sg-bg)))] text-[rgb(var(--sg-text,var(--sg-fg)))] shadow-lg", style: dropdownStyle, children: [_jsx("div", { className: "max-h-64 overflow-auto", children: listContent() }), renderFooter ? (_jsx("div", { className: "border-t border-border bg-[rgb(var(--sg-surface,var(--sg-bg)))] px-3 py-2", children: renderFooter(inputValue, items.length > 0) })) : null] }), document.body)
|
|
341
|
+
: null] }));
|
|
260
342
|
}
|
|
@@ -16,6 +16,7 @@ export type SgComboboxProps<T = SgAutocompleteItem> = SgComboboxBaseProps & {
|
|
|
16
16
|
openOnFocus?: boolean;
|
|
17
17
|
onSelect?: (value: T) => void;
|
|
18
18
|
renderItem?: (item: SgAutocompleteItem, isActive: boolean) => React.ReactNode;
|
|
19
|
+
renderValue?: (item: SgAutocompleteItem | null) => React.ReactNode;
|
|
19
20
|
renderGroupHeader?: (group: string) => React.ReactNode;
|
|
20
21
|
renderFooter?: (query: string, hasResults: boolean) => React.ReactNode;
|
|
21
22
|
itemTooltip?: (item: SgAutocompleteItem) => React.ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgCombobox.d.ts","sourceRoot":"","sources":["../../src/inputs/SgCombobox.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SgCombobox.d.ts","sourceRoot":"","sources":["../../src/inputs/SgCombobox.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAGtF,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAE5C,KAAK,mBAAmB,GAAG,IAAI,CAC7B,gBAAgB,EAChB,UAAU,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAC1F,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,kBAAkB,IAAI,CAAC,EAAE,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC;AAErF,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,kBAAkB,IAAI,mBAAmB,GAAG;IAC1E,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,kBAAkB,CAAC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9E,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,KAAK,KAAK,CAAC,SAAS,CAAC;IACnE,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IACvD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IACvE,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5D,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;CAC7C,CAAC;AA8jBF,wBAAgB,UAAU,CAAC,CAAC,GAAG,kBAAkB,EAAE,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,2CAsBrF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import { ChevronDown } from "lucide-react";
|
|
5
6
|
import { Controller } from "react-hook-form";
|
|
6
7
|
import { resolveFieldError } from "../rhf";
|
|
@@ -51,13 +52,15 @@ function findTypeAheadMatchIndex(entries, query, startIndex) {
|
|
|
51
52
|
return -1;
|
|
52
53
|
}
|
|
53
54
|
function SgComboboxBase(props) {
|
|
54
|
-
const { source, mapItem: mapItemProp, value, onValueChange, grouped, groupped, loadingText: loadingTextProp, emptyText: emptyTextProp, openOnFocus = false, onSelect, renderItem, renderGroupHeader, renderFooter, itemTooltip, inputProps, enabled, borderRadius, ...rest } = props;
|
|
55
|
+
const { source, mapItem: mapItemProp, value, onValueChange, grouped, groupped, loadingText: loadingTextProp, emptyText: emptyTextProp, openOnFocus = false, onSelect, renderItem, renderValue, renderGroupHeader, renderFooter, itemTooltip, inputProps, enabled, borderRadius, ...rest } = props;
|
|
55
56
|
const i18n = useComponentsI18n();
|
|
56
57
|
const loadingText = loadingTextProp ?? t(i18n, "components.autocomplete.loading");
|
|
57
58
|
const emptyText = emptyTextProp ?? t(i18n, "components.autocomplete.empty");
|
|
58
59
|
const effectiveGrouped = grouped ?? groupped ?? false;
|
|
59
60
|
const isControlled = value !== undefined;
|
|
60
61
|
const wrapperRef = React.useRef(null);
|
|
62
|
+
const inputRef = React.useRef(null);
|
|
63
|
+
const dropdownRef = React.useRef(null);
|
|
61
64
|
const ignoreBlurRef = React.useRef(false);
|
|
62
65
|
const requestIdRef = React.useRef(0);
|
|
63
66
|
const typeAheadRef = React.useRef({
|
|
@@ -70,6 +73,7 @@ function SgComboboxBase(props) {
|
|
|
70
73
|
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
71
74
|
const [internalValue, setInternalValue] = React.useState(null);
|
|
72
75
|
const [lastSelectedLabel, setLastSelectedLabel] = React.useState("");
|
|
76
|
+
const [dropdownStyle, setDropdownStyle] = React.useState({});
|
|
73
77
|
const resolvedBorderRadius = React.useMemo(() => {
|
|
74
78
|
if (borderRadius === undefined)
|
|
75
79
|
return undefined;
|
|
@@ -149,6 +153,18 @@ function SgComboboxBase(props) {
|
|
|
149
153
|
const displayedValue = resolvedValue == null || resolvedValue === ""
|
|
150
154
|
? ""
|
|
151
155
|
: selectedEntry?.item.label ?? lastSelectedLabel;
|
|
156
|
+
const selectedVisualItem = React.useMemo(() => {
|
|
157
|
+
if (selectedEntry?.item)
|
|
158
|
+
return selectedEntry.item;
|
|
159
|
+
if (resolvedValue == null || resolvedValue === "")
|
|
160
|
+
return null;
|
|
161
|
+
if (!lastSelectedLabel)
|
|
162
|
+
return null;
|
|
163
|
+
return {
|
|
164
|
+
id: resolvedValue,
|
|
165
|
+
label: lastSelectedLabel
|
|
166
|
+
};
|
|
167
|
+
}, [lastSelectedLabel, resolvedValue, selectedEntry]);
|
|
152
168
|
const setSelectedValue = React.useCallback((nextValue) => {
|
|
153
169
|
if (!isControlled) {
|
|
154
170
|
setInternalValue(nextValue);
|
|
@@ -166,6 +182,21 @@ function SgComboboxBase(props) {
|
|
|
166
182
|
setOpen(true);
|
|
167
183
|
void refreshFromSource();
|
|
168
184
|
}, [isDisabled, refreshFromSource]);
|
|
185
|
+
const syncDropdownPosition = React.useCallback(() => {
|
|
186
|
+
const anchor = wrapperRef.current?.querySelector("input") ?? inputRef.current ?? wrapperRef.current;
|
|
187
|
+
if (!anchor)
|
|
188
|
+
return;
|
|
189
|
+
const rect = anchor.getBoundingClientRect();
|
|
190
|
+
setDropdownStyle({
|
|
191
|
+
position: "fixed",
|
|
192
|
+
top: rect.bottom + 4,
|
|
193
|
+
left: rect.left,
|
|
194
|
+
minWidth: rect.width,
|
|
195
|
+
width: "max-content",
|
|
196
|
+
maxWidth: "min(32rem, calc(100vw - 24px))",
|
|
197
|
+
borderRadius: resolvedBorderRadius
|
|
198
|
+
});
|
|
199
|
+
}, [resolvedBorderRadius]);
|
|
169
200
|
const selectIndex = React.useCallback((index) => {
|
|
170
201
|
const entry = entries[index];
|
|
171
202
|
if (!entry || entry.item.disabled)
|
|
@@ -217,11 +248,27 @@ function SgComboboxBase(props) {
|
|
|
217
248
|
const handleOutside = (event) => {
|
|
218
249
|
if (wrapperRef.current?.contains(event.target))
|
|
219
250
|
return;
|
|
251
|
+
if (dropdownRef.current?.contains(event.target))
|
|
252
|
+
return;
|
|
220
253
|
closeDropdown();
|
|
221
254
|
};
|
|
222
255
|
document.addEventListener("mousedown", handleOutside);
|
|
223
256
|
return () => document.removeEventListener("mousedown", handleOutside);
|
|
224
257
|
}, [closeDropdown, open]);
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
if (!open)
|
|
260
|
+
return;
|
|
261
|
+
syncDropdownPosition();
|
|
262
|
+
const handleLayoutChange = () => {
|
|
263
|
+
syncDropdownPosition();
|
|
264
|
+
};
|
|
265
|
+
window.addEventListener("resize", handleLayoutChange);
|
|
266
|
+
window.addEventListener("scroll", handleLayoutChange, true);
|
|
267
|
+
return () => {
|
|
268
|
+
window.removeEventListener("resize", handleLayoutChange);
|
|
269
|
+
window.removeEventListener("scroll", handleLayoutChange, true);
|
|
270
|
+
};
|
|
271
|
+
}, [open, syncDropdownPosition]);
|
|
225
272
|
React.useEffect(() => {
|
|
226
273
|
if (!open)
|
|
227
274
|
return;
|
|
@@ -261,9 +308,17 @@ function SgComboboxBase(props) {
|
|
|
261
308
|
}
|
|
262
309
|
openDropdown();
|
|
263
310
|
}, "aria-label": t(i18n, "components.actions.openList"), children: _jsx(ChevronDown, { size: 16 }) }));
|
|
264
|
-
return (_jsxs("div", { className: "relative", ref: wrapperRef, children: [_jsx(SgInputText, { ...rest, enabled: enabled, borderRadius: borderRadius, clearButton: false, readOnly: true, iconButtons: [dropdownButton], inputProps: {
|
|
311
|
+
return (_jsxs("div", { className: open ? "relative z-[1100]" : "relative", ref: wrapperRef, children: [_jsx(SgInputText, { ...rest, enabled: enabled, borderRadius: borderRadius, clearButton: false, readOnly: true, iconButtons: [dropdownButton], inputProps: {
|
|
265
312
|
...inputProps,
|
|
313
|
+
ref: inputRef,
|
|
266
314
|
value: displayedValue,
|
|
315
|
+
style: renderValue && displayedValue
|
|
316
|
+
? {
|
|
317
|
+
...(inputProps?.style ?? {}),
|
|
318
|
+
color: "transparent",
|
|
319
|
+
textShadow: "none"
|
|
320
|
+
}
|
|
321
|
+
: inputProps?.style,
|
|
267
322
|
onMouseDown: (event) => {
|
|
268
323
|
inputProps?.onMouseDown?.(event);
|
|
269
324
|
if (isDisabled)
|
|
@@ -340,19 +395,21 @@ function SgComboboxBase(props) {
|
|
|
340
395
|
}
|
|
341
396
|
}
|
|
342
397
|
}
|
|
343
|
-
} }),
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
398
|
+
} }), renderValue && displayedValue ? (_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-3 right-10 z-10 flex items-center overflow-hidden", children: renderValue(selectedVisualItem) })) : null, open && !isDisabled && typeof document !== "undefined"
|
|
399
|
+
? createPortal(_jsxs("div", { ref: dropdownRef, className: "z-[1100] overflow-hidden rounded-md border border-border bg-[rgb(var(--sg-surface,var(--sg-bg)))] text-[rgb(var(--sg-text,var(--sg-fg)))] shadow-lg", style: dropdownStyle, children: [_jsx("div", { className: "max-h-64 overflow-auto", children: loading ? (_jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: loadingText })) : entries.length === 0 ? (_jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: emptyText })) : groupedEntries ? (groupedEntries.map(({ group, list }) => (_jsxs("div", { className: "border-b border-border last:border-b-0", children: [_jsx("div", { className: "px-3 py-1 text-xs font-semibold text-muted-foreground", children: renderGroupHeader ? renderGroupHeader(group) : group || " " }), list.map(({ entry, index }) => {
|
|
400
|
+
const isActive = activeIndex === index;
|
|
401
|
+
return (_jsxs("div", { className: `group relative cursor-pointer px-3 py-2 text-sm ${isActive ? "bg-muted/60" : ""} ${entry.item.disabled ? "cursor-not-allowed opacity-50" : "hover:bg-muted/40"}`, onMouseEnter: () => setActiveIndex(index), onMouseDown: (event) => {
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
ignoreBlurRef.current = true;
|
|
404
|
+
}, onClick: () => selectIndex(index), children: [renderItem ? renderItem(entry.item, isActive) : entry.item.label, itemTooltip ? (_jsx("div", { className: "pointer-events-none absolute left-full top-1/2 z-20 ml-2 -translate-y-1/2 rounded border border-border bg-[rgb(var(--sg-surface,var(--sg-bg)))] px-2 py-1 text-xs text-[rgb(var(--sg-text,var(--sg-fg)))] shadow-md opacity-0 transition-opacity group-hover:opacity-100", children: itemTooltip(entry.item) })) : null] }, entry.item.id));
|
|
405
|
+
})] }, group || "default")))) : (entries.map((entry, index) => {
|
|
406
|
+
const isActive = activeIndex === index;
|
|
407
|
+
return (_jsxs("div", { className: `group relative cursor-pointer px-3 py-2 text-sm ${isActive ? "bg-muted/60" : ""} ${entry.item.disabled ? "cursor-not-allowed opacity-50" : "hover:bg-muted/40"}`, onMouseEnter: () => setActiveIndex(index), onMouseDown: (event) => {
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
ignoreBlurRef.current = true;
|
|
410
|
+
}, onClick: () => selectIndex(index), children: [renderItem ? renderItem(entry.item, isActive) : entry.item.label, itemTooltip ? (_jsx("div", { className: "pointer-events-none absolute left-full top-1/2 z-20 ml-2 -translate-y-1/2 rounded border border-border bg-[rgb(var(--sg-surface,var(--sg-bg)))] px-2 py-1 text-xs text-[rgb(var(--sg-text,var(--sg-fg)))] shadow-md opacity-0 transition-opacity group-hover:opacity-100", children: itemTooltip(entry.item) })) : null] }, entry.item.id));
|
|
411
|
+
})) }), renderFooter ? (_jsx("div", { className: "border-t border-border bg-[rgb(var(--sg-surface,var(--sg-bg)))] px-3 py-2", children: renderFooter("", entries.length > 0) })) : null] }), document.body)
|
|
412
|
+
: null] }));
|
|
356
413
|
}
|
|
357
414
|
export function SgCombobox(props) {
|
|
358
415
|
const { control, name, rules, ...rest } = props;
|