@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/react-native-ui",
3
- "version": "0.5.0-alpha.20261023075443",
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.20261023075443",
186
- "@delacour/react-native-charts": "0.2.0-alpha.20261023075443",
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's cap takes the keyboard out.** The engine lifts the sheet by the
108
- keyboard's height, and Select's cap assumes an empty screen, so with the
109
- search field focused a long list rose under the status bar.
110
- `resolveComboboxSheetMaxHeight` caps the list at the room between the
111
- keyboard and the top inset (less Select's gap) while one is up. Passing the
112
- engine a `topInset` instead was tried and rejected: it shrinks the frame, and
113
- the scrim with it, leaving the status bar uncovered.
114
- - **The cap expects the keyboard before it arrives.** The search field focuses
115
- as the sheet settles, so the sheet sized itself for an empty screen and the
116
- keyboard's lift then carried its top under the Dynamic Island for a few
117
- frames — `useKeyboardState` and even `useKeyboardHandler`'s `onStart` land a
118
- frame too late to stop it. With `openOnFocus`, `resolveExpectedKeyboardHeight`
119
- feeds the cap the keyboard it will get: the live height once one is up, else
120
- the last one seen, else `COMBOBOX_KEYBOARD_ESTIMATE_FRACTION` (0.42) of the
121
- window.
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 sizes to its rows. */
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
- // count towards the height the sheet sizes itself to and scroll with it.
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={snapPoints === undefined}
527
+ dynamicSizing={false}
528
+ enableBlurKeyboardOnGesture={false}
549
529
  isOpen={isOpen}
550
- maxDynamicContentSize={resolveComboboxSheetMaxHeight({
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
+ }
@@ -36,6 +36,7 @@ export {
36
36
  comboboxVariants,
37
37
  resolveComboboxChipColor,
38
38
  resolveComboboxChipSize,
39
+ resolveComboboxSheetHeight,
39
40
  resolveComboboxSheetMaxHeight,
40
41
  resolveComboboxTriggerClass,
41
42
  resolveComboboxTriggerText,