@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 sugestoes ao focar.",
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
- const handler = setTimeout(() => runSearch(inputValue), delay);
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
- setOpen(true);
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
- setOpen(false);
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
- setOpen(false);
265
- onOpenChange?.(false);
309
+ closeList();
266
310
  return;
267
311
  }
268
- setOpen(true);
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,QAgCpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WAmBrB,CAAC"}
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 sugestoes ao focar.", semanticRole: "behavior", bindable: true },
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"],