@lotics/ui 7.7.0 → 7.8.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/AGENTS.md +4 -1
- package/package.json +1 -1
- package/src/combobox.tsx +28 -11
package/AGENTS.md
CHANGED
|
@@ -227,7 +227,10 @@ parts render the CHROME, composed as children — never a render-prop pile:
|
|
|
227
227
|
|
|
228
228
|
It's a SELECT by default (`ComboboxInput` with NO `icon` → a trailing chevron); opt INTO the
|
|
229
229
|
search-box look with `ComboboxInput icon="search"` only when typing-to-search is primary (a
|
|
230
|
-
large/remote set).
|
|
230
|
+
large/remote set). For a select that the user can RESET, pass `clearable` + `onClear` (deselect):
|
|
231
|
+
the chevron shows while the field is empty and a clear ✕ replaces it once there's a value/query;
|
|
232
|
+
pressing ✕ clears the text and reopens the full-browse list, so `onClear` lands back on the whole
|
|
233
|
+
set (on focus an empty field already browses everything — `recentOptions ?? all`). `allowCustom` appends a "Create …" row (`customOptionLabel`) when the query
|
|
231
234
|
matches no option, emitting the typed text as the value — so a value not in the known set means
|
|
232
235
|
CREATE. Wire that branch to a create overlay (a modal `Dialog` for 4+ fields, an anchored popover
|
|
233
236
|
for 1–3) that builds the new record and attaches it; existing matches attach directly. The create
|
package/package.json
CHANGED
package/src/combobox.tsx
CHANGED
|
@@ -182,7 +182,13 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
182
182
|
const single = value ?? null;
|
|
183
183
|
const reflectedText = single ? (single.label ?? single.value) : "";
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
// The live edit buffer. `null` = the user isn't editing: the input reflects the
|
|
186
|
+
// committed selection's label and the list browses the FULL set. A string (incl.
|
|
187
|
+
// "") = the user is searching: the input shows it and the list filters by it.
|
|
188
|
+
// This one nullable carries the bit that (value, inputText) alone can't — after
|
|
189
|
+
// selecting FPP, the input reads "FPP" whether the user just reopened (browse)
|
|
190
|
+
// or retyped it (filter); `draft` is null in the first case, "FPP" in the second.
|
|
191
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
186
192
|
const [open, setOpen] = useState(autoFocus);
|
|
187
193
|
const triggerRef = useRef<View>(null);
|
|
188
194
|
const inputRef = useRef<RNTextInput>(null);
|
|
@@ -197,9 +203,11 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
197
203
|
const isServer = onSearchChange !== undefined;
|
|
198
204
|
const debouncedSearch = useDebouncedCallback(onSearchChange ?? (() => {}), searchDebounceMs);
|
|
199
205
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
// What the input shows: the live draft while editing, else the reflected label.
|
|
207
|
+
const inputText = draft ?? (reflectSelection && single ? reflectedText : "");
|
|
208
|
+
// What the list filters by: nothing while reflecting a selection (browse), the
|
|
209
|
+
// draft while editing — so retyping the selected label still filters to it.
|
|
210
|
+
const filterQuery = draft ?? "";
|
|
203
211
|
|
|
204
212
|
const findOption = useCallback(
|
|
205
213
|
(val: T): PickerOption<T, D> =>
|
|
@@ -215,7 +223,9 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
215
223
|
debouncedSearch.cancel();
|
|
216
224
|
onSearchChange?.("");
|
|
217
225
|
}
|
|
218
|
-
|
|
226
|
+
// Stop editing — the input reflects the new label (or clears, when
|
|
227
|
+
// reflectSelection is off) and the next open browses the full set.
|
|
228
|
+
setDraft(null);
|
|
219
229
|
setOpen(false);
|
|
220
230
|
// The refocus below must not reopen the just-closed menu.
|
|
221
231
|
suppressFocusOpenRef.current = true;
|
|
@@ -247,7 +257,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
247
257
|
|
|
248
258
|
const handleChangeText = useCallback(
|
|
249
259
|
(text: string) => {
|
|
250
|
-
|
|
260
|
+
setDraft(text);
|
|
251
261
|
if (!disabled) setOpen(true);
|
|
252
262
|
if (isServer) debouncedSearch(text);
|
|
253
263
|
},
|
|
@@ -285,7 +295,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
285
295
|
// Boundary cast: the engine is generic per-instance, the context type is not.
|
|
286
296
|
// `ComboboxContent` casts back to its own `<T, D>` before touching rows.
|
|
287
297
|
list: list as unknown as UseOptionList<string, unknown>,
|
|
288
|
-
query,
|
|
298
|
+
query: inputText,
|
|
289
299
|
open,
|
|
290
300
|
showList,
|
|
291
301
|
loading,
|
|
@@ -349,9 +359,14 @@ export interface ComboboxInputProps {
|
|
|
349
359
|
export function ComboboxInput(props: ComboboxInputProps) {
|
|
350
360
|
const { icon, placeholder, clearable = false, onClear, clearLabel, accessibilityLabel, testID } = props;
|
|
351
361
|
const ctx = useComboboxContext("ComboboxInput");
|
|
352
|
-
// Select mode (no leading icon):
|
|
353
|
-
//
|
|
354
|
-
|
|
362
|
+
// Select mode (no leading icon): the right slot holds the clear ✕ when the field
|
|
363
|
+
// carries text (a reflected selection or a live query) AND it's clearable;
|
|
364
|
+
// otherwise the trailing chevron, so an EMPTY picker still reads as a picker.
|
|
365
|
+
// The two never show together — ✕ replaces the chevron once there's something to
|
|
366
|
+
// clear, and pressing it resets the query + reopens the browse list (via
|
|
367
|
+
// `handleChangeText("")`) so the consumer's `onClear` lands back on the full set.
|
|
368
|
+
const showClear = clearable && ctx.query.length > 0;
|
|
369
|
+
const showChevron = !icon && !showClear;
|
|
355
370
|
|
|
356
371
|
return (
|
|
357
372
|
<View ref={ctx.triggerRef}>
|
|
@@ -375,7 +390,9 @@ export function ComboboxInput(props: ComboboxInputProps) {
|
|
|
375
390
|
autoCorrect={false}
|
|
376
391
|
accessibilityLabel={accessibilityLabel}
|
|
377
392
|
style={[
|
|
378
|
-
|
|
393
|
+
// Select mode reserves the right gutter for whichever trailing affordance
|
|
394
|
+
// is up (chevron when empty, ✕ when there's text) so neither overlaps it.
|
|
395
|
+
!icon ? styles.selectInput : undefined,
|
|
379
396
|
// Gate on `showList`, not `open`: an empty query with no options has
|
|
380
397
|
// `open=true` but renders NO dropdown, so a ring tied to `open` would
|
|
381
398
|
// show with nothing below it AND linger after blur (no popover fires a
|