@motor-hero/ui-kit 0.44.0 → 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/date-picker.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 +30 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +48 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/combobox.tsx +18 -3
- package/src/components/date-picker.tsx +9 -6
- 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);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
2
2
|
import { InputMask } from "@react-input/mask"
|
|
3
|
-
import { endOfYear, format, isValid, parse, subYears } from "date-fns"
|
|
3
|
+
import { addYears, endOfYear, format, isValid, parse, subYears } from "date-fns"
|
|
4
4
|
import { ptBR } from "date-fns/locale"
|
|
5
5
|
import { CalendarIcon, ChevronDown, ChevronLeft, ChevronRight, Clock } from "lucide-react"
|
|
6
6
|
import * as React from "react"
|
|
@@ -171,12 +171,15 @@ function DatePickerImpl({
|
|
|
171
171
|
const committedDate = withTime ? dateTimeToDate(value) : dateOnlyToDate(value)
|
|
172
172
|
const displayValue = displayFor(value, withTime)
|
|
173
173
|
const matchers = toMatchers(minDate, maxDate, disabledDates)
|
|
174
|
-
// Alcance do dropdown de ano: minDate/maxDate quando informados
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// prática: só uma opção de ano no lado sem prop)
|
|
174
|
+
// Alcance do dropdown de ano: minDate/maxDate quando informados; senão,
|
|
175
|
+
// calculado explícito nos dois lados porque passar só um dos dois via prop
|
|
176
|
+
// faz a lib truncar o outro em vez de cair no próprio padrão dela (visto na
|
|
177
|
+
// prática: só uma opção de ano no lado sem prop). Do lado do fim, não usamos
|
|
178
|
+
// o padrão da lib (fim do ano corrente) — vencimento de campanha, validade
|
|
179
|
+
// de premiação e agendamentos legitimamente caem anos à frente; +20 anos
|
|
180
|
+
// evita recriar esse teto sem abrir um intervalo sem sentido.
|
|
178
181
|
const startMonth = dateOnlyToDate(minDate) ?? subYears(new Date(), 100)
|
|
179
|
-
const endMonth = dateOnlyToDate(maxDate) ?? endOfYear(new Date())
|
|
182
|
+
const endMonth = dateOnlyToDate(maxDate) ?? endOfYear(addYears(new Date(), 20))
|
|
180
183
|
|
|
181
184
|
const setOpen = (next: boolean) => {
|
|
182
185
|
setOpenRaw(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
|
}
|