@12-apps/ui 6.41.0 → 6.41.1
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/_internal/components/data-display/DataViews/DataViewsGrid.js +8 -8
- package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +9 -9
- package/dist/_internal/components/data-display/DataViews/data-views-category-pill.js +1 -1
- package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +2 -2
- package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +7 -7
- package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +24 -1
- package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +23 -2
- package/dist/_internal/components/data-display/DataViews/data-views-overflow.js +1 -1
- package/dist/_internal/components/form/Autocomplete/Autocomplete.js +2 -2
- package/dist/_internal/components/form/Autocomplete/useAutocomplete.js +1 -1
- package/dist/_internal/components/form/index.js +2 -2
- package/dist/_internal/components/index.js +2 -2
- package/dist/{chunk-ODW2D5PN.js → chunk-6QYKZMEE.js} +5 -2
- package/dist/{chunk-ODW2D5PN.js.map → chunk-6QYKZMEE.js.map} +1 -1
- package/dist/{chunk-Y2UXX5IQ.js → chunk-BKZERHNC.js} +2 -2
- package/dist/{chunk-M4P7NS2L.js → chunk-BR2T72JI.js} +23 -15
- package/dist/chunk-BR2T72JI.js.map +1 -0
- package/dist/{chunk-UGPRBJWB.js → chunk-DH77PLFF.js} +5 -5
- package/dist/{chunk-EIII4QU4.js → chunk-HFVYWHJT.js} +4 -4
- package/dist/{chunk-VEEG4UXZ.js → chunk-IUD6N52Q.js} +2 -2
- package/dist/{chunk-J6KLT7HP.js → chunk-MEBQKYH4.js} +2 -2
- package/dist/{chunk-XVZG52CM.js → chunk-NOLSQK4J.js} +23 -25
- package/dist/chunk-NOLSQK4J.js.map +1 -0
- package/dist/{chunk-VWJEM2ER.js → chunk-NPPJ3IJP.js} +2 -2
- package/dist/chunk-NPPJ3IJP.js.map +1 -0
- package/dist/{chunk-FTQBVZ7V.js → chunk-TJ3334AJ.js} +2 -2
- package/dist/{chunk-VOJBLBXE.js → chunk-UT73NGRS.js} +2 -2
- package/dist/data-display/DataViews.js +9 -9
- package/dist/form/Autocomplete.js +2 -2
- package/dist/types/components/data-display/DataViews/data-views-category-pill.d.ts +6 -1
- package/dist/types/components/data-display/DataViews/data-views-category-pill.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-more-fields.d.ts +0 -11
- package/dist/types/components/data-display/DataViews/data-views-more-fields.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-overflow.d.ts.map +1 -1
- package/dist/types/components/form/Autocomplete/useAutocomplete.d.ts +9 -0
- package/dist/types/components/form/Autocomplete/useAutocomplete.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/data-display/DataViews/data-views-category-pill.tsx +6 -1
- package/src/components/data-display/DataViews/data-views-more-fields.tsx +43 -37
- package/src/components/data-display/DataViews/data-views-overflow.ts +12 -1
- package/src/components/form/Autocomplete/useAutocomplete.ts +46 -16
- package/dist/chunk-M4P7NS2L.js.map +0 -1
- package/dist/chunk-VWJEM2ER.js.map +0 -1
- package/dist/chunk-XVZG52CM.js.map +0 -1
- /package/dist/{chunk-Y2UXX5IQ.js.map → chunk-BKZERHNC.js.map} +0 -0
- /package/dist/{chunk-UGPRBJWB.js.map → chunk-DH77PLFF.js.map} +0 -0
- /package/dist/{chunk-EIII4QU4.js.map → chunk-HFVYWHJT.js.map} +0 -0
- /package/dist/{chunk-VEEG4UXZ.js.map → chunk-IUD6N52Q.js.map} +0 -0
- /package/dist/{chunk-J6KLT7HP.js.map → chunk-MEBQKYH4.js.map} +0 -0
- /package/dist/{chunk-FTQBVZ7V.js.map → chunk-TJ3334AJ.js.map} +0 -0
- /package/dist/{chunk-VOJBLBXE.js.map → chunk-UT73NGRS.js.map} +0 -0
|
@@ -35,6 +35,15 @@ interface AutocompleteState<T> {
|
|
|
35
35
|
setJustCompletedGhost: Setter<boolean>;
|
|
36
36
|
userClosedDropdown: boolean;
|
|
37
37
|
setUserClosedDropdown: Setter<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Mirrors `userClosedDropdown`, set synchronously wherever the state is
|
|
40
|
+
* (see {@link setDeliberateClose}). An effect scheduled by one render and
|
|
41
|
+
* flushed after a LATER render has already changed this flag reads a
|
|
42
|
+
* stale, closed-over `userClosedDropdown` — but `.current` on a ref is
|
|
43
|
+
* dereferenced live, so it always reflects the latest write regardless of
|
|
44
|
+
* which render's closure is doing the reading (FUT-2780).
|
|
45
|
+
*/
|
|
46
|
+
userClosedDropdownRef: React.RefObject<boolean>;
|
|
38
47
|
inputRef: React.RefObject<HTMLInputElement | null>;
|
|
39
48
|
listRef: React.RefObject<HTMLUListElement | null>;
|
|
40
49
|
debounceRef: React.RefObject<number | undefined>;
|
|
@@ -50,6 +59,7 @@ function useAutocompleteState<T>(value: string, suggestions: T[]): AutocompleteS
|
|
|
50
59
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
|
51
60
|
const [justCompletedGhost, setJustCompletedGhost] = useState(false);
|
|
52
61
|
const [userClosedDropdown, setUserClosedDropdown] = useState(false);
|
|
62
|
+
const userClosedDropdownRef = useRef(false);
|
|
53
63
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
54
64
|
const listRef = useRef<HTMLUListElement | null>(null);
|
|
55
65
|
const debounceRef = useRef<number | undefined>(undefined);
|
|
@@ -57,10 +67,21 @@ function useAutocompleteState<T>(value: string, suggestions: T[]): AutocompleteS
|
|
|
57
67
|
open, setOpen, activeIndex, setActiveIndex, ghost, setGhost, inputValue, setInputValue,
|
|
58
68
|
composition, setComposition, filteredSuggestions, setFilteredSuggestions, isInputFocused,
|
|
59
69
|
setIsInputFocused, justCompletedGhost, setJustCompletedGhost, userClosedDropdown,
|
|
60
|
-
setUserClosedDropdown, inputRef, listRef, debounceRef,
|
|
70
|
+
setUserClosedDropdown, userClosedDropdownRef, inputRef, listRef, debounceRef,
|
|
61
71
|
};
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Set the "this close was deliberate" flag in both the ref effects read
|
|
76
|
+
* synchronously (live, even from a stale closure) and the state the rest of
|
|
77
|
+
* the component reads for render — every call site that touches
|
|
78
|
+
* `userClosedDropdown` goes through this so the two never drift (FUT-2780).
|
|
79
|
+
*/
|
|
80
|
+
function setDeliberateClose<T>(s: AutocompleteState<T>, closed: boolean): void {
|
|
81
|
+
s.userClosedDropdownRef.current = closed;
|
|
82
|
+
s.setUserClosedDropdown(closed);
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
// ---- effects (one tiny hook each; explicit params keep deps complete) --------
|
|
65
86
|
|
|
66
87
|
function useValueSync(value: string, setInputValue: Setter<string>): void {
|
|
@@ -101,17 +122,17 @@ function useLoadingOpen(
|
|
|
101
122
|
isLoading: boolean,
|
|
102
123
|
inputValue: string,
|
|
103
124
|
disabled: boolean,
|
|
104
|
-
|
|
125
|
+
userClosedDropdownRef: React.RefObject<boolean>,
|
|
105
126
|
setOpen: Setter<boolean>,
|
|
106
127
|
setActiveIndex: Setter<number>,
|
|
107
128
|
): void {
|
|
108
129
|
useEffect(() => {
|
|
109
|
-
if (isLoading && inputValue.trim().length > 0 && !disabled && !
|
|
130
|
+
if (isLoading && inputValue.trim().length > 0 && !disabled && !userClosedDropdownRef.current) setOpen(true);
|
|
110
131
|
if (disabled) {
|
|
111
132
|
setOpen(false);
|
|
112
133
|
setActiveIndex(-1);
|
|
113
134
|
}
|
|
114
|
-
}, [isLoading, inputValue, disabled,
|
|
135
|
+
}, [isLoading, inputValue, disabled, userClosedDropdownRef, setOpen, setActiveIndex]);
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
/** Inputs to the auto-open check, all primitive/stable so effect deps stay complete. */
|
|
@@ -120,7 +141,7 @@ interface AutoOpenArgs {
|
|
|
120
141
|
isInputFocused: boolean;
|
|
121
142
|
composition: boolean;
|
|
122
143
|
justCompletedGhost: boolean;
|
|
123
|
-
|
|
144
|
+
userClosedDropdownRef: React.RefObject<boolean>;
|
|
124
145
|
activeIndex: number;
|
|
125
146
|
setOpen: Setter<boolean>;
|
|
126
147
|
setActiveIndex: Setter<number>;
|
|
@@ -133,7 +154,7 @@ function maybeAutoOpen(a: AutoOpenArgs, hasResults: boolean): void {
|
|
|
133
154
|
a.isInputFocused &&
|
|
134
155
|
!a.composition &&
|
|
135
156
|
!a.justCompletedGhost &&
|
|
136
|
-
!a.
|
|
157
|
+
!a.userClosedDropdownRef.current;
|
|
137
158
|
if (!canAutoOpen) return;
|
|
138
159
|
a.setOpen(true);
|
|
139
160
|
if (hasResults && a.activeIndex === -1) a.setActiveIndex(0);
|
|
@@ -146,7 +167,7 @@ function useLocalFilter<T>(p: AutocompleteProps<T>, s: AutocompleteState<T>): vo
|
|
|
146
167
|
const async = Boolean(p.async);
|
|
147
168
|
const {
|
|
148
169
|
inputValue, isInputFocused, composition, justCompletedGhost, userClosedDropdown, activeIndex,
|
|
149
|
-
setFilteredSuggestions, setOpen, setActiveIndex,
|
|
170
|
+
setFilteredSuggestions, setOpen, setActiveIndex, userClosedDropdownRef,
|
|
150
171
|
} = s;
|
|
151
172
|
useEffect(() => {
|
|
152
173
|
const suggestions = propSuggestions ?? [];
|
|
@@ -157,12 +178,17 @@ function useLocalFilter<T>(p: AutocompleteProps<T>, s: AutocompleteState<T>): vo
|
|
|
157
178
|
const filtered = filterSuggestions(suggestions, inputValue, matchMode, getLabel);
|
|
158
179
|
setFilteredSuggestions(filtered);
|
|
159
180
|
maybeAutoOpen(
|
|
160
|
-
{ inputValue, isInputFocused, composition, justCompletedGhost,
|
|
181
|
+
{ inputValue, isInputFocused, composition, justCompletedGhost, userClosedDropdownRef, activeIndex, setOpen, setActiveIndex },
|
|
161
182
|
filtered.length > 0,
|
|
162
183
|
);
|
|
184
|
+
// `userClosedDropdown` (state) stays a dependency so this effect still
|
|
185
|
+
// reruns on every change of it, same as before — only the VALUE read
|
|
186
|
+
// inside `maybeAutoOpen` moved to the ref (see `userClosedDropdownRef`'s
|
|
187
|
+
// doc comment).
|
|
163
188
|
}, [
|
|
164
189
|
propSuggestions, inputValue, async, getLabel, matchMode, isInputFocused, composition,
|
|
165
190
|
justCompletedGhost, userClosedDropdown, activeIndex, setFilteredSuggestions, setOpen, setActiveIndex,
|
|
191
|
+
userClosedDropdownRef,
|
|
166
192
|
]);
|
|
167
193
|
}
|
|
168
194
|
|
|
@@ -189,7 +215,7 @@ function commitMultiSelect<T>(item: T, p: AutocompleteProps<T>, s: AutocompleteS
|
|
|
189
215
|
if (!current.some((sel) => getKey(sel) === getKey(item))) p.onSelectedItemsChange?.([...current, item]);
|
|
190
216
|
s.setInputValue('');
|
|
191
217
|
onChange('');
|
|
192
|
-
s
|
|
218
|
+
setDeliberateClose(s, false);
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
function commitSingleSelect<T>(item: T, p: AutocompleteProps<T>, s: AutocompleteState<T>, onChange: (v: string) => void): void {
|
|
@@ -227,8 +253,11 @@ function runSelectItem<T>(item: T, p: AutocompleteProps<T>, s: AutocompleteState
|
|
|
227
253
|
// Set LAST so a re-fired `onFocus` cannot clear the flag. The close itself
|
|
228
254
|
// relies on the input never blurring during a pick (the option's
|
|
229
255
|
// `onMouseDown` preventDefault, and Enter keeps focus): `onFocus` reopens a
|
|
230
|
-
// non-empty input directly, without reading this flag.
|
|
231
|
-
|
|
256
|
+
// non-empty input directly, without reading this flag. The REF half is set
|
|
257
|
+
// in the same synchronous tick as the pick, so an effect scheduled by an
|
|
258
|
+
// earlier keystroke — and still pending when this runs — reads the
|
|
259
|
+
// up-to-date value instead of the one it closed over (FUT-2780).
|
|
260
|
+
if (isSingleSelectPick) setDeliberateClose(s, true);
|
|
232
261
|
}
|
|
233
262
|
|
|
234
263
|
function buildKeyContext<T>(
|
|
@@ -244,7 +273,8 @@ function buildKeyContext<T>(
|
|
|
244
273
|
multiple: Boolean(p.multiple), selectedItems: p.selectedItems ?? [],
|
|
245
274
|
getLabel: p.getLabel ?? defaultGetLabel,
|
|
246
275
|
getSuggestionType: p.getSuggestionType ?? defaultGetSuggestionType,
|
|
247
|
-
setOpen: s.setOpen, setActiveIndex: s.setActiveIndex,
|
|
276
|
+
setOpen: s.setOpen, setActiveIndex: s.setActiveIndex,
|
|
277
|
+
setUserClosedDropdown: (closed: boolean) => setDeliberateClose(s, closed),
|
|
248
278
|
setInputValue: s.setInputValue, setGhost: s.setGhost, setJustCompletedGhost: s.setJustCompletedGhost,
|
|
249
279
|
debouncedOnChange: onChange, selectItem, onSelectedItemsChange: p.onSelectedItemsChange,
|
|
250
280
|
};
|
|
@@ -287,7 +317,7 @@ export function useAutocomplete<T>(p: AutocompleteProps<T>): AutocompleteView<T>
|
|
|
287
317
|
Boolean(p.isLoading),
|
|
288
318
|
s.inputValue,
|
|
289
319
|
Boolean(p.disabled),
|
|
290
|
-
s.
|
|
320
|
+
s.userClosedDropdownRef,
|
|
291
321
|
s.setOpen,
|
|
292
322
|
s.setActiveIndex,
|
|
293
323
|
);
|
|
@@ -315,11 +345,11 @@ export function useAutocomplete<T>(p: AutocompleteProps<T>): AutocompleteView<T>
|
|
|
315
345
|
s.setInputValue(e.target.value);
|
|
316
346
|
debouncedOnChange(e.target.value);
|
|
317
347
|
if (!s.open || s.filteredSuggestions.length === 0) s.setActiveIndex(-1);
|
|
318
|
-
s
|
|
348
|
+
setDeliberateClose(s, false);
|
|
319
349
|
},
|
|
320
350
|
onFocus: (): void => {
|
|
321
351
|
s.setIsInputFocused(true);
|
|
322
|
-
s
|
|
352
|
+
setDeliberateClose(s, false);
|
|
323
353
|
if (!s.inputValue.trim()) return;
|
|
324
354
|
s.setOpen(true);
|
|
325
355
|
if (s.filteredSuggestions.length > 0 && s.activeIndex === -1) s.setActiveIndex(0);
|
|
@@ -330,7 +360,7 @@ export function useAutocomplete<T>(p: AutocompleteProps<T>): AutocompleteView<T>
|
|
|
330
360
|
onClickAway: (): void => {
|
|
331
361
|
s.setOpen(false);
|
|
332
362
|
s.setActiveIndex(-1);
|
|
333
|
-
s
|
|
363
|
+
setDeliberateClose(s, true);
|
|
334
364
|
},
|
|
335
365
|
};
|
|
336
366
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/form/Autocomplete/useAutocomplete.ts"],"sourcesContent":["import { useEffect, useId, useRef, useState } from 'react';\n\nimport {\n defaultGetKey,\n defaultGetLabel,\n defaultGetSuggestionType,\n defaultGetUrl,\n defaultOpenLink,\n filterSuggestions,\n firstGhostSuggestion,\n type MatchMode,\n} from './Autocomplete.helpers';\nimport { handleAutocompleteKey, type KeyContext } from './Autocomplete.keys';\nimport type { AutocompleteProps } from './Autocomplete.types';\n\ntype Setter<V> = React.Dispatch<React.SetStateAction<V>>;\n\n/** All mutable state + refs for one Autocomplete instance. */\ninterface AutocompleteState<T> {\n open: boolean;\n setOpen: Setter<boolean>;\n activeIndex: number;\n setActiveIndex: Setter<number>;\n ghost: string;\n setGhost: Setter<string>;\n inputValue: string;\n setInputValue: Setter<string>;\n composition: boolean;\n setComposition: Setter<boolean>;\n filteredSuggestions: T[];\n setFilteredSuggestions: Setter<T[]>;\n isInputFocused: boolean;\n setIsInputFocused: Setter<boolean>;\n justCompletedGhost: boolean;\n setJustCompletedGhost: Setter<boolean>;\n userClosedDropdown: boolean;\n setUserClosedDropdown: Setter<boolean>;\n inputRef: React.RefObject<HTMLInputElement | null>;\n listRef: React.RefObject<HTMLUListElement | null>;\n debounceRef: React.RefObject<number | undefined>;\n}\n\nfunction useAutocompleteState<T>(value: string, suggestions: T[]): AutocompleteState<T> {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(-1);\n const [ghost, setGhost] = useState('');\n const [inputValue, setInputValue] = useState(value);\n const [composition, setComposition] = useState(false);\n const [filteredSuggestions, setFilteredSuggestions] = useState<T[]>(suggestions);\n const [isInputFocused, setIsInputFocused] = useState(false);\n const [justCompletedGhost, setJustCompletedGhost] = useState(false);\n const [userClosedDropdown, setUserClosedDropdown] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement | null>(null);\n const debounceRef = useRef<number | undefined>(undefined);\n return {\n open, setOpen, activeIndex, setActiveIndex, ghost, setGhost, inputValue, setInputValue,\n composition, setComposition, filteredSuggestions, setFilteredSuggestions, isInputFocused,\n setIsInputFocused, justCompletedGhost, setJustCompletedGhost, userClosedDropdown,\n setUserClosedDropdown, inputRef, listRef, debounceRef,\n };\n}\n\n// ---- effects (one tiny hook each; explicit params keep deps complete) --------\n\nfunction useValueSync(value: string, setInputValue: Setter<string>): void {\n useEffect(() => setInputValue(value), [value, setInputValue]);\n}\n\nfunction useDebounceCleanup(debounceRef: React.RefObject<number | undefined>): void {\n useEffect(\n () => () => {\n if (debounceRef.current) window.clearTimeout(debounceRef.current);\n },\n [debounceRef],\n );\n}\n\nfunction useScrollActiveIntoView(\n activeIndex: number,\n optionIdPrefix: string,\n listRef: React.RefObject<HTMLUListElement | null>,\n): void {\n useEffect(() => {\n if (activeIndex < 0 || !listRef.current) return;\n document\n .getElementById(`${optionIdPrefix}-${activeIndex}`)\n ?.scrollIntoView({ block: 'nearest' });\n }, [activeIndex, optionIdPrefix, listRef]);\n}\n\nfunction useGhostFlagReset(justCompletedGhost: boolean, setJustCompletedGhost: Setter<boolean>): void {\n useEffect(() => {\n if (!justCompletedGhost) return;\n const timer = setTimeout(() => setJustCompletedGhost(false), 50);\n return () => clearTimeout(timer);\n }, [justCompletedGhost, setJustCompletedGhost]);\n}\n\nfunction useLoadingOpen(\n isLoading: boolean,\n inputValue: string,\n disabled: boolean,\n userClosedDropdown: boolean,\n setOpen: Setter<boolean>,\n setActiveIndex: Setter<number>,\n): void {\n useEffect(() => {\n if (isLoading && inputValue.trim().length > 0 && !disabled && !userClosedDropdown) setOpen(true);\n if (disabled) {\n setOpen(false);\n setActiveIndex(-1);\n }\n }, [isLoading, inputValue, disabled, userClosedDropdown, setOpen, setActiveIndex]);\n}\n\n/** Inputs to the auto-open check, all primitive/stable so effect deps stay complete. */\ninterface AutoOpenArgs {\n inputValue: string;\n isInputFocused: boolean;\n composition: boolean;\n justCompletedGhost: boolean;\n userClosedDropdown: boolean;\n activeIndex: number;\n setOpen: Setter<boolean>;\n setActiveIndex: Setter<number>;\n}\n\n/** Auto-open the dropdown once local filtering has a fresh result set. */\nfunction maybeAutoOpen(a: AutoOpenArgs, hasResults: boolean): void {\n const canAutoOpen =\n a.inputValue.trim() &&\n a.isInputFocused &&\n !a.composition &&\n !a.justCompletedGhost &&\n !a.userClosedDropdown;\n if (!canAutoOpen) return;\n a.setOpen(true);\n if (hasResults && a.activeIndex === -1) a.setActiveIndex(0);\n}\n\nfunction useLocalFilter<T>(p: AutocompleteProps<T>, s: AutocompleteState<T>): void {\n const propSuggestions = p.suggestions;\n const getLabel = p.getLabel ?? defaultGetLabel;\n const matchMode: MatchMode = p.matchMode ?? 'contains';\n const async = Boolean(p.async);\n const {\n inputValue, isInputFocused, composition, justCompletedGhost, userClosedDropdown, activeIndex,\n setFilteredSuggestions, setOpen, setActiveIndex,\n } = s;\n useEffect(() => {\n const suggestions = propSuggestions ?? [];\n if (async || !inputValue) {\n setFilteredSuggestions(suggestions);\n return;\n }\n const filtered = filterSuggestions(suggestions, inputValue, matchMode, getLabel);\n setFilteredSuggestions(filtered);\n maybeAutoOpen(\n { inputValue, isInputFocused, composition, justCompletedGhost, userClosedDropdown, activeIndex, setOpen, setActiveIndex },\n filtered.length > 0,\n );\n }, [\n propSuggestions, inputValue, async, getLabel, matchMode, isInputFocused, composition,\n justCompletedGhost, userClosedDropdown, activeIndex, setFilteredSuggestions, setOpen, setActiveIndex,\n ]);\n}\n\nfunction useGhostText<T>(p: AutocompleteProps<T>, s: AutocompleteState<T>): void {\n const getLabel = p.getLabel ?? defaultGetLabel;\n const getType = p.getSuggestionType ?? defaultGetSuggestionType;\n const showGhost = p.showGhostText ?? true;\n const { inputValue, filteredSuggestions, isInputFocused, composition, setGhost } = s;\n useEffect(() => {\n if (!inputValue || !isInputFocused || composition || !showGhost) {\n setGhost('');\n return;\n }\n const first = firstGhostSuggestion(filteredSuggestions, inputValue, getLabel, getType);\n setGhost(first ? getLabel(first).slice(inputValue.length) : '');\n }, [inputValue, filteredSuggestions, isInputFocused, composition, showGhost, getLabel, getType, setGhost]);\n}\n\n// ---- selection (split by mode to keep each function under the complexity bar) -\n\nfunction commitMultiSelect<T>(item: T, p: AutocompleteProps<T>, s: AutocompleteState<T>, onChange: (v: string) => void): void {\n const getKey = p.getKey ?? defaultGetKey;\n const current = p.selectedItems ?? [];\n if (!current.some((sel) => getKey(sel) === getKey(item))) p.onSelectedItemsChange?.([...current, item]);\n s.setInputValue('');\n onChange('');\n s.setUserClosedDropdown(false);\n}\n\nfunction commitSingleSelect<T>(item: T, p: AutocompleteProps<T>, s: AutocompleteState<T>, onChange: (v: string) => void): void {\n const label = (p.getLabel ?? defaultGetLabel)(item);\n s.setInputValue(label);\n onChange(label);\n}\n\n/** Apply a pick: open a link suggestion, or commit a search one to the input. */\nfunction commitPick<T>(\n item: T,\n linkUrl: string | null,\n p: AutocompleteProps<T>,\n s: AutocompleteState<T>,\n onChange: (v: string) => void,\n): void {\n if (linkUrl !== null) (p.openLink ?? defaultOpenLink)(linkUrl, item);\n else if (p.multiple) commitMultiSelect(item, p, s, onChange);\n else commitSingleSelect(item, p, s, onChange);\n}\n\nfunction runSelectItem<T>(item: T, p: AutocompleteProps<T>, s: AutocompleteState<T>, onChange: (v: string) => void): void {\n const url = (p.getUrl ?? defaultGetUrl)(item);\n const isLink = (p.getSuggestionType ?? defaultGetSuggestionType)(item) === 'link' && Boolean(url);\n // A single-select pick (search or link) ends the current search: the list\n // stays closed until the user types again. `multiple` mode is unchanged —\n // commitMultiSelect already clears inputValue and resets the flag itself,\n // so the empty-input branch of the filter effect keeps the list closed.\n const isSingleSelectPick = isLink || !p.multiple;\n commitPick(item, isLink ? (url as string) : null, p, s, onChange);\n s.setOpen(false);\n s.setActiveIndex(-1);\n p.onSelect?.(item);\n s.inputRef.current?.focus();\n // Set LAST so a re-fired `onFocus` cannot clear the flag. The close itself\n // relies on the input never blurring during a pick (the option's\n // `onMouseDown` preventDefault, and Enter keeps focus): `onFocus` reopens a\n // non-empty input directly, without reading this flag.\n if (isSingleSelectPick) s.setUserClosedDropdown(true);\n}\n\nfunction buildKeyContext<T>(\n p: AutocompleteProps<T>,\n s: AutocompleteState<T>,\n selectItem: (item: T) => void,\n onChange: (v: string) => void,\n): KeyContext<T> {\n return {\n open: s.open, activeIndex: s.activeIndex, filteredSuggestions: s.filteredSuggestions,\n maxVisibleItems: p.maxVisibleItems ?? 10, allowFreeText: p.allowFreeText ?? true,\n inputValue: s.inputValue, ghost: s.ghost, isInputFocused: s.isInputFocused,\n multiple: Boolean(p.multiple), selectedItems: p.selectedItems ?? [],\n getLabel: p.getLabel ?? defaultGetLabel,\n getSuggestionType: p.getSuggestionType ?? defaultGetSuggestionType,\n setOpen: s.setOpen, setActiveIndex: s.setActiveIndex, setUserClosedDropdown: s.setUserClosedDropdown,\n setInputValue: s.setInputValue, setGhost: s.setGhost, setJustCompletedGhost: s.setJustCompletedGhost,\n debouncedOnChange: onChange, selectItem, onSelectedItemsChange: p.onSelectedItemsChange,\n };\n}\n\n/** The full view-model the presentational component renders. */\nexport interface AutocompleteView<T> {\n id: string;\n listId: string;\n optionIdPrefix: string;\n state: AutocompleteState<T>;\n visibleSuggestions: T[];\n activeDescendant: string | undefined;\n selectItem: (item: T) => void;\n removeSelectedItem: (item: T) => void;\n onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;\n onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n onFocus: () => void;\n onBlur: () => void;\n onClickAway: () => void;\n}\n\n/** Wire state, effects, and (plain, per-render) handlers for the Autocomplete component. */\nexport function useAutocomplete<T>(p: AutocompleteProps<T>): AutocompleteView<T> {\n const componentId = useId();\n const id = p.id || componentId;\n const optionIdPrefix = `${id}-option`;\n const s = useAutocompleteState<T>(p.value, p.suggestions ?? []);\n\n const debouncedOnChange = (val: string): void => {\n if (s.debounceRef.current) window.clearTimeout(s.debounceRef.current);\n s.debounceRef.current = window.setTimeout(() => p.onChange(val), p.debounceMs ?? 150);\n };\n\n useValueSync(p.value, s.setInputValue);\n useDebounceCleanup(s.debounceRef);\n useScrollActiveIntoView(s.activeIndex, optionIdPrefix, s.listRef);\n useGhostFlagReset(s.justCompletedGhost, s.setJustCompletedGhost);\n useLoadingOpen(\n Boolean(p.isLoading),\n s.inputValue,\n Boolean(p.disabled),\n s.userClosedDropdown,\n s.setOpen,\n s.setActiveIndex,\n );\n useLocalFilter(p, s);\n useGhostText(p, s);\n\n const selectItem = (item: T): void => runSelectItem(item, p, s, debouncedOnChange);\n\n return {\n id,\n listId: `${id}-listbox`,\n optionIdPrefix,\n state: s,\n visibleSuggestions: s.filteredSuggestions.slice(0, p.maxVisibleItems ?? 10),\n activeDescendant: s.activeIndex >= 0 ? `${optionIdPrefix}-${s.activeIndex}` : undefined,\n selectItem,\n removeSelectedItem: (item: T): void => {\n const getKey = p.getKey ?? defaultGetKey;\n p.onSelectedItemsChange?.((p.selectedItems ?? []).filter((sel) => getKey(sel) !== getKey(item)));\n s.inputRef.current?.focus();\n },\n onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>): void =>\n handleAutocompleteKey(e, s.composition, buildKeyContext(p, s, selectItem, debouncedOnChange)),\n onInputChange: (e: React.ChangeEvent<HTMLInputElement>): void => {\n s.setInputValue(e.target.value);\n debouncedOnChange(e.target.value);\n if (!s.open || s.filteredSuggestions.length === 0) s.setActiveIndex(-1);\n s.setUserClosedDropdown(false);\n },\n onFocus: (): void => {\n s.setIsInputFocused(true);\n s.setUserClosedDropdown(false);\n if (!s.inputValue.trim()) return;\n s.setOpen(true);\n if (s.filteredSuggestions.length > 0 && s.activeIndex === -1) s.setActiveIndex(0);\n },\n onBlur: (): void => {\n window.setTimeout(() => s.setIsInputFocused(false), 150);\n },\n onClickAway: (): void => {\n s.setOpen(false);\n s.setActiveIndex(-1);\n s.setUserClosedDropdown(true);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,WAAW,OAAO,QAAQ,gBAAgB;AA0CnD,SAAS,qBAAwB,OAAe,aAAwC;AACtF,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,EAAE;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AACpD,QAAM,CAAC,qBAAqB,sBAAsB,IAAI,SAAc,WAAW;AAC/E,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,KAAK;AAC1D,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,KAAK;AAClE,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,KAAK;AAClE,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,UAAU,OAAgC,IAAI;AACpD,QAAM,cAAc,OAA2B,MAAS;AACxD,SAAO;AAAA,IACL;AAAA,IAAM;AAAA,IAAS;AAAA,IAAa;AAAA,IAAgB;AAAA,IAAO;AAAA,IAAU;AAAA,IAAY;AAAA,IACzE;AAAA,IAAa;AAAA,IAAgB;AAAA,IAAqB;AAAA,IAAwB;AAAA,IAC1E;AAAA,IAAmB;AAAA,IAAoB;AAAA,IAAuB;AAAA,IAC9D;AAAA,IAAuB;AAAA,IAAU;AAAA,IAAS;AAAA,EAC5C;AACF;AAIA,SAAS,aAAa,OAAe,eAAqC;AACxE,YAAU,MAAM,cAAc,KAAK,GAAG,CAAC,OAAO,aAAa,CAAC;AAC9D;AAEA,SAAS,mBAAmB,aAAwD;AAClF;AAAA,IACE,MAAM,MAAM;AACV,UAAI,YAAY,QAAS,QAAO,aAAa,YAAY,OAAO;AAAA,IAClE;AAAA,IACA,CAAC,WAAW;AAAA,EACd;AACF;AAEA,SAAS,wBACP,aACA,gBACA,SACM;AACN,YAAU,MAAM;AACd,QAAI,cAAc,KAAK,CAAC,QAAQ,QAAS;AACzC,aACG,eAAe,GAAG,cAAc,IAAI,WAAW,EAAE,GAChD,eAAe,EAAE,OAAO,UAAU,CAAC;AAAA,EACzC,GAAG,CAAC,aAAa,gBAAgB,OAAO,CAAC;AAC3C;AAEA,SAAS,kBAAkB,oBAA6B,uBAA8C;AACpG,YAAU,MAAM;AACd,QAAI,CAAC,mBAAoB;AACzB,UAAM,QAAQ,WAAW,MAAM,sBAAsB,KAAK,GAAG,EAAE;AAC/D,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,oBAAoB,qBAAqB,CAAC;AAChD;AAEA,SAAS,eACP,WACA,YACA,UACA,oBACA,SACA,gBACM;AACN,YAAU,MAAM;AACd,QAAI,aAAa,WAAW,KAAK,EAAE,SAAS,KAAK,CAAC,YAAY,CAAC,mBAAoB,SAAQ,IAAI;AAC/F,QAAI,UAAU;AACZ,cAAQ,KAAK;AACb,qBAAe,EAAE;AAAA,IACnB;AAAA,EACF,GAAG,CAAC,WAAW,YAAY,UAAU,oBAAoB,SAAS,cAAc,CAAC;AACnF;AAeA,SAAS,cAAc,GAAiB,YAA2B;AACjE,QAAM,cACJ,EAAE,WAAW,KAAK,KAClB,EAAE,kBACF,CAAC,EAAE,eACH,CAAC,EAAE,sBACH,CAAC,EAAE;AACL,MAAI,CAAC,YAAa;AAClB,IAAE,QAAQ,IAAI;AACd,MAAI,cAAc,EAAE,gBAAgB,GAAI,GAAE,eAAe,CAAC;AAC5D;AAEA,SAAS,eAAkB,GAAyB,GAA+B;AACjF,QAAM,kBAAkB,EAAE;AAC1B,QAAM,WAAW,EAAE,YAAY;AAC/B,QAAM,YAAuB,EAAE,aAAa;AAC5C,QAAM,QAAQ,QAAQ,EAAE,KAAK;AAC7B,QAAM;AAAA,IACJ;AAAA,IAAY;AAAA,IAAgB;AAAA,IAAa;AAAA,IAAoB;AAAA,IAAoB;AAAA,IACjF;AAAA,IAAwB;AAAA,IAAS;AAAA,EACnC,IAAI;AACJ,YAAU,MAAM;AACd,UAAM,cAAc,mBAAmB,CAAC;AACxC,QAAI,SAAS,CAAC,YAAY;AACxB,6BAAuB,WAAW;AAClC;AAAA,IACF;AACA,UAAM,WAAW,kBAAkB,aAAa,YAAY,WAAW,QAAQ;AAC/E,2BAAuB,QAAQ;AAC/B;AAAA,MACE,EAAE,YAAY,gBAAgB,aAAa,oBAAoB,oBAAoB,aAAa,SAAS,eAAe;AAAA,MACxH,SAAS,SAAS;AAAA,IACpB;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IAAiB;AAAA,IAAY;AAAA,IAAO;AAAA,IAAU;AAAA,IAAW;AAAA,IAAgB;AAAA,IACzE;AAAA,IAAoB;AAAA,IAAoB;AAAA,IAAa;AAAA,IAAwB;AAAA,IAAS;AAAA,EACxF,CAAC;AACH;AAEA,SAAS,aAAgB,GAAyB,GAA+B;AAC/E,QAAM,WAAW,EAAE,YAAY;AAC/B,QAAM,UAAU,EAAE,qBAAqB;AACvC,QAAM,YAAY,EAAE,iBAAiB;AACrC,QAAM,EAAE,YAAY,qBAAqB,gBAAgB,aAAa,SAAS,IAAI;AACnF,YAAU,MAAM;AACd,QAAI,CAAC,cAAc,CAAC,kBAAkB,eAAe,CAAC,WAAW;AAC/D,eAAS,EAAE;AACX;AAAA,IACF;AACA,UAAM,QAAQ,qBAAqB,qBAAqB,YAAY,UAAU,OAAO;AACrF,aAAS,QAAQ,SAAS,KAAK,EAAE,MAAM,WAAW,MAAM,IAAI,EAAE;AAAA,EAChE,GAAG,CAAC,YAAY,qBAAqB,gBAAgB,aAAa,WAAW,UAAU,SAAS,QAAQ,CAAC;AAC3G;AAIA,SAAS,kBAAqB,MAAS,GAAyB,GAAyB,UAAqC;AAC5H,QAAM,SAAS,EAAE,UAAU;AAC3B,QAAM,UAAU,EAAE,iBAAiB,CAAC;AACpC,MAAI,CAAC,QAAQ,KAAK,CAAC,QAAQ,OAAO,GAAG,MAAM,OAAO,IAAI,CAAC,EAAG,GAAE,wBAAwB,CAAC,GAAG,SAAS,IAAI,CAAC;AACtG,IAAE,cAAc,EAAE;AAClB,WAAS,EAAE;AACX,IAAE,sBAAsB,KAAK;AAC/B;AAEA,SAAS,mBAAsB,MAAS,GAAyB,GAAyB,UAAqC;AAC7H,QAAM,SAAS,EAAE,YAAY,iBAAiB,IAAI;AAClD,IAAE,cAAc,KAAK;AACrB,WAAS,KAAK;AAChB;AAGA,SAAS,WACP,MACA,SACA,GACA,GACA,UACM;AACN,MAAI,YAAY,KAAM,EAAC,EAAE,YAAY,iBAAiB,SAAS,IAAI;AAAA,WAC1D,EAAE,SAAU,mBAAkB,MAAM,GAAG,GAAG,QAAQ;AAAA,MACtD,oBAAmB,MAAM,GAAG,GAAG,QAAQ;AAC9C;AAEA,SAAS,cAAiB,MAAS,GAAyB,GAAyB,UAAqC;AACxH,QAAM,OAAO,EAAE,UAAU,eAAe,IAAI;AAC5C,QAAM,UAAU,EAAE,qBAAqB,0BAA0B,IAAI,MAAM,UAAU,QAAQ,GAAG;AAKhG,QAAM,qBAAqB,UAAU,CAAC,EAAE;AACxC,aAAW,MAAM,SAAU,MAAiB,MAAM,GAAG,GAAG,QAAQ;AAChE,IAAE,QAAQ,KAAK;AACf,IAAE,eAAe,EAAE;AACnB,IAAE,WAAW,IAAI;AACjB,IAAE,SAAS,SAAS,MAAM;AAK1B,MAAI,mBAAoB,GAAE,sBAAsB,IAAI;AACtD;AAEA,SAAS,gBACP,GACA,GACA,YACA,UACe;AACf,SAAO;AAAA,IACL,MAAM,EAAE;AAAA,IAAM,aAAa,EAAE;AAAA,IAAa,qBAAqB,EAAE;AAAA,IACjE,iBAAiB,EAAE,mBAAmB;AAAA,IAAI,eAAe,EAAE,iBAAiB;AAAA,IAC5E,YAAY,EAAE;AAAA,IAAY,OAAO,EAAE;AAAA,IAAO,gBAAgB,EAAE;AAAA,IAC5D,UAAU,QAAQ,EAAE,QAAQ;AAAA,IAAG,eAAe,EAAE,iBAAiB,CAAC;AAAA,IAClE,UAAU,EAAE,YAAY;AAAA,IACxB,mBAAmB,EAAE,qBAAqB;AAAA,IAC1C,SAAS,EAAE;AAAA,IAAS,gBAAgB,EAAE;AAAA,IAAgB,uBAAuB,EAAE;AAAA,IAC/E,eAAe,EAAE;AAAA,IAAe,UAAU,EAAE;AAAA,IAAU,uBAAuB,EAAE;AAAA,IAC/E,mBAAmB;AAAA,IAAU;AAAA,IAAY,uBAAuB,EAAE;AAAA,EACpE;AACF;AAoBO,SAAS,gBAAmB,GAA8C;AAC/E,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,EAAE,MAAM;AACnB,QAAM,iBAAiB,GAAG,EAAE;AAC5B,QAAM,IAAI,qBAAwB,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC;AAE9D,QAAM,oBAAoB,CAAC,QAAsB;AAC/C,QAAI,EAAE,YAAY,QAAS,QAAO,aAAa,EAAE,YAAY,OAAO;AACpE,MAAE,YAAY,UAAU,OAAO,WAAW,MAAM,EAAE,SAAS,GAAG,GAAG,EAAE,cAAc,GAAG;AAAA,EACtF;AAEA,eAAa,EAAE,OAAO,EAAE,aAAa;AACrC,qBAAmB,EAAE,WAAW;AAChC,0BAAwB,EAAE,aAAa,gBAAgB,EAAE,OAAO;AAChE,oBAAkB,EAAE,oBAAoB,EAAE,qBAAqB;AAC/D;AAAA,IACE,QAAQ,EAAE,SAAS;AAAA,IACnB,EAAE;AAAA,IACF,QAAQ,EAAE,QAAQ;AAAA,IAClB,EAAE;AAAA,IACF,EAAE;AAAA,IACF,EAAE;AAAA,EACJ;AACA,iBAAe,GAAG,CAAC;AACnB,eAAa,GAAG,CAAC;AAEjB,QAAM,aAAa,CAAC,SAAkB,cAAc,MAAM,GAAG,GAAG,iBAAiB;AAEjF,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,GAAG,EAAE;AAAA,IACb;AAAA,IACA,OAAO;AAAA,IACP,oBAAoB,EAAE,oBAAoB,MAAM,GAAG,EAAE,mBAAmB,EAAE;AAAA,IAC1E,kBAAkB,EAAE,eAAe,IAAI,GAAG,cAAc,IAAI,EAAE,WAAW,KAAK;AAAA,IAC9E;AAAA,IACA,oBAAoB,CAAC,SAAkB;AACrC,YAAM,SAAS,EAAE,UAAU;AAC3B,QAAE,yBAAyB,EAAE,iBAAiB,CAAC,GAAG,OAAO,CAAC,QAAQ,OAAO,GAAG,MAAM,OAAO,IAAI,CAAC,CAAC;AAC/F,QAAE,SAAS,SAAS,MAAM;AAAA,IAC5B;AAAA,IACA,WAAW,CAAC,MACV,sBAAsB,GAAG,EAAE,aAAa,gBAAgB,GAAG,GAAG,YAAY,iBAAiB,CAAC;AAAA,IAC9F,eAAe,CAAC,MAAiD;AAC/D,QAAE,cAAc,EAAE,OAAO,KAAK;AAC9B,wBAAkB,EAAE,OAAO,KAAK;AAChC,UAAI,CAAC,EAAE,QAAQ,EAAE,oBAAoB,WAAW,EAAG,GAAE,eAAe,EAAE;AACtE,QAAE,sBAAsB,KAAK;AAAA,IAC/B;AAAA,IACA,SAAS,MAAY;AACnB,QAAE,kBAAkB,IAAI;AACxB,QAAE,sBAAsB,KAAK;AAC7B,UAAI,CAAC,EAAE,WAAW,KAAK,EAAG;AAC1B,QAAE,QAAQ,IAAI;AACd,UAAI,EAAE,oBAAoB,SAAS,KAAK,EAAE,gBAAgB,GAAI,GAAE,eAAe,CAAC;AAAA,IAClF;AAAA,IACA,QAAQ,MAAY;AAClB,aAAO,WAAW,MAAM,EAAE,kBAAkB,KAAK,GAAG,GAAG;AAAA,IACzD;AAAA,IACA,aAAa,MAAY;AACvB,QAAE,QAAQ,KAAK;AACf,QAAE,eAAe,EAAE;AACnB,QAAE,sBAAsB,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/DataViews/data-views-category-pill.tsx"],"sourcesContent":["\"use client\";\n\nimport { useDataViewsCopy } from \"./data-views-copy-context\";\nimport { CategorySelect } from \"../../form/CategorySelect\";\nimport { MultiSelectDropdown } from \"../../layout/ContentToolbar\";\n\nimport type { FilterFieldConfig, FilterOption } from \"./data-views-types\";\n\n/**\n * Diff a whole-array selection back into per-value toggles.\n *\n * The filter state's only channel is `onTogglePill(field, value, checked)`, but\n * a tree hands back the entire selection at once. Diffing here keeps that one\n * channel the single way a pill ever changes, rather than giving the\n * hierarchical control a private path into the state.\n */\nexport function emitPillDiff(\n fieldId: string,\n before: readonly string[],\n after: readonly string[],\n onTogglePill: (fieldId: string, value: string, checked: boolean) => void,\n): void {\n const had = new Set(before);\n const wants = new Set(after);\n after.forEach((value) => {\n if (!had.has(value)) onTogglePill(fieldId, value, true);\n });\n before.forEach((value) => {\n if (!wants.has(value)) onTogglePill(fieldId, value, false);\n });\n}\n\n/** A filter option as the category tree wants it. */\nconst toCategoryOption = (option: FilterOption) => ({\n id: option.value,\n name: option.label,\n parentId: option.parentId ?? null,\n});\n\ninterface PillControlProps<T extends Record<string, unknown>> {\n fieldId: string;\n pill: FilterFieldConfig<T>;\n selected: string[];\n onTogglePill: (fieldId: string, value: string, checked: boolean) => void;\n onClearField: (fieldId: string) => void;\n onOpenChange?: (open: boolean) => void;\n testIdPrefix: string;\n /** `stacked` in the filter panel, `pill` on the inline bar. */\n layout?: \"stacked\" | \"pill\";\n}\n\n/**\n * One filter pill: the hierarchical category tree, or the flat multi-select.\n *\n * A category set nests, and flattening it into one list is what made \"Massas\"\n * (mercearia) indistinguishable from \"Massas\" (pratos principais). `control:\n * \"category\"` opts a field into the tree, which shows each hit under its parent.\n *\n * {@link FilterFieldConfig.allowParentSelection} decides whether a parent row is\n * itself tickable. It is forwarded rather than fixed here because it is a\n * per-SCREEN question: a filter over a catalogue where products sit on leaves\n * wants \"the whole category\" as one click, while one whose parents are pure\n * grouping would only offer a tick that means nothing.\n */\nexport function PillControl<T extends Record<string, unknown>>({\n fieldId,\n pill,\n selected,\n onTogglePill,\n onClearField,\n onOpenChange,\n testIdPrefix,\n layout = \"pill\",\n}: PillControlProps<T>): React.JSX.Element {\n const copy = useDataViewsCopy();\n const testId = `${testIdPrefix}-filter-${fieldId}`;\n if (pill.control === \"category\") {\n return (\n <CategorySelect\n copy={copy.categorySelect}\n placeholder={pill.label}\n label={layout === \"stacked\" ? pill.label : undefined}\n fullWidth={layout === \"stacked\"}\n options={pill.options.map(toCategoryOption)}\n allowParentSelection={pill.allowParentSelection ?? false}\n value={selected}\n onChange={(next) => emitPillDiff(fieldId, selected, next, onTogglePill)}\n dataTestId={testId}\n />\n );\n }\n return (\n <MultiSelectDropdown\n extraOptionsHeading={copy.filters.optionsHeading}\n clearText={copy.filters.clear}\n clearLabel={copy.filters.clearRange(pill.label)}\n label={pill.label}\n options={pill.options}\n selected={new Set(selected)}\n onToggle={(value, checked) => onTogglePill(fieldId, value, checked)}\n onClear={() => onClearField(fieldId)}\n allLabel={copy.filters.allOption}\n searchable={pill.searchEnabled ? true : undefined}\n searchPlaceholder={copy.filters.optionSearchPlaceholder}\n noResultsLabel={copy.filters.optionsEmpty}\n layout={layout}\n onOpenChange={onOpenChange}\n data-testid={testId}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;AA8EM;AA9DC,SAAS,aACd,SACA,QACA,OACA,cACM;AACN,QAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,QAAM,QAAQ,IAAI,IAAI,KAAK;AAC3B,QAAM,QAAQ,CAAC,UAAU;AACvB,QAAI,CAAC,IAAI,IAAI,KAAK,EAAG,cAAa,SAAS,OAAO,IAAI;AAAA,EACxD,CAAC;AACD,SAAO,QAAQ,CAAC,UAAU;AACxB,QAAI,CAAC,MAAM,IAAI,KAAK,EAAG,cAAa,SAAS,OAAO,KAAK;AAAA,EAC3D,CAAC;AACH;AAGA,IAAM,mBAAmB,CAAC,YAA0B;AAAA,EAClD,IAAI,OAAO;AAAA,EACX,MAAM,OAAO;AAAA,EACb,UAAU,OAAO,YAAY;AAC/B;AA2BO,SAAS,YAA+C;AAAA,EAC7D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AACX,GAA2C;AACzC,QAAM,OAAO,iBAAiB;AAC9B,QAAM,SAAS,GAAG,YAAY,WAAW,OAAO;AAChD,MAAI,KAAK,YAAY,YAAY;AAC/B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,KAAK;AAAA,QACX,aAAa,KAAK;AAAA,QAClB,OAAO,WAAW,YAAY,KAAK,QAAQ;AAAA,QAC3C,WAAW,WAAW;AAAA,QACtB,SAAS,KAAK,QAAQ,IAAI,gBAAgB;AAAA,QAC1C,sBAAsB,KAAK,wBAAwB;AAAA,QACnD,OAAO;AAAA,QACP,UAAU,CAAC,SAAS,aAAa,SAAS,UAAU,MAAM,YAAY;AAAA,QACtE,YAAY;AAAA;AAAA,IACd;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,qBAAqB,KAAK,QAAQ;AAAA,MAClC,WAAW,KAAK,QAAQ;AAAA,MACxB,YAAY,KAAK,QAAQ,WAAW,KAAK,KAAK;AAAA,MAC9C,OAAO,KAAK;AAAA,MACZ,SAAS,KAAK;AAAA,MACd,UAAU,IAAI,IAAI,QAAQ;AAAA,MAC1B,UAAU,CAAC,OAAO,YAAY,aAAa,SAAS,OAAO,OAAO;AAAA,MAClE,SAAS,MAAM,aAAa,OAAO;AAAA,MACnC,UAAU,KAAK,QAAQ;AAAA,MACvB,YAAY,KAAK,gBAAgB,OAAO;AAAA,MACxC,mBAAmB,KAAK,QAAQ;AAAA,MAChC,gBAAgB,KAAK,QAAQ;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,eAAa;AAAA;AAAA,EACf;AAEJ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/DataViews/data-views-more-fields.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * ONE FIELD, RENDERED INSIDE THE \"MAIS\" PANEL.\n *\n * Split from `data-views-more-filters` at the file-size gate, along the seam\n * that was already there: that module is the trigger, the panel and its\n * footer — the chrome — and this is what goes in it.\n *\n * A field with one or two options renders FLAT: inside a panel there is no\n * space to win back, and two labelled checkboxes are faster than a dropdown\n * needing a second click. Past that it becomes a {@link MultiSelectDropdown} —\n * see `INLINE_OPTION_LIMIT`.\n */\nimport Checkbox from \"@mui/material/Checkbox/index.js\";\n\nimport { MultiSelectDropdown } from \"../../layout/ContentToolbar\";\nimport { Box } from \"../../../mui/Box\";\nimport { Text } from \"../../typography/Text\";\n\nimport type { OverflowField } from \"./data-views-overflow\";\nimport { RangeBounds } from \"./data-views-range-pill\";\nimport { isRangeSet } from \"./data-views-range-values\";\nimport type { RangeValue } from \"./data-views-types\";\nimport { useDataViewsCopy } from \"./data-views-copy-context\";\nimport { fieldRadiusPx } from \"../../../tokens/field-radius\";\nimport { fieldHeight } from \"../../../tokens/field-height\";\nimport { fieldEdge } from \"../../../tokens/field-edge\";\nimport { sxRem } from \"../../../tokens/relative\";\n\n/** What each field's control needs, minus the panel's own chrome. */\nexport interface MoreFieldProps {\n pills: Record<string, string[]>;\n ranges: Record<string, RangeValue>;\n onTogglePill: (fieldId: string, value: string, checked: boolean) => void;\n onChangeRange: (fieldId: string, range: RangeValue) => void;\n testIdPrefix: string;\n}\n\n/**\n * Above this many options a field is a DROPDOWN rather than a flat row of\n * checkboxes. Two options cost two lines and read faster laid out; a \"Cliente\"\n * with a dozen turns the panel into a scrolling wall and buries every field\n * under it, which is exactly what the overflow was meant to avoid.\n */\nconst INLINE_OPTION_LIMIT = 2;\n\n/** One overflowed pill: its options as checkboxes, all visible at once. */\nfunction OverflowPill<T extends Record<string, unknown>>({\n field,\n values,\n onToggle,\n onClear,\n testIdPrefix,\n}: {\n field: OverflowField<T>;\n values: string[];\n onToggle: (value: string, checked: boolean) => void;\n onClear: () => void;\n testIdPrefix: string;\n}): React.JSX.Element {\n const copy = useDataViewsCopy();\n const options = field.pill?.options ?? [];\n if (options.length > INLINE_OPTION_LIMIT) {\n return (\n <MultiSelectDropdown\n extraOptionsHeading={copy.filters.optionsHeading}\n clearText={copy.filters.clear}\n clearLabel={copy.filters.clearRange(field.label)}\n label={field.pill?.label ?? field.label}\n options={options}\n selected={new Set(values)}\n onToggle={onToggle}\n onClear={onClear}\n allLabel={copy.filters.allOption}\n searchable={options.length > 6 ? true : undefined}\n searchPlaceholder={copy.filters.optionSearchPlaceholder}\n noResultsLabel={copy.filters.optionsEmpty}\n layout=\"pill\"\n data-testid={`${testIdPrefix}-more-${field.id}`}\n />\n );\n }\n return (\n <Box sx={{ display: \"flex\", flexWrap: \"wrap\", gap: 0.75 }}>\n {(field.pill?.options ?? []).map((option) => {\n const checked = values.includes(option.value);\n return (\n <Box\n key={option.value}\n component=\"label\"\n data-testid={`${testIdPrefix}-more-${field.id}-${option.value}`}\n sx={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 0.25,\n pr: 1,\n border: 1,\n borderStyle: \"solid\",\n minHeight: fieldHeight,\n borderColor: (theme) => (checked ? theme.palette.primary.main : fieldEdge(theme)),\n bgcolor: checked ? \"action.selected\" : \"transparent\",\n borderRadius: fieldRadiusPx,\n cursor: \"pointer\",\n fontSize: sxRem(13),\n color: checked ? \"primary.main\" : \"text.secondary\",\n }}\n >\n <Checkbox\n size=\"small\"\n checked={checked}\n onChange={(event) => onToggle(option.value, event.target.checked)}\n inputProps={{ \"aria-label\": `${field.label}: ${option.label}` }}\n />\n {option.label}\n </Box>\n );\n })}\n </Box>\n );\n}\n\n\n/**\n * One overflowed range: its presets and its two bounds, as the same controls\n * the pill uses.\n *\n * The whole control DELEGATES to {@link RangeBounds} rather than being built\n * here, which is the point — this panel used to render a raw\n * `<input type=\"date\">`, so the masked `dd/mm/aaaa` field existed only while\n * the filter FITTED on the bar. The moment \"Data\" overflowed it reverted to the\n * native control the mask replaced, and a merchant on a narrow screen never saw\n * the fix at all (FUT-744). `Valor` had the twin bug: a bare `type=\"number\"`\n * plus `Number(raw)` dropped the decimal comma this panel is meant to accept.\n *\n * That also retires the intrinsic-width workaround this pair used to need — a\n * native date input reports a wide minimum (its own mask plus the picker\n * glyph), which is what made two of them refuse to shrink and scrolled the\n * panel sideways. A text input has no such minimum.\n *\n * `RangeBounds` renders the preset chips ITSELF, above the inputs. This panel\n * must NOT render its own row as well: when the delegation above landed, the\n * chips it used to draw by hand stayed behind, and \"Data\" showed Hoje/Ontem/\n * Esta semana/Este mês/Este ano twice — two rows, duplicated test ids, and half\n * the panel's height spent saying the same thing (FUT-751).\n */\nfunction OverflowRange<T extends Record<string, unknown>>({\n field,\n value,\n onChange,\n testIdPrefix,\n}: {\n field: OverflowField<T>;\n value: RangeValue;\n onChange: (range: RangeValue) => void;\n testIdPrefix: string;\n}): React.JSX.Element | null {\n // `group === \"range\"` is what selects this component, so `range` is always\n // set; the guard is for the type, not for a state the caller can reach.\n if (!field.range) return null;\n return (\n <RangeBounds\n field={field.range}\n value={value}\n onChange={onChange}\n testId={`${testIdPrefix}-more-${field.id}`}\n />\n );\n}\n\n/**\n * The trigger, badged with how many fields had no room on the bar — and told\n * apart when some of them are APPLIED.\n *\n * An applied filter can land in here now (it is ranked first, not exempt), so\n * this button is the only thing on screen still saying it exists. A neutral\n * badge reading \"3\" would make \"three filters you have not used\" and \"three\n * filters narrowing this list\" look identical, which is the whole failure the\n * old exemption was written to avoid. So an overflow holding applied filters\n * takes the applied tone and counts them in its own badge.\n\n/**\n * Whether a field has anything applied, and how to unapply it — the two shapes\n * (a pill's selected values, a range's bounds) answered in one place so the\n * group that renders them stays a renderer.\n */\nexport function fieldClearing<T extends Record<string, unknown>>({\n field,\n pills,\n ranges,\n onTogglePill,\n onChangeRange,\n}: {\n field: OverflowField<T>;\n} & Pick<MoreFieldProps, \"pills\" | \"ranges\" | \"onTogglePill\" | \"onChangeRange\">): {\n applied: boolean;\n clear: () => void;\n} {\n if (field.group === \"range\") {\n return {\n applied: isRangeSet(ranges[field.id] ?? {}),\n clear: () => onChangeRange(field.id, {}),\n };\n }\n const values = pills[field.id] ?? [];\n return {\n applied: values.length > 0,\n clear: () => values.forEach((value) => onTogglePill(field.id, value, false)),\n };\n}\n\n\n/** One labelled group in the panel: the field's name, then its control. */\nexport function MoreGroup<T extends Record<string, unknown>>({\n field,\n pills,\n ranges,\n onTogglePill,\n onChangeRange,\n testIdPrefix,\n}: {\n field: OverflowField<T>;\n} & MoreFieldProps): React.JSX.Element {\n const copy = useDataViewsCopy();\n const { applied, clear } = fieldClearing({ field, pills, ranges, onTogglePill, onChangeRange });\n return (\n <Box sx={{ mb: 1.5, \"&:last-of-type\": { mb: 0 } }}>\n <Box sx={{ display: \"flex\", alignItems: \"center\", gap: 1, mb: 0.5 }}>\n <Text variant=\"caption\" as=\"span\">\n <Box component=\"span\" sx={{ color: \"text.secondary\" }}>\n {field.label}\n {field.group === \"range\" && isRangeSet(ranges[field.id] ?? {}) ? \" •\" : \"\"}\n </Box>\n </Text>\n {/* The panel's equivalent of the pill's ✕. Without it a field applied\n in here could only be cleared by finding it again on the bar —\n which is where it goes the moment it becomes active, so the\n operator has to close this panel to undo what they just did. */}\n {applied && (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={clear}\n data-testid={`${testIdPrefix}-more-${field.id}-clear`}\n sx={{\n ml: \"auto\",\n border: 0,\n p: 0,\n bgcolor: \"transparent\",\n cursor: \"pointer\",\n font: \"inherit\",\n fontSize: sxRem(12),\n color: \"primary.main\",\n \"&:hover\": { textDecoration: \"underline\" },\n }}\n >\n {copy.filters.clear}\n </Box>\n )}\n </Box>\n {field.group === \"pill\" ? (\n <OverflowPill\n field={field}\n values={pills[field.id] ?? []}\n onToggle={(value, checked) => onTogglePill(field.id, value, checked)}\n onClear={() => (pills[field.id] ?? []).forEach((v) => onTogglePill(field.id, v, false))}\n testIdPrefix={testIdPrefix}\n />\n ) : (\n <OverflowRange\n field={field}\n value={ranges[field.id] ?? {}}\n onChange={(range) => onChangeRange(field.id, range)}\n testIdPrefix={testIdPrefix}\n />\n )}\n </Box>\n );\n}\n\n"],"mappings":"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","names":["default"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|