@delacour/react-native-ui 0.5.0-alpha.20261022200024 → 0.5.0-alpha.20261023075140
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/direction/AGENTS.md +5 -2
- package/src/components/input/AGENTS.md +7 -0
- package/src/components/input/input-group-block.tsx +2 -2
- package/src/components/input/input.variants.test.ts +15 -0
- package/src/components/input/input.variants.ts +18 -0
- package/src/components/markdown-editor/AGENTS.md +5 -1
- package/src/components/markdown-editor/markdown-editor-preview.tsx +3 -2
- package/src/components/markdown-editor/markdown-editor.variants.test.ts +17 -0
- package/src/components/markdown-editor/markdown-editor.variants.ts +26 -6
- package/src/components/select/AGENTS.md +10 -8
- package/src/components/select/select.tsx +2 -28
- package/src/components/select/select.variants.test.ts +0 -11
- package/src/components/select/select.variants.ts +5 -7
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.20261023075140",
|
|
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": "
|
|
186
|
-
"@delacour/react-native-charts": "0.2.0-alpha.
|
|
185
|
+
"@delacour/react-native-bottom-sheet": "0.3.0-alpha.20261023075140",
|
|
186
|
+
"@delacour/react-native-charts": "0.2.0-alpha.20261023075140",
|
|
187
187
|
"@legendapp/list": ">=3.3",
|
|
188
188
|
"react-native-skia": ">=3.7.3",
|
|
189
189
|
"expo-linear-gradient": ">=15",
|
|
@@ -84,8 +84,11 @@ they are unit-testable.
|
|
|
84
84
|
- `bun test packages/react-native-ui/src/components/direction` — the resolvers.
|
|
85
85
|
<!-- /registry:omit -->
|
|
86
86
|
- Simulator, 2026-10-05, via warden + agent-device on the toggle demo: before the toggle,
|
|
87
|
-
`dir-step-one` at x 37; after, at x 261 and `dir-step-three` at x 37. The
|
|
88
|
-
|
|
87
|
+
`dir-step-one` at x 37; after, at x 261 and `dir-step-three` at x 37. The demo then also held a
|
|
88
|
+
nested LTR number row that kept x 37 / 149 / 261 in both states; it was dropped from `toggle`
|
|
89
|
+
(2026-10-22) because a row that stays put while the card flips reads as broken in the clip.
|
|
90
|
+
Nesting is shown by `nested-override`, and the number row (`dir-number-1..3`) now mirrors with
|
|
91
|
+
the card.
|
|
89
92
|
- Previews captured 2026-10-06 on an iPhone 17 Pro dev client (`--dev`): `toggle` is the hero clip
|
|
90
93
|
(flow `direction/toggle`, flips to RTL and back so it loops); `mirrored-rows`, `nested-override` and
|
|
91
94
|
`uniwind-variants` are stills. All four are `align: "stretch"`.
|
|
@@ -119,6 +119,13 @@ or drops its focus.
|
|
|
119
119
|
- **A block makes the box a floor, not a height.** With one present the box
|
|
120
120
|
takes the multiline branch (min height + vertical padding) whatever the field
|
|
121
121
|
is; the row keeps its own alignment, so a single-line field stays centred.
|
|
122
|
+
- **A block bleeds through the box's vertical padding on its outer edge.** That
|
|
123
|
+
padding is the field's. Stacked on the block's own `py-*`, it set a toolbar
|
|
124
|
+
half again as far from the border as from its separator and left the same
|
|
125
|
+
slack under a footer's counter. The block takes a `-mt-*` / `-mb-*` equal to
|
|
126
|
+
the multiline `py-*` (the `blockPosition` compounds), the way a horizontal
|
|
127
|
+
`Separator` bleeds through the horizontal inset, so its own padding is all
|
|
128
|
+
there is on either side.
|
|
122
129
|
- **A `Block` draws no border.** Divide it from the row with a `Separator`, or
|
|
123
130
|
leave it flush. Bare text and `Icon`s in it take the decorator treatment.
|
|
124
131
|
- **`Separator` has no `orientation` prop.** `InputGroupPlacementProvider` tells
|
|
@@ -38,10 +38,10 @@ export type InputGroupBlockProps = {
|
|
|
38
38
|
* </Input.Group.Block>
|
|
39
39
|
* </Input.Group>
|
|
40
40
|
*/
|
|
41
|
-
export function InputGroupBlock({ className, children }: InputGroupBlockProps): ReactElement {
|
|
41
|
+
export function InputGroupBlock({ position, className, children }: InputGroupBlockProps): ReactElement {
|
|
42
42
|
const { isInvalid, size } = useInputGroupPart("Input.Group.Block");
|
|
43
43
|
const placement = useInputGroupPlacement();
|
|
44
|
-
const slots = inputVariants({ isInvalid, size });
|
|
44
|
+
const slots = inputVariants({ blockPosition: position, isInvalid, size });
|
|
45
45
|
|
|
46
46
|
if (process.env.NODE_ENV !== "production" && placement === "row") {
|
|
47
47
|
console.warn(
|
|
@@ -496,6 +496,21 @@ describe("a block", () => {
|
|
|
496
496
|
expect(inputVariants().block()).toContain("flex-row");
|
|
497
497
|
});
|
|
498
498
|
|
|
499
|
+
// The box's vertical padding is the field's. Stacked on a block's own, it
|
|
500
|
+
// left a toolbar half again as far from the border as from its separator,
|
|
501
|
+
// and the same slack under a footer. A block bleeds out by exactly that
|
|
502
|
+
// padding on its outer edge, so its own padding is all there is either side.
|
|
503
|
+
test("bleeds out through the box's vertical padding on its outer edge", () => {
|
|
504
|
+
for (const size of INPUT_SIZES) {
|
|
505
|
+
const padding = resolveInputGroupClass({ hasBlocks: true, size }).match(/\bpy-([\d.]+)\b/)?.[1];
|
|
506
|
+
expect(padding).toBeDefined();
|
|
507
|
+
expect(inputVariants({ blockPosition: "start", size }).block()).toMatch(new RegExp(`(?:^|\\s)-mt-${padding}\\b`));
|
|
508
|
+
expect(inputVariants({ blockPosition: "end", size }).block()).toMatch(new RegExp(`(?:^|\\s)-mb-${padding}\\b`));
|
|
509
|
+
expect(inputVariants({ blockPosition: "start", size }).block()).not.toMatch(/-mb-/);
|
|
510
|
+
expect(inputVariants({ blockPosition: "end", size }).block()).not.toMatch(/-mt-/);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
499
514
|
test("carries no raw palette colour and no chrome", () => {
|
|
500
515
|
for (const state of everyBoxState()) {
|
|
501
516
|
const block = inputVariants(state).block();
|
|
@@ -140,6 +140,11 @@ export const inputVariants = tv({
|
|
|
140
140
|
row: { separator: "my-2 w-px self-stretch" },
|
|
141
141
|
box: { separator: "h-px self-stretch" },
|
|
142
142
|
},
|
|
143
|
+
/**
|
|
144
|
+
* Which edge of the box a block sits on. `none` is everything that is
|
|
145
|
+
* not a block.
|
|
146
|
+
*/
|
|
147
|
+
blockPosition: { none: {}, start: {}, end: {} },
|
|
143
148
|
isDisabled: { true: { root: "opacity-50" }, false: {} },
|
|
144
149
|
isMultiline: { true: {}, false: {} },
|
|
145
150
|
// A grouped field shares its row, so it stretches — and it clears React
|
|
@@ -190,6 +195,18 @@ export const inputVariants = tv({
|
|
|
190
195
|
{ placement: "box", size: "sm", class: { separator: "-mx-2.5" } },
|
|
191
196
|
{ placement: "box", size: "md", class: { separator: "-mx-3" } },
|
|
192
197
|
{ placement: "box", size: "lg", class: { separator: "-mx-3.5" } },
|
|
198
|
+
// A block bleeds out through the box's vertical padding on its outer
|
|
199
|
+
// edge, the way a horizontal hairline bleeds through the horizontal. That
|
|
200
|
+
// padding is the field's breathing room; stacked on the block's own it
|
|
201
|
+
// set a toolbar half again as far from the border as from its separator,
|
|
202
|
+
// and left the same slack under a footer. Each value is the multiline
|
|
203
|
+
// `py-*` above, so the block's own padding is all there is either side.
|
|
204
|
+
{ blockPosition: "start", size: "sm", class: { block: "-mt-2" } },
|
|
205
|
+
{ blockPosition: "start", size: "md", class: { block: "-mt-2.5" } },
|
|
206
|
+
{ blockPosition: "start", size: "lg", class: { block: "-mt-3" } },
|
|
207
|
+
{ blockPosition: "end", size: "sm", class: { block: "-mb-2" } },
|
|
208
|
+
{ blockPosition: "end", size: "md", class: { block: "-mb-2.5" } },
|
|
209
|
+
{ blockPosition: "end", size: "lg", class: { block: "-mb-3" } },
|
|
193
210
|
// The corner a lone field draws: the generic ramp, deliberately not the
|
|
194
211
|
// button's, because a field and the button beside it are meant to be
|
|
195
212
|
// retunable apart. `sm` steps down a notch; the other two share `lg`.
|
|
@@ -288,6 +305,7 @@ export const inputVariants = tv({
|
|
|
288
305
|
isMultiline: false,
|
|
289
306
|
isGrouped: false,
|
|
290
307
|
placement: "row",
|
|
308
|
+
blockPosition: "none",
|
|
291
309
|
orientation: "horizontal",
|
|
292
310
|
groupPosition: "none",
|
|
293
311
|
isSeamed: false,
|
|
@@ -99,7 +99,11 @@ parts.
|
|
|
99
99
|
on `bg-card` and its `secondary` filled `bg-secondary`, and its `etched` the same
|
|
100
100
|
card fill with the highlight set into its edge; the preview draws the same
|
|
101
101
|
three — a test reads `inputVariants` and fails if they part —
|
|
102
|
-
so flipping modes never changes what kind of box is on screen.
|
|
102
|
+
so flipping modes never changes what kind of box is on screen. Its corners
|
|
103
|
+
are the write pane's too — read from `inputVariants` per size — and it hands
|
|
104
|
+
its box to `resolveMarkdownPaneClass` as `base`, merged before the `bar`'s
|
|
105
|
+
`rounded-t-none`; passed as a className, its own `rounded-lg` once won and
|
|
106
|
+
the preview grew round top corners the write pane did not have. On the
|
|
103
107
|
filled one an inline code run lifts to `bg-card`, or `Text.Code`'s muted run
|
|
104
108
|
would vanish into it. Floored at the write pane's height
|
|
105
109
|
(`resolveTextareaRowsHeight`) so flipping does not jump the screen. It is a
|
|
@@ -39,13 +39,14 @@ export function MarkdownEditorPreview({
|
|
|
39
39
|
|
|
40
40
|
if (editor.mode !== "preview") return null;
|
|
41
41
|
|
|
42
|
-
const slots = markdownEditorVariants({ variant: editor.variant });
|
|
42
|
+
const slots = markdownEditorVariants({ size: editor.size, variant: editor.variant });
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<Animated.View
|
|
46
46
|
accessibilityLabel="Preview"
|
|
47
47
|
className={resolveMarkdownPaneClass({
|
|
48
|
-
|
|
48
|
+
base: slots.preview(),
|
|
49
|
+
className,
|
|
49
50
|
isAttached: editor.toolbarVariant === "bar",
|
|
50
51
|
})}
|
|
51
52
|
entering={isCalm ? undefined : FadeIn.duration(PREVIEW_FADE_MS)}
|
|
@@ -143,6 +143,23 @@ describe("panes", () => {
|
|
|
143
143
|
expect(resolveMarkdownPaneClass({ className: "rounded-t-xl", isAttached: true })).not.toContain("rounded-t-none");
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
test("the preview's corners are the write pane's at every size", () => {
|
|
147
|
+
const radius = (cls: string | undefined): string[] =>
|
|
148
|
+
(cls ?? "").split(/\s+/).filter((c) => c.startsWith("rounded"));
|
|
149
|
+
for (const size of MARKDOWN_EDITOR_SIZES) {
|
|
150
|
+
const field = radius(inputVariants({ isMultiline: true, size }).root());
|
|
151
|
+
expect(radius(markdownEditorVariants({ size }).preview())).toEqual(field);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a preview under a bar toolbar loses its top corners too", () => {
|
|
156
|
+
for (const size of MARKDOWN_EDITOR_SIZES) {
|
|
157
|
+
const base = markdownEditorVariants({ size }).preview();
|
|
158
|
+
expect(resolveMarkdownPaneClass({ base, isAttached: true })).toContain("rounded-t-none");
|
|
159
|
+
expect(resolveMarkdownPaneClass({ base, isAttached: false })).not.toContain("rounded-t-none");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
146
163
|
test("the write pane is monospaced", () => {
|
|
147
164
|
expect(markdownEditorVariants().input()).toContain("font-mono");
|
|
148
165
|
});
|
|
@@ -25,6 +25,18 @@ function inputSurface(variant: InputVariant): string {
|
|
|
25
25
|
.join(" ");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The corner utilities a multiline Input draws at a size, read from
|
|
30
|
+
* `inputVariants` itself so the preview's corners follow the write pane's.
|
|
31
|
+
*/
|
|
32
|
+
function inputRadius(size: InputSize): string {
|
|
33
|
+
return inputVariants({ isMultiline: true, size })
|
|
34
|
+
.root()
|
|
35
|
+
.split(/\s+/)
|
|
36
|
+
.filter((cls) => cls.startsWith("rounded"))
|
|
37
|
+
.join(" ");
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
export const MARKDOWN_EDITOR_VARIANTS = INPUT_VARIANTS;
|
|
29
41
|
export const MARKDOWN_TOOLBAR_VARIANTS = ["bar", "pill"] as const;
|
|
30
42
|
|
|
@@ -85,6 +97,10 @@ export const MARKDOWN_EDITOR_TABS_SIZE: Record<MarkdownEditorSize, TabsSize> = {
|
|
|
85
97
|
* flipping modes does not change what kind of box is on screen: outlined on
|
|
86
98
|
* `bg-card` for `primary`, filled `bg-secondary` for `secondary`.
|
|
87
99
|
*
|
|
100
|
+
* Its corners are the write pane's too, per size, and it takes its box as
|
|
101
|
+
* `base` in {@link resolveMarkdownPaneClass} rather than as a className, so a
|
|
102
|
+
* `bar` toolbar's square top corners are not overridden by its own radius.
|
|
103
|
+
*
|
|
88
104
|
* `inlineCode` lifts `Text.Code`'s muted run to `bg-card` on the filled
|
|
89
105
|
* `secondary` preview, where muted on secondary would draw no run at all.
|
|
90
106
|
*
|
|
@@ -107,7 +123,7 @@ export const markdownEditorVariants = tv({
|
|
|
107
123
|
actionRow: "flex-row items-center",
|
|
108
124
|
input: "font-mono",
|
|
109
125
|
pane: "",
|
|
110
|
-
preview: "gap-3
|
|
126
|
+
preview: "gap-3 px-3 py-3",
|
|
111
127
|
quote: "gap-2 border-l-2 border-border pl-3",
|
|
112
128
|
codeBlock: "rounded-md border border-border bg-card p-3",
|
|
113
129
|
listItem: "flex-row gap-2",
|
|
@@ -117,9 +133,9 @@ export const markdownEditorVariants = tv({
|
|
|
117
133
|
},
|
|
118
134
|
variants: {
|
|
119
135
|
size: {
|
|
120
|
-
sm: { modeSwitch: "w-40" },
|
|
121
|
-
md: { modeSwitch: "w-52" },
|
|
122
|
-
lg: { modeSwitch: "w-60" },
|
|
136
|
+
sm: { modeSwitch: "w-40", preview: inputRadius("sm") },
|
|
137
|
+
md: { modeSwitch: "w-52", preview: inputRadius("md") },
|
|
138
|
+
lg: { modeSwitch: "w-60", preview: inputRadius("lg") },
|
|
123
139
|
},
|
|
124
140
|
toolbar: {
|
|
125
141
|
bar: {
|
|
@@ -153,16 +169,20 @@ export type MarkdownEditorVariantProps = VariantProps<typeof markdownEditorVaria
|
|
|
153
169
|
* The class for a pane — the write field or the preview box.
|
|
154
170
|
*
|
|
155
171
|
* Under a `bar` toolbar the pane drops its top corners so the two read as one
|
|
156
|
-
* box, the bar its lid.
|
|
172
|
+
* box, the bar its lid. `base` is the pane's own box, merged first so its
|
|
173
|
+
* radius cannot win over the lid's square corners. A caller's className is
|
|
174
|
+
* merged last and still wins.
|
|
157
175
|
*/
|
|
158
176
|
export function resolveMarkdownPaneClass({
|
|
159
177
|
isAttached,
|
|
178
|
+
base,
|
|
160
179
|
className,
|
|
161
180
|
}: {
|
|
162
181
|
isAttached: boolean;
|
|
182
|
+
base?: string;
|
|
163
183
|
className?: string;
|
|
164
184
|
}): string {
|
|
165
|
-
return markdownEditorVariants().pane({ class: [isAttached ? "rounded-t-none" : undefined, className] }) ?? "";
|
|
185
|
+
return markdownEditorVariants().pane({ class: [base, isAttached ? "rounded-t-none" : undefined, className] }) ?? "";
|
|
166
186
|
}
|
|
167
187
|
|
|
168
188
|
/**
|
|
@@ -82,14 +82,16 @@ engine. An app that imports no picker still never resolves it.
|
|
|
82
82
|
the sheet `maxDynamicContentSize` — the lower of 85% of the window and the room
|
|
83
83
|
below the top inset less a 24pt gap — and the list scrolls inside it. Without
|
|
84
84
|
the cap two hundred options measured taller than the screen and the sheet rose
|
|
85
|
-
under the status bar with nothing of the page left above it.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
on the
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`
|
|
92
|
-
|
|
85
|
+
under the status bar with nothing of the page left above it.
|
|
86
|
+
- **The keyboard never lifts the sheet past that cap.** The root passes
|
|
87
|
+
`keyboardLiftLimit="maxDynamicContentSize"`, and the engine clamps its lift
|
|
88
|
+
on the UI thread: a short list rides the keyboard up until its top reaches
|
|
89
|
+
the cap, and a full one holds still while the keyboard covers its bottom and
|
|
90
|
+
the list shrinks by it. It used to shrink the cap itself from
|
|
91
|
+
`keyboardWillShow` — a JS event that lands after the UI-thread lift has
|
|
92
|
+
started — so a full list shot to the top of the screen and settled back
|
|
93
|
+
about a quarter of a second later. `snapPoints` bypasses dynamic sizing, and
|
|
94
|
+
the cap and the limit with it.
|
|
93
95
|
- **The row floor is ListGroup's**, `min-h-12/14/16`, pinned by the test, so a
|
|
94
96
|
row in the sheet sits level with a `ListGroup` row on the screen behind it. It
|
|
95
97
|
is a floor, never a height, so a description can wrap.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { useWindowDimensions, View } from "react-native";
|
|
3
|
-
import { KeyboardEvents } from "react-native-keyboard-controller";
|
|
4
3
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
5
4
|
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
6
5
|
import { IconMagnifyingGlass } from "../../icons/central";
|
|
@@ -103,31 +102,6 @@ export type SelectProps = SelectSingleProps | SelectMultipleProps;
|
|
|
103
102
|
|
|
104
103
|
const NO_VALUES: readonly string[] = [];
|
|
105
104
|
|
|
106
|
-
/**
|
|
107
|
-
* The keyboard's height while `isActive`, else zero.
|
|
108
|
-
*
|
|
109
|
-
* Read on the JS thread from the will-show and will-hide events, so the sheet's
|
|
110
|
-
* cap shrinks as the keyboard starts to rise rather than after it lands.
|
|
111
|
-
*/
|
|
112
|
-
function useKeyboardHeight(isActive: boolean): number {
|
|
113
|
-
const [height, setHeight] = useState(0);
|
|
114
|
-
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
if (!isActive) {
|
|
117
|
-
setHeight(0);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const show = KeyboardEvents.addListener("keyboardWillShow", (event) => setHeight(event.height));
|
|
121
|
-
const hide = KeyboardEvents.addListener("keyboardWillHide", () => setHeight(0));
|
|
122
|
-
return () => {
|
|
123
|
-
show.remove();
|
|
124
|
-
hide.remove();
|
|
125
|
-
};
|
|
126
|
-
}, [isActive]);
|
|
127
|
-
|
|
128
|
-
return height;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
105
|
function SelectRoot(props: SelectProps): ReactElement {
|
|
132
106
|
const {
|
|
133
107
|
options,
|
|
@@ -180,7 +154,6 @@ function SelectRoot(props: SelectProps): ReactElement {
|
|
|
180
154
|
|
|
181
155
|
const [isOpen, setOpen] = useState(false);
|
|
182
156
|
const [query, setQuery] = useState("");
|
|
183
|
-
const keyboardHeight = useKeyboardHeight(isOpen && isSearchable);
|
|
184
157
|
const canOpen = !resolvedIsDisabled && !isReadOnly;
|
|
185
158
|
|
|
186
159
|
const open = useCallback(() => {
|
|
@@ -322,7 +295,8 @@ function SelectRoot(props: SelectProps): ReactElement {
|
|
|
322
295
|
<BottomSheet
|
|
323
296
|
dynamicSizing={snapPoints === undefined}
|
|
324
297
|
isOpen={isOpen}
|
|
325
|
-
maxDynamicContentSize
|
|
298
|
+
keyboardLiftLimit="maxDynamicContentSize"
|
|
299
|
+
maxDynamicContentSize={resolveSelectSheetMaxHeight({ topInset, windowHeight })}
|
|
326
300
|
onOpenChange={handleOpenChange}
|
|
327
301
|
snapPoints={snapPoints}
|
|
328
302
|
>
|
|
@@ -551,17 +551,6 @@ describe("resolveSelectSheetMaxHeight", () => {
|
|
|
551
551
|
expect(Number.isInteger(resolveSelectSheetMaxHeight({ topInset: 59, windowHeight: 874 }))).toBe(true);
|
|
552
552
|
});
|
|
553
553
|
|
|
554
|
-
test("leaves room for the keyboard, which the sheet is lifted above", () => {
|
|
555
|
-
const open = resolveSelectSheetMaxHeight({ keyboardHeight: 336, topInset: 62, windowHeight: 874 });
|
|
556
|
-
expect(open).toBe(874 - 62 - SELECT_SHEET_TOP_GAP - 336);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test("an absent keyboard is a zero-height one", () => {
|
|
560
|
-
expect(resolveSelectSheetMaxHeight({ keyboardHeight: 0, topInset: 62, windowHeight: 874 })).toBe(
|
|
561
|
-
resolveSelectSheetMaxHeight({ topInset: 62, windowHeight: 874 })
|
|
562
|
-
);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
554
|
test("never goes negative on a degenerate window", () => {
|
|
566
555
|
expect(resolveSelectSheetMaxHeight({ topInset: 50, windowHeight: 20 })).toBe(0);
|
|
567
556
|
});
|
|
@@ -49,22 +49,20 @@ export const SELECT_SHEET_TOP_GAP = 24;
|
|
|
49
49
|
*
|
|
50
50
|
* Without one, two hundred options measure taller than the screen and the sheet
|
|
51
51
|
* rises under the status bar with nothing of the page left above it. The lower
|
|
52
|
-
* of a fraction of the window and the room below the top inset wins
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* of a fraction of the window and the room below the top inset wins. The
|
|
53
|
+
* keyboard does not come off it here: the sheet opts into
|
|
54
|
+
* `keyboardLiftLimit="maxDynamicContentSize"`, so the engine stops its lift at
|
|
55
|
+
* this cap on the UI thread, in the keyboard's first frame.
|
|
55
56
|
*/
|
|
56
57
|
export function resolveSelectSheetMaxHeight({
|
|
57
58
|
windowHeight,
|
|
58
59
|
topInset,
|
|
59
|
-
keyboardHeight = 0,
|
|
60
60
|
}: {
|
|
61
61
|
windowHeight: number;
|
|
62
62
|
topInset: number;
|
|
63
|
-
/** The sheet is lifted above an open keyboard, so the room it may take shrinks by the keyboard's height. */
|
|
64
|
-
keyboardHeight?: number;
|
|
65
63
|
}): number {
|
|
66
64
|
const byFraction = windowHeight * SELECT_SHEET_MAX_FRACTION;
|
|
67
|
-
const belowInset = windowHeight - topInset - SELECT_SHEET_TOP_GAP
|
|
65
|
+
const belowInset = windowHeight - topInset - SELECT_SHEET_TOP_GAP;
|
|
68
66
|
return Math.max(0, Math.floor(Math.min(byFraction, belowInset)));
|
|
69
67
|
}
|
|
70
68
|
|