expo-interface 0.1.1 → 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.
- package/README.md +119 -20
- package/package.json +9 -9
- package/src/accent.tsx +21 -8
- package/src/alert/alert.css +3 -3
- package/src/alert/index.tsx +1 -2
- package/src/button/button.css +12 -0
- package/src/button/index.android.tsx +14 -3
- package/src/button/index.ios.tsx +13 -4
- package/src/button/index.tsx +8 -1
- package/src/button/types.ts +20 -0
- package/src/checkbox/index.tsx +1 -1
- package/src/color-picker/color-picker.css +136 -0
- package/src/color-picker/index.android.tsx +145 -0
- package/src/color-picker/index.ios.tsx +80 -0
- package/src/color-picker/index.tsx +112 -0
- package/src/color-picker/shared.ts +204 -0
- package/src/color-picker/sheet.tsx +432 -0
- package/src/color-picker/types.ts +40 -0
- package/src/context-menu/index.android.tsx +39 -11
- package/src/context-menu/index.ios.tsx +3 -1
- package/src/context-menu/index.tsx +24 -7
- package/src/fab/fab.css +72 -0
- package/src/fab/index.android.tsx +72 -0
- package/src/fab/index.ios.tsx +68 -0
- package/src/fab/index.tsx +37 -0
- package/src/fab/shared.ts +23 -0
- package/src/fab/types.ts +39 -0
- package/src/field-group/field-group.css +23 -7
- package/src/field-group/index.android.tsx +44 -38
- package/src/field-group/index.tsx +36 -5
- package/src/field-group/index.web.tsx +27 -11
- package/src/field-group/shared.tsx +49 -0
- package/src/field-group/types.ts +25 -0
- package/src/gauge/gauge.css +200 -0
- package/src/gauge/index.android.tsx +212 -0
- package/src/gauge/index.ios.tsx +40 -0
- package/src/gauge/index.tsx +165 -0
- package/src/gauge/shared.ts +79 -0
- package/src/gauge/types.ts +64 -0
- package/src/header-menu/index.tsx +76 -0
- package/src/host/index.tsx +32 -0
- package/src/index.ts +35 -6
- package/src/keyboard/index.tsx +81 -0
- package/src/keyboard/library.native.ts +17 -0
- package/src/keyboard/library.ts +12 -0
- package/src/keyboard/types.ts +20 -0
- package/src/list-item/index.android.tsx +23 -6
- package/src/list-item/index.tsx +20 -4
- package/src/list-item/index.web.tsx +60 -0
- package/src/list-item/list-item.css +65 -0
- package/src/list-item/types.ts +21 -0
- package/src/menu/index.android.tsx +22 -9
- package/src/menu/index.ios.tsx +26 -11
- package/src/menu/index.tsx +26 -10
- package/src/menu/list.tsx +25 -10
- package/src/menu/menu.css +52 -0
- package/src/menu/types.ts +41 -2
- package/src/progress/progress.css +9 -0
- package/src/scheme.ts +140 -0
- package/src/screen/header.tsx +1 -0
- package/src/screen/index.tsx +49 -5
- package/src/segmented/index.tsx +1 -1
- package/src/segmented/segmented.css +6 -7
- package/src/slider/index.tsx +1 -1
- package/src/stepper/index.tsx +1 -1
- package/src/switch/index.tsx +2 -0
- package/src/tab-stack/index.tsx +15 -2
- package/src/tabs/index.tsx +2 -1
- package/src/tabs/index.web.tsx +29 -4
- package/src/tabs/types.ts +20 -2
- package/src/text-field/index.android.tsx +30 -6
- package/src/text-field/index.ios.tsx +16 -3
- package/src/text-field/index.tsx +24 -4
- package/src/text-field/inline.tsx +87 -0
- package/src/text-field/shared.ts +40 -1
- package/src/text-field/types.ts +47 -2
- package/src/theme.ts +44 -5
- package/src/qr/index.tsx +0 -26
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {StyleProp, ViewStyle} from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visual style of the gauge, named after the SwiftUI `GaugeStyle` it renders
|
|
5
|
+
* (the `gaugeStyle` modifier names in `@expo/ui`):
|
|
6
|
+
* - `automatic` — the default in-app style: a capacity bar with the label
|
|
7
|
+
* centered above and the current value centered below.
|
|
8
|
+
* - `linear` — `accessoryLinear`: a bar with a point marker at the current
|
|
9
|
+
* value (no label).
|
|
10
|
+
* - `linearCapacity` — `accessoryLinearCapacity`: a thin capacity bar with
|
|
11
|
+
* the label above and the current value below, aligned to its leading edge.
|
|
12
|
+
* - `circular` — `accessoryCircular`: an open ring with a point marker, the
|
|
13
|
+
* current value in the center and the bounds under the ring.
|
|
14
|
+
* - `circularCapacity` — `accessoryCircularCapacity`: a closed ring filled to
|
|
15
|
+
* the current value with the current value in the center.
|
|
16
|
+
*/
|
|
17
|
+
export type GaugeVariant = 'automatic' | 'linear' | 'linearCapacity' | 'circular' | 'circularCapacity';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cross-platform gauge showing a value within a range.
|
|
21
|
+
*
|
|
22
|
+
* Bridges the SwiftUI `Gauge` (iOS 16+) on iOS; Android (Jetpack Compose) and
|
|
23
|
+
* web (DOM/SVG) redraw each SwiftUI style with the same geometry: the
|
|
24
|
+
* bounds and current value labels take the accent, the descriptive label
|
|
25
|
+
* keeps the label color, exactly as a tinted SwiftUI gauge does.
|
|
26
|
+
*/
|
|
27
|
+
export interface GaugeProps {
|
|
28
|
+
/** Current value, between `min` and `max`. */
|
|
29
|
+
value: number;
|
|
30
|
+
/**
|
|
31
|
+
* Lower bound of the range.
|
|
32
|
+
* @default 0
|
|
33
|
+
*/
|
|
34
|
+
min?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Upper bound of the range.
|
|
37
|
+
* @default 1
|
|
38
|
+
*/
|
|
39
|
+
max?: number;
|
|
40
|
+
/**
|
|
41
|
+
* SwiftUI gauge style to render.
|
|
42
|
+
* @default 'automatic'
|
|
43
|
+
*/
|
|
44
|
+
variant?: GaugeVariant;
|
|
45
|
+
/**
|
|
46
|
+
* Text describing the gauge's purpose. Shown above the bar in the linear
|
|
47
|
+
* capacity styles and in the center of the circular styles when there is
|
|
48
|
+
* no `currentValueLabel`; the `linear` style only exposes it to assistive
|
|
49
|
+
* technology.
|
|
50
|
+
*/
|
|
51
|
+
label?: string;
|
|
52
|
+
/** Text showing the current value. */
|
|
53
|
+
currentValueLabel?: string;
|
|
54
|
+
/** Text showing the lower bound. */
|
|
55
|
+
minimumValueLabel?: string;
|
|
56
|
+
/** Text showing the upper bound. */
|
|
57
|
+
maximumValueLabel?: string;
|
|
58
|
+
/** Tint of the indicator and the value labels (overrides the theme accent). */
|
|
59
|
+
accentColor?: string;
|
|
60
|
+
/** Identifier used to locate the component in end-to-end tests. */
|
|
61
|
+
testID?: string;
|
|
62
|
+
/** Style applied to the container (web only). */
|
|
63
|
+
style?: StyleProp<ViewStyle>;
|
|
64
|
+
}
|
|
@@ -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,41 +4,63 @@ 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';
|
|
26
40
|
export type {CollapsibleProps} from './collapsible/types';
|
|
41
|
+
export {ColorPicker} from './color-picker';
|
|
42
|
+
export type {ColorPickerProps} from './color-picker/types';
|
|
27
43
|
export {ContextMenu} from './context-menu';
|
|
28
44
|
export {DateTimePicker} from './date-time';
|
|
29
45
|
export type {DateTimeMode, DateTimePickerProps} from './date-time/types';
|
|
30
46
|
export {Divider} from './divider';
|
|
31
47
|
export type {DividerProps} from './divider/types';
|
|
32
|
-
export {
|
|
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';
|
|
52
|
+
export {Gauge} from './gauge';
|
|
53
|
+
export type {GaugeProps, GaugeVariant} from './gauge/types';
|
|
54
|
+
export {HeaderMenu} from './header-menu';
|
|
55
|
+
export type {HeaderMenuProps} from './header-menu';
|
|
33
56
|
export {ListItem} from './list-item';
|
|
34
|
-
export type {ListItemProps} from './list-item/types';
|
|
57
|
+
export type {ListItemAction, ListItemProps} from './list-item/types';
|
|
35
58
|
export {Menu} from './menu';
|
|
36
|
-
export type {ContextMenuProps, MenuItem, MenuProps} from './menu/types';
|
|
59
|
+
export type {ContextMenuProps, MenuItem, MenuPoint, MenuProps, MenuTrigger} from './menu/types';
|
|
37
60
|
export {Picker} from './picker';
|
|
38
61
|
export type {PickerItemProps, PickerOption, PickerProps, PickerValue} from './picker/types';
|
|
39
62
|
export {Progress} from './progress';
|
|
40
63
|
export type {ProgressProps, ProgressVariant} from './progress/types';
|
|
41
|
-
export {QRCode, type QRCodeProps} from './qr';
|
|
42
64
|
export {SegmentedControl} from './segmented';
|
|
43
65
|
export type {SegmentedControlProps} from './segmented/types';
|
|
44
66
|
export {Slider} from './slider';
|
|
@@ -50,7 +72,14 @@ export type {SwitchProps} from './switch/types';
|
|
|
50
72
|
export {Tooltip} from './tooltip';
|
|
51
73
|
export type {TooltipProps} from './tooltip/types';
|
|
52
74
|
export {TextField} from './text-field';
|
|
53
|
-
export type {
|
|
75
|
+
export type {
|
|
76
|
+
TextFieldCapitalize,
|
|
77
|
+
TextFieldKeyboard,
|
|
78
|
+
TextFieldProps,
|
|
79
|
+
TextFieldReturnKey,
|
|
80
|
+
TextFieldSubmitBehavior,
|
|
81
|
+
TextFieldVariant,
|
|
82
|
+
} from './text-field/types';
|
|
54
83
|
export {ExternalLink} from './router/external-link';
|
|
55
84
|
|
|
56
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
|
+
}
|
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
import type {ReactNode} from 'react';
|
|
2
2
|
import type {ListItemProps} from './types';
|
|
3
|
-
import {ListItem as ComposeListItem, Text} from '@expo/ui/jetpack-compose';
|
|
4
|
-
import {clickable, testID as testIDModifier} from '@expo/ui/jetpack-compose/modifiers';
|
|
3
|
+
import {ListItem as ComposeListItem, Row, Text, TextButton} from '@expo/ui/jetpack-compose';
|
|
4
|
+
import {clickable, testID as testIDModifier, wrapContentHeight, wrapContentWidth} from '@expo/ui/jetpack-compose/modifiers';
|
|
5
5
|
import {useColor} from '../theme';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Android uses the Material 3 Compose `ListItem` directly so the container
|
|
9
9
|
* can be made transparent — the M3 default paints the Host palette's
|
|
10
10
|
* `surface`, which reads as a grey panel over the app's screen background
|
|
11
|
-
* (web/iOS rows are transparent).
|
|
11
|
+
* (web/iOS rows are transparent). An `action` is a `TextButton` in the
|
|
12
|
+
* trailing slot, after any `trailing` content.
|
|
12
13
|
*/
|
|
13
|
-
export function ListItem({children, leading, trailing, supporting, onPress, testID}: ListItemProps) {
|
|
14
|
+
export function ListItem({children, leading, trailing, action, supporting, onPress, testID}: ListItemProps) {
|
|
14
15
|
const label = useColor('label');
|
|
15
16
|
const subtle = useColor('secondaryLabel');
|
|
17
|
+
const tint = useColor('tint');
|
|
18
|
+
const destructive = useColor('destructive');
|
|
19
|
+
const muted = useColor('tertiaryLabel');
|
|
16
20
|
const modifiers = [
|
|
17
21
|
...(onPress ? [clickable(onPress)] : []),
|
|
18
22
|
...(testID ? [testIDModifier(testID)] : []),
|
|
19
23
|
];
|
|
24
|
+
const actionColor = action?.role === 'destructive' ? destructive : tint;
|
|
25
|
+
const trailingContent = action ? (
|
|
26
|
+
<Row verticalAlignment="center" horizontalArrangement={{spacedBy: 8}}>
|
|
27
|
+
{trailing}
|
|
28
|
+
<TextButton
|
|
29
|
+
onClick={action.disabled ? undefined : action.onPress}
|
|
30
|
+
enabled={!action.disabled}
|
|
31
|
+
colors={{contentColor: actionColor}}
|
|
32
|
+
modifiers={[wrapContentWidth('end'), wrapContentHeight('centerVertically')]}>
|
|
33
|
+
<Text color={action.disabled ? muted : actionColor}>{action.label}</Text>
|
|
34
|
+
</TextButton>
|
|
35
|
+
</Row>
|
|
36
|
+
) : trailing;
|
|
20
37
|
return (
|
|
21
38
|
<ComposeListItem colors={{containerColor: '#00000000'}} modifiers={modifiers}>
|
|
22
39
|
<ComposeListItem.HeadlineContent>
|
|
@@ -34,8 +51,8 @@ export function ListItem({children, leading, trailing, supporting, onPress, test
|
|
|
34
51
|
)}
|
|
35
52
|
</ComposeListItem.SupportingContent>
|
|
36
53
|
) : null}
|
|
37
|
-
{
|
|
38
|
-
<ComposeListItem.TrailingContent>{
|
|
54
|
+
{trailingContent != null ? (
|
|
55
|
+
<ComposeListItem.TrailingContent>{trailingContent}</ComposeListItem.TrailingContent>
|
|
39
56
|
) : null}
|
|
40
57
|
</ComposeListItem>
|
|
41
58
|
);
|
package/src/list-item/index.tsx
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import type {ListItemProps} from './types';
|
|
2
2
|
import {ListItem as UIListItem} from '@expo/ui';
|
|
3
|
+
import {Button} from '../button';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* iOS renders the universal `@expo/ui` `ListItem` (a SwiftUI list row)
|
|
7
|
+
* through explicit slot props. An `action` is the kit's text `Button` at the
|
|
8
|
+
* end of the trailing slot, after any `trailing` content; the row itself
|
|
9
|
+
* only responds when it also has an `onPress`.
|
|
7
10
|
*/
|
|
8
|
-
export function ListItem({children, leading, trailing, supporting, onPress, testID}: ListItemProps) {
|
|
11
|
+
export function ListItem({children, leading, trailing, action, supporting, onPress, testID}: ListItemProps) {
|
|
12
|
+
const trailingContent = action ? (
|
|
13
|
+
<>
|
|
14
|
+
{trailing}
|
|
15
|
+
<Button
|
|
16
|
+
label={action.label}
|
|
17
|
+
variant="text"
|
|
18
|
+
size="small"
|
|
19
|
+
role={action.role === 'destructive' ? 'destructive' : 'default'}
|
|
20
|
+
disabled={action.disabled}
|
|
21
|
+
onPress={action.onPress}
|
|
22
|
+
/>
|
|
23
|
+
</>
|
|
24
|
+
) : trailing;
|
|
9
25
|
return (
|
|
10
26
|
<UIListItem
|
|
11
27
|
onPress={onPress}
|
|
12
28
|
leading={leading}
|
|
13
|
-
trailing={
|
|
29
|
+
trailing={trailingContent}
|
|
14
30
|
supportingText={supporting}
|
|
15
31
|
testID={testID}>
|
|
16
32
|
{children}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import './list-item.css';
|
|
2
|
+
import type {ListItemProps} from './types';
|
|
3
|
+
import {ListItem as UIListItem} from '@expo/ui';
|
|
4
|
+
import {Button} from '../button';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Web renders the universal `@expo/ui` `ListItem` (a React Native row)
|
|
8
|
+
* through explicit slot props. With an `action` the row is drawn here
|
|
9
|
+
* instead, so the action's `<button>` sits beside the row's own (a `<button>`
|
|
10
|
+
* when the row has an `onPress`, a `<div>` otherwise) rather than inside it:
|
|
11
|
+
* nested buttons are not valid HTML, and a click on the action would also
|
|
12
|
+
* press the row.
|
|
13
|
+
*/
|
|
14
|
+
export function ListItem({children, leading, trailing, action, supporting, onPress, testID}: ListItemProps) {
|
|
15
|
+
if (!action) {
|
|
16
|
+
return (
|
|
17
|
+
<UIListItem
|
|
18
|
+
onPress={onPress}
|
|
19
|
+
leading={leading}
|
|
20
|
+
trailing={trailing}
|
|
21
|
+
supportingText={supporting}
|
|
22
|
+
testID={testID}>
|
|
23
|
+
{children}
|
|
24
|
+
</UIListItem>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const content = (
|
|
28
|
+
<>
|
|
29
|
+
{leading != null ? <span className="ui-list-item__slot">{leading}</span> : null}
|
|
30
|
+
<span className="ui-list-item__main">
|
|
31
|
+
<span className="ui-list-item__headline">{children}</span>
|
|
32
|
+
{supporting != null ? (
|
|
33
|
+
typeof supporting === 'string' || typeof supporting === 'number'
|
|
34
|
+
? <span className="ui-list-item__supporting">{supporting}</span>
|
|
35
|
+
: supporting
|
|
36
|
+
) : null}
|
|
37
|
+
</span>
|
|
38
|
+
{trailing != null ? <span className="ui-list-item__slot">{trailing}</span> : null}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
return (
|
|
42
|
+
<div className="ui-list-item" data-testid={testID}>
|
|
43
|
+
{onPress ? (
|
|
44
|
+
<button type="button" className="ui-list-item__row ui-list-item__row--pressable" onClick={onPress}>{content}</button>
|
|
45
|
+
) : (
|
|
46
|
+
<div className="ui-list-item__row">{content}</div>
|
|
47
|
+
)}
|
|
48
|
+
<Button
|
|
49
|
+
label={action.label}
|
|
50
|
+
variant="text"
|
|
51
|
+
size="small"
|
|
52
|
+
role={action.role === 'destructive' ? 'destructive' : 'default'}
|
|
53
|
+
disabled={action.disabled}
|
|
54
|
+
onPress={action.onPress}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type {ListItemProps} from './types';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The web row with a trailing action: the universal row's geometry (12px by
|
|
3
|
+
* 16px padding, a 12px gap between the slots), the row's own control beside
|
|
4
|
+
* the action's `<button>` rather than around it.
|
|
5
|
+
*/
|
|
6
|
+
.ui-list-item {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: row;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 12px;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
width: 100%;
|
|
13
|
+
padding: 12px 16px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.ui-list-item__row {
|
|
17
|
+
-webkit-appearance: none;
|
|
18
|
+
appearance: none;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 12px;
|
|
23
|
+
flex: 1;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
border: none;
|
|
28
|
+
background: transparent;
|
|
29
|
+
color: var(--color-label);
|
|
30
|
+
font: inherit;
|
|
31
|
+
text-align: start;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ui-list-item__row--pressable {
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ui-list-item__main {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: 2px;
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ui-list-item__headline {
|
|
47
|
+
font-family: var(--font-display);
|
|
48
|
+
font-size: 14px;
|
|
49
|
+
line-height: 18px;
|
|
50
|
+
color: var(--color-label);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-list-item__supporting {
|
|
54
|
+
font-family: var(--font-display);
|
|
55
|
+
font-size: 13px;
|
|
56
|
+
line-height: 18px;
|
|
57
|
+
color: var(--color-secondary-label);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.ui-list-item__slot {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: row;
|
|
63
|
+
align-items: center;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
}
|
package/src/list-item/types.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type {ReactNode} from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A text action at the trailing edge of a `ListItem` (sign in, sign out,
|
|
5
|
+
* clear cache): a Compose `TextButton`, a borderless SwiftUI `Button`, a DOM
|
|
6
|
+
* button. The row itself stays inert unless it also has an `onPress`.
|
|
7
|
+
*/
|
|
8
|
+
export interface ListItemAction {
|
|
9
|
+
/** The action's text. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Called when the action is pressed. */
|
|
12
|
+
onPress: () => void;
|
|
13
|
+
/** Greys the action out and ignores presses. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* `destructive` renders the action in the danger color.
|
|
17
|
+
* @default 'default'
|
|
18
|
+
*/
|
|
19
|
+
role?: 'default' | 'destructive';
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
/**
|
|
4
23
|
* Props for the app `ListItem`: a tappable row with leading/trailing slots
|
|
5
24
|
* and optional supporting text. Bridges the universal `@expo/ui` `ListItem`
|
|
@@ -12,6 +31,8 @@ export interface ListItemProps {
|
|
|
12
31
|
leading?: ReactNode;
|
|
13
32
|
/** Trailing (end) slot — chevron, value, control, etc. */
|
|
14
33
|
trailing?: ReactNode;
|
|
34
|
+
/** A text action rendered natively at the trailing edge, after `trailing`. */
|
|
35
|
+
action?: ListItemAction;
|
|
15
36
|
/** Secondary content below the headline; strings get subtle styling. */
|
|
16
37
|
supporting?: string | ReactNode;
|
|
17
38
|
/** Tap handler, active over the entire row. */
|