@mrmeg/expo-ui 0.25.1 → 0.27.0
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/CHANGELOG.md +140 -0
- package/LLM_USAGE.md +15 -7
- package/README.md +94 -23
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/BottomSheet.d.ts +24 -4
- package/dist/components/BottomSheet.js +37 -52
- package/dist/components/DropdownMenu.js +1 -1
- package/dist/components/Icon.d.ts +24 -22
- package/dist/components/Icon.js +26 -3
- package/dist/components/Notification.js +3 -3
- package/dist/components/Popover.js +1 -1
- package/dist/components/Select.js +1 -1
- package/dist/components/StatCard.d.ts +1 -1
- package/dist/components/StyledText.d.ts +6 -2
- package/dist/components/StyledText.js +10 -4
- package/dist/components/TextInput.js +35 -11
- package/dist/components/Tooltip.js +1 -1
- package/dist/components/iconRegistry.generated.d.ts +148 -0
- package/dist/components/iconRegistry.generated.js +296 -0
- package/dist/components/keyboardFocusRegistry.d.ts +10 -2
- package/dist/components/keyboardFocusRegistry.js +13 -5
- package/dist/components/nativeTextField.android.d.ts +3 -0
- package/dist/components/nativeTextField.android.js +226 -0
- package/dist/components/nativeTextField.d.ts +14 -0
- package/dist/components/nativeTextField.js +14 -0
- package/dist/design-system.json +151 -2
- package/dist/hooks/useResources.d.ts +5 -18
- package/dist/hooks/useResources.js +5 -43
- package/llms-full.md +4 -4
- package/llms.txt +4 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,146 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.27.0]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **`StyledText` is non-selectable by default on Android.** RN implements
|
|
13
|
+
`selectable` with `TextView.setTextIsSelectable(true)`, which makes the label
|
|
14
|
+
focusable-in-touch-mode, so a tap on any default `StyledText` next to a
|
|
15
|
+
focused `TextInput` moved view focus to the label and hid the keyboard
|
|
16
|
+
(tractor-tools-direct #19, mindmap #38 worked around it per label). The
|
|
17
|
+
Android default is now `false`; pass `selectable` to opt copyable content in.
|
|
18
|
+
iOS and web keep `true`, an explicit `selectable` prop still wins everywhere,
|
|
19
|
+
and `TextSelectabilityContext` still overrides the platform default. Consumers
|
|
20
|
+
can drop `selectable={false}` added only to keep the Android keyboard up;
|
|
21
|
+
keeping it on control chrome is harmless and still recommended for iOS/web.
|
|
22
|
+
- **`BottomSheet.Content avoidKeyboard` is documented as platform-owned, with
|
|
23
|
+
the iOS behavior verified on device.** The prop stays accepted-and-ignored,
|
|
24
|
+
like `swipeEnabled` and `dismissKeyboardOnDrag`. On iOS (iPhone 17 Pro, iOS
|
|
25
|
+
27, `@expo/ui` 58.0.2) UIKit's sheet presentation keeps the hosted RN column
|
|
26
|
+
above the keyboard by itself: a 45% sheet is lifted whole (its column shrinks
|
|
27
|
+
393 → 363 pt), a 92% sheet is shrunk in place (700 → 468 pt), and in both
|
|
28
|
+
cases the column's bottom edge lands at the keyboard's top, so `Footer` and
|
|
29
|
+
the tail of `Body` sit 50 pt (`spacing.md` + home-indicator inset) above the
|
|
30
|
+
keyboard and a long `Body` scrolls. The package therefore adds no inset, and
|
|
31
|
+
none could be correct from JS: `measureInWindow` inside the `layoutRoot`
|
|
32
|
+
sheet host reports host-relative coordinates (the 45% column read bottom 409
|
|
33
|
+
while sitting at screen y 546), and a window-height estimate would lift a
|
|
34
|
+
lifted sheet twice. Android: Material3's `ModalBottomSheet` owns avoidance
|
|
35
|
+
(no JS keyboard signal exists inside the Compose dialog window: RN reads IME
|
|
36
|
+
insets from the main root view, and keyboard-controller watches only the
|
|
37
|
+
main window and RN `Modal` dialogs). Web: none. Never nest a
|
|
38
|
+
`KeyboardAvoidingView` inside a sheet.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`TextInput` no longer forwards the Android mount blur.** Compose reports
|
|
43
|
+
`isFocused=false` when the field is first composed and the Android field
|
|
44
|
+
forwarded it as `onBlur`, so forms validating on blur showed "required"
|
|
45
|
+
errors before the user typed anything (tractor-tools-direct #28 gated it in
|
|
46
|
+
app code). `onBlur` now fires only after the field has reported focus, and a
|
|
47
|
+
repeated blur with no focus in between is dropped too. Android only; iOS and
|
|
48
|
+
web are unchanged. Consumers can drop touched-field guards added for this.
|
|
49
|
+
- **`BottomSheet.Body` no longer swallows the first tap on its controls while a
|
|
50
|
+
sheet field is focused on iOS.** `Body`'s `ScrollView` now sets
|
|
51
|
+
`keyboardShouldPersistTaps="always"` (before `{...props}`, so an explicit
|
|
52
|
+
consumer value still wins). Without it RN's default `never` policy claimed the
|
|
53
|
+
tap in the capture phase whenever a registered field was focused and the
|
|
54
|
+
keyboard was visible, then blurred the field on release, so chips, buttons,
|
|
55
|
+
tabs and field-to-field handoff inside `Body` needed a second tap. The
|
|
56
|
+
`BottomSheet.Content` column boundary already owns tap-away dismissal, so
|
|
57
|
+
dead-space taps inside `Body` still dismiss on release and a short drag still
|
|
58
|
+
does not. Android >= 30 was already unaffected: RN reads keyboard insets from
|
|
59
|
+
the main root view, and the Material sheet is its own window, so RN's policy
|
|
60
|
+
never armed there.
|
|
61
|
+
|
|
62
|
+
## [0.26.0]
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **`Icon` renders Lucide.** `<Icon name>` draws `lucide-react-native` SVGs
|
|
67
|
+
instead of the `@expo/vector-icons` Feather font. Lucide is a maintained
|
|
68
|
+
superset of Feather in the same 24px, 2px round-stroke style, so glyphs look
|
|
69
|
+
the same, but the set is ten times larger and nothing is loaded as a font.
|
|
70
|
+
`IconName` is now the union of kebab-case Lucide names in the package
|
|
71
|
+
registry (`src/components/icon-names.json`, generated into
|
|
72
|
+
`iconRegistry.generated.ts` by `bun run ui:icons`); only those icons ship in
|
|
73
|
+
the bundle. The `component` escape hatch is unchanged and accepts any
|
|
74
|
+
`LucideIcon` (`CustomIconComponentProps.style` is a view style). Names Lucide
|
|
75
|
+
renamed, which consumers must update: `alert-circle` → `circle-alert`,
|
|
76
|
+
`check-circle` → `circle-check-big`, `x-circle` → `circle-x`,
|
|
77
|
+
`alert-triangle` → `triangle-alert`, `help-circle` → `circle-question-mark`,
|
|
78
|
+
`edit` → `square-pen`, `edit-2` → `pen`, `edit-3` → `pencil`,
|
|
79
|
+
`sliders` → `sliders-horizontal`, `grid` → `layout-grid`, `home` → `house`,
|
|
80
|
+
`unlock` → `lock-open`, `trash-2` → `trash`, `tool` → `wrench`,
|
|
81
|
+
`layout` → `panels-top-left`, `bar-chart-2` → `chart-no-axes-column`,
|
|
82
|
+
`plus-circle` → `circle-plus`, `stop-circle` → `circle-stop`,
|
|
83
|
+
`more-vertical` → `ellipsis-vertical`, `more-horizontal` → `ellipsis`,
|
|
84
|
+
`sidebar` → `panel-left`, `smile` → `face-slightly-smiling`. Brand glyphs
|
|
85
|
+
(`github`, `chrome`, `facebook`, `instagram`, `linkedin`, `twitter`) have
|
|
86
|
+
no Lucide equivalent; pass your own SVG through `component`. Every other
|
|
87
|
+
Feather name resolves unchanged. A name outside the registry is a type error.
|
|
88
|
+
|
|
89
|
+
### Removed
|
|
90
|
+
|
|
91
|
+
- **`@expo/vector-icons` is no longer a dependency**, and `useResources` no
|
|
92
|
+
longer loads or registers an icon font. `ensureIconFontRegistered` is gone:
|
|
93
|
+
SVG icons render in export-time HTML as `<svg>`, so the hydration fix it
|
|
94
|
+
existed for has nothing left to fix.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- **Peers `lucide-react-native` (`>=1.46 <2`) and `react-native-svg`
|
|
99
|
+
(`>=15 <16`).** Install `react-native-svg` with `npx expo install` so it
|
|
100
|
+
matches your SDK. They are peers, not dependencies, so an app that imports
|
|
101
|
+
Lucide components for `<Icon component>` shares one copy.
|
|
102
|
+
- **`icons.names` in `dist/design-system.json`**, the registry's name list, so
|
|
103
|
+
`@mrmeg/eslint-plugin-expo-ui` can validate icon names against an installed
|
|
104
|
+
release (a lint rule is a follow-up). `schemaVersion` stays `1`.
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- **Popover, DropdownMenu, Select, and Tooltip content receives Android
|
|
109
|
+
touches.** The fade wrapper between each overlay and its absolutely
|
|
110
|
+
positioned content now fills the overlay with `pointerEvents="box-none"`, so
|
|
111
|
+
the card is inside its parent's bounds. Android dispatches `ACTION_DOWN` and
|
|
112
|
+
accessibility traversal only to children inside their parent's bounds, so
|
|
113
|
+
native controls (`Switch`, `TextInput`) inside a `PopoverContent` ignored
|
|
114
|
+
taps and the content was missing from the accessibility tree. Tap-away still
|
|
115
|
+
closes.
|
|
116
|
+
- **Sheet taps reach controls while the keyboard is up.** `BottomSheet.Content`
|
|
117
|
+
no longer mounts an absolute-fill "Dismiss keyboard" overlay that swallowed
|
|
118
|
+
the first tap on every button, tab and field inside the sheet. It spreads the
|
|
119
|
+
same release-based tap-away boundary `DismissKeyboard` uses onto its content
|
|
120
|
+
column: the boundary never claims the touch, an unclaimed dead-space tap
|
|
121
|
+
dismisses on release only, drags and multitouch do not dismiss, and presence
|
|
122
|
+
still comes from the focus registry so it works in the sheet's isolated
|
|
123
|
+
native window.
|
|
124
|
+
- **`dismissKeyboard()` clears the focus registry immediately.**
|
|
125
|
+
`dismissKeyboardFocusedInput()` blurs the registered field and drops its
|
|
126
|
+
registration in the same call instead of waiting for a native blur callback
|
|
127
|
+
that may never arrive (submit handlers, isolated sheet window, iOS
|
|
128
|
+
secure/plain handoff), so no stale focus presence survives a dismissal. A
|
|
129
|
+
field that takes focus during the blur keeps its registration.
|
|
130
|
+
- **Android sheet bodies fill the rendered sheet.** The content column's
|
|
131
|
+
`maxHeight` cap (last snap point as a window percentage) now applies only
|
|
132
|
+
off Android, where the iOS SwiftUI host needs it; Material's
|
|
133
|
+
`ModalBottomSheet` ignores percentage snap points and its host already bounds
|
|
134
|
+
the column, so a `55%` sheet no longer shows a blank strip below a capped
|
|
135
|
+
`BottomSheet.Body`.
|
|
136
|
+
- **Android secure fields declare a password keyboard.** `TextInput` with
|
|
137
|
+
`secureTextEntry` now reports `textPassword` to the IME (or `numberPassword`
|
|
138
|
+
for the `number-pad`, `decimal-pad`, `numeric` and `phone-pad` keyboards), so
|
|
139
|
+
Gboard shows no suggestion strip and learns nothing typed; autocorrect is off
|
|
140
|
+
and capitalization is `none` unless the props say otherwise. Revealing the
|
|
141
|
+
text with the eye toggle keeps the password keyboard and only drops the
|
|
142
|
+
masking, so focus, selection and the input connection survive exactly as in
|
|
143
|
+
0.25.1. The Android field is now package-owned — a port of `@expo/ui` 58.0.2's
|
|
144
|
+
universal Android `TextInput` on top of `@expo/ui/jetpack-compose`'s
|
|
145
|
+
`BasicTextField` — because the universal field offers no way to set Compose's
|
|
146
|
+
password keyboard type. iOS keeps `@expo/ui`'s universal field.
|
|
147
|
+
|
|
8
148
|
## [0.25.1]
|
|
9
149
|
|
|
10
150
|
### Fixed
|
package/LLM_USAGE.md
CHANGED
|
@@ -85,7 +85,13 @@ call `dismissKeyboard()` explicitly.
|
|
|
85
85
|
SwiftUI `.sheet()`, Android Material3 `ModalBottomSheet`, web `vaul`). The
|
|
86
86
|
platform owns gestures and keyboard avoidance: `swipeEnabled`, `avoidKeyboard`,
|
|
87
87
|
and `dismissKeyboardOnDrag` are accepted for call-site ergonomics but have no
|
|
88
|
-
effect.
|
|
88
|
+
effect. On iOS the sheet presentation lifts or shrinks the hosted content so
|
|
89
|
+
`Footer` and the tail of `Body` end at the keyboard's top (device-verified);
|
|
90
|
+
Android avoidance is Material3's; do not nest a `KeyboardAvoidingView` inside a
|
|
91
|
+
sheet. `BottomSheet.Content` mounts the tap-away keyboard-dismiss boundary on
|
|
92
|
+
its column, and `BottomSheet.Body` sets `keyboardShouldPersistTaps="always"` on
|
|
93
|
+
its ScrollView so that boundary owns dismissal; do not pass `never`. `Slider`
|
|
94
|
+
and `SegmentedControl` are also `@expo/ui`-backed.
|
|
89
95
|
|
|
90
96
|
`BottomSheet.Content` themes the native sheet surface with the card color. Pass
|
|
91
97
|
`backgroundStyle={{ backgroundColor: "transparent" }}`, plus a `style` clearing
|
|
@@ -228,7 +234,7 @@ Check this before creating a new app-local primitive. All components come from
|
|
|
228
234
|
| `DropdownMenu` | Menus and command lists | Homemade popover menus |
|
|
229
235
|
| `EmptyState` | No-data or recoverable error regions | One-off empty placeholders |
|
|
230
236
|
| `ErrorBoundary` | React render error fallback | Unhandled screen crashes |
|
|
231
|
-
| `Icon` |
|
|
237
|
+
| `Icon` | Lucide or custom icons with theme tokens | Raw vector icons with hardcoded colors |
|
|
232
238
|
| `InputOTP` | Verification code entry | Several manually managed text inputs |
|
|
233
239
|
| `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions` | List / settings rows with density tokens | Hand-rolled row `View`s |
|
|
234
240
|
| `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers |
|
|
@@ -258,14 +264,15 @@ Check this before creating a new app-local primitive. All components come from
|
|
|
258
264
|
`caption`, `label`, `eyebrow`), `size` (`xs`, `sm`, `base`, `body`, `lg`, `xl`,
|
|
259
265
|
`xxl`, `display`), `fontWeight` (`light`–`bold`), `variant` (`sansSerif`,
|
|
260
266
|
`serif`, `mono`), `align`, `text`, `tx`, `txOptions`, `selectable` (default
|
|
261
|
-
`true`). Aliases: `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`,
|
|
267
|
+
`true` on iOS and web, `false` on Android since 0.27.0). Aliases: `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`,
|
|
262
268
|
`BodyText`, `CaptionText`, `LabelText`, `EyebrowText`, `MonoText`, `SerifText`,
|
|
263
269
|
`SansSerifText`, `SerifBoldText`, `SansSerifBoldText`.
|
|
264
270
|
|
|
265
|
-
Set `selectable={false}` on text labels inside app-owned `Pressable`s
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
Set `selectable={false}` on text labels inside app-owned `Pressable`s on every
|
|
272
|
+
platform (control chrome shows no selection cursor). On Android `StyledText` is
|
|
273
|
+
non-selectable by default because a selectable `Text` takes input focus and hides
|
|
274
|
+
the keyboard; pass `selectable` to opt copyable content in. Package controls
|
|
275
|
+
already scope label selectability; ordinary iOS/web text stays selectable.
|
|
269
276
|
|
|
270
277
|
## Component Selection Rules
|
|
271
278
|
|
|
@@ -275,6 +282,7 @@ should remain selectable.
|
|
|
275
282
|
- `Carousel` for a horizontal snap row of a known, small set of slides. It renders every child (no virtualization), so slides survive into the exported HTML shell and the first client frame; use `FlatList` for large or unbounded data. Dots are pressable and jump to their slide. A fractional `itemWidth` (default `0.85`) measures the viewport until the first layout, so pass absolute pixels (`> 1`) when the parent is narrower than the window and the first frame matters.
|
|
276
283
|
- `Avatar` with both `source` and `name` whenever both exist: `name` supplies the initials shown when the image is absent, still loading, or failed, plus the default accessibility label. Inside `AvatarGroup`, set `size`/`shape` on the group — children inherit them and gain the ring; the group's count is a hidden summary node, so each member stays individually announceable and the group needs no `accessible` wrapper.
|
|
277
284
|
- Pair a standalone `Label` with its control using two DISTINCT ids: `nativeID` is the label's own id, `htmlFor` is the input's id (`<Label nativeID="email-label" htmlFor="email-input">` + `<TextInput nativeID="email-input" />`). One id on both renders duplicate ids on web and associates nothing. Prefer `TextInput`'s own `label` prop when no separate label element is needed.
|
|
285
|
+
- `TextInput` `onBlur` fires only after a real focus on Android (the native field's first-composition blur is dropped in the package); validate on blur directly and do not add a touched-fields guard in app code.
|
|
278
286
|
- `Drawer.Header` takes `icon`, `title`, and `action` slots for a compact app-brand row; put `Drawer.ToggleCollapse` in `action` for a trailing rail control. `Drawer.Content` owns safe-area top/bottom padding — do not duplicate it in children.
|
|
279
287
|
|
|
280
288
|
## Minimal Examples
|
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ Web 0.21. Install these peers at the versions your Expo SDK recommends: `expo`,
|
|
|
32
32
|
`react-native-web`, `react-native-gesture-handler`,
|
|
33
33
|
`react-native-keyboard-controller`, `react-native-safe-area-context`,
|
|
34
34
|
`react-native-screens`, `@react-native-async-storage/async-storage`, `zustand`.
|
|
35
|
+
`Icon` renders [Lucide](https://lucide.dev/icons) SVGs, so also install its two
|
|
36
|
+
peers: `npx expo install react-native-svg` (a native module, pinned by your
|
|
37
|
+
SDK) and `bun add lucide-react-native` (`>=1.46 <2`).
|
|
35
38
|
|
|
36
39
|
`@rn-primitives/*` and `@expo-google-fonts/inter` are package dependencies, not
|
|
37
40
|
peers. Animations use React Native `Animated`. `BottomSheet`, `Slider`, and
|
|
@@ -311,12 +314,17 @@ import { BodyText, CaptionText, HeadingText, StyledText } from "@mrmeg/expo-ui/c
|
|
|
311
314
|
- `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
|
|
312
315
|
- `variant`: `sansSerif`, `serif`, `mono`
|
|
313
316
|
- `align`, `text`, `tx`, `txOptions`
|
|
314
|
-
- `selectable`: defaults to `true
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
- `selectable`: defaults to `true` on iOS and web and to `false` on Android
|
|
318
|
+
(0.27.0), where a selectable `Text` is focusable and takes focus and the IME
|
|
319
|
+
from an active input. Opt copyable content in per element with `selectable`
|
|
320
|
+
(codes, addresses, chat bubbles). Package controls disable it on every
|
|
321
|
+
platform for labels and interactive chrome where accidental drag selection
|
|
322
|
+
would feel broken.
|
|
323
|
+
|
|
324
|
+
For app-owned `Pressable` labels, still set `selectable={false}` on the nested
|
|
325
|
+
`StyledText` so iOS and web show no selection cursor on control chrome. Ordinary
|
|
326
|
+
readable text keeps selection on iOS and web; on Android it selects only when
|
|
327
|
+
opted in.
|
|
320
328
|
|
|
321
329
|
Aliases: `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`,
|
|
322
330
|
`BodyText`, `CaptionText`, `LabelText`, `EyebrowText`, `MonoText` (code, IDs,
|
|
@@ -343,9 +351,10 @@ Inter is the sans face on every platform, in four static weights
|
|
|
343
351
|
from the bundled `@expo-google-fonts/inter`. Serif is Georgia; mono is the
|
|
344
352
|
platform system monospace.
|
|
345
353
|
|
|
346
|
-
`useResources()` loads those four weights
|
|
347
|
-
|
|
348
|
-
`
|
|
354
|
+
`useResources()` loads those four weights on native, so `StyledText`'s
|
|
355
|
+
`light`–`bold` range resolves to real files instead of a faked OS bold. Icons
|
|
356
|
+
are SVG (`lucide-react-native`), so there is no icon font to load on any
|
|
357
|
+
platform. On web
|
|
349
358
|
it injects one Google Fonts Inter stylesheet (all four weights) after hydration
|
|
350
359
|
unless the app already added it; web weight differentiation then comes from a
|
|
351
360
|
numeric `fontWeight` on the shared `"Inter"` family. For better first paint in
|
|
@@ -391,7 +400,7 @@ When `sansSerif` is overridden, `useResources` skips downloading the packaged
|
|
|
391
400
|
Inter faces entirely (native `.ttf`s and the web stylesheet). For the skip to
|
|
392
401
|
apply, call `setFonts` before the hook mounts (module scope, or ahead of
|
|
393
402
|
rendering the root); a later call still re-skins all text, it just doesn't
|
|
394
|
-
prevent the download.
|
|
403
|
+
prevent the download.
|
|
395
404
|
|
|
396
405
|
| `webWeightStrategy` | Use when | Effect |
|
|
397
406
|
|---|---|---|
|
|
@@ -432,7 +441,7 @@ building a new primitive.
|
|
|
432
441
|
| `DropdownMenu` | Menus and command lists | Row actions, account menu, sort menu, checkbox/radio groups |
|
|
433
442
|
| `EmptyState` | No-data or recoverable error regions | Empty inbox, no results, failed list load |
|
|
434
443
|
| `ErrorBoundary` | React render error fallback | Route-level and feature-level fallbacks |
|
|
435
|
-
| `Icon` |
|
|
444
|
+
| `Icon` | Lucide or custom icons on theme tokens | Button accessories, menu icons, status glyphs |
|
|
436
445
|
| `InputOTP` | Verification code entry | Email/SMS codes, MFA, invite codes |
|
|
437
446
|
| `Item` | List and settings rows on the density tokens | Settings lists, inbox rows, pickers, detail rows |
|
|
438
447
|
| `KeyboardAvoidingView` | Native keyboard-aware layout | Screen roots, composer footers, form-heavy subtrees |
|
|
@@ -458,6 +467,43 @@ building a new primitive.
|
|
|
458
467
|
| `ToggleGroup` | Single or multi toggle groups | Alignment, formatting toolbar, filter chips |
|
|
459
468
|
| `Tooltip` | Short hover/focus help | Icon button labels, field hints, disabled-action reasons |
|
|
460
469
|
|
|
470
|
+
### Icons
|
|
471
|
+
|
|
472
|
+
`Icon` renders `lucide-react-native` — Lucide is a maintained superset of
|
|
473
|
+
Feather in the same 24px, 2px round-stroke style. `name` is typed by
|
|
474
|
+
`IconName`, the union of kebab-case Lucide names in the package registry
|
|
475
|
+
(`src/components/icon-names.json`, about 150 names), so only the icons the package
|
|
476
|
+
and its consumers name ship in the bundle: the root `lucide-react-native`
|
|
477
|
+
entry (1,800+ icons) is never imported. `color` takes a theme color name or a
|
|
478
|
+
literal; `decorative` hides the glyph from assistive tech.
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
import { Icon } from "@mrmeg/expo-ui/components";
|
|
482
|
+
import Rocket from "lucide-react-native/icons/rocket";
|
|
483
|
+
|
|
484
|
+
<Icon name="circle-check-big" color="success" size={16} />
|
|
485
|
+
<Icon component={Rocket} color="accent" />
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Adding an icon: in this repo, add the kebab-case Lucide name to
|
|
489
|
+
`packages/ui/src/components/icon-names.json` (keep it sorted), run
|
|
490
|
+
`bun run ui:icons`, and commit both files — an unknown name fails with the
|
|
491
|
+
alias lines from Lucide's root entry that point at its current name
|
|
492
|
+
(`bun run ui:icons:check` guards freshness in CI). From a consuming app, pass
|
|
493
|
+
any Lucide component through `component` instead; it needs no registry entry.
|
|
494
|
+
Feather-era names that Lucide renamed: `alert-circle` → `circle-alert`,
|
|
495
|
+
`check-circle` → `circle-check-big`, `x-circle` → `circle-x`,
|
|
496
|
+
`alert-triangle` → `triangle-alert`, `help-circle` → `circle-question-mark`,
|
|
497
|
+
`edit` → `square-pen`, `edit-2` → `pen`, `edit-3` → `pencil`,
|
|
498
|
+
`sliders` → `sliders-horizontal`, `grid` → `layout-grid`, `home` → `house`,
|
|
499
|
+
`unlock` → `lock-open`, `trash-2` → `trash`, `tool` → `wrench`,
|
|
500
|
+
`layout` → `panels-top-left`, `bar-chart-2` → `chart-no-axes-column`,
|
|
501
|
+
`plus-circle` → `circle-plus`, `stop-circle` → `circle-stop`,
|
|
502
|
+
`more-vertical` → `ellipsis-vertical`, `more-horizontal` → `ellipsis`,
|
|
503
|
+
`sidebar` → `panel-left`, `smile` → `face-slightly-smiling`. Brand glyphs
|
|
504
|
+
(`github`, `chrome`, `facebook`, `instagram`, `linkedin`, `twitter`) have no
|
|
505
|
+
Lucide equivalent; use `component` with your own SVG.
|
|
506
|
+
|
|
461
507
|
### Compound Parts
|
|
462
508
|
|
|
463
509
|
| Root | Exported Parts |
|
|
@@ -492,7 +538,7 @@ are named exports only.
|
|
|
492
538
|
|
|
493
539
|
| Hook | Returns |
|
|
494
540
|
|------|---------|
|
|
495
|
-
| `useResources()` | `{ loaded, error }` — loads
|
|
541
|
+
| `useResources()` | `{ loaded, error }` — loads the Inter faces (icons are SVG, nothing to load) |
|
|
496
542
|
| `useTheme()` | Active theme, scheme helpers, shadow/contrast/alpha helpers |
|
|
497
543
|
| `useStyles(factory)` | Memoized theme-aware styles plus the `useTheme()` helpers |
|
|
498
544
|
| `useDimensions()` | `{ width, height, orientation, isSmallScreen, isMediumScreen, isLargeScreen }` against `SCREEN_SIZES` (768 / 1000 / 1200) |
|
|
@@ -518,19 +564,43 @@ web, where the window cannot be read during export or hydration.
|
|
|
518
564
|
nativeID="email-input" />`). One id on both renders duplicate ids on web and
|
|
519
565
|
associates nothing. Prefer `TextInput`'s own `label` prop when no separate
|
|
520
566
|
label element is needed.
|
|
567
|
+
- `TextInput` renders a native field on iOS and Android. On Android a
|
|
568
|
+
`secureTextEntry` field declares a password keyboard (`textPassword`, or
|
|
569
|
+
`numberPassword` for numeric keyboards) in both the masked and the revealed
|
|
570
|
+
eye-toggle state, with autocorrect off by default; that Android field is
|
|
571
|
+
package-owned on top of `@expo/ui/jetpack-compose`'s `BasicTextField`, while
|
|
572
|
+
iOS uses `@expo/ui`'s universal field (SwiftUI `SecureField`). On Android the
|
|
573
|
+
native field reports a blur when it is first composed; the package forwards
|
|
574
|
+
`onBlur` only after the field has reported focus, so blur validation never
|
|
575
|
+
flags an untouched field and app code needs no touched-fields guard.
|
|
521
576
|
- `BottomSheet` renders the platform's native sheet through `@expo/ui`: iOS
|
|
522
577
|
SwiftUI `.sheet()`, Android Material3 `ModalBottomSheet`, web `vaul`. The
|
|
523
578
|
platform owns gestures and keyboard avoidance, so `swipeEnabled`,
|
|
524
579
|
`avoidKeyboard`, and `dismissKeyboardOnDrag` are accepted for call-site
|
|
525
|
-
ergonomics but have no effect
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
580
|
+
ergonomics but have no effect. On iOS the sheet presentation itself keeps the
|
|
581
|
+
hosted content above the keyboard — a short sheet is lifted whole, a tall one
|
|
582
|
+
is shrunk — so `Footer` and the tail of `Body` end at the keyboard's top with
|
|
583
|
+
only their home-indicator padding as clearance (device-verified on iOS 27 with
|
|
584
|
+
`@expo/ui` 58); on Android Material3's `ModalBottomSheet` owns it (no JS
|
|
585
|
+
keyboard signal exists inside the Compose dialog window); web has none. Do not
|
|
586
|
+
wrap sheet content in another `KeyboardAvoidingView`: it would lift twice.
|
|
587
|
+
The sheet hosts its content in a separate
|
|
588
|
+
native window outside the app's `DismissKeyboard`, so `BottomSheet.Content`
|
|
589
|
+
mounts its own tap-away keyboard-dismiss boundary on the content column: it
|
|
590
|
+
never claims the touch (buttons, tabs and other fields fire on the first tap
|
|
591
|
+
with the keyboard up) and dismisses an unclaimed dead-space tap on release,
|
|
592
|
+
matching `DismissKeyboard`. `BottomSheet.Body` sets
|
|
593
|
+
`keyboardShouldPersistTaps="always"` on its ScrollView so that column boundary
|
|
594
|
+
owns dismissal; do not pass `never`, which lets RN claim the first tap on a
|
|
595
|
+
body control and blur the field instead. Android has only two snap states
|
|
596
|
+
(partial / expanded) and maps extra snap points to the nearest; because
|
|
597
|
+
Material ignores percentage snap points, the Android body fills the rendered
|
|
598
|
+
sheet height rather than a window-percentage cap. `BottomSheet.Content` also
|
|
599
|
+
takes `backgroundStyle`, merged over the themed card default on the native
|
|
600
|
+
sheet surface (web panel, Android `containerColor`, iOS
|
|
601
|
+
`presentationBackground`) — pass `{ backgroundColor: "transparent" }`, plus a
|
|
602
|
+
`style` clearing the content column's card fill, to let custom chrome such as
|
|
603
|
+
a glass backdrop show through.
|
|
534
604
|
- `Carousel` renders every child (no virtualization), so slides survive into
|
|
535
605
|
the exported HTML shell and the first client frame; use `FlatList` for large
|
|
536
606
|
or unbounded data. An `itemWidth` below 1 (default `0.85`) is a fraction of
|
|
@@ -691,8 +761,9 @@ packed package at the workspace's Expo version, without a custom Metro config.
|
|
|
691
761
|
`ui:build` also writes `dist/design-system.json`
|
|
692
762
|
([`scripts/build-design-system-manifest.mjs`](../../scripts/build-design-system-manifest.mjs)):
|
|
693
763
|
the spacing, radius and icon scales, the palette, the light and dark themes, the
|
|
694
|
-
font variants,
|
|
695
|
-
`src` by the same extractor the lint
|
|
764
|
+
font variants, every component's variant and size values, and `icons.names` (the
|
|
765
|
+
`IconName` registry), serialized out of `src` by the same extractor the lint
|
|
766
|
+
rules use. It is exported as
|
|
696
767
|
`@mrmeg/expo-ui/design-system.json`, and `@mrmeg/eslint-plugin-expo-ui` reads it
|
|
697
768
|
in projects that install this package instead of checking out its sources — which
|
|
698
769
|
is what lets the design-system rules quote this release's presets and tokens.
|
|
@@ -26,7 +26,7 @@ export interface AvatarProps {
|
|
|
26
26
|
source?: ImageSourcePropType;
|
|
27
27
|
/** Display name; supplies the fallback initials and the default accessibility label. */
|
|
28
28
|
name?: string;
|
|
29
|
-
/**
|
|
29
|
+
/** Icon (Lucide name) rendered when there is no image and no derivable initials. @default "user" */
|
|
30
30
|
icon?: IconName;
|
|
31
31
|
/** Size token or explicit pixel diameter. Inherited from `AvatarGroup` when unset. @default "md" */
|
|
32
32
|
size?: AvatarSize;
|
|
@@ -10,8 +10,18 @@ import { ViewProps, StyleProp, ViewStyle, ScrollViewProps } from "react-native";
|
|
|
10
10
|
*
|
|
11
11
|
* The compound surface (Trigger / Content / Handle / Header / Body / Footer /
|
|
12
12
|
* Close), controlled + uncontrolled state, and theming match a hand-rolled
|
|
13
|
-
* sheet, but the platform owns gestures
|
|
14
|
-
*
|
|
13
|
+
* sheet, but the platform owns gestures — so there's no PanResponder or
|
|
14
|
+
* snap-physics code to maintain. Keyboard avoidance is the platform's too.
|
|
15
|
+
* iOS (device-verified, iOS 27 / `@expo/ui` 58): UIKit's sheet presentation
|
|
16
|
+
* keeps the hosted RN column above the keyboard by itself — a short sheet is
|
|
17
|
+
* lifted whole, a tall one is shrunk — so the column's bottom edge lands at the
|
|
18
|
+
* keyboard's top and `Footer` / the tail of `Body` stay reachable with only
|
|
19
|
+
* their own home-indicator padding as clearance. The package adds no inset:
|
|
20
|
+
* nothing in JS can measure the column in screen space (`measureInWindow`
|
|
21
|
+
* inside the `layoutRoot` sheet host reports host-relative coordinates), and a
|
|
22
|
+
* window-height estimate would lift a lifted sheet twice. Android: Material3's
|
|
23
|
+
* `ModalBottomSheet` owns it (no JS keyboard signal exists inside the Compose
|
|
24
|
+
* dialog window). Web: none. Never nest a `KeyboardAvoidingView` in a sheet.
|
|
15
25
|
*
|
|
16
26
|
* Platform-owned behaviors (props accepted for ergonomics, but the platform
|
|
17
27
|
* decides):
|
|
@@ -24,6 +34,10 @@ import { ViewProps, StyleProp, ViewStyle, ScrollViewProps } from "react-native";
|
|
|
24
34
|
* platform's on native; theming reaches the content + background color.
|
|
25
35
|
* - On Android only two snap states exist (partial / expanded); extra snap
|
|
26
36
|
* points map to the nearest of those two.
|
|
37
|
+
* - `Body` sets `keyboardShouldPersistTaps="always"` on its ScrollView so RN's
|
|
38
|
+
* own tap-dismissal never claims the first tap on a chip, button or field
|
|
39
|
+
* while a sheet field is focused; the `Content` boundary owns tap-away
|
|
40
|
+
* dismissal instead.
|
|
27
41
|
*
|
|
28
42
|
* Scrollable bodies: the native sheet doesn't bound the hosted RN content to
|
|
29
43
|
* the detent height, so a tall `Body` overflows and clips its footer/tail. When
|
|
@@ -113,7 +127,13 @@ interface BottomSheetTriggerProps {
|
|
|
113
127
|
interface BottomSheetContentProps extends ViewProps {
|
|
114
128
|
/** Accepted for call-site ergonomics; ignored (platform owns gestures). */
|
|
115
129
|
swipeEnabled?: boolean;
|
|
116
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Accepted for call-site ergonomics; ignored (platform owns keyboard
|
|
132
|
+
* avoidance). iOS: UIKit's sheet presentation lifts or shrinks the hosted
|
|
133
|
+
* content so its bottom edge sits at the keyboard's top (device-verified).
|
|
134
|
+
* Android: Material3's `ModalBottomSheet`. Web: none. `false` has no effect;
|
|
135
|
+
* do not wrap sheet content in another `KeyboardAvoidingView`.
|
|
136
|
+
*/
|
|
117
137
|
avoidKeyboard?: boolean;
|
|
118
138
|
/** Accepted for call-site ergonomics; ignored (platform owns keyboard). */
|
|
119
139
|
dismissKeyboardOnDrag?: boolean;
|
|
@@ -148,7 +168,7 @@ interface BottomSheetCloseProps {
|
|
|
148
168
|
declare function useBottomSheetContext(): BottomSheetContextValue;
|
|
149
169
|
declare function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, snapPoints, closeOnBackdropPress, children, }: BottomSheetProps): React.JSX.Element;
|
|
150
170
|
declare function BottomSheetTrigger({ asChild, children, style: styleOverride }: BottomSheetTriggerProps): React.JSX.Element;
|
|
151
|
-
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
171
|
+
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
152
172
|
declare function BottomSheetHandle({ style }: BottomSheetHandleProps): React.JSX.Element;
|
|
153
173
|
declare function BottomSheetHeader({ children, style, ...props }: BottomSheetHeaderProps): React.JSX.Element;
|
|
154
174
|
declare function BottomSheetBody({ children, style, contentContainerStyle, onContentSizeChange, onLayout, ...props }: BottomSheetBodyProps): React.JSX.Element;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef
|
|
2
|
+
import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
|
3
3
|
import { View, Pressable, StyleSheet, ScrollView, Platform, Animated, useWindowDimensions, } from "react-native";
|
|
4
4
|
import { BottomSheet as NativeBottomSheet } from "@expo/ui/community/bottom-sheet";
|
|
5
|
-
import { KeyboardController } from "./keyboardController";
|
|
6
5
|
import { useSafeAreaInsets, initialWindowMetrics } from "react-native-safe-area-context";
|
|
7
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
7
|
import { spacing } from "../constants/spacing.js";
|
|
9
8
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
10
9
|
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
11
10
|
import { Icon } from "./Icon.js";
|
|
12
|
-
import {
|
|
11
|
+
import { useKeyboardDismissResponder } from "./keyboardDismiss.js";
|
|
13
12
|
const DEFAULT_SNAP_POINTS = ["50%"];
|
|
14
13
|
function clampSnapIndex(index, maxSnapIndex) {
|
|
15
14
|
return Math.min(Math.max(index, 0), maxSnapIndex);
|
|
@@ -162,48 +161,6 @@ function SheetCloseButton({ style }) {
|
|
|
162
161
|
], children: _jsx(Icon, { name: "x", size: 22, color: "text" }) }) }));
|
|
163
162
|
}
|
|
164
163
|
// ============================================================================
|
|
165
|
-
// Keyboard dismiss overlay — tap-away dismissal inside the sheet
|
|
166
|
-
// ============================================================================
|
|
167
|
-
/**
|
|
168
|
-
* Tap-away keyboard dismissal for sheet content.
|
|
169
|
-
*
|
|
170
|
-
* The native sheet (SwiftUI `.sheet()` / Material `ModalBottomSheet`) hosts its
|
|
171
|
-
* RN children in a separate native window via @expo/ui's `RNHostView`, OUTSIDE
|
|
172
|
-
* the app's `KeyboardProvider` / `KeyboardDismissBoundary`. Two consequences,
|
|
173
|
-
* both worked around here:
|
|
174
|
-
*
|
|
175
|
-
* 1. Detection: `react-native-keyboard-controller`'s `useKeyboardState` watches
|
|
176
|
-
* the main window and does NOT see a keyboard raised inside the sheet's
|
|
177
|
-
* window (the same isolation that makes `useSafeAreaInsets()` read zero in
|
|
178
|
-
* here). So presence is read from the `keyboardFocusRegistry` instead, which
|
|
179
|
-
* is fed by @expo/ui's own native focus events and is window-independent.
|
|
180
|
-
* 2. Dispatch: RN's JS responder chain (onStartShouldSetResponder*) is NOT
|
|
181
|
-
* dispatched across the `RNHostView` boundary on Android — only `Pressable`
|
|
182
|
-
* hit-testing (native `measure()`) works inside the host. So this is a
|
|
183
|
-
* transparent `Pressable` mounted ONLY while a field is focused, mirroring
|
|
184
|
-
* a tap-to-dismiss overlay in the app. The tap blurs the focused field via
|
|
185
|
-
* its own native ref (`dismissKeyboardFocusedInput`), which resigns the
|
|
186
|
-
* responder regardless of window; `KeyboardController.dismiss()` is a
|
|
187
|
-
* best-effort fallback for the (rare) case with no registered blur handle.
|
|
188
|
-
*
|
|
189
|
-
* iOS doesn't strictly need this (SwiftUI resigns first-responder on outside
|
|
190
|
-
* taps for free), but the same path is harmless and keeps behavior identical.
|
|
191
|
-
*/
|
|
192
|
-
function SheetKeyboardDismissOverlay() {
|
|
193
|
-
const hasFocus = useSyncExternalStore(subscribeKeyboardFocus, hasKeyboardFocusedInput, () => false);
|
|
194
|
-
if (Platform.OS === "web" || !hasFocus)
|
|
195
|
-
return null;
|
|
196
|
-
return (_jsx(Pressable, { style: [StyleSheet.absoluteFill, styles.keyboardDismissOverlay], onPressIn: () => {
|
|
197
|
-
if (!dismissKeyboardFocusedInput())
|
|
198
|
-
KeyboardController.dismiss();
|
|
199
|
-
}, accessibilityLabel: "Dismiss keyboard", accessibilityRole: "button" }));
|
|
200
|
-
}
|
|
201
|
-
const styles = StyleSheet.create({
|
|
202
|
-
keyboardDismissOverlay: {
|
|
203
|
-
zIndex: 999,
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
// ============================================================================
|
|
207
164
|
// Root
|
|
208
165
|
// ============================================================================
|
|
209
166
|
function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, snapPoints = DEFAULT_SNAP_POINTS, closeOnBackdropPress = true, children, }) {
|
|
@@ -296,21 +253,41 @@ function BottomSheetTrigger({ asChild, children, style: styleOverride }) {
|
|
|
296
253
|
// ============================================================================
|
|
297
254
|
function BottomSheetContent({
|
|
298
255
|
// Accepted-but-ignored ergonomics props (platform owns these behaviors):
|
|
299
|
-
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, children, }) {
|
|
256
|
+
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }) {
|
|
300
257
|
const { open, onOpenChange, snapPoints, snapIndex, setSnapIndex, hasHeader } = useBottomSheetContext();
|
|
301
258
|
const { theme } = useTheme();
|
|
302
259
|
const dismissDisabled = useDismissDisabled();
|
|
303
260
|
const showClose = useShowClose();
|
|
304
261
|
const { height: winH } = useWindowDimensions();
|
|
262
|
+
// Tap-away keyboard dismissal inside the sheet. The native sheet (SwiftUI
|
|
263
|
+
// `.sheet()` / Material `ModalBottomSheet`) hosts its RN children in a separate
|
|
264
|
+
// native window via @expo/ui's `RNHostView`, OUTSIDE the app's
|
|
265
|
+
// `KeyboardProvider` / `DismissKeyboard`, so an app-level boundary never sees
|
|
266
|
+
// these touches and the sheet mounts its own on the content column. It is the
|
|
267
|
+
// same boundary `DismissKeyboard` uses: presence comes from the
|
|
268
|
+
// `keyboardFocusRegistry` (keyboard-controller's `useKeyboardState` watches
|
|
269
|
+
// the main window and does not observe a keyboard raised in here), the
|
|
270
|
+
// boundary never claims the touch — buttons, tabs and other fields win the
|
|
271
|
+
// responder negotiation and fire on the first tap — and an unclaimed
|
|
272
|
+
// dead-space tap dismisses on release only, through the registered field's
|
|
273
|
+
// own window-independent blur handle.
|
|
274
|
+
const dismissResponderProps = useKeyboardDismissResponder();
|
|
305
275
|
// Boolean open → native imperative index. The root resets snapIndex to the
|
|
306
276
|
// highest point while closed; -1 keeps the native sheet closed.
|
|
307
277
|
const index = open ? snapIndex : -1;
|
|
308
|
-
//
|
|
278
|
+
// iOS: the RN-in-SwiftUI host does NOT clamp our RN column to the sheet's
|
|
309
279
|
// detent height — it lays the column out at its full intrinsic content height,
|
|
310
280
|
// SwiftUI then clips it at the sheet edge (footer/tail fall off-screen), and
|
|
311
281
|
// `flex:1` never gets a definite parent height so the ScrollView can't bound
|
|
312
282
|
// and scroll. Cap the column to the expanded detent height (highest snap
|
|
313
|
-
// point) so `flex:1` resolves and the Body becomes scrollable.
|
|
283
|
+
// point) so `flex:1` resolves and the Body becomes scrollable. Web (vaul)
|
|
284
|
+
// sizes the panel itself from the snap point, so the cap is inert there.
|
|
285
|
+
// Android: the Compose host wraps children in a `flexGrow: 1, height: 0` view
|
|
286
|
+
// that fills `RNHostView`'s measured height, so `flex:1` already resolves,
|
|
287
|
+
// and Material's `ModalBottomSheet` ignores percentage snap points
|
|
288
|
+
// (partial / expanded only) — a window-percentage cap would leave the Body
|
|
289
|
+
// short of the rendered sheet with a blank strip below it. Android therefore
|
|
290
|
+
// keeps `flex:1` alone (see the content column style).
|
|
314
291
|
const expandedSnap = snapPoints[snapPoints.length - 1];
|
|
315
292
|
const detentHeight = typeof expandedSnap === "number"
|
|
316
293
|
? expandedSnap
|
|
@@ -343,16 +320,16 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
343
320
|
backgroundStyle: StyleSheet.flatten([
|
|
344
321
|
{ backgroundColor: theme.colors.card },
|
|
345
322
|
backgroundStyleOverride,
|
|
346
|
-
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { style: [
|
|
323
|
+
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { testID: testID, style: [
|
|
347
324
|
{
|
|
348
325
|
flex: 1,
|
|
349
|
-
maxHeight: detentHeight,
|
|
350
326
|
// Themes the content surface across all platforms regardless of
|
|
351
327
|
// native sheet chrome.
|
|
352
328
|
backgroundColor: theme.colors.card,
|
|
353
329
|
},
|
|
330
|
+
Platform.OS !== "android" && { maxHeight: detentHeight },
|
|
354
331
|
styleOverride,
|
|
355
|
-
], children: [children,
|
|
332
|
+
], ...dismissResponderProps, children: [children, showFloatingClose && (_jsx(SheetCloseButton, { style: {
|
|
356
333
|
// The sheet card sits below the status bar, so no top inset
|
|
357
334
|
// is needed — just clear the sheet's own top edge / handle.
|
|
358
335
|
position: "absolute",
|
|
@@ -458,7 +435,15 @@ function BottomSheetBody({ children, style, contentContainerStyle, onContentSize
|
|
|
458
435
|
paddingBottom: spacing.md + (hasFooter ? 0 : insets.bottom),
|
|
459
436
|
},
|
|
460
437
|
contentContainerStyle,
|
|
461
|
-
],
|
|
438
|
+
],
|
|
439
|
+
// The `Content` column's keyboard-dismiss boundary owns tap-away
|
|
440
|
+
// dismissal (see DismissKeyboard). RN's default `never` claims the first
|
|
441
|
+
// tap on any non-input child while a registered field is focused and
|
|
442
|
+
// blurs it on release; `handled` can still blur independently after a
|
|
443
|
+
// non-scrolling drag. `always` leaves every tap to its target and lets
|
|
444
|
+
// dead-space taps bubble to the boundary. Placed before `{...props}` so an
|
|
445
|
+
// explicit consumer value still wins.
|
|
446
|
+
keyboardShouldPersistTaps: "always", showsVerticalScrollIndicator: false, onLayout: (e) => {
|
|
462
447
|
viewportH.current = e.nativeEvent.layout.height;
|
|
463
448
|
evaluate();
|
|
464
449
|
onLayout?.(e);
|
|
@@ -76,7 +76,7 @@ function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost
|
|
|
76
76
|
return (_jsx(DropdownMenuPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(DropdownMenuPrimitive.Overlay, { style: Platform.select({
|
|
77
77
|
native: StyleSheet.absoluteFill,
|
|
78
78
|
default: undefined,
|
|
79
|
-
}), children: _jsx(AnimatedView, { type: "fade", children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
|
|
79
|
+
}), children: _jsx(AnimatedView, { type: "fade", style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
|
|
80
80
|
backgroundColor: theme.colors.popover,
|
|
81
81
|
borderWidth: 1,
|
|
82
82
|
borderColor: theme.colors.border,
|