@delacour/react-native-ui 0.5.0-alpha.20261024103119 → 0.5.0-alpha.20261024121604

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.20261024103119",
3
+ "version": "0.5.0-alpha.20261024121604",
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.20261024103119",
186
- "@delacour/react-native-charts": "0.2.0-alpha.20261024103119",
185
+ "@delacour/react-native-bottom-sheet": "0.3.0-alpha.20261024121604",
186
+ "@delacour/react-native-charts": "0.2.0-alpha.20261024121604",
187
187
  "@legendapp/list": ">=3.3",
188
188
  "react-native-skia": ">=3.7.3",
189
189
  "expo-linear-gradient": ">=15",
@@ -136,6 +136,32 @@ read those before touching behaviour, because behaviour is not here.
136
136
  `SectionList`; `@legendapp/list` is this library's optional peer, so its
137
137
  body is made here, once at module scope, and registered as a flat list.
138
138
 
139
+ ## A sheet that filters as you type holds its height
140
+
141
+ A sheet sized to its content (`dynamicSizing`, the engine's default) re-measures
142
+ whenever its rows change, so a search field over a list resizes the sheet on
143
+ every keystroke that drops or restores a row. Any sheet whose content changes
144
+ while an input in it has focus holds one height instead:
145
+
146
+ - **Fixed snap points.** `dynamicSizing={false}` and a `snapPoints` of one
147
+ height — the SearchBar demo's `"50%"`. The keyboard lifts the sheet as usual,
148
+ and nothing it types moves it.
149
+ - **A floor at the cap.** A scrollable list that should fill the sheet keeps
150
+ `dynamicSizing` and `maxDynamicContentSize`, passes
151
+ `keyboardLiftLimit="maxDynamicContentSize"`, and floors its content at the
152
+ cap — `Select`'s searchable sheet. Under that limit the keyboard covers the
153
+ sheet's bottom rather than lifting it, which a fixed snap point at the cap
154
+ cannot do: the limit only clamps a dynamically sized sheet, and an
155
+ unclamped lift from the cap carries the top under the status bar.
156
+ - **A held snap point.** `Combobox` computes its one snap point at open, for
157
+ the keyboard it expects, and keeps it until the next open.
158
+
159
+ Scrolling the filtered list should not drop the keyboard either: `Select` and
160
+ `Combobox` pass `enableBlurKeyboardOnGesture={false}` to the sheet and
161
+ `keyboardDismissMode="none"` to the list while a search field is in it.
162
+
163
+ A form whose fields never change the content's height needs none of this.
164
+
139
165
  ## Compat
140
166
 
141
167
  Two names survive from the previous implementation for callers that imported
@@ -123,7 +123,8 @@ scroll.
123
123
  `keyboard` demo measures it with `measureInWindow`.
124
124
 
125
125
  **Preview media.** Six demos are captured, each `align: "stretch"`. Three are clips: `basic`
126
- (the hero) scrolls back through the history and returns to the end, `load-older` reaches the top,
126
+ (the hero) scrolls back through the history and returns to the end — it renders through `Message`,
127
+ so the runs it scrolls past show their tightened corners, not a stack of pills — `load-older` reaches the top,
127
128
  pulls in a page and comes back down, and `jump-to-latest` scrolls up while replies arrive and taps
128
129
  the button. `stick-to-bottom`, `date-separators` and `unread-divider` are stills.
129
130
  `stick-to-bottom` stays a still because every press appends a message, so no flow can bring it back
@@ -24,7 +24,7 @@ engine. An app that imports no picker still never resolves it.
24
24
  | `select.collect.ts` | `collectOptions` / `findChildOfType` — reads the markers into the `options` shape |
25
25
  | `select.context.tsx` | `SelectProvider`, `useSelect()`, `useSelectContext()`, `useSelectPart()` |
26
26
  | `select.types.ts` | `SelectOption`, `SelectOptionGroup`, `SelectRowOption`, `SelectEntry`, `SelectListRow`, `SelectHighlightRange` |
27
- | `select.variants.ts` | Pure `tv()` slots + every resolver, no RN imports |
27
+ | `select.variants.ts` | Pure `tv()` slots + every resolver, no RN imports — the sheet cap and the searchable list's floor among them |
28
28
  | `select.variants.test.ts` | |
29
29
  | `select.collect.test.ts` | |
30
30
 
@@ -92,6 +92,25 @@ engine. An app that imports no picker still never resolves it.
92
92
  started — so a full list shot to the top of the screen and settled back
93
93
  about a quarter of a second later. `snapPoints` bypasses dynamic sizing, and
94
94
  the cap and the limit with it.
95
+ - **A searchable sheet opens at the cap and holds it.** Sized to its rows, it
96
+ shrank and grew on every keystroke as the filter dropped and restored them —
97
+ typing "zur" took the sheet from nine rows to one. `resolveSelectListMinHeight`
98
+ gives the list's content a floor of the cap, so the dynamic snap point is the
99
+ cap whatever the filter leaves, and the lift limit above still stops the
100
+ keyboard there: the keyboard covers the sheet's bottom and its top never
101
+ moves. Dynamic sizing stays on rather than passing the cap as a snap point,
102
+ because `keyboardLiftLimit` only clamps a dynamically sized sheet — a fixed
103
+ snap point at the cap would ride the keyboard to the top of the screen. A
104
+ short searchable list sits at the top of a full-height sheet, as `Combobox`'s
105
+ does. Without search the rows cannot change while it is open, so it keeps
106
+ sizing to them.
107
+ - **Scrolling a searchable list keeps the keyboard up**, as in `Combobox`. The
108
+ engine puts the keyboard away the moment any pan starts on the sheet
109
+ (`enableBlurKeyboardOnGesture`), and its lists dismiss interactively, so
110
+ scrolling the results past the first few dropped the keyboard and the next
111
+ keystroke needed a tap on the field first. A searchable sheet turns the pan's
112
+ blur off and the list passes `keyboardDismissMode="none"`. A drag that carries
113
+ the sheet down still puts it away, and every close clears the query.
95
114
  - **The row floor is ListGroup's**, `min-h-12/14/16`, pinned by the test, so a
96
115
  row in the sheet sits level with a `ListGroup` row on the screen behind it. It
97
116
  is a floor, never a height, so a description can wrap.
@@ -20,6 +20,7 @@ import {
20
20
  filterGroups,
21
21
  normalizeOptions,
22
22
  resolveSelectedOptions,
23
+ resolveSelectListMinHeight,
23
24
  resolveSelectRowState,
24
25
  resolveSelectSheetMaxHeight,
25
26
  resolveSelectTriggerText,
@@ -60,7 +61,7 @@ type SelectBaseProps = {
60
61
  emptyText?: string;
61
62
  /** Where the options open. Only `sheet` today. */
62
63
  presentation?: SelectPresentation;
63
- /** Forwarded to the sheet. Leave it off and the sheet sizes to its rows. */
64
+ /** Forwarded to the sheet. Leave it off and the sheet sizes to its rows — or, searchable, opens at its cap and holds it while you type. */
64
65
  snapPoints?: BottomSheetProps["snapPoints"];
65
66
  /** Custom trigger content for a non-empty selection. */
66
67
  renderValue?: (selected: SelectOption[]) => ReactNode;
@@ -214,6 +215,15 @@ function SelectRoot(props: SelectProps): ReactElement {
214
215
  variant,
215
216
  };
216
217
 
218
+ const sheetMaxHeight = resolveSelectSheetMaxHeight({ topInset, windowHeight });
219
+ // A searchable list measures at least the cap, so the sheet holds its height
220
+ // while the filter drops rows — see `resolveSelectListMinHeight`.
221
+ const listMinHeight = resolveSelectListMinHeight({ isSearchable, maxHeight: sheetMaxHeight, snapPoints });
222
+ const listContentStyle = useMemo(
223
+ () => (listMinHeight === undefined ? undefined : { minHeight: listMinHeight }),
224
+ [listMinHeight]
225
+ );
226
+
217
227
  const rows = buildSelectRows(isSearchable ? filterGroups(entries, query) : entries);
218
228
  const slots = selectVariants({ size, variant });
219
229
 
@@ -294,9 +304,10 @@ function SelectRoot(props: SelectProps): ReactElement {
294
304
  {customTrigger ?? <SelectTrigger className={className} />}
295
305
  <BottomSheet
296
306
  dynamicSizing={snapPoints === undefined}
307
+ enableBlurKeyboardOnGesture={!isSearchable}
297
308
  isOpen={isOpen}
298
309
  keyboardLiftLimit="maxDynamicContentSize"
299
- maxDynamicContentSize={resolveSelectSheetMaxHeight({ topInset, windowHeight })}
310
+ maxDynamicContentSize={sheetMaxHeight}
300
311
  onOpenChange={handleOpenChange}
301
312
  snapPoints={snapPoints}
302
313
  >
@@ -305,8 +316,10 @@ function SelectRoot(props: SelectProps): ReactElement {
305
316
  <BottomSheet.Container>
306
317
  <BottomSheet.FlatList
307
318
  contentContainerClassName={slots.sheetBody()}
319
+ contentContainerStyle={listContentStyle}
308
320
  data={rows}
309
321
  extraData={selected}
322
+ keyboardDismissMode={isSearchable ? "none" : undefined}
310
323
  keyboardShouldPersistTaps="handled"
311
324
  keyExtractor={(row) => row.key}
312
325
  ListEmptyComponent={empty}
@@ -11,6 +11,7 @@ import {
11
11
  normalizeOptions,
12
12
  normalizeSearchText,
13
13
  resolveSelectIconToken,
14
+ resolveSelectListMinHeight,
14
15
  resolveSelectRowState,
15
16
  resolveSelectSheetMaxHeight,
16
17
  resolveSelectTriggerClass,
@@ -555,3 +556,19 @@ describe("resolveSelectSheetMaxHeight", () => {
555
556
  expect(resolveSelectSheetMaxHeight({ topInset: 50, windowHeight: 20 })).toBe(0);
556
557
  });
557
558
  });
559
+
560
+ describe("resolveSelectListMinHeight", () => {
561
+ const maxHeight = resolveSelectSheetMaxHeight({ topInset: 62, windowHeight: 874 });
562
+
563
+ test("holds a searchable list at the cap, so a filter that drops rows cannot shrink the sheet", () => {
564
+ expect(resolveSelectListMinHeight({ isSearchable: true, maxHeight, snapPoints: undefined })).toBe(maxHeight);
565
+ });
566
+
567
+ test("leaves a list without search sized to its rows", () => {
568
+ expect(resolveSelectListMinHeight({ isSearchable: false, maxHeight, snapPoints: undefined })).toBeUndefined();
569
+ });
570
+
571
+ test("steps aside for caller snap points, which replace dynamic sizing", () => {
572
+ expect(resolveSelectListMinHeight({ isSearchable: true, maxHeight, snapPoints: ["50%"] })).toBeUndefined();
573
+ });
574
+ });
@@ -66,6 +66,29 @@ export function resolveSelectSheetMaxHeight({
66
66
  return Math.max(0, Math.floor(Math.min(byFraction, belowInset)));
67
67
  }
68
68
 
69
+ /**
70
+ * The floor under a searchable list's content, so the sheet holds the cap.
71
+ *
72
+ * Sized to its rows, the sheet shrank and grew on every keystroke as the filter
73
+ * dropped and restored them. Measuring at least the cap, it opens there and
74
+ * stays: the engine's dynamic snap point is clamped to the cap either way, and
75
+ * `keyboardLiftLimit` still stops the keyboard's lift at it on the UI thread,
76
+ * so the keyboard covers the sheet's bottom and its top never moves. Without
77
+ * search the rows cannot change while it is open, and caller `snapPoints` turn
78
+ * dynamic sizing off, so neither needs one.
79
+ */
80
+ export function resolveSelectListMinHeight({
81
+ isSearchable,
82
+ maxHeight,
83
+ snapPoints,
84
+ }: {
85
+ isSearchable: boolean;
86
+ maxHeight: number;
87
+ snapPoints: readonly unknown[] | undefined;
88
+ }): number | undefined {
89
+ return isSearchable && snapPoints === undefined ? maxHeight : undefined;
90
+ }
91
+
69
92
  /**
70
93
  * Styling for every part of a select.
71
94
  *