@mrmeg/expo-ui 0.11.0 → 0.12.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 CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to `@mrmeg/expo-ui` are documented here. This project
4
4
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.12.0]
7
+
8
+ ### Added
9
+
10
+ - **`KeyboardAvoidingView` is now a public package component.** Native uses
11
+ `react-native-keyboard-controller` with `automaticOffset` enabled by default,
12
+ while web renders a plain `View`.
13
+ - **`UIProvider` now owns app-wide native keyboard avoidance by default.** Apps
14
+ that mount `KeyboardProvider` above `UIProvider` get root-level keyboard
15
+ avoiding behavior without adding per-screen `KeyboardAvoidingView` wrappers.
16
+ Pass `keyboardAvoiding={false}` to opt out, or `keyboardAvoidingProps` to tune
17
+ the root wrapper. Web skips the root keyboard wrapper unless explicitly
18
+ enabled.
19
+
20
+ ### Fixed
21
+
22
+ - **`DismissKeyboard` no longer nests keyboard-avoiding wrappers when the root
23
+ provider already owns keyboard avoidance.**
24
+
6
25
  ## [0.11.0]
7
26
 
8
27
  ### Added
package/LLM_USAGE.md CHANGED
@@ -177,6 +177,7 @@ Use this table before creating a new app-local primitive.
177
177
  | `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text |
178
178
  | `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays | Confirm delete, edit profile, invite user |
179
179
  | `DismissKeyboard` | Tap-away keyboard dismissal | Screen-level keyboard handling | Forms, search screens, sign-in screens |
180
+ | `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers | Screen roots, composer footers, form-heavy subtrees |
180
181
  | `Drawer` | Side panels and drawer navigation | Custom sliding panels | Filter drawer, app navigation drawer, inspector panel |
181
182
  | `DropdownMenu` | Menus and command lists | Homemade popover menus | Row actions, account menu, sort menu |
182
183
  | `EmptyState` | No-data or recoverable error regions | One-off empty placeholders | Empty inbox, no search results, failed list load |
package/README.md CHANGED
@@ -222,6 +222,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
222
222
  | `ErrorBoundary` | React render error fallback | Unhandled screen crashes | Route-level fallback, feature boundary, recoverable widget crashes |
223
223
  | `Icon` | Feather or custom icons with theme tokens | Raw vector icons with hardcoded colors | Button accessories, empty-state icons, menu icons, status glyphs |
224
224
  | `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
225
+ | `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers | Screen roots, composer footers, form-heavy subtrees |
225
226
  | `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
226
227
  | `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms, auth panels |
227
228
  | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, loading toast, bottom-position alerts |
@@ -272,7 +273,7 @@ Use `Button.preset`, not `variant`. `default` is the neutral primary action, `se
272
273
 
273
274
  Use `StyledText` or its aliases instead of raw `Text` whenever the text is part of app UI. Use `TextInput` for labeled fields because it already owns label, helper text, error text, clear buttons, password visibility, numeric filtering, and left/right elements.
274
275
 
275
- Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `BottomSheet.Content` listens to React Native keyboard events when `avoidKeyboard` is enabled; it defaults to `true` and can be disabled per sheet. Trigger transient feedback with `notify`.
276
+ Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `UIProvider` also wraps app content in the package keyboard-avoiding root by default, so ordinary screens and fixed footers stay above the soft keyboard without repeated app-local wrappers; pass `keyboardAvoiding={false}` to opt out, or use `KeyboardAvoidingView` directly for a subtree with custom behavior. Web skips the root keyboard wrapper unless `keyboardAvoiding` is explicitly enabled. `BottomSheet.Content` listens to React Native keyboard events when `avoidKeyboard` is enabled; it defaults to `true` and can be disabled per sheet. Trigger transient feedback with `notify`.
276
277
 
277
278
  Use `Skeleton` components for loading content with stable dimensions, `EmptyState` for no-data/recoverable errors, `Alert` for blocking confirm/alert dialogs, and `Notification` for transient global feedback.
278
279
 
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Pressable, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, View, } from "react-native";
2
+ import { Pressable, StyleSheet, Platform, ScrollView, View, } from "react-native";
3
3
  import { KeyboardController, useKeyboardState } from "react-native-keyboard-controller";
4
+ import { KeyboardAvoidingView, useKeyboardAvoidance } from "./KeyboardAvoidingView.js";
4
5
  /**
5
6
  * Full-screen tap-catcher that dismisses the keyboard, mounted ONLY while the
6
7
  * keyboard is visible.
@@ -21,18 +22,24 @@ function KeyboardDismissOverlay() {
21
22
  const isVisible = useKeyboardState((state) => state.isVisible);
22
23
  if (!isVisible)
23
24
  return null;
24
- return (_jsx(Pressable, { style: StyleSheet.absoluteFill, onPress: () => KeyboardController.dismiss(), accessibilityLabel: "Dismiss keyboard", accessibilityRole: "button" }));
25
+ return (_jsx(Pressable, { style: [StyleSheet.absoluteFill, styles.overlay], onPressIn: () => KeyboardController.dismiss(), accessibilityLabel: "Dismiss keyboard", accessibilityRole: "button" }));
25
26
  }
26
27
  /**
27
28
  * @returns Wrapper for a view that dismisses the keyboard when tapped outside of a text input
28
29
  */
29
30
  export const DismissKeyboard = ({ children, style, avoidKeyboard = true, scrollable = true }) => {
31
+ const hasKeyboardAvoidance = useKeyboardAvoidance();
30
32
  const content = scrollable ? (_jsx(ScrollView, { style: { flex: 1 }, contentContainerStyle: { flexGrow: 1, justifyContent: "center" }, keyboardShouldPersistTaps: "handled", showsVerticalScrollIndicator: false, children: children })) : (children);
31
33
  // Web has no software keyboard, so the tap-catcher is native-only. The overlay
32
34
  // is never created on web, keeping `useKeyboardState` off that platform.
33
35
  const overlay = Platform.OS !== "web" ? _jsx(KeyboardDismissOverlay, {}) : null;
34
- if (!avoidKeyboard) {
36
+ if (!avoidKeyboard || hasKeyboardAvoidance) {
35
37
  return (_jsxs(View, { style: { flex: 1 }, children: [content, overlay] }));
36
38
  }
37
- return (_jsxs(KeyboardAvoidingView, { style: [{ flex: 1, width: "100%" }, style], behavior: Platform.OS === "ios" ? "padding" : "height", keyboardVerticalOffset: Platform.OS === "ios" ? 0 : 0, children: [content, overlay] }));
39
+ return (_jsxs(KeyboardAvoidingView, { style: [{ flex: 1, width: "100%" }, style], keyboardVerticalOffset: Platform.OS === "ios" ? 0 : 0, children: [content, overlay] }));
38
40
  };
41
+ const styles = StyleSheet.create({
42
+ overlay: {
43
+ zIndex: 999,
44
+ },
45
+ });
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { type StyleProp, type ViewProps, type ViewStyle } from "react-native";
3
+ type KeyboardAvoidingBehavior = "height" | "padding" | "position" | "translate-with-padding";
4
+ export interface KeyboardAvoidingViewProps extends ViewProps {
5
+ children: React.ReactNode;
6
+ style?: StyleProp<ViewStyle>;
7
+ behavior?: KeyboardAvoidingBehavior;
8
+ contentContainerStyle?: ViewProps["style"];
9
+ keyboardVerticalOffset?: number;
10
+ automaticOffset?: boolean;
11
+ }
12
+ export declare function useKeyboardAvoidance(): boolean;
13
+ /**
14
+ * Package-level keyboard avoiding wrapper.
15
+ *
16
+ * Native uses `react-native-keyboard-controller` so screens can avoid the soft
17
+ * keyboard with `automaticOffset`; web renders a plain `View`.
18
+ */
19
+ export declare function KeyboardAvoidingView({ children, style, behavior, automaticOffset, contentContainerStyle, keyboardVerticalOffset, ...props }: KeyboardAvoidingViewProps): React.JSX.Element;
20
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ import { Platform, View, } from "react-native";
4
+ import { KeyboardAvoidingView as NativeKeyboardAvoidingView, } from "react-native-keyboard-controller";
5
+ const KeyboardAvoidanceContext = createContext(false);
6
+ export function useKeyboardAvoidance() {
7
+ return useContext(KeyboardAvoidanceContext);
8
+ }
9
+ /**
10
+ * Package-level keyboard avoiding wrapper.
11
+ *
12
+ * Native uses `react-native-keyboard-controller` so screens can avoid the soft
13
+ * keyboard with `automaticOffset`; web renders a plain `View`.
14
+ */
15
+ export function KeyboardAvoidingView({ children, style, behavior = Platform.OS === "ios" ? "padding" : "height", automaticOffset = true, contentContainerStyle, keyboardVerticalOffset, ...props }) {
16
+ if (Platform.OS === "web") {
17
+ return (_jsx(KeyboardAvoidanceContext.Provider, { value: true, children: _jsx(View, { style: style, ...props, children: children }) }));
18
+ }
19
+ return (_jsx(KeyboardAvoidanceContext.Provider, { value: true, children: _jsx(NativeKeyboardAvoidingView, { style: style, behavior: behavior, automaticOffset: automaticOffset, contentContainerStyle: contentContainerStyle, keyboardVerticalOffset: keyboardVerticalOffset, ...props, children: children }) }));
20
+ }
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { type KeyboardAvoidingViewProps } from "./KeyboardAvoidingView";
2
3
  export interface UIProviderProps {
3
4
  children: React.ReactNode;
4
5
  /**
@@ -19,5 +20,15 @@ export interface UIProviderProps {
19
20
  * @default true
20
21
  */
21
22
  statusBar?: boolean;
23
+ /**
24
+ * Wrap app content in the package keyboard-avoiding root.
25
+ *
26
+ * @default true on native, false on web
27
+ */
28
+ keyboardAvoiding?: boolean;
29
+ /**
30
+ * Props forwarded to the keyboard-avoiding root when enabled.
31
+ */
32
+ keyboardAvoidingProps?: Omit<KeyboardAvoidingViewProps, "children">;
22
33
  }
23
- export declare function UIProvider({ children, notification, portalHost, statusBar, }: UIProviderProps): React.JSX.Element;
34
+ export declare function UIProvider({ children, notification, portalHost, statusBar, keyboardAvoiding, keyboardAvoidingProps, }: UIProviderProps): React.JSX.Element;
@@ -1,7 +1,11 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Platform } from "react-native";
2
3
  import { PortalHost } from "@rn-primitives/portal";
3
4
  import { Notification } from "./Notification.js";
4
5
  import { StatusBar } from "./StatusBar.js";
5
- export function UIProvider({ children, notification = true, portalHost = true, statusBar = true, }) {
6
- return (_jsxs(_Fragment, { children: [children, notification ? _jsx(Notification, {}) : null, portalHost ? _jsx(PortalHost, {}) : null, statusBar ? _jsx(StatusBar, {}) : null] }));
6
+ import { KeyboardAvoidingView, } from "./KeyboardAvoidingView.js";
7
+ export function UIProvider({ children, notification = true, portalHost = true, statusBar = true, keyboardAvoiding = Platform.OS !== "web", keyboardAvoidingProps, }) {
8
+ const { style: keyboardAvoidingStyle, ...restKeyboardAvoidingProps } = keyboardAvoidingProps ?? {};
9
+ const content = keyboardAvoiding ? (_jsx(KeyboardAvoidingView, { style: [{ flex: 1 }, keyboardAvoidingStyle], ...restKeyboardAvoidingProps, children: children })) : (children);
10
+ return (_jsxs(_Fragment, { children: [content, notification ? _jsx(Notification, {}) : null, portalHost ? _jsx(PortalHost, {}) : null, statusBar ? _jsx(StatusBar, {}) : null] }));
7
11
  }
@@ -15,6 +15,7 @@ export * from "./EmptyState";
15
15
  export * from "./ErrorBoundary";
16
16
  export * from "./Icon";
17
17
  export * from "./InputOTP";
18
+ export * from "./KeyboardAvoidingView";
18
19
  export * from "./Label";
19
20
  export * from "./MaxWidthContainer";
20
21
  export * from "./Notification";
@@ -15,6 +15,7 @@ export * from "./EmptyState.js";
15
15
  export * from "./ErrorBoundary.js";
16
16
  export * from "./Icon.js";
17
17
  export * from "./InputOTP.js";
18
+ export * from "./KeyboardAvoidingView.js";
18
19
  export * from "./Label.js";
19
20
  export * from "./MaxWidthContainer.js";
20
21
  export * from "./Notification.js";
package/llms-full.md CHANGED
@@ -31,10 +31,14 @@ app-local component files.
31
31
  Call `useResources()` once near the Expo app root. Mount `UIProvider` once near
32
32
  the root when the app uses package feedback or overlay components.
33
33
 
34
- `UIProvider` owns the package `Notification`, `StatusBar`, and default
35
- `@rn-primitives` portal host. Mount it before using `Dialog`, `AlertDialog`,
36
- `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`,
37
- `Tooltip`, or `notify` / `globalUIStore` notifications.
34
+ `UIProvider` owns the package `Notification`, `StatusBar`, default
35
+ `@rn-primitives` portal host, and native keyboard-avoiding root. Mount it
36
+ before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`,
37
+ `Popover`, `SelectContent`, `Tooltip`, or `notify` / `globalUIStore`
38
+ notifications. Pass `keyboardAvoiding={false}` to opt out of the native root
39
+ keyboard avoidance, or use `KeyboardAvoidingView` directly for a subtree with
40
+ custom behavior. Web skips the root keyboard wrapper unless `keyboardAvoiding`
41
+ is explicitly enabled.
38
42
 
39
43
  On native, `BottomSheet.Content` composes its sheet transform with React Native
40
44
  keyboard event values. Pass `avoidKeyboard={false}` for sheets that should not
@@ -99,6 +103,7 @@ Use this catalog before creating a new app-local primitive.
99
103
  | `ErrorBoundary` | `@mrmeg/expo-ui/components` | React render error fallback | Use for route or feature boundaries. |
100
104
  | `Icon` | `@mrmeg/expo-ui/components` | Feather or custom icons with theme tokens | Avoid raw vector icons with hardcoded colors. |
101
105
  | `InputOTP` | `@mrmeg/expo-ui/components` | Verification code entry | Prefer over manually managed text input groups. |
106
+ | `KeyboardAvoidingView` | `@mrmeg/expo-ui/components` | Native keyboard-aware layout roots, composer footers, and form-heavy subtrees | `UIProvider` already mounts one root by default; use this directly only for custom subtrees. |
102
107
  | `Label` | `@mrmeg/expo-ui/components` | Accessible form labels | Use with package form controls. |
103
108
  | `MaxWidthContainer` | `@mrmeg/expo-ui/components` | Centered responsive width | Use for web and tablet constrained layouts. |
104
109
  | `Notification` | `@mrmeg/expo-ui/components` | Global toast surface | Trigger through `notify` (or `globalUIStore` for subscriptions/tests) with root `UIProvider`; optional actions dismiss after press. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrmeg/expo-ui",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
6
6
  "keywords": [