@delacour/react-native-ui 0.5.0-alpha.20261024114755 → 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.
|
|
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.
|
|
186
|
-
"@delacour/react-native-charts": "0.2.0-alpha.
|
|
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
|
|
@@ -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={
|
|
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
|
*
|