@delacour/react-native-ui 0.5.0-alpha.20261023075443 → 0.5.0-alpha.20261024103119
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/package.json +3 -3
- package/src/components/combobox/AGENTS.md +27 -16
- package/src/components/combobox/combobox.tsx +11 -39
- package/src/components/combobox/combobox.variants.test.ts +15 -0
- package/src/components/combobox/combobox.variants.ts +21 -0
- package/src/components/combobox/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/react-native-ui",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.20261024103119",
|
|
4
4
|
"description": "React Native UI components — Uniwind, Reanimated, Gesture Handler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -182,8 +182,8 @@
|
|
|
182
182
|
"expo-blur": ">=15",
|
|
183
183
|
"@maplibre/maplibre-react-native": "^11.5.0",
|
|
184
184
|
"@central-icons-react-native/round-outlined-radius-1-stroke-1.5": "^1.1",
|
|
185
|
-
"@delacour/react-native-bottom-sheet": "0.3.0-alpha.
|
|
186
|
-
"@delacour/react-native-charts": "0.2.0-alpha.
|
|
185
|
+
"@delacour/react-native-bottom-sheet": "0.3.0-alpha.20261024103119",
|
|
186
|
+
"@delacour/react-native-charts": "0.2.0-alpha.20261024103119",
|
|
187
187
|
"@legendapp/list": ">=3.3",
|
|
188
188
|
"react-native-skia": ">=3.7.3",
|
|
189
189
|
"expo-linear-gradient": ">=15",
|
|
@@ -32,7 +32,7 @@ typed text can become a value.
|
|
|
32
32
|
| `combobox.context.tsx` | `ComboboxProvider`, `useCombobox()`, `useComboboxContext()`, `useComboboxPart()` |
|
|
33
33
|
| `combobox.types.ts` | `ComboboxOption`, `ComboboxOptionGroup`, `ComboboxEntry`, `ComboboxFilter`, `ComboboxListRow`, … |
|
|
34
34
|
| `combobox.filter.ts` | Pure: normalise, match, filter, create row, highlight ranges, pending remove, query reset, announcement, remembered selection |
|
|
35
|
-
| `combobox.variants.ts` | Pure: the added slots, the trigger class, chip size / colour / cap, trigger text, the keyboard-aware sheet cap |
|
|
35
|
+
| `combobox.variants.ts` | Pure: the added slots, the trigger class, chip size / colour / cap, trigger text, the keyboard-aware sheet cap and the sheet's held height |
|
|
36
36
|
| `*.test.ts` | |
|
|
37
37
|
|
|
38
38
|
## Design
|
|
@@ -104,21 +104,32 @@ typed text can become a value.
|
|
|
104
104
|
sheet's chip row and multiple mode's rows sit in a view whose
|
|
105
105
|
`onStartShouldSetResponder` returns true. A single pick closes the sheet, so
|
|
106
106
|
its rows let the keyboard go.
|
|
107
|
-
- **The sheet
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
the
|
|
121
|
-
|
|
107
|
+
- **The sheet opens at its full height and the keyboard never moves it.** It
|
|
108
|
+
used to size itself to its rows, so every keystroke resized it. Then it was
|
|
109
|
+
sized to the room above the keyboard and the engine lifted it by the
|
|
110
|
+
keyboard: it opened half-height, rode the keyboard up once the field
|
|
111
|
+
focused, and dropped back as the keyboard left on close — two moves per open
|
|
112
|
+
and one per close, all of it in the launch clip.
|
|
113
|
+
`resolveComboboxSheetHeight` is now Select's cap for an empty screen, the
|
|
114
|
+
sheet's one snap point, with `dynamicSizing` off and the engine's
|
|
115
|
+
`keyboardLiftLimit="highestSnapPoint"`: a sheet resting at its highest snap
|
|
116
|
+
point is not lifted, so the keyboard covers its bottom and the list's
|
|
117
|
+
viewport shrinks by it. Rows are top-aligned, so nothing visible moves but the
|
|
118
|
+
keyboard. Passing the engine a `topInset` was tried and rejected: it shrinks
|
|
119
|
+
the frame, and the scrim with it, leaving the status bar uncovered. A short
|
|
120
|
+
list sits at the top of a full-height sheet; a caller who wants it shorter
|
|
121
|
+
passes `snapPoints`, which replaces the cap, and the lift still stops at the
|
|
122
|
+
highest of them. `resolveComboboxSheetMaxHeight` and
|
|
123
|
+
`resolveExpectedKeyboardHeight` remain exported for callers sizing a sheet of
|
|
124
|
+
their own above a keyboard; the combobox no longer reads either.
|
|
125
|
+
- **Scrolling the list keeps the keyboard up.** The engine puts the keyboard
|
|
126
|
+
away the moment any pan starts on the sheet (`enableBlurKeyboardOnGesture`),
|
|
127
|
+
and its lists dismiss interactively, so scrolling the results to look past
|
|
128
|
+
the first few dropped the keyboard and the next keystroke needed a tap on the
|
|
129
|
+
field first. The sheet turns the pan's blur off and the list passes
|
|
130
|
+
`keyboardDismissMode="none"`. A drag that carries the sheet down still puts
|
|
131
|
+
it away — the engine does that on any downward release — and every close
|
|
132
|
+
blurs the field.
|
|
122
133
|
- **Closing blurs the search field first.** Done, the scrim, the close button
|
|
123
134
|
and a single pick all `blur()` before the sheet leaves, so the keyboard goes
|
|
124
135
|
down with it rather than lingering over the screen for a beat.
|
|
@@ -7,9 +7,7 @@ import {
|
|
|
7
7
|
useWindowDimensions,
|
|
8
8
|
View,
|
|
9
9
|
} from "react-native";
|
|
10
|
-
import { useKeyboardHandler } from "react-native-keyboard-controller";
|
|
11
10
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
12
|
-
import { scheduleOnRN } from "react-native-worklets";
|
|
13
11
|
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
14
12
|
import { IconCrossSmall, IconMagnifyingGlass } from "../../icons/central";
|
|
15
13
|
import { BottomSheet, type BottomSheetProps } from "../bottom-sheet";
|
|
@@ -53,12 +51,7 @@ import type {
|
|
|
53
51
|
ComboboxOptionInput,
|
|
54
52
|
ComboboxPresentation,
|
|
55
53
|
} from "./combobox.types";
|
|
56
|
-
import {
|
|
57
|
-
comboboxVariants,
|
|
58
|
-
resolveComboboxSheetMaxHeight,
|
|
59
|
-
resolveComboboxTriggerText,
|
|
60
|
-
resolveExpectedKeyboardHeight,
|
|
61
|
-
} from "./combobox.variants";
|
|
54
|
+
import { comboboxVariants, resolveComboboxSheetHeight, resolveComboboxTriggerText } from "./combobox.variants";
|
|
62
55
|
import { ComboboxChipRow } from "./combobox-chip-row";
|
|
63
56
|
import { ComboboxCreateRow } from "./combobox-create-row";
|
|
64
57
|
import { ComboboxEmpty } from "./combobox-empty";
|
|
@@ -109,7 +102,7 @@ type ComboboxBaseProps = {
|
|
|
109
102
|
formatResultsCount?: (count: number) => string;
|
|
110
103
|
/** Where the options open. Only `sheet` today. */
|
|
111
104
|
presentation?: ComboboxPresentation;
|
|
112
|
-
/** Forwarded to the sheet. Leave it off and the sheet
|
|
105
|
+
/** Forwarded to the sheet. Leave it off and the sheet opens at its cap and holds that height while you type. */
|
|
113
106
|
snapPoints?: BottomSheetProps["snapPoints"];
|
|
114
107
|
/** Bold what the query matched in each row. @default true */
|
|
115
108
|
highlight?: boolean;
|
|
@@ -200,23 +193,6 @@ function ComboboxRoot(props: ComboboxProps): ReactElement {
|
|
|
200
193
|
const field = useFieldContext();
|
|
201
194
|
const { height: windowHeight } = useWindowDimensions();
|
|
202
195
|
const { top: topInset } = useSafeAreaInsets();
|
|
203
|
-
// The keyboard's target height, read as it starts to move, and the last one
|
|
204
|
-
// measured — see `resolveExpectedKeyboardHeight` for why both.
|
|
205
|
-
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
206
|
-
const [lastKeyboardHeight, setLastKeyboardHeight] = useState(0);
|
|
207
|
-
const handleKeyboardStart = (height: number) => {
|
|
208
|
-
setKeyboardHeight(height);
|
|
209
|
-
if (height > 0) setLastKeyboardHeight(height);
|
|
210
|
-
};
|
|
211
|
-
useKeyboardHandler(
|
|
212
|
-
{
|
|
213
|
-
onStart: (event) => {
|
|
214
|
-
"worklet";
|
|
215
|
-
scheduleOnRN(handleKeyboardStart, event.height);
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
[]
|
|
219
|
-
);
|
|
220
196
|
const resolvedIsInvalid = isInvalid ?? field?.isInvalid ?? false;
|
|
221
197
|
const resolvedIsDisabled = isDisabled ?? field?.isDisabled ?? false;
|
|
222
198
|
|
|
@@ -280,6 +256,9 @@ function ComboboxRoot(props: ComboboxProps): ReactElement {
|
|
|
280
256
|
};
|
|
281
257
|
const close = () => handleOpenChange(false);
|
|
282
258
|
|
|
259
|
+
const sheetHeight = resolveComboboxSheetHeight({ topInset, windowHeight });
|
|
260
|
+
const fixedSnapPoints = useMemo(() => [sheetHeight], [sheetHeight]);
|
|
261
|
+
|
|
283
262
|
// The field is focused once the sheet has settled, never with it: a
|
|
284
263
|
// keyboard rising while the sheet is still animating up fights it for the
|
|
285
264
|
// same space.
|
|
@@ -485,7 +464,7 @@ function ComboboxRoot(props: ComboboxProps): ReactElement {
|
|
|
485
464
|
const searchLabel = field?.label ? `${field.label}, ${searchPlaceholder}` : searchPlaceholder;
|
|
486
465
|
|
|
487
466
|
// The title, chips and search field ride at the head of the list, so they
|
|
488
|
-
//
|
|
467
|
+
// scroll with it.
|
|
489
468
|
const header = (
|
|
490
469
|
<View className={slots.header()}>
|
|
491
470
|
<BottomSheet.Close />
|
|
@@ -545,21 +524,13 @@ function ComboboxRoot(props: ComboboxProps): ReactElement {
|
|
|
545
524
|
<ComboboxProvider value={context}>
|
|
546
525
|
{customTrigger ?? <ComboboxTrigger className={className} />}
|
|
547
526
|
<BottomSheet
|
|
548
|
-
dynamicSizing={
|
|
527
|
+
dynamicSizing={false}
|
|
528
|
+
enableBlurKeyboardOnGesture={false}
|
|
549
529
|
isOpen={isOpen}
|
|
550
|
-
|
|
551
|
-
keyboardHeight: resolveExpectedKeyboardHeight({
|
|
552
|
-
expectsKeyboard: openOnFocus,
|
|
553
|
-
keyboardHeight,
|
|
554
|
-
lastKnown: lastKeyboardHeight,
|
|
555
|
-
windowHeight,
|
|
556
|
-
}),
|
|
557
|
-
topInset,
|
|
558
|
-
windowHeight,
|
|
559
|
-
})}
|
|
530
|
+
keyboardLiftLimit="highestSnapPoint"
|
|
560
531
|
onIndexChange={handleIndexChange}
|
|
561
532
|
onOpenChange={handleOpenChange}
|
|
562
|
-
snapPoints={snapPoints}
|
|
533
|
+
snapPoints={snapPoints ?? fixedSnapPoints}
|
|
563
534
|
>
|
|
564
535
|
<BottomSheet.Portal>
|
|
565
536
|
<BottomSheet.Overlay />
|
|
@@ -568,6 +539,7 @@ function ComboboxRoot(props: ComboboxProps): ReactElement {
|
|
|
568
539
|
contentContainerClassName={slots.sheetBody()}
|
|
569
540
|
data={rows}
|
|
570
541
|
extraData={selected}
|
|
542
|
+
keyboardDismissMode="none"
|
|
571
543
|
keyboardShouldPersistTaps="handled"
|
|
572
544
|
keyExtractor={(row) => row.key}
|
|
573
545
|
ListEmptyComponent={empty}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
comboboxVariants,
|
|
13
13
|
resolveComboboxChipColor,
|
|
14
14
|
resolveComboboxChipSize,
|
|
15
|
+
resolveComboboxSheetHeight,
|
|
15
16
|
resolveComboboxSheetMaxHeight,
|
|
16
17
|
resolveComboboxTriggerClass,
|
|
17
18
|
resolveComboboxTriggerText,
|
|
@@ -223,6 +224,20 @@ describe("resolveExpectedKeyboardHeight", () => {
|
|
|
223
224
|
});
|
|
224
225
|
});
|
|
225
226
|
|
|
227
|
+
describe("resolveComboboxSheetHeight", () => {
|
|
228
|
+
const room = { topInset: 62, windowHeight: 874 };
|
|
229
|
+
|
|
230
|
+
test("is Select's cap for an empty screen — the keyboard covers the sheet's bottom rather than lifting it", () => {
|
|
231
|
+
expect(resolveComboboxSheetHeight(room)).toBe(resolveSelectSheetMaxHeight(room));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("is taller than the room above any keyboard, so a keyboard never moves it", () => {
|
|
235
|
+
expect(resolveComboboxSheetHeight(room)).toBeGreaterThan(
|
|
236
|
+
resolveComboboxSheetMaxHeight({ ...room, keyboardHeight: 336 })
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
226
241
|
describe("tokens", () => {
|
|
227
242
|
test("every colour any slot names exists in both themes", () => {
|
|
228
243
|
const missing = new Set<string>();
|
|
@@ -195,3 +195,24 @@ export function resolveExpectedKeyboardHeight({
|
|
|
195
195
|
if (!expectsKeyboard) return 0;
|
|
196
196
|
return lastKnown > 0 ? lastKnown : Math.round(windowHeight * COMBOBOX_KEYBOARD_ESTIMATE_FRACTION);
|
|
197
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The sheet's one snap point: Select's cap for an empty screen.
|
|
201
|
+
*
|
|
202
|
+
* The sheet opens there and holds it. Sized to its rows it shrank and grew on
|
|
203
|
+
* every keystroke; sized to the room above the keyboard it opened half-height,
|
|
204
|
+
* then rode the keyboard up once the field focused and dropped back as the
|
|
205
|
+
* keyboard left on close — two moves for every open and one for every close.
|
|
206
|
+
* At this height the engine's `keyboardLiftLimit="highestSnapPoint"` never
|
|
207
|
+
* lifts it: the keyboard covers the sheet's bottom and the list's viewport
|
|
208
|
+
* shrinks by it instead. The keyboard is not an input.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveComboboxSheetHeight({
|
|
211
|
+
topInset,
|
|
212
|
+
windowHeight,
|
|
213
|
+
}: {
|
|
214
|
+
topInset: number;
|
|
215
|
+
windowHeight: number;
|
|
216
|
+
}): number {
|
|
217
|
+
return resolveSelectSheetMaxHeight({ topInset, windowHeight });
|
|
218
|
+
}
|