expo-interface 0.2.0 → 0.3.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.
Files changed (58) hide show
  1. package/README.md +87 -10
  2. package/package.json +6 -1
  3. package/src/accent.tsx +21 -8
  4. package/src/button/button.css +6 -0
  5. package/src/button/index.android.tsx +5 -1
  6. package/src/button/index.ios.tsx +5 -1
  7. package/src/button/index.tsx +6 -1
  8. package/src/button/types.ts +13 -0
  9. package/src/color-picker/color-picker.css +70 -0
  10. package/src/color-picker/index.android.tsx +77 -49
  11. package/src/color-picker/index.ios.tsx +58 -11
  12. package/src/color-picker/index.tsx +65 -15
  13. package/src/color-picker/types.ts +9 -3
  14. package/src/context-menu/index.android.tsx +39 -11
  15. package/src/context-menu/index.ios.tsx +3 -1
  16. package/src/context-menu/index.tsx +24 -7
  17. package/src/fab/fab.css +72 -0
  18. package/src/fab/index.android.tsx +72 -0
  19. package/src/fab/index.ios.tsx +68 -0
  20. package/src/fab/index.tsx +37 -0
  21. package/src/fab/shared.ts +23 -0
  22. package/src/fab/types.ts +39 -0
  23. package/src/field-group/index.android.tsx +22 -16
  24. package/src/field-group/index.tsx +36 -5
  25. package/src/field-group/index.web.tsx +25 -9
  26. package/src/field-group/shared.tsx +49 -0
  27. package/src/field-group/types.ts +25 -0
  28. package/src/header-menu/index.tsx +76 -0
  29. package/src/host/index.tsx +32 -0
  30. package/src/index.ts +31 -5
  31. package/src/keyboard/index.tsx +81 -0
  32. package/src/keyboard/library.native.ts +17 -0
  33. package/src/keyboard/library.ts +12 -0
  34. package/src/keyboard/types.ts +20 -0
  35. package/src/list-item/index.android.tsx +23 -6
  36. package/src/list-item/index.tsx +20 -4
  37. package/src/list-item/index.web.tsx +60 -0
  38. package/src/list-item/list-item.css +65 -0
  39. package/src/list-item/types.ts +21 -0
  40. package/src/menu/index.android.tsx +22 -9
  41. package/src/menu/index.ios.tsx +26 -11
  42. package/src/menu/index.tsx +26 -10
  43. package/src/menu/list.tsx +25 -10
  44. package/src/menu/menu.css +52 -0
  45. package/src/menu/types.ts +41 -2
  46. package/src/scheme.ts +140 -0
  47. package/src/screen/index.tsx +49 -5
  48. package/src/tab-stack/index.tsx +15 -2
  49. package/src/tabs/index.tsx +2 -1
  50. package/src/tabs/index.web.tsx +29 -4
  51. package/src/tabs/types.ts +20 -2
  52. package/src/text-field/index.android.tsx +30 -6
  53. package/src/text-field/index.ios.tsx +16 -3
  54. package/src/text-field/index.tsx +24 -4
  55. package/src/text-field/inline.tsx +87 -0
  56. package/src/text-field/shared.ts +40 -1
  57. package/src/text-field/types.ts +47 -2
  58. package/src/theme.ts +44 -5
@@ -0,0 +1,39 @@
1
+ import type {IconToken} from '../icons';
2
+ import type {MenuItem} from '../menu/types';
3
+
4
+ /**
5
+ * Size of a `Fab`. `regular`, `small` and `large` are circles (56, 40 and
6
+ * 96 points); `extended` is a capsule that shows the label beside the icon.
7
+ */
8
+ export type FabSize = 'small' | 'regular' | 'large' | 'extended';
9
+
10
+ /**
11
+ * Floating action button: the screen's primary action, floating over its
12
+ * content at the bottom trailing corner (`Screen`'s `fab` slot places it).
13
+ *
14
+ * Android renders the Material 3 `FloatingActionButton` family; iOS, which
15
+ * has no such control, draws the same geometry in SwiftUI (a circle or
16
+ * capsule filled with the tint, the icon in `onTint`, a soft shadow); web
17
+ * renders a DOM button with that geometry. With `items` the button opens a
18
+ * menu instead of pressing: the same menu `Menu` renders, only the trigger
19
+ * differs.
20
+ */
21
+ export interface FabProps {
22
+ /** Accessibility name of the button, and the text of an `extended` one. */
23
+ label: string;
24
+ /** The icon. */
25
+ icon: IconToken;
26
+ /** Called when the button is pressed (ignored when `items` are given). */
27
+ onPress?: () => void;
28
+ /** With items the button opens a menu instead of pressing. */
29
+ items?: MenuItem[];
30
+ /**
31
+ * The size.
32
+ * @default 'regular'
33
+ */
34
+ size?: FabSize;
35
+ /** Disables interaction and dims the button. */
36
+ disabled?: boolean;
37
+ /** Identifier used to locate the button in end-to-end tests. */
38
+ testID?: string;
39
+ }
@@ -1,12 +1,8 @@
1
1
  import type {ReactNode, ReactElement} from 'react';
2
- import type {
3
- FieldGroupProps,
4
- FieldSectionProps,
5
- FieldSectionHeaderProps,
6
- FieldSectionFooterProps,
7
- } from '@expo/ui';
2
+ import type {FieldSectionHeaderProps, FieldSectionFooterProps} from '@expo/ui';
3
+ import type {FieldGroupProps, FieldGroupSectionProps} from './types';
8
4
  import {Children, Fragment, isValidElement} from 'react';
9
- import {Box, Column, LazyColumn, Text} from '@expo/ui/jetpack-compose';
5
+ import {Box, Column, Text} from '@expo/ui/jetpack-compose';
10
6
  import {
11
7
  background,
12
8
  clip,
@@ -15,6 +11,7 @@ import {
15
11
  padding,
16
12
  Shapes,
17
13
  testID as testIDModifier,
14
+ verticalScroll,
18
15
  type ModifierConfig,
19
16
  } from '@expo/ui/jetpack-compose/modifiers';
20
17
  import {useColor} from '../theme';
@@ -22,6 +19,12 @@ import {useColor} from '../theme';
22
19
  /**
23
20
  * Android `FieldGroup`. Mirrors `@expo/ui`'s Material 3 connected-list
24
21
  * layout, with app deviations for web parity:
22
+ * - the group is a scrolling `Column`, not a `LazyColumn`: a lazy list
23
+ * disposes and recomposes its items as it scrolls and on any state change,
24
+ * and every React Native view hosted inside an item is then added to the
25
+ * view tree a second time while still attached ("The specified child
26
+ * already has a parent"). A settings form has a dozen rows and does not
27
+ * need laziness; composed once, its rows can hold anything the kit offers;
25
28
  * - the group has no background (the universal one paints the Host palette's
26
29
  * `surface`, a grey panel over the app's screen background);
27
30
  * - rows use the `backgroundElement` token instead of the seeded
@@ -33,16 +36,13 @@ import {useColor} from '../theme';
33
36
  */
34
37
  function FieldGroupBase({children, style, hidden, testID}: FieldGroupProps) {
35
38
  if (hidden) return null;
36
- const modifiers: ModifierConfig[] = [];
39
+ const modifiers: ModifierConfig[] = [fillMaxWidth(), verticalScroll(), padding(16, 16, 16, 16)];
37
40
  if (style?.backgroundColor) modifiers.push(background(String(style.backgroundColor)));
38
41
  if (testID) modifiers.push(testIDModifier(testID));
39
42
  return (
40
- <LazyColumn
41
- verticalArrangement={{spacedBy: 24}}
42
- contentPadding={{start: 16, end: 16, top: 16, bottom: 16}}
43
- modifiers={modifiers}>
43
+ <Column verticalArrangement={{spacedBy: 24}} modifiers={modifiers}>
44
44
  {groupChildren(children)}
45
- </LazyColumn>
45
+ </Column>
46
46
  );
47
47
  }
48
48
 
@@ -56,9 +56,10 @@ function SectionFooter(props: FieldSectionFooterProps) {
56
56
  return <>{props.children}</>;
57
57
  }
58
58
 
59
- function Section({children, title, titleUppercase = false, hidden}: FieldSectionProps) {
59
+ function Section({children, title, titleUppercase = false, hidden, footer: footerText, footerColor = 'secondaryLabel'}: FieldGroupSectionProps) {
60
60
  const card = useColor('backgroundElement');
61
61
  const subtle = useColor('secondaryLabel');
62
+ const danger = useColor('destructive');
62
63
  const label = useColor('label');
63
64
  if (hidden) return null;
64
65
 
@@ -70,6 +71,11 @@ function Section({children, title, titleUppercase = false, hidden}: FieldSection
70
71
  {titleUppercase ? title.toUpperCase() : title}
71
72
  </Text>
72
73
  ) : null);
74
+ const footerNode = footer ?? (footerText != null ? (
75
+ <Text color={footerColor === 'destructive' ? danger : subtle} style={{typography: 'bodySmall'}}>
76
+ {footerText}
77
+ </Text>
78
+ ) : null);
73
79
 
74
80
  return (
75
81
  <Column verticalArrangement={{spacedBy: 4}} modifiers={[fillMaxWidth()]}>
@@ -94,7 +100,7 @@ function Section({children, title, titleUppercase = false, hidden}: FieldSection
94
100
  ))}
95
101
  </Column>
96
102
  ) : null}
97
- {footer ? <Column modifiers={[padding(16, 4, 16, 0)]}>{footer}</Column> : null}
103
+ {footerNode ? <Column modifiers={[padding(16, 4, 16, 0)]}>{footerNode}</Column> : null}
98
104
  </Column>
99
105
  );
100
106
  }
@@ -105,7 +111,7 @@ export const FieldGroup = Object.assign(FieldGroupBase, {
105
111
  SectionFooter,
106
112
  });
107
113
 
108
- export type {FieldGroupProps};
114
+ export type {FieldGroupProps, FieldGroupSectionProps};
109
115
 
110
116
  /**
111
117
  * Per-position corner radii producing the Material 3 grouped-list look:
@@ -1,8 +1,39 @@
1
+ import type {FieldGroupProps, FieldGroupSectionProps, FieldSectionFooterColor} from './types';
2
+ import {FieldGroup as Base} from '@expo/ui';
3
+ import {Text} from '@expo/ui/swift-ui';
4
+ import {font, foregroundStyle} from '@expo/ui/swift-ui/modifiers';
5
+ import {useColor} from '../theme';
6
+ import {baseSection, mapSections} from './shared';
7
+
1
8
  /**
2
9
  * App `FieldGroup`: a scrollable container of grouped settings-style rows.
3
- * iOS uses `@expo/ui`'s SwiftUI `Form`. Android (`index.android.tsx`) keeps
4
- * the Material 3 connected-list look with app palette colors. Web
5
- * (`index.web.tsx` + `field-group.css`) re-themes the universal component via
6
- * CSS instead of forking its layout.
10
+ * iOS (this file) uses `@expo/ui`'s SwiftUI `Form`, whose `Section` gains
11
+ * the `footer` prop as the native section footer slot. Android
12
+ * (`index.android.tsx`) keeps the Material 3 connected-list look with app
13
+ * palette colors. Web (`index.web.tsx` + `field-group.css`) re-themes the
14
+ * universal component via CSS instead of forking its layout.
7
15
  */
8
- export {FieldGroup, type FieldGroupProps} from '@expo/ui';
16
+
17
+ /** The section `footer` as SwiftUI text, in the footnote size and the token's color. */
18
+ function FooterText({color, children}: {color: FieldSectionFooterColor; children: string}) {
19
+ const resolved = useColor(color);
20
+ return <Text modifiers={[foregroundStyle({type: 'color', color: resolved}), font({size: 13})]}>{children}</Text>;
21
+ }
22
+
23
+ const renderFooter = (footer: string, color: FieldSectionFooterColor) => <FooterText color={color}>{footer}</FooterText>;
24
+
25
+ function Section(props: FieldGroupSectionProps) {
26
+ return baseSection(props, renderFooter);
27
+ }
28
+
29
+ function FieldGroupBase({children, ...props}: FieldGroupProps) {
30
+ return <Base {...props}>{mapSections(children, Section, renderFooter)}</Base>;
31
+ }
32
+
33
+ export const FieldGroup = Object.assign(FieldGroupBase, {
34
+ Section,
35
+ SectionHeader: Base.SectionHeader,
36
+ SectionFooter: Base.SectionFooter,
37
+ });
38
+
39
+ export type {FieldGroupProps, FieldGroupSectionProps};
@@ -1,24 +1,40 @@
1
1
  import './field-group.css';
2
- import {FieldGroup as BaseFieldGroup, type FieldGroupProps} from '@expo/ui';
2
+ import type {FieldGroupProps, FieldGroupSectionProps, FieldSectionFooterColor} from './types';
3
+ import {FieldGroup as Base} from '@expo/ui';
4
+ import {Footnote} from '../typography';
5
+ import {baseSection, mapSections} from './shared';
6
+
7
+ const renderFooter = (footer: string, color: FieldSectionFooterColor) => <Footnote color={color}>{footer}</Footnote>;
8
+
9
+ /**
10
+ * Web: the universal `FieldGroup.Section` with the `footer` prop as its
11
+ * footer slot, drawn as the kit's `Footnote`.
12
+ */
13
+ function Section(props: FieldGroupSectionProps) {
14
+ return baseSection(props, renderFooter);
15
+ }
3
16
 
4
17
  /**
5
18
  * Web hook for `field-group.css`. Clears the universal component's hardcoded
6
19
  * scroll background so the wrapper supplies the app palette; section cards
7
20
  * and row dividers are recolored to the kit tokens in the CSS.
8
21
  */
9
- function FieldGroup({style, ...props}: FieldGroupProps) {
22
+ function FieldGroupBase({style, children, ...props}: FieldGroupProps) {
10
23
  return (
11
24
  <div className="field-group">
12
- <BaseFieldGroup
25
+ <Base
13
26
  {...props}
14
- style={{...style, backgroundColor: 'transparent'}}
15
- />
27
+ style={{...style, backgroundColor: 'transparent'}}>
28
+ {mapSections(children, Section, renderFooter)}
29
+ </Base>
16
30
  </div>
17
31
  );
18
32
  }
19
33
 
20
- FieldGroup.Section = BaseFieldGroup.Section;
21
- FieldGroup.SectionHeader = BaseFieldGroup.SectionHeader;
22
- FieldGroup.SectionFooter = BaseFieldGroup.SectionFooter;
34
+ export const FieldGroup = Object.assign(FieldGroupBase, {
35
+ Section,
36
+ SectionHeader: Base.SectionHeader,
37
+ SectionFooter: Base.SectionFooter,
38
+ });
23
39
 
24
- export {FieldGroup, type FieldGroupProps};
40
+ export type {FieldGroupProps, FieldGroupSectionProps};
@@ -0,0 +1,49 @@
1
+ import type {ComponentType, ReactElement, ReactNode} from 'react';
2
+ import type {FieldGroupSectionProps, FieldSectionFooterColor} from './types';
3
+ import {Children, Fragment, isValidElement} from 'react';
4
+ import {FieldGroup as Base} from '@expo/ui';
5
+
6
+ /** Renders the `footer` prop's text in the platform's own text control. */
7
+ export type FooterRenderer = (footer: string, color: FieldSectionFooterColor) => ReactNode;
8
+
9
+ /** Whether the section's children already carry a `SectionFooter` slot. */
10
+ export function hasFooterSlot(children: ReactNode): boolean {
11
+ return Children.toArray(children).some(child => isValidElement(child) && child.type === Base.SectionFooter);
12
+ }
13
+
14
+ /**
15
+ * The universal `FieldGroup.Section` with the `footer` prop turned into a
16
+ * `SectionFooter` slot (unless the children carry one already).
17
+ */
18
+ export function baseSection(
19
+ {footer, footerColor = 'secondaryLabel', children, ...props}: FieldGroupSectionProps,
20
+ renderFooter: FooterRenderer,
21
+ ): ReactElement {
22
+ const note = footer != null && !hasFooterSlot(children) ? (
23
+ <Base.SectionFooter>{renderFooter(footer, footerColor)}</Base.SectionFooter>
24
+ ) : null;
25
+ return (
26
+ <Base.Section {...props}>
27
+ {children}
28
+ {note}
29
+ </Base.Section>
30
+ );
31
+ }
32
+
33
+ /**
34
+ * The universal `FieldGroup` groups loose rows into implicit sections by
35
+ * checking each child's type against its own `Section`, so a kit `Section`
36
+ * wrapping it would be grouped as a row. The kit sections are therefore
37
+ * replaced by the universal ones (with the footer slot) before the children
38
+ * reach the group; fragments are walked like the group walks them.
39
+ */
40
+ export function mapSections(children: ReactNode, Section: ComponentType<FieldGroupSectionProps>, renderFooter: FooterRenderer): ReactNode {
41
+ return Children.map(children, child => {
42
+ if (!isValidElement(child)) return child;
43
+ if (child.type === Section) return baseSection(child.props as FieldGroupSectionProps, renderFooter);
44
+ if (child.type === Fragment) {
45
+ return <Fragment>{mapSections((child.props as {children?: ReactNode}).children, Section, renderFooter)}</Fragment>;
46
+ }
47
+ return child;
48
+ });
49
+ }
@@ -0,0 +1,25 @@
1
+ import type {FieldGroupProps, FieldSectionProps} from '@expo/ui';
2
+
3
+ /** Color of a section's `footer` text. */
4
+ export type FieldSectionFooterColor = 'secondaryLabel' | 'destructive';
5
+
6
+ /**
7
+ * Props of `FieldGroup.Section`: `@expo/ui`'s, plus a plain-text footer.
8
+ * SwiftUI's `Section` has a footer, the web draws a caption under the group
9
+ * and Android under the rows, so a note under a section (what a setting
10
+ * does, why it failed) is one prop on every platform.
11
+ */
12
+ export interface FieldGroupSectionProps extends FieldSectionProps {
13
+ /**
14
+ * A note under the rows, in the secondary color. Ignored when a
15
+ * `<FieldGroup.SectionFooter>` child is given.
16
+ */
17
+ footer?: string;
18
+ /**
19
+ * Color of the footer text: `destructive` for an error.
20
+ * @default 'secondaryLabel'
21
+ */
22
+ footerColor?: FieldSectionFooterColor;
23
+ }
24
+
25
+ export type {FieldGroupProps};
@@ -0,0 +1,76 @@
1
+ import type {ButtonTone} from '../button/types';
2
+ import type {IconToken} from '../icons';
3
+ import type {MenuItem} from '../menu/types';
4
+ import {Platform} from 'react-native';
5
+ import {useIsFocused} from 'expo-router';
6
+ import {NativeHost} from '../host';
7
+ import {Menu} from '../menu';
8
+
9
+ export interface HeaderMenuProps {
10
+ /** Trigger text (kept for accessibility when `hideLabel` is set). */
11
+ label: string;
12
+ /** Trigger icon. */
13
+ icon?: IconToken;
14
+ /** Entries shown when the menu opens. */
15
+ items: MenuItem[];
16
+ /** Show only the icon; `label` is kept for accessibility. */
17
+ hideLabel?: boolean;
18
+ /**
19
+ * Color of the trigger: the accent, or the label color like the header's
20
+ * own buttons.
21
+ * @default 'accent'
22
+ */
23
+ tone?: ButtonTone;
24
+ /** Disables the trigger. */
25
+ disabled?: boolean;
26
+ /** Identifier used to locate the trigger in end-to-end tests. */
27
+ testID?: string;
28
+ }
29
+
30
+ /**
31
+ * A `Menu` for a stack header's trailing slot (`TabStack`'s `headerRight`,
32
+ * a `Stack.Screen`'s `headerRight` option): a small text trigger in its own
33
+ * accent-seeded host, so it can live in the React Native header.
34
+ *
35
+ * On Android the native stack re-parents the header's views on a tab switch,
36
+ * and a Compose view refuses a second parent ("The specified child already
37
+ * has a parent"). The host is therefore keyed on the screen's focus, so the
38
+ * Compose view is created afresh each time the header is rebuilt rather
39
+ * than re-added. Needs a navigator above it on Android (it reads the
40
+ * screen's focus); on web it is the plain `Menu` trigger, for a custom
41
+ * header such as `ConstrainedStackHeader`.
42
+ */
43
+ export function HeaderMenu(props: HeaderMenuProps) {
44
+ if (Platform.OS === 'web') return <HeaderMenuTrigger {...props}/>;
45
+ if (Platform.OS === 'android') return <AndroidHeaderMenu {...props}/>;
46
+ return (
47
+ <NativeHost fit>
48
+ <HeaderMenuTrigger {...props}/>
49
+ </NativeHost>
50
+ );
51
+ }
52
+
53
+ function HeaderMenuTrigger({label, icon, items, hideLabel, tone = 'accent', disabled, testID}: HeaderMenuProps) {
54
+ return (
55
+ <Menu
56
+ label={label}
57
+ icon={icon}
58
+ items={items}
59
+ hideLabel={hideLabel}
60
+ tone={tone}
61
+ disabled={disabled}
62
+ variant="text"
63
+ size="small"
64
+ testID={testID}
65
+ />
66
+ );
67
+ }
68
+
69
+ function AndroidHeaderMenu(props: HeaderMenuProps) {
70
+ // Keyed on the screen's focus: a fresh host, and Compose view, per rebuild.
71
+ return (
72
+ <NativeHost key={useIsFocused() ? 'focused' : 'blurred'} fit>
73
+ <HeaderMenuTrigger {...props}/>
74
+ </NativeHost>
75
+ );
76
+ }
@@ -0,0 +1,32 @@
1
+ import type {PropsWithChildren} from 'react';
2
+ import type {StyleProp, ViewStyle} from 'react-native';
3
+ import {Host} from '@expo/ui';
4
+ import {useAccentSeed} from '../accent';
5
+ import {hostAccentProps} from '../screen/host-accent';
6
+
7
+ export interface NativeHostProps extends PropsWithChildren {
8
+ style?: StyleProp<ViewStyle>;
9
+ /**
10
+ * Size the host to its content on both axes (a group of buttons in a row);
11
+ * by default only vertically, the width filling its container.
12
+ * @default false
13
+ */
14
+ fit?: boolean;
15
+ }
16
+
17
+ /**
18
+ * An accent-seeded `@expo/ui` `Host` for controls that sit inside a React
19
+ * Native layout: a toolbar next to a canvas, a search row, a floating
20
+ * button. `Screen native` mounts the same host around a whole screen; this
21
+ * one is for the places a screen cannot be native, sized to its content and
22
+ * seeded like the screen would be (`hostAccentProps`). On web it is a plain
23
+ * view carrying the `@expo/ui` palette.
24
+ */
25
+ export function NativeHost({children, style, fit = false}: NativeHostProps) {
26
+ const seed = useAccentSeed();
27
+ return (
28
+ <Host matchContents={fit ? true : {vertical: true}} style={style} {...hostAccentProps(seed)}>
29
+ {children}
30
+ </Host>
31
+ );
32
+ }
package/src/index.ts CHANGED
@@ -4,22 +4,36 @@ export * from './theme';
4
4
  export * from './accent';
5
5
  export * from './icons';
6
6
  export {fillWidth} from './fill';
7
+ export {
8
+ SCHEME_STORAGE_KEY,
9
+ getColorSchemeMode,
10
+ getThemeBootScript,
11
+ setColorScheme,
12
+ useColorScheme,
13
+ } from './scheme';
14
+ export type {ColorScheme, ColorSchemeMode} from './scheme';
7
15
 
8
16
  // Layout
9
17
  export {Screen} from './screen';
18
+ export type {ScreenProps} from './screen';
10
19
  export {ScreenHeader} from './screen/header';
11
20
  export {hostAccentProps} from './screen/host-accent';
21
+ export {NativeHost} from './host';
22
+ export type {NativeHostProps} from './host';
12
23
  export {Sheet} from './sheet';
13
24
  export {ConstrainedStackHeader} from './stack-header';
14
25
  export {TabStack} from './tab-stack';
26
+ export type {TabStackProps} from './tab-stack';
15
27
  export {Tabs} from './tabs';
16
28
  export type {TabBarProps, TabRoute, WebLogo} from './tabs/types';
29
+ export {KeyboardBar} from './keyboard';
30
+ export type {KeyboardBarProps, KeyboardLibrary, KeyboardState} from './keyboard';
17
31
 
18
32
  // Components
19
33
  export {Alert} from './alert';
20
34
  export type {AlertAction, AlertActionRole, AlertProps} from './alert/types';
21
35
  export {Button} from './button';
22
- export type {ButtonProps, ButtonRole, ButtonShape, ButtonSize, ButtonVariant} from './button/types';
36
+ export type {ButtonProps, ButtonRole, ButtonShape, ButtonSize, ButtonTone, ButtonVariant} from './button/types';
23
37
  export {Checkbox} from './checkbox';
24
38
  export type {CheckboxProps} from './checkbox/types';
25
39
  export {Collapsible} from './collapsible';
@@ -31,13 +45,18 @@ export {DateTimePicker} from './date-time';
31
45
  export type {DateTimeMode, DateTimePickerProps} from './date-time/types';
32
46
  export {Divider} from './divider';
33
47
  export type {DividerProps} from './divider/types';
34
- export {FieldGroup, type FieldGroupProps} from './field-group';
48
+ export {Fab} from './fab';
49
+ export type {FabProps, FabSize} from './fab/types';
50
+ export {FieldGroup} from './field-group';
51
+ export type {FieldGroupProps, FieldGroupSectionProps, FieldSectionFooterColor} from './field-group/types';
35
52
  export {Gauge} from './gauge';
36
53
  export type {GaugeProps, GaugeVariant} from './gauge/types';
54
+ export {HeaderMenu} from './header-menu';
55
+ export type {HeaderMenuProps} from './header-menu';
37
56
  export {ListItem} from './list-item';
38
- export type {ListItemProps} from './list-item/types';
57
+ export type {ListItemAction, ListItemProps} from './list-item/types';
39
58
  export {Menu} from './menu';
40
- export type {ContextMenuProps, MenuItem, MenuProps} from './menu/types';
59
+ export type {ContextMenuProps, MenuItem, MenuPoint, MenuProps, MenuTrigger} from './menu/types';
41
60
  export {Picker} from './picker';
42
61
  export type {PickerItemProps, PickerOption, PickerProps, PickerValue} from './picker/types';
43
62
  export {Progress} from './progress';
@@ -53,7 +72,14 @@ export type {SwitchProps} from './switch/types';
53
72
  export {Tooltip} from './tooltip';
54
73
  export type {TooltipProps} from './tooltip/types';
55
74
  export {TextField} from './text-field';
56
- export type {TextFieldCapitalize, TextFieldKeyboard, TextFieldProps} from './text-field/types';
75
+ export type {
76
+ TextFieldCapitalize,
77
+ TextFieldKeyboard,
78
+ TextFieldProps,
79
+ TextFieldReturnKey,
80
+ TextFieldSubmitBehavior,
81
+ TextFieldVariant,
82
+ } from './text-field/types';
57
83
  export {ExternalLink} from './router/external-link';
58
84
 
59
85
  // Typography
@@ -0,0 +1,81 @@
1
+ import type {PropsWithChildren} from 'react';
2
+ import type {StyleProp, ViewStyle} from 'react-native';
3
+ import type {KeyboardLibrary} from './types';
4
+ import {useCallback, useEffect, useRef, useState} from 'react';
5
+ import {StyleSheet, useWindowDimensions, View} from 'react-native';
6
+ import {useColor} from '../theme';
7
+ import {loadKeyboardController} from './library';
8
+
9
+ /** The keyboard library, when the app has it: loaded once, natively only. */
10
+ const library = loadKeyboardController();
11
+
12
+ export interface KeyboardBarProps extends PropsWithChildren {
13
+ /**
14
+ * The keyboard's height from the screen's bottom edge once it is up, `0`
15
+ * once it is away, for the screen's content to pad or scroll by (it is
16
+ * not resized: the bar rides over it).
17
+ */
18
+ onKeyboard?: (height: number) => void;
19
+ /** Style of the bar (its background is the screen's, opaque). */
20
+ style?: StyleProp<ViewStyle>;
21
+ }
22
+
23
+ /**
24
+ * A bottom bar that sticks to the keyboard: it rides up on it by a
25
+ * transform animated with the keyboard on the UI thread, so the layout never
26
+ * changes (no resize, no lines shaking with the animation), and reports the
27
+ * keyboard's height instead so the content can keep its caret above it.
28
+ * Opaque in `background`, since it rides over the content's bottom.
29
+ *
30
+ * Needs `react-native-keyboard-controller` (an optional peer, whose
31
+ * `KeyboardProvider` the kit's `AccentProvider` mounts natively); without it,
32
+ * and on web, where the browser keeps the page above the keyboard itself,
33
+ * the bar is a plain view.
34
+ */
35
+ export function KeyboardBar({children, onKeyboard, style}: KeyboardBarProps) {
36
+ const background = useColor('background');
37
+ if (!library) {
38
+ return <View style={[{backgroundColor: background}, style]}>{children}</View>;
39
+ }
40
+ return (
41
+ <Sticky library={library} background={background} onKeyboard={onKeyboard} style={style}>
42
+ {children}
43
+ </Sticky>
44
+ );
45
+ }
46
+
47
+ interface StickyProps extends KeyboardBarProps {
48
+ library: KeyboardLibrary;
49
+ background: string;
50
+ }
51
+
52
+ function Sticky({library: {KeyboardStickyView, useKeyboardState}, background, children, onKeyboard, style}: StickyProps) {
53
+ const height = useKeyboardState(s => (s.isVisible ? s.height : 0));
54
+ useEffect(() => {
55
+ onKeyboard?.(height);
56
+ }, [height, onKeyboard]);
57
+ // The keyboard's height is from the window's bottom edge, the bar sits
58
+ // above the navigation inset: the sticky translation is corrected by what
59
+ // lies under the bar, measured while the keyboard is away (a measure while
60
+ // it is up would see the bar moved).
61
+ const window = useWindowDimensions().height;
62
+ const view = useRef<View>(null);
63
+ const [below, setBelow] = useState(0);
64
+ const onLayout = useCallback(() => {
65
+ if (height > 0) return;
66
+ view.current?.measureInWindow((_x, y, _w, h) => setBelow(Math.max(0, window - (y + h))));
67
+ }, [height, window]);
68
+ return (
69
+ <KeyboardStickyView offset={{opened: below}}>
70
+ <View ref={view} onLayout={onLayout} style={[styles.bar, {backgroundColor: background}, style]}>
71
+ {children}
72
+ </View>
73
+ </KeyboardStickyView>
74
+ );
75
+ }
76
+
77
+ const styles = StyleSheet.create({
78
+ bar: {width: '100%'},
79
+ });
80
+
81
+ export type {KeyboardLibrary, KeyboardState} from './types';
@@ -0,0 +1,17 @@
1
+ import type {KeyboardLibrary} from './types';
2
+
3
+ /**
4
+ * iOS and Android: `react-native-keyboard-controller` when the app has it
5
+ * installed (an optional peer), `null` otherwise. The `require` sits in a
6
+ * `try` so Metro treats the dependency as optional and a bundle without the
7
+ * library still builds; `KeyboardBar` and `AccentProvider` then fall back to
8
+ * plain views.
9
+ */
10
+ export function loadKeyboardController(): KeyboardLibrary | null {
11
+ try {
12
+ // eslint-disable-next-line typescript/no-require-imports -- optional peer, resolved only when installed.
13
+ return require('react-native-keyboard-controller') as KeyboardLibrary;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ import type {KeyboardLibrary} from './types';
2
+
3
+ /**
4
+ * Web (and any platform without a native keyboard): nothing is loaded. The
5
+ * browser keeps the page above the keyboard itself, and
6
+ * `react-native-keyboard-controller` must never reach the web bundle: it is
7
+ * built on Reanimated, whose server render (the static export) calls
8
+ * `requestAnimationFrame` and dies. `library.native.ts` loads it natively.
9
+ */
10
+ export function loadKeyboardController(): KeyboardLibrary | null {
11
+ return null;
12
+ }
@@ -0,0 +1,20 @@
1
+ import type {ComponentType, PropsWithChildren} from 'react';
2
+
3
+ /** What `react-native-keyboard-controller` reports about the keyboard. */
4
+ export interface KeyboardState {
5
+ isVisible: boolean;
6
+ /** The keyboard's height from the window's bottom edge. */
7
+ height: number;
8
+ }
9
+
10
+ /**
11
+ * The pieces of `react-native-keyboard-controller` the kit uses. The library
12
+ * is an optional peer: `KeyboardBar` sticks to the keyboard through it when
13
+ * it is installed and is a plain view otherwise (and always on web, where
14
+ * the library's Reanimated cannot render on the server).
15
+ */
16
+ export interface KeyboardLibrary {
17
+ KeyboardProvider: ComponentType<PropsWithChildren>;
18
+ KeyboardStickyView: ComponentType<PropsWithChildren<{offset?: {closed?: number; opened?: number}}>>;
19
+ useKeyboardState: <T>(selector: (state: KeyboardState) => T) => T;
20
+ }