@motor-hero/ui-kit 0.44.1 → 0.45.0
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/components/combobox.d.ts.map +1 -1
- package/dist/components/multi-combobox.d.ts.map +1 -1
- package/dist/hooks/use-infinite-scroll-sentinel.d.ts +15 -7
- package/dist/hooks/use-infinite-scroll-sentinel.d.ts.map +1 -1
- package/dist/index.cjs +29 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +46 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/combobox.tsx +18 -3
- package/src/components/multi-combobox.tsx +23 -7
- package/src/hooks/use-infinite-scroll-sentinel.ts +30 -13
package/package.json
CHANGED
|
@@ -93,11 +93,26 @@ export function Combobox({
|
|
|
93
93
|
const [search, setSearch] = React.useState("");
|
|
94
94
|
const isDesktop = useIsDesktop();
|
|
95
95
|
const isServer = onSearchChange !== undefined;
|
|
96
|
-
|
|
96
|
+
// Mesmo cache do multi-combobox: em modo servidor `options` é só a página
|
|
97
|
+
// carregada, então o rótulo do valor escolhido precisa sobreviver à troca de
|
|
98
|
+
// página — senão o gatilho volta ao placeholder com o valor ainda setado.
|
|
99
|
+
const labelCache = React.useRef(new Map<string, ComboboxOption>());
|
|
100
|
+
const fromPage =
|
|
97
101
|
selectedOption?.value === value
|
|
98
102
|
? selectedOption
|
|
99
|
-
:
|
|
100
|
-
|
|
103
|
+
: options.find((option) => option.value === value);
|
|
104
|
+
|
|
105
|
+
// No efeito, nunca no render — mutar a ref durante o render é impuro.
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
for (const option of options) labelCache.current.set(option.value, option);
|
|
108
|
+
if (selectedOption)
|
|
109
|
+
labelCache.current.set(selectedOption.value, selectedOption);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const selected =
|
|
113
|
+
fromPage ??
|
|
114
|
+
(value ? labelCache.current.get(value) : undefined) ??
|
|
115
|
+
(creatable && value ? { value, label: value } : undefined);
|
|
101
116
|
|
|
102
117
|
const handleOpenChange = (next: boolean) => {
|
|
103
118
|
setOpen(next);
|
|
@@ -73,14 +73,30 @@ export function MultiCombobox({
|
|
|
73
73
|
const [open, setOpen] = React.useState(false)
|
|
74
74
|
const isDesktop = useIsDesktop()
|
|
75
75
|
const isServer = onSearchChange !== undefined
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
// Em modo servidor `options` é só a página carregada no momento, então um
|
|
77
|
+
// rótulo visto uma vez precisa sobreviver à troca de página: sem esse cache,
|
|
78
|
+
// buscar (ou paginar) fazia o chip de um valor ainda selecionado sumir do
|
|
79
|
+
// gatilho, como se nada estivesse escolhido. Prioridade de rótulo:
|
|
80
|
+
// `selectedOptions` (o salvo) → página atual → cache.
|
|
81
|
+
const labelCache = React.useRef(new Map<string, ComboboxOption>())
|
|
82
|
+
const byValue = new Map<string, ComboboxOption>()
|
|
83
|
+
for (const option of options) byValue.set(option.value, option)
|
|
84
|
+
for (const option of selectedOptions ?? []) byValue.set(option.value, option)
|
|
85
|
+
|
|
86
|
+
// Escrita no efeito, nunca no render — mutar a ref durante o render é impuro.
|
|
87
|
+
// Não pisca: no render em que a opção fica selecionável ela ainda está em
|
|
88
|
+
// `options`, e este efeito a grava antes de `options` mudar.
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
for (const [key, option] of byValue) labelCache.current.set(key, option)
|
|
83
91
|
})
|
|
92
|
+
|
|
93
|
+
// Ordem de seleção (`value`), que é estável — a antiga vinha da ordem da
|
|
94
|
+
// página carregada e mudava sozinha conforme as páginas chegavam. Um valor
|
|
95
|
+
// sem rótulo conhecido (edição sem `selectedOptions`) segue sem chip.
|
|
96
|
+
const selected = value
|
|
97
|
+
.map((v) => byValue.get(v) ?? labelCache.current.get(v))
|
|
98
|
+
.filter((option): option is ComboboxOption => option !== undefined)
|
|
99
|
+
|
|
84
100
|
const { listRef, sentinelRef } = useInfiniteScrollSentinel({
|
|
85
101
|
enabled: isServer && !!hasMore,
|
|
86
102
|
loading,
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Dispara `onLoadMore` quando o
|
|
5
|
-
* (raiz = a lista
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Dispara `onLoadMore` quando o sentinela entra na viewport da lista rolável
|
|
5
|
+
* (raiz = a lista, não a janela — ela rola por dentro do Popover/Drawer). Usa
|
|
6
|
+
* refs "latest" para `loading`/`onLoadMore` e não reassina o observer a cada
|
|
7
|
+
* render.
|
|
8
|
+
*
|
|
9
|
+
* Os nós entram como **estado** (callback refs), não como `useRef`: lista e
|
|
10
|
+
* sentinela vivem dentro do Portal do Popover/Drawer e só existem no DOM com o
|
|
11
|
+
* dropdown aberto, o que acontece bem depois de `enabled` virar `true`. Com
|
|
12
|
+
* `useRef` + deps `[enabled]` o efeito rodava uma única vez, com o dropdown
|
|
13
|
+
* fechado e os dois refs `null`, e o observer nunca chegava a ser registrado —
|
|
14
|
+
* a paginação por scroll simplesmente não acontecia. Guardar os nós em estado
|
|
15
|
+
* também refaz a assinatura quando o sentinela desmonta e remonta (`hasMore`
|
|
16
|
+
* oscilando), em vez de deixar o observer preso a um nó já destacado.
|
|
8
17
|
*/
|
|
9
18
|
export function useInfiniteScrollSentinel({
|
|
10
19
|
enabled,
|
|
@@ -15,21 +24,29 @@ export function useInfiniteScrollSentinel({
|
|
|
15
24
|
loading?: boolean
|
|
16
25
|
onLoadMore?: () => void
|
|
17
26
|
}) {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
27
|
+
const [listNode, setListNode] = React.useState<HTMLDivElement | null>(null)
|
|
28
|
+
const [sentinelNode, setSentinelNode] = React.useState<HTMLDivElement | null>(
|
|
29
|
+
null,
|
|
30
|
+
)
|
|
20
31
|
const loadingRef = React.useRef(loading)
|
|
21
32
|
const onLoadMoreRef = React.useRef(onLoadMore)
|
|
22
33
|
|
|
34
|
+
// Memoizadas de propósito: uma callback ref recriada a cada render é chamada
|
|
35
|
+
// com `null` e de novo com o nó em *todo* render, realimentando o estado.
|
|
36
|
+
const listRef = React.useCallback((node: HTMLDivElement | null) => {
|
|
37
|
+
setListNode(node)
|
|
38
|
+
}, [])
|
|
39
|
+
const sentinelRef = React.useCallback((node: HTMLDivElement | null) => {
|
|
40
|
+
setSentinelNode(node)
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
23
43
|
React.useEffect(() => {
|
|
24
44
|
loadingRef.current = loading
|
|
25
45
|
onLoadMoreRef.current = onLoadMore
|
|
26
46
|
})
|
|
27
47
|
|
|
28
48
|
React.useEffect(() => {
|
|
29
|
-
if (!enabled) return
|
|
30
|
-
const root = listRef.current
|
|
31
|
-
const sentinel = sentinelRef.current
|
|
32
|
-
if (!root || !sentinel) return
|
|
49
|
+
if (!enabled || !listNode || !sentinelNode) return
|
|
33
50
|
|
|
34
51
|
const observer = new IntersectionObserver(
|
|
35
52
|
(entries) => {
|
|
@@ -37,11 +54,11 @@ export function useInfiniteScrollSentinel({
|
|
|
37
54
|
onLoadMoreRef.current?.()
|
|
38
55
|
}
|
|
39
56
|
},
|
|
40
|
-
{ root, rootMargin: "0px 0px 100px 0px" },
|
|
57
|
+
{ root: listNode, rootMargin: "0px 0px 100px 0px" },
|
|
41
58
|
)
|
|
42
|
-
observer.observe(
|
|
59
|
+
observer.observe(sentinelNode)
|
|
43
60
|
return () => observer.disconnect()
|
|
44
|
-
}, [enabled])
|
|
61
|
+
}, [enabled, listNode, sentinelNode])
|
|
45
62
|
|
|
46
63
|
return { listRef, sentinelRef }
|
|
47
64
|
}
|