@mrmeg/expo-ui 0.26.0 → 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 +54 -0
- package/LLM_USAGE.md +14 -6
- package/README.md +35 -17
- package/dist/components/BottomSheet.d.ts +23 -3
- package/dist/components/BottomSheet.js +9 -1
- package/dist/components/StyledText.d.ts +6 -2
- package/dist/components/StyledText.js +10 -4
- package/dist/components/TextInput.js +11 -0
- package/dist/design-system.json +1 -1
- package/llms-full.md +1 -1
- package/llms.txt +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,60 @@ 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
|
+
|
|
8
62
|
## [0.26.0]
|
|
9
63
|
|
|
10
64
|
### Changed
|
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
|
|
@@ -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
|
@@ -314,12 +314,17 @@ import { BodyText, CaptionText, HeadingText, StyledText } from "@mrmeg/expo-ui/c
|
|
|
314
314
|
- `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
|
|
315
315
|
- `variant`: `sansSerif`, `serif`, `mono`
|
|
316
316
|
- `align`, `text`, `tx`, `txOptions`
|
|
317
|
-
- `selectable`: defaults to `true
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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.
|
|
323
328
|
|
|
324
329
|
Aliases: `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`,
|
|
325
330
|
`BodyText`, `CaptionText`, `LabelText`, `EyebrowText`, `MonoText` (code, IDs,
|
|
@@ -564,25 +569,38 @@ web, where the window cannot be read during export or hydration.
|
|
|
564
569
|
`numberPassword` for numeric keyboards) in both the masked and the revealed
|
|
565
570
|
eye-toggle state, with autocorrect off by default; that Android field is
|
|
566
571
|
package-owned on top of `@expo/ui/jetpack-compose`'s `BasicTextField`, while
|
|
567
|
-
iOS uses `@expo/ui`'s universal field (SwiftUI `SecureField`).
|
|
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.
|
|
568
576
|
- `BottomSheet` renders the platform's native sheet through `@expo/ui`: iOS
|
|
569
577
|
SwiftUI `.sheet()`, Android Material3 `ModalBottomSheet`, web `vaul`. The
|
|
570
578
|
platform owns gestures and keyboard avoidance, so `swipeEnabled`,
|
|
571
579
|
`avoidKeyboard`, and `dismissKeyboardOnDrag` are accepted for call-site
|
|
572
|
-
ergonomics but have no effect.
|
|
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
|
|
573
588
|
native window outside the app's `DismissKeyboard`, so `BottomSheet.Content`
|
|
574
589
|
mounts its own tap-away keyboard-dismiss boundary on the content column: it
|
|
575
590
|
never claims the touch (buttons, tabs and other fields fire on the first tap
|
|
576
591
|
with the keyboard up) and dismisses an unclaimed dead-space tap on release,
|
|
577
|
-
matching `DismissKeyboard`.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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.
|
|
586
604
|
- `Carousel` renders every child (no virtualization), so slides survive into
|
|
587
605
|
the exported HTML shell and the first client frame; use `FlatList` for large
|
|
588
606
|
or unbounded data. An `itemWidth` below 1 (default `0.85`) is a fraction of
|
|
@@ -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;
|
|
@@ -435,7 +435,15 @@ function BottomSheetBody({ children, style, contentContainerStyle, onContentSize
|
|
|
435
435
|
paddingBottom: spacing.md + (hasFooter ? 0 : insets.bottom),
|
|
436
436
|
},
|
|
437
437
|
contentContainerStyle,
|
|
438
|
-
],
|
|
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) => {
|
|
439
447
|
viewportH.current = e.nativeEvent.layout.height;
|
|
440
448
|
evaluate();
|
|
441
449
|
onLayout?.(e);
|
|
@@ -74,8 +74,12 @@ export type TextProps = RNTextProps & {
|
|
|
74
74
|
* real Inter font files on native, and family + numeric fontWeight on web
|
|
75
75
|
* - Per-size letter-spacing scale (tight on large headings, positive on micro
|
|
76
76
|
* labels); explicit `style` letterSpacing always wins
|
|
77
|
-
* - Text selection
|
|
78
|
-
*
|
|
77
|
+
* - Text selection on by default on iOS and web, off by default on Android:
|
|
78
|
+
* a selectable Android `Text` is focusable-in-touch-mode, so a tap on a label
|
|
79
|
+
* next to a focused input moved view focus to the label and hid the keyboard.
|
|
80
|
+
* Pass `selectable` to opt copyable content in (codes, addresses, chat
|
|
81
|
+
* bubbles); pass `selectable={false}` on every platform for control chrome
|
|
82
|
+
* such as button labels, tabs, badges, and field labels
|
|
79
83
|
* - numberOfLines and ellipsizeMode support from RN TextProps
|
|
80
84
|
*/
|
|
81
85
|
export declare function StyledText(props: TextProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { use } from "react";
|
|
3
|
-
import { Text as RNText, StyleSheet } from "react-native";
|
|
3
|
+
import { Platform, Text as RNText, StyleSheet } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { resolveFontStyle } from "../constants/fonts.js";
|
|
6
6
|
import { useThemeStore } from "../state/themeStore.js";
|
|
@@ -67,8 +67,12 @@ const SEMANTIC_CONFIGS = {
|
|
|
67
67
|
* real Inter font files on native, and family + numeric fontWeight on web
|
|
68
68
|
* - Per-size letter-spacing scale (tight on large headings, positive on micro
|
|
69
69
|
* labels); explicit `style` letterSpacing always wins
|
|
70
|
-
* - Text selection
|
|
71
|
-
*
|
|
70
|
+
* - Text selection on by default on iOS and web, off by default on Android:
|
|
71
|
+
* a selectable Android `Text` is focusable-in-touch-mode, so a tap on a label
|
|
72
|
+
* next to a focused input moved view focus to the label and hid the keyboard.
|
|
73
|
+
* Pass `selectable` to opt copyable content in (codes, addresses, chat
|
|
74
|
+
* bubbles); pass `selectable={false}` on every platform for control chrome
|
|
75
|
+
* such as button labels, tabs, badges, and field labels
|
|
72
76
|
* - numberOfLines and ellipsizeMode support from RN TextProps
|
|
73
77
|
*/
|
|
74
78
|
export function StyledText(props) {
|
|
@@ -81,7 +85,9 @@ export function StyledText(props) {
|
|
|
81
85
|
const contextColor = use(TextColorContext);
|
|
82
86
|
const contextTextStyle = use(TextStyleContext);
|
|
83
87
|
const contextSelectable = use(TextSelectabilityContext);
|
|
84
|
-
|
|
88
|
+
// Read at render time (not module load) so tests can flip `Platform.OS`.
|
|
89
|
+
const platformDefaultSelectable = Platform.OS !== "android";
|
|
90
|
+
const resolvedSelectable = selectable ?? contextSelectable ?? platformDefaultSelectable;
|
|
85
91
|
// Use context color if provided, otherwise use theme default
|
|
86
92
|
const color = contextColor ?? theme.colors.text;
|
|
87
93
|
// If semantic variant is provided, use its config
|
|
@@ -207,6 +207,10 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
207
207
|
* toggle laid out beside the native Host. Clear buttons, left/right elements,
|
|
208
208
|
* and the error icon remain web-only; RN overlays are not layered over the
|
|
209
209
|
* native host view.
|
|
210
|
+
*
|
|
211
|
+
* On Android `onBlur` is reported only after a forwarded `onFocus`: Compose
|
|
212
|
+
* reports `isFocused=false` on first composition and the field forwards it,
|
|
213
|
+
* so an unguarded consumer saw a blur before any interaction.
|
|
210
214
|
*/
|
|
211
215
|
function NativeTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, forceLight, inputMode, onChangeText, onFocus, onBlur, value, defaultValue, editable = true, multiline, secureTextEntry, showSecureEntryToggle, ref, wrapperStyle,
|
|
212
216
|
// Web-only affordances. Destructured out of `rest` so they're NOT forwarded to
|
|
@@ -321,9 +325,16 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
321
325
|
const handleBlur = useCallback((secure) => {
|
|
322
326
|
if (Platform.OS === "ios" && secure !== activeSecureRef.current)
|
|
323
327
|
return;
|
|
328
|
+
// Compose reports `isFocused=false` when the field is first composed and
|
|
329
|
+
// the Android field forwards it as a blur. A blur the field never
|
|
330
|
+
// preceded with a focus is not the user leaving the field: keep it from
|
|
331
|
+
// the consumer so blur validation does not flag untouched fields.
|
|
332
|
+
const wasFocused = isFocusedRef.current;
|
|
324
333
|
isFocusedRef.current = false;
|
|
325
334
|
restoreAfterSecureHandoffRef.current = false;
|
|
326
335
|
clearKeyboardFocusedInput(focusRegistryToken);
|
|
336
|
+
if (Platform.OS === "android" && !wasFocused)
|
|
337
|
+
return;
|
|
327
338
|
parentOnBlur?.();
|
|
328
339
|
}, [focusRegistryToken, parentOnBlur]);
|
|
329
340
|
const togglePasswordVisible = useCallback(() => {
|
package/dist/design-system.json
CHANGED
package/llms-full.md
CHANGED
|
@@ -103,7 +103,7 @@ before creating a new app-local primitive.
|
|
|
103
103
|
| `AnimatedView` | Entrance and visibility animation | Keep simple reveal effects in this wrapper. |
|
|
104
104
|
| `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback | Pass both `source` and `name`; set `size`/`shape` on the group, not per child. |
|
|
105
105
|
| `Badge` | Short status labels | Prefer over custom pill views. |
|
|
106
|
-
| `BottomSheet` | Mobile-first modal sheets | Requires root `UIProvider`. Native sheet via `@expo/ui`; `swipeEnabled`, `avoidKeyboard`, `dismissKeyboardOnDrag` are accepted but ignored. |
|
|
106
|
+
| `BottomSheet` | Mobile-first modal sheets | Requires root `UIProvider`. Native sheet via `@expo/ui`; `swipeEnabled`, `avoidKeyboard`, `dismissKeyboardOnDrag` are accepted but ignored (the iOS sheet presentation and Android Material3 keep content above the keyboard; never nest a `KeyboardAvoidingView` in a sheet). |
|
|
107
107
|
| `Button` | Commands and CTAs | Use `preset`, not `variant`; heights are 28/32/40. |
|
|
108
108
|
| `Card` | Individual framed content groups | Do not wrap whole page sections in cards. |
|
|
109
109
|
| `Carousel` | Small known set of horizontally snapping slides | Renders every child (no virtualization); fractional `itemWidth` measures the viewport until first layout. |
|
package/llms.txt
CHANGED
|
@@ -27,7 +27,10 @@ mount react-native-keyboard-controller's KeyboardProvider above UIProvider.
|
|
|
27
27
|
|
|
28
28
|
BottomSheet renders the platform's native sheet via @expo/ui, which owns
|
|
29
29
|
gestures and keyboard avoidance; swipeEnabled, avoidKeyboard, and
|
|
30
|
-
dismissKeyboardOnDrag are accepted but ignored.
|
|
30
|
+
dismissKeyboardOnDrag are accepted but ignored. On iOS the sheet presentation
|
|
31
|
+
lifts or shrinks the hosted content so Footer and Body end at the keyboard's
|
|
32
|
+
top (device-verified); Android avoidance is Material3's. Do not nest a
|
|
33
|
+
KeyboardAvoidingView inside a sheet.
|
|
31
34
|
|
|
32
35
|
Use useTheme(), useStyles(), semantic tokens, StyledText, and package controls.
|
|
33
36
|
Do not add app-local Appearance or matchMedia listeners for theme sync. On web
|