expo-interface 0.1.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/package.json +78 -0
  4. package/src/accent.tsx +62 -0
  5. package/src/button/button.css +118 -0
  6. package/src/button/index.android.tsx +114 -0
  7. package/src/button/index.ios.tsx +78 -0
  8. package/src/button/index.tsx +75 -0
  9. package/src/button/shared.ts +42 -0
  10. package/src/button/types.ts +72 -0
  11. package/src/css.d.ts +2 -0
  12. package/src/date-time/index.android.tsx +108 -0
  13. package/src/date-time/index.ios.tsx +54 -0
  14. package/src/date-time/index.tsx +119 -0
  15. package/src/date-time/shared.ts +160 -0
  16. package/src/date-time/types.ts +41 -0
  17. package/src/field-group/field-group.css +18 -0
  18. package/src/field-group/index.android.tsx +193 -0
  19. package/src/field-group/index.tsx +8 -0
  20. package/src/field-group/index.web.tsx +24 -0
  21. package/src/fill/index.android.ts +4 -0
  22. package/src/fill/index.ios.ts +9 -0
  23. package/src/fill/index.ts +10 -0
  24. package/src/global.css +17 -0
  25. package/src/icons.ts +42 -0
  26. package/src/index.ts +59 -0
  27. package/src/link.ts +36 -0
  28. package/src/list-item/index.android.tsx +52 -0
  29. package/src/list-item/index.tsx +21 -0
  30. package/src/list-item/types.ts +21 -0
  31. package/src/picker/index.android.tsx +80 -0
  32. package/src/picker/index.ios.tsx +48 -0
  33. package/src/picker/index.tsx +145 -0
  34. package/src/picker/shared.ts +70 -0
  35. package/src/picker/types.ts +48 -0
  36. package/src/progress/index.android.tsx +24 -0
  37. package/src/progress/index.ios.tsx +20 -0
  38. package/src/progress/index.tsx +30 -0
  39. package/src/progress/progress.css +27 -0
  40. package/src/progress/types.ts +19 -0
  41. package/src/qr/index.tsx +26 -0
  42. package/src/router/external-link.tsx +26 -0
  43. package/src/screen/header.tsx +65 -0
  44. package/src/screen/host-accent.android.ts +8 -0
  45. package/src/screen/host-accent.ios.ts +9 -0
  46. package/src/screen/host-accent.ts +15 -0
  47. package/src/screen/index.tsx +79 -0
  48. package/src/sheet/index.android.tsx +25 -0
  49. package/src/sheet/index.ios.tsx +20 -0
  50. package/src/sheet/index.tsx +18 -0
  51. package/src/stack-header/index.tsx +3 -0
  52. package/src/stack-header/index.web.tsx +33 -0
  53. package/src/switch/index.android.tsx +60 -0
  54. package/src/switch/index.ios.tsx +33 -0
  55. package/src/switch/index.tsx +62 -0
  56. package/src/switch/types.ts +23 -0
  57. package/src/tab-stack/index.tsx +21 -0
  58. package/src/tabs/index.tsx +34 -0
  59. package/src/tabs/index.web.tsx +114 -0
  60. package/src/tabs/types.ts +64 -0
  61. package/src/text-field/index.android.tsx +123 -0
  62. package/src/text-field/index.ios.tsx +73 -0
  63. package/src/text-field/index.tsx +78 -0
  64. package/src/text-field/shared.ts +83 -0
  65. package/src/text-field/types.ts +64 -0
  66. package/src/theme.ts +528 -0
  67. package/src/typography/index.tsx +83 -0
  68. package/src/typography/index.web.tsx +80 -0
  69. package/src/typography/types.ts +45 -0
@@ -0,0 +1,123 @@
1
+ import type {TextFieldKeyboard, TextFieldProps} from './types';
2
+ import type {TextFieldColors, TextFieldKeyboardType} from '@expo/ui/jetpack-compose';
3
+
4
+ import {TextField as ComposeTextField, Text, useMaterialColors, useNativeState} from '@expo/ui/jetpack-compose';
5
+ import {fillMaxWidth, offset, testID as testIDModifier} from '@expo/ui/jetpack-compose/modifiers';
6
+ import {useColor} from '../theme';
7
+ import {useSyncedState} from './shared';
8
+
9
+ const TRANSPARENT = 'transparent';
10
+
11
+ /**
12
+ * Material 3's filled `TextField` bakes a 16dp horizontal content padding into
13
+ * the field itself (`TextFieldDefaults.contentPaddingWithoutLabel`), which the
14
+ * published `@expo/ui` does not expose (`BasicTextField` is documented for
15
+ * v56 but not shipped yet). Inside a `FieldGroup` row that padding stacks on
16
+ * the row's own 16dp inset, pushing the text 16dp right of sibling rows — so
17
+ * the field is shifted back by the same amount to line up.
18
+ */
19
+ const CONTENT_PADDING = 16;
20
+
21
+ /**
22
+ * Android's Material `TextField` ships with a filled background and a bottom
23
+ * indicator line that clash with the iOS `Form` look. Here those are stripped
24
+ * to transparent so the field reads as a plain borderless row — the placeholder
25
+ * doubles as the label — living natively inside the surrounding
26
+ * `Host`/`FieldGroup`.
27
+ */
28
+ export function TextField({
29
+ placeholder,
30
+ value,
31
+ onChangeText,
32
+ onSubmit,
33
+ disabled,
34
+ secureTextEntry,
35
+ keyboardType,
36
+ autoCapitalize,
37
+ autoCorrect,
38
+ multiline,
39
+ autoFocus,
40
+ maxLength,
41
+ accentColor,
42
+ testID,
43
+ }: TextFieldProps) {
44
+ const colors = useMaterialColors();
45
+ const tint = useColor('tint');
46
+ // Placeholder uses the app palette's tertiaryLabel (like web and iOS's
47
+ // `placeholderText`) instead of Material's onSurfaceVariant, which reads
48
+ // too bright in dark mode next to the other platforms.
49
+ const placeholderColor = useColor('tertiaryLabel');
50
+ const text = useNativeState(value ?? '');
51
+ useSyncedState(text, value);
52
+
53
+ const fieldColors: TextFieldColors = {
54
+ focusedContainerColor: TRANSPARENT,
55
+ unfocusedContainerColor: TRANSPARENT,
56
+ disabledContainerColor: TRANSPARENT,
57
+ errorContainerColor: TRANSPARENT,
58
+ focusedIndicatorColor: TRANSPARENT,
59
+ unfocusedIndicatorColor: TRANSPARENT,
60
+ disabledIndicatorColor: TRANSPARENT,
61
+ focusedTextColor: colors.onSurface,
62
+ unfocusedTextColor: colors.onSurface,
63
+ disabledTextColor: colors.onSurfaceVariant,
64
+ // Live accent seed by default — matches the web cursor (`theme.tint`) and
65
+ // the iOS field tint, even inside sheets whose native host is unseeded.
66
+ cursorColor: accentColor ?? tint,
67
+ };
68
+
69
+ return (
70
+ <ComposeTextField
71
+ value={text}
72
+ onValueChange={onChangeText}
73
+ enabled={disabled !== true}
74
+ singleLine={!multiline}
75
+ autoFocus={autoFocus}
76
+ maxLength={maxLength}
77
+ visualTransformation={secureTextEntry ? 'password' : 'none'}
78
+ keyboardOptions={{
79
+ keyboardType: keyboardTypeFor(keyboardType, secureTextEntry),
80
+ capitalization: autoCapitalize,
81
+ autoCorrectEnabled: autoCorrect,
82
+ imeAction: onSubmit ? 'done' : 'default',
83
+ }}
84
+ keyboardActions={onSubmit ? {onDone: onSubmit} : undefined}
85
+ colors={fieldColors}
86
+ textStyle={{fontSize: 16, color: colors.onSurface}}
87
+ modifiers={[
88
+ fillMaxWidth(),
89
+ offset(-CONTENT_PADDING, 0),
90
+ ...(testID ? [testIDModifier(testID)] : []),
91
+ ]}>
92
+ {placeholder != null ? (
93
+ <ComposeTextField.Placeholder>
94
+ <Text color={placeholderColor}>{placeholder}</Text>
95
+ </ComposeTextField.Placeholder>
96
+ ) : null}
97
+ </ComposeTextField>
98
+ );
99
+ }
100
+
101
+ function keyboardTypeFor(
102
+ type: TextFieldKeyboard | undefined,
103
+ secure: boolean | undefined,
104
+ ): TextFieldKeyboardType {
105
+ if (secure) {
106
+ return type === 'number' ? 'numberPassword' : 'password';
107
+ }
108
+ switch (type) {
109
+ case 'email':
110
+ return 'email';
111
+ case 'number':
112
+ return 'number';
113
+ case 'phone':
114
+ return 'phone';
115
+ case 'decimal':
116
+ return 'decimal';
117
+ case 'url':
118
+ return 'uri';
119
+ case 'default':
120
+ default:
121
+ return 'text';
122
+ }
123
+ }
@@ -0,0 +1,73 @@
1
+ import type {TextFieldCapitalize, TextFieldProps} from './types';
2
+ import type {ViewModifier} from '@expo/ui/swift-ui/modifiers';
3
+
4
+ import {SecureField, TextField as SwiftUITextField, useNativeState} from '@expo/ui/swift-ui';
5
+ import {autocorrectionDisabled, disabled as disabledMod, keyboardType as keyboardTypeMod, onSubmit as onSubmitMod, textInputAutocapitalization, tint} from '@expo/ui/swift-ui/modifiers';
6
+ import {keyboardTypeFor, useSyncedState} from './shared';
7
+
8
+ /**
9
+ * iOS renders the field inline using SwiftUI's `TextField` (or `SecureField`
10
+ * for masked input), which is exactly the borderless `Form` row look the other
11
+ * platforms emulate: a placeholder that doubles as the label and the value
12
+ * filling the row. Drop it straight into a `FieldGroup.Section`.
13
+ */
14
+ export function TextField({
15
+ placeholder,
16
+ value,
17
+ onChangeText,
18
+ onSubmit,
19
+ disabled,
20
+ secureTextEntry,
21
+ keyboardType,
22
+ autoCapitalize,
23
+ autoCorrect,
24
+ multiline,
25
+ autoFocus,
26
+ maxLength,
27
+ accentColor,
28
+ testID,
29
+ }: TextFieldProps) {
30
+ const text = useNativeState(value ?? '');
31
+ useSyncedState(text, value);
32
+
33
+ const modifiers: ViewModifier[] = [];
34
+ if (accentColor) modifiers.push(tint(accentColor));
35
+ if (keyboardType) modifiers.push(keyboardTypeMod(keyboardTypeFor(keyboardType)));
36
+ if (autoCorrect === false) modifiers.push(autocorrectionDisabled(true));
37
+ if (autoCapitalize) modifiers.push(textInputAutocapitalization(autocapitalizationFor(autoCapitalize)));
38
+ if (onSubmit) modifiers.push(onSubmitMod(() => onSubmit(text.value)));
39
+ if (disabled) modifiers.push(disabledMod(true));
40
+
41
+ if (secureTextEntry) {
42
+ return (
43
+ <SecureField
44
+ text={text}
45
+ placeholder={placeholder}
46
+ autoFocus={autoFocus}
47
+ maxLength={maxLength}
48
+ onTextChange={onChangeText}
49
+ modifiers={modifiers}
50
+ testID={testID}
51
+ />
52
+ );
53
+ }
54
+
55
+ return (
56
+ <SwiftUITextField
57
+ text={text}
58
+ placeholder={placeholder}
59
+ autoFocus={autoFocus}
60
+ maxLength={maxLength}
61
+ axis={multiline ? 'vertical' : 'horizontal'}
62
+ onTextChange={onChangeText}
63
+ modifiers={modifiers}
64
+ testID={testID}
65
+ />
66
+ );
67
+ }
68
+
69
+ function autocapitalizationFor(
70
+ value: TextFieldCapitalize,
71
+ ): 'never' | 'words' | 'sentences' | 'characters' {
72
+ return value === 'none' ? 'never' : value;
73
+ }
@@ -0,0 +1,78 @@
1
+ import type {TextFieldProps} from './types';
2
+ import type {TextStyle} from 'react-native';
3
+
4
+ import {StyleSheet, TextInput} from 'react-native';
5
+ import {fonts, fontWeights, theme, variants} from '../theme';
6
+ import {keyboardTypeFor, useTextValue} from './shared';
7
+
8
+ /**
9
+ * On web the field mirrors the native iOS/Android row: a borderless, full-width
10
+ * input whose placeholder doubles as the label, styled with the shared body
11
+ * typography so it sits flush inside a `FieldGroup.Section`. The browser focus
12
+ * outline is suppressed to match the chromeless iOS `Form` look.
13
+ */
14
+ export function TextField({
15
+ placeholder,
16
+ value,
17
+ onChangeText,
18
+ onSubmit,
19
+ disabled,
20
+ secureTextEntry,
21
+ keyboardType,
22
+ autoCapitalize,
23
+ autoCorrect,
24
+ multiline,
25
+ autoFocus,
26
+ maxLength,
27
+ accentColor,
28
+ testID,
29
+ style,
30
+ }: TextFieldProps) {
31
+ const [current, setValue] = useTextValue(value, onChangeText);
32
+ const cursor = accentColor ?? (theme.tint as string);
33
+
34
+ return (
35
+ <TextInput
36
+ value={current}
37
+ onChangeText={setValue}
38
+ placeholder={placeholder}
39
+ placeholderTextColor={theme.tertiaryLabel}
40
+ editable={!disabled}
41
+ secureTextEntry={secureTextEntry}
42
+ keyboardType={keyboardTypeFor(keyboardType)}
43
+ autoCapitalize={autoCapitalize}
44
+ autoCorrect={autoCorrect}
45
+ multiline={multiline}
46
+ autoFocus={autoFocus}
47
+ maxLength={maxLength}
48
+ cursorColor={cursor}
49
+ selectionColor={cursor}
50
+ onSubmitEditing={onSubmit ? event => onSubmit(event.nativeEvent.text) : undefined}
51
+ aria-label={placeholder}
52
+ testID={testID}
53
+ style={[styles.input, disabled && styles.disabled, style]}
54
+ />
55
+ );
56
+ }
57
+
58
+ const styles = StyleSheet.create({
59
+ input: {
60
+ width: '100%',
61
+ margin: 0,
62
+ paddingVertical: 0,
63
+ // `label` (14px) matches the sibling rows in a `FieldGroup.Section`:
64
+ // `@expo/ui`'s universal `Text` renders at react-native-web's 14px
65
+ // default, and rows like `DateTimePicker` use the `label` variant.
66
+ fontSize: variants.label.fontSize,
67
+ lineHeight: variants.label.lineHeight,
68
+ fontFamily: fonts?.sans,
69
+ fontWeight: fontWeights.normal,
70
+ color: theme.label,
71
+ // Web-only: drop the default browser focus ring so the field stays flush
72
+ // with the surrounding card, matching the iOS Form row.
73
+ ...({outlineStyle: 'none'} as unknown as TextStyle),
74
+ },
75
+ disabled: {
76
+ opacity: 0.4,
77
+ },
78
+ });
@@ -0,0 +1,83 @@
1
+ import type {TextFieldKeyboard} from './types';
2
+ import type {ObservableState} from '@expo/ui';
3
+ import {useCallback, useEffect, useState} from 'react';
4
+
5
+ /**
6
+ * Keyboard variants understood by both React Native's `keyboardType` prop and
7
+ * SwiftUI's `keyboardType` modifier — the overlap the web and iOS fields share.
8
+ */
9
+ type AppleKeyboardType =
10
+ | 'default'
11
+ | 'email-address'
12
+ | 'numeric'
13
+ | 'phone-pad'
14
+ | 'decimal-pad'
15
+ | 'url';
16
+
17
+ /**
18
+ * Bridges controlled and uncontrolled usage on web, mirroring `useDateValue`.
19
+ * When `value` is provided the component is controlled; otherwise it falls back
20
+ * to internal state seeded with an empty string.
21
+ * @param value - The current value of the field.
22
+ * @param onChangeText - The function to call when the text changes.
23
+ * @returns The current text and the function to call when the text changes.
24
+ */
25
+ export function useTextValue(
26
+ value: string | undefined,
27
+ onChangeText: ((text: string) => void) | undefined,
28
+ ): [string, (next: string) => void] {
29
+ const [internal, setInternal] = useState(value ?? '');
30
+ const current = value ?? internal;
31
+
32
+ const setValue = useCallback(
33
+ (next: string) => {
34
+ if (value === undefined) {
35
+ setInternal(next);
36
+ }
37
+ onChangeText?.(next);
38
+ },
39
+ [value, onChangeText],
40
+ );
41
+
42
+ return [current, setValue];
43
+ }
44
+
45
+ /**
46
+ * Pushes a controlled `value` prop into a native `useNativeState` observable so
47
+ * parent-driven updates reflect in the field. Uncontrolled fields (no `value`)
48
+ * are left to manage their own state. The native field writes user input back
49
+ * into the same observable, so this only fires for external changes.
50
+ * @param state - The observable bound to the native field's text.
51
+ * @param value - The controlled value, or `undefined` when uncontrolled.
52
+ */
53
+ export function useSyncedState(state: ObservableState<string>, value: string | undefined): void {
54
+ useEffect(() => {
55
+ if (value !== undefined && state.value !== value) {
56
+ state.value = value;
57
+ }
58
+ }, [state, value]);
59
+ }
60
+
61
+ /**
62
+ * Maps the conformed keyboard variant to the React Native / SwiftUI keyboard
63
+ * type, shared by the web and iOS implementations.
64
+ * @param type - The cross-platform keyboard variant.
65
+ * @returns The matching `KeyboardTypeOptions` value.
66
+ */
67
+ export function keyboardTypeFor(type: TextFieldKeyboard | undefined): AppleKeyboardType {
68
+ switch (type) {
69
+ case 'email':
70
+ return 'email-address';
71
+ case 'number':
72
+ return 'numeric';
73
+ case 'phone':
74
+ return 'phone-pad';
75
+ case 'decimal':
76
+ return 'decimal-pad';
77
+ case 'url':
78
+ return 'url';
79
+ case 'default':
80
+ default:
81
+ return 'default';
82
+ }
83
+ }
@@ -0,0 +1,64 @@
1
+ import type {StyleProp, TextStyle} from 'react-native';
2
+
3
+ /** Keyboard variant shown while editing, conformed across platforms. */
4
+ export type TextFieldKeyboard =
5
+ | 'default'
6
+ | 'email'
7
+ | 'number'
8
+ | 'phone'
9
+ | 'decimal'
10
+ | 'url';
11
+
12
+ /** Automatic capitalization behaviour while typing. */
13
+ export type TextFieldCapitalize = 'none' | 'sentences' | 'words' | 'characters';
14
+
15
+ /**
16
+ * Cross-platform single/multi-line text input with a conformed iOS-style
17
+ * appearance — a borderless field whose placeholder doubles as the row label,
18
+ * exactly the SwiftUI `Form` row look the other platforms emulate. Drop it
19
+ * straight into a `FieldGroup.Section` alongside other rows.
20
+ *
21
+ * The control may be used controlled (pass `value` + `onChangeText`) or
22
+ * uncontrolled (omit both and it manages its own state).
23
+ */
24
+ export interface TextFieldProps {
25
+ /** Placeholder shown when the field is empty, doubling as the row's label. */
26
+ placeholder?: string;
27
+ /** Current text (controlled). When omitted the component keeps its own state. */
28
+ value?: string;
29
+ /** Called whenever the text changes. */
30
+ onChangeText?: (text: string) => void;
31
+ /** Called when the user presses the keyboard return key. Receives the text. */
32
+ onSubmit?: (text: string) => void;
33
+ /** Disables editing and dims the field. */
34
+ disabled?: boolean;
35
+ /** Masks the input for sensitive values such as passwords. */
36
+ secureTextEntry?: boolean;
37
+ /**
38
+ * Keyboard variant to display.
39
+ * @default 'default'
40
+ */
41
+ keyboardType?: TextFieldKeyboard;
42
+ /**
43
+ * Automatic capitalization behaviour.
44
+ * @default 'sentences'
45
+ */
46
+ autoCapitalize?: TextFieldCapitalize;
47
+ /**
48
+ * Enables autocorrect / spellcheck suggestions.
49
+ * @default true
50
+ */
51
+ autoCorrect?: boolean;
52
+ /** Allows multiple lines of input that grow vertically. */
53
+ multiline?: boolean;
54
+ /** Focuses the field automatically when mounted. */
55
+ autoFocus?: boolean;
56
+ /** Maximum number of characters allowed. Truncates natively as the user types. */
57
+ maxLength?: number;
58
+ /** Tint applied to the cursor/selection (web/android) and the field (iOS). */
59
+ accentColor?: string;
60
+ /** Identifier used to locate the component in end-to-end tests. */
61
+ testID?: string;
62
+ /** Style applied to the text content (web only). */
63
+ style?: StyleProp<TextStyle>;
64
+ }