@seedgrid/fe-components 2026.8.7 → 2026.8.8
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.
|
@@ -100,7 +100,16 @@
|
|
|
100
100
|
"type": "boolean",
|
|
101
101
|
"required": false,
|
|
102
102
|
"default": false,
|
|
103
|
-
"description": "Abre
|
|
103
|
+
"description": "Abre a lista ao focar; com um item ja selecionado abre a lista completa.",
|
|
104
|
+
"semanticRole": "behavior",
|
|
105
|
+
"bindable": true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "showDropDownButton",
|
|
109
|
+
"type": "boolean",
|
|
110
|
+
"required": false,
|
|
111
|
+
"default": false,
|
|
112
|
+
"description": "Exibe o botao que abre/fecha a lista. Abre a lista completa quando o texto e uma selecao; preserva o filtro quando o usuario digitou.",
|
|
104
113
|
"semanticRole": "behavior",
|
|
105
114
|
"bindable": true
|
|
106
115
|
},
|
|
@@ -342,11 +351,6 @@
|
|
|
342
351
|
"type": "boolean",
|
|
343
352
|
"required": false
|
|
344
353
|
},
|
|
345
|
-
{
|
|
346
|
-
"name": "showDropDownButton",
|
|
347
|
-
"type": "boolean",
|
|
348
|
-
"required": false
|
|
349
|
-
},
|
|
350
354
|
{
|
|
351
355
|
"name": "suffixText",
|
|
352
356
|
"type": "string",
|
|
@@ -33,6 +33,7 @@ function SgAutocompleteBase(props) {
|
|
|
33
33
|
const wrapperRef = React.useRef(null);
|
|
34
34
|
const inputRef = React.useRef(null);
|
|
35
35
|
const dropdownRef = React.useRef(null);
|
|
36
|
+
const listRef = React.useRef(null);
|
|
36
37
|
const ignoreBlurRef = React.useRef(false);
|
|
37
38
|
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
38
39
|
const [lastSelected, setLastSelected] = React.useState("");
|
|
@@ -42,6 +43,16 @@ function SgAutocompleteBase(props) {
|
|
|
42
43
|
const requestIdRef = React.useRef(0);
|
|
43
44
|
const openRef = React.useRef(false);
|
|
44
45
|
openRef.current = open;
|
|
46
|
+
// Quando a lista abre pelo botao dropdown (ou por openOnFocus) com um item ja
|
|
47
|
+
// selecionado, ela deve listar o catalogo inteiro em vez de refiltrar pelo
|
|
48
|
+
// label do proprio item selecionado -- senao a lista volta com 1 item so.
|
|
49
|
+
const browseModeRef = React.useRef(false);
|
|
50
|
+
// true enquanto o texto do input for algo que o usuario digitou (e ainda nao
|
|
51
|
+
// virou selecao). Enquanto for false, o texto e uma selecao (ou esta vazio) e
|
|
52
|
+
// abrir a lista deve mostrar o catalogo inteiro, nao refiltrar por ele.
|
|
53
|
+
const typedRef = React.useRef(false);
|
|
54
|
+
const inputValueRef = React.useRef(inputValue);
|
|
55
|
+
inputValueRef.current = inputValue;
|
|
45
56
|
// While the user is focused (typing), every incoming `value` prop change is
|
|
46
57
|
// just the parent echoing back our own onChange. Overwriting inputValue in
|
|
47
58
|
// that window drops characters typed faster than one render cycle.
|
|
@@ -57,6 +68,12 @@ function SgAutocompleteBase(props) {
|
|
|
57
68
|
// Never clobber what the user is actively typing.
|
|
58
69
|
if (isFocusedRef.current)
|
|
59
70
|
return;
|
|
71
|
+
// Valor identico ao que ja exibimos e apenas o pai ecoando nosso onChange:
|
|
72
|
+
// nao ha o que sincronizar, e trata-lo como valor externo apagaria o fato de
|
|
73
|
+
// o texto ter sido digitado pelo usuario.
|
|
74
|
+
if (value === inputValueRef.current)
|
|
75
|
+
return;
|
|
76
|
+
typedRef.current = false;
|
|
60
77
|
setLastSelected(value);
|
|
61
78
|
setInputValue(value);
|
|
62
79
|
if (!value) {
|
|
@@ -133,9 +150,32 @@ function SgAutocompleteBase(props) {
|
|
|
133
150
|
React.useEffect(() => {
|
|
134
151
|
if (!openRef.current)
|
|
135
152
|
return;
|
|
136
|
-
|
|
153
|
+
if (browseModeRef.current)
|
|
154
|
+
return;
|
|
155
|
+
const handler = setTimeout(() => {
|
|
156
|
+
// A lista pode ter fechado (ou entrado em modo browse) enquanto o debounce
|
|
157
|
+
// estava pendente; disparar aqui reabriria/refiltraria a lista sozinha.
|
|
158
|
+
if (!openRef.current || browseModeRef.current)
|
|
159
|
+
return;
|
|
160
|
+
runSearch(inputValue);
|
|
161
|
+
}, delay);
|
|
137
162
|
return () => clearTimeout(handler);
|
|
138
163
|
}, [delay, inputValue, runSearch]);
|
|
164
|
+
// Abre a lista em modo "browse" (catalogo completo) quando o texto do input
|
|
165
|
+
// nao foi digitado pelo usuario -- e uma selecao ou esta vazio. Se o usuario
|
|
166
|
+
// digitou algo, o filtro digitado e respeitado.
|
|
167
|
+
const openList = React.useCallback(() => {
|
|
168
|
+
const browse = !typedRef.current;
|
|
169
|
+
browseModeRef.current = browse;
|
|
170
|
+
setOpen(true);
|
|
171
|
+
onOpenChange?.(true);
|
|
172
|
+
runSearch(browse ? "" : inputValue, true);
|
|
173
|
+
}, [inputValue, onOpenChange, runSearch]);
|
|
174
|
+
const closeList = React.useCallback(() => {
|
|
175
|
+
browseModeRef.current = false;
|
|
176
|
+
setOpen(false);
|
|
177
|
+
onOpenChange?.(false);
|
|
178
|
+
}, [onOpenChange]);
|
|
139
179
|
const selectItem = (item) => {
|
|
140
180
|
if (item.disabled)
|
|
141
181
|
return;
|
|
@@ -146,6 +186,8 @@ function SgAutocompleteBase(props) {
|
|
|
146
186
|
onChange?.(selection);
|
|
147
187
|
setLastSelected(selection);
|
|
148
188
|
onSelect?.(item);
|
|
189
|
+
typedRef.current = false;
|
|
190
|
+
browseModeRef.current = false;
|
|
149
191
|
setOpen(false);
|
|
150
192
|
onOpenChange?.(false);
|
|
151
193
|
if (clearOnSelect) {
|
|
@@ -157,6 +199,8 @@ function SgAutocompleteBase(props) {
|
|
|
157
199
|
if (selectedItem && next !== (formatSelection ? formatSelection(selectedItem) : selectedItem.label)) {
|
|
158
200
|
setSelectedItem(null);
|
|
159
201
|
}
|
|
202
|
+
browseModeRef.current = false;
|
|
203
|
+
typedRef.current = next.length > 0;
|
|
160
204
|
setInternalError(null);
|
|
161
205
|
setInputValue(next);
|
|
162
206
|
if (allowCustomValue || next.length === 0) {
|
|
@@ -189,6 +233,9 @@ function SgAutocompleteBase(props) {
|
|
|
189
233
|
return;
|
|
190
234
|
}
|
|
191
235
|
isFocusedRef.current = false;
|
|
236
|
+
if (!allowCustomValue) {
|
|
237
|
+
typedRef.current = false;
|
|
238
|
+
}
|
|
192
239
|
if (!allowCustomValue && inputValue !== lastSelected) {
|
|
193
240
|
if (lastSelected) {
|
|
194
241
|
setInputValue(lastSelected);
|
|
@@ -204,6 +251,7 @@ function SgAutocompleteBase(props) {
|
|
|
204
251
|
onChange?.("");
|
|
205
252
|
}
|
|
206
253
|
}
|
|
254
|
+
browseModeRef.current = false;
|
|
207
255
|
setOpen(false);
|
|
208
256
|
onOpenChange?.(false);
|
|
209
257
|
};
|
|
@@ -214,9 +262,7 @@ function SgAutocompleteBase(props) {
|
|
|
214
262
|
setInputValue(selection);
|
|
215
263
|
}
|
|
216
264
|
if (openOnFocus) {
|
|
217
|
-
|
|
218
|
-
onOpenChange?.(true);
|
|
219
|
-
runSearch(inputValue, true);
|
|
265
|
+
openList();
|
|
220
266
|
}
|
|
221
267
|
};
|
|
222
268
|
const handleKeyDown = (event) => {
|
|
@@ -237,8 +283,7 @@ function SgAutocompleteBase(props) {
|
|
|
237
283
|
selectItem(item);
|
|
238
284
|
}
|
|
239
285
|
else if (event.key === "Escape") {
|
|
240
|
-
|
|
241
|
-
onOpenChange?.(false);
|
|
286
|
+
closeList();
|
|
242
287
|
}
|
|
243
288
|
};
|
|
244
289
|
const syncDropdownPosition = React.useCallback(() => {
|
|
@@ -261,13 +306,10 @@ function SgAutocompleteBase(props) {
|
|
|
261
306
|
ignoreBlurRef.current = true;
|
|
262
307
|
}, onClick: () => {
|
|
263
308
|
if (open) {
|
|
264
|
-
|
|
265
|
-
onOpenChange?.(false);
|
|
309
|
+
closeList();
|
|
266
310
|
return;
|
|
267
311
|
}
|
|
268
|
-
|
|
269
|
-
onOpenChange?.(true);
|
|
270
|
-
runSearch(inputValue, true);
|
|
312
|
+
openList();
|
|
271
313
|
}, "aria-label": t(i18n, "components.actions.openList"), children: _jsx(ChevronDown, { size: 16 }) })) : null;
|
|
272
314
|
const mergedIconButtons = dropdownButton ? [...(iconButtons ?? []), dropdownButton] : iconButtons;
|
|
273
315
|
const groupedItems = React.useMemo(() => {
|
|
@@ -293,7 +335,7 @@ function SgAutocompleteBase(props) {
|
|
|
293
335
|
return groupedItems.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((item, index) => {
|
|
294
336
|
const flatIndex = items.indexOf(item);
|
|
295
337
|
const isActive = flatIndex === activeIndex;
|
|
296
|
-
return (_jsxs("div", { className: `group relative cursor-pointer px-3 py-2 text-sm ${isActive ? "bg-muted/60" : ""} ${item.disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-muted/40"}`, onMouseEnter: () => setActiveIndex(flatIndex), onMouseDown: (event) => {
|
|
338
|
+
return (_jsxs("div", { "data-sg-index": flatIndex, className: `group relative cursor-pointer px-3 py-2 text-sm ${isActive ? "bg-muted/60" : ""} ${item.disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-muted/40"}`, onMouseEnter: () => setActiveIndex(flatIndex), onMouseDown: (event) => {
|
|
297
339
|
event.preventDefault();
|
|
298
340
|
ignoreBlurRef.current = true;
|
|
299
341
|
}, 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));
|
|
@@ -301,12 +343,38 @@ function SgAutocompleteBase(props) {
|
|
|
301
343
|
}
|
|
302
344
|
return items.map((item, index) => {
|
|
303
345
|
const isActive = index === activeIndex;
|
|
304
|
-
return (_jsxs("div", { className: `group relative cursor-pointer px-3 py-2 text-sm ${isActive ? "bg-muted/60" : ""} ${item.disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-muted/40"}`, onMouseEnter: () => setActiveIndex(index), onMouseDown: (event) => {
|
|
346
|
+
return (_jsxs("div", { "data-sg-index": index, className: `group relative cursor-pointer px-3 py-2 text-sm ${isActive ? "bg-muted/60" : ""} ${item.disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-muted/40"}`, onMouseEnter: () => setActiveIndex(index), onMouseDown: (event) => {
|
|
305
347
|
event.preventDefault();
|
|
306
348
|
ignoreBlurRef.current = true;
|
|
307
349
|
}, 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));
|
|
308
350
|
});
|
|
309
351
|
};
|
|
352
|
+
// No modo browse a lista abre inteira: destaca o item ja selecionado para o
|
|
353
|
+
// usuario se situar dentro dela.
|
|
354
|
+
React.useEffect(() => {
|
|
355
|
+
if (!open || !browseModeRef.current || !selectedItem)
|
|
356
|
+
return;
|
|
357
|
+
const index = items.findIndex((item) => item.id === selectedItem.id);
|
|
358
|
+
if (index >= 0)
|
|
359
|
+
setActiveIndex(index);
|
|
360
|
+
}, [items, open, selectedItem]);
|
|
361
|
+
// Mantem o item ativo visivel (item destacado ao abrir e navegacao por teclado).
|
|
362
|
+
React.useEffect(() => {
|
|
363
|
+
if (!open || activeIndex < 0)
|
|
364
|
+
return;
|
|
365
|
+
const container = listRef.current;
|
|
366
|
+
const node = container?.querySelector(`[data-sg-index="${activeIndex}"]`);
|
|
367
|
+
if (!container || !node)
|
|
368
|
+
return;
|
|
369
|
+
const top = node.offsetTop - container.offsetTop;
|
|
370
|
+
const bottom = top + node.offsetHeight;
|
|
371
|
+
if (top < container.scrollTop) {
|
|
372
|
+
container.scrollTop = top;
|
|
373
|
+
}
|
|
374
|
+
else if (bottom > container.scrollTop + container.clientHeight) {
|
|
375
|
+
container.scrollTop = bottom - container.clientHeight;
|
|
376
|
+
}
|
|
377
|
+
}, [activeIndex, items, open]);
|
|
310
378
|
React.useEffect(() => {
|
|
311
379
|
if (!open)
|
|
312
380
|
return;
|
|
@@ -330,6 +398,7 @@ function SgAutocompleteBase(props) {
|
|
|
330
398
|
onChange?.("");
|
|
331
399
|
}
|
|
332
400
|
}
|
|
401
|
+
browseModeRef.current = false;
|
|
333
402
|
setOpen(false);
|
|
334
403
|
onOpenChange?.(false);
|
|
335
404
|
};
|
|
@@ -379,6 +448,6 @@ function SgAutocompleteBase(props) {
|
|
|
379
448
|
handleKeyDown(event);
|
|
380
449
|
}
|
|
381
450
|
} }), 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"
|
|
382
|
-
? 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)
|
|
451
|
+
? 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", { ref: listRef, 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)
|
|
383
452
|
: null] }));
|
|
384
453
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgAutocomplete.meta.d.ts","sourceRoot":"","sources":["../../src/inputs/SgAutocomplete.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"SgAutocomplete.meta.d.ts","sourceRoot":"","sources":["../../src/inputs/SgAutocomplete.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,QAiCpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WAmBrB,CAAC"}
|
|
@@ -19,7 +19,8 @@ export const sgMeta = {
|
|
|
19
19
|
{ name: "delay", type: "number", description: "Debounce da pesquisa em milissegundos.", semanticRole: "behavior", bindable: true },
|
|
20
20
|
{ name: "maxResult", type: "number", description: "Limite de resultados exibidos.", semanticRole: "behavior", bindable: true },
|
|
21
21
|
{ name: "allowCustomValue", type: "boolean", default: false, description: "Permite valor fora da lista.", semanticRole: "behavior", bindable: true },
|
|
22
|
-
{ name: "openOnFocus", type: "boolean", default: false, description: "Abre
|
|
22
|
+
{ name: "openOnFocus", type: "boolean", default: false, description: "Abre a lista ao focar; com um item ja selecionado abre a lista completa.", semanticRole: "behavior", bindable: true },
|
|
23
|
+
{ name: "showDropDownButton", type: "boolean", default: false, description: "Exibe o botao que abre/fecha a lista. Abre a lista completa quando o texto e uma selecao; preserva o filtro quando o usuario digitou.", semanticRole: "behavior", bindable: true },
|
|
23
24
|
{ name: "onSelect", type: "(item: SgAutocompleteItem) => void", description: "Callback disparado ao selecionar item.", semanticRole: "event", bindable: false }
|
|
24
25
|
],
|
|
25
26
|
states: ["default", "focused", "open", "loading", "disabled", "error"],
|