@hzblj/kara-components 0.17.0 → 0.18.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 +49 -0
- package/lib/module/atoms/badge/badge.android.js +5 -0
- package/lib/module/atoms/badge/badge.android.js.map +1 -1
- package/lib/module/atoms/badge/badge.ios.js +2 -0
- package/lib/module/atoms/badge/badge.ios.js.map +1 -1
- package/lib/module/atoms/button/button-tokens.js +8 -0
- package/lib/module/atoms/button/button-tokens.js.map +1 -1
- package/lib/module/atoms/button/button.android.js +43 -15
- package/lib/module/atoms/button/button.android.js.map +1 -1
- package/lib/module/atoms/button/button.ios.js +66 -10
- package/lib/module/atoms/button/button.ios.js.map +1 -1
- package/lib/module/atoms/checkbox/checkbox.android.js +5 -1
- package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
- package/lib/module/atoms/checkbox/checkbox.ios.js +3 -1
- package/lib/module/atoms/checkbox/checkbox.ios.js.map +1 -1
- package/lib/module/atoms/color-picker/color-picker.android.js +4 -0
- package/lib/module/atoms/color-picker/color-picker.android.js.map +1 -1
- package/lib/module/atoms/color-picker/color-picker.ios.js +24 -2
- package/lib/module/atoms/color-picker/color-picker.ios.js.map +1 -1
- package/lib/module/atoms/date-picker/date-picker.android.js +85 -10
- package/lib/module/atoms/date-picker/date-picker.android.js.map +1 -1
- package/lib/module/atoms/date-picker/date-picker.ios.js +9 -7
- package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
- package/lib/module/atoms/divider/divider.android.js +17 -4
- package/lib/module/atoms/divider/divider.android.js.map +1 -1
- package/lib/module/atoms/divider/divider.ios.js +4 -0
- package/lib/module/atoms/divider/divider.ios.js.map +1 -1
- package/lib/module/atoms/fab/fab.android.js +64 -8
- package/lib/module/atoms/fab/fab.android.js.map +1 -1
- package/lib/module/atoms/fab/fab.ios.js +68 -12
- package/lib/module/atoms/fab/fab.ios.js.map +1 -1
- package/lib/module/atoms/icon-button/icon-button.android.js +48 -5
- package/lib/module/atoms/icon-button/icon-button.android.js.map +1 -1
- package/lib/module/atoms/icon-button/icon-button.ios.js +60 -10
- package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
- package/lib/module/atoms/list/list.android.js +32 -4
- package/lib/module/atoms/list/list.android.js.map +1 -1
- package/lib/module/atoms/list/list.ios.js +15 -5
- package/lib/module/atoms/list/list.ios.js.map +1 -1
- package/lib/module/atoms/progress/progress.android.js +21 -7
- package/lib/module/atoms/progress/progress.android.js.map +1 -1
- package/lib/module/atoms/progress/progress.ios.js +5 -1
- package/lib/module/atoms/progress/progress.ios.js.map +1 -1
- package/lib/module/atoms/slider/slider.android.js +16 -2
- package/lib/module/atoms/slider/slider.android.js.map +1 -1
- package/lib/module/atoms/slider/slider.ios.js +22 -2
- package/lib/module/atoms/slider/slider.ios.js.map +1 -1
- package/lib/module/atoms/switch/switch.android.js +5 -1
- package/lib/module/atoms/switch/switch.android.js.map +1 -1
- package/lib/module/atoms/switch/switch.ios.js +3 -1
- package/lib/module/atoms/switch/switch.ios.js.map +1 -1
- package/lib/module/atoms/text/spans.js +2 -1
- package/lib/module/atoms/text/spans.js.map +1 -1
- package/lib/module/atoms/text/text.android.js +18 -6
- package/lib/module/atoms/text/text.android.js.map +1 -1
- package/lib/module/atoms/text/text.ios.js +6 -2
- package/lib/module/atoms/text/text.ios.js.map +1 -1
- package/lib/module/atoms/text-field/text-field-input.js +14 -0
- package/lib/module/atoms/text-field/text-field-input.js.map +1 -0
- package/lib/module/atoms/text-field/text-field.android.js +113 -6
- package/lib/module/atoms/text-field/text-field.android.js.map +1 -1
- package/lib/module/atoms/text-field/text-field.ios.js +75 -3
- package/lib/module/atoms/text-field/text-field.ios.js.map +1 -1
- package/lib/module/molecules/collapsible/collapsible.android.js +4 -3
- package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
- package/lib/module/molecules/collapsible/collapsible.ios.js +3 -1
- package/lib/module/molecules/collapsible/collapsible.ios.js.map +1 -1
- package/lib/module/molecules/radio-group/radio-group.android.js +10 -2
- package/lib/module/molecules/radio-group/radio-group.android.js.map +1 -1
- package/lib/module/molecules/radio-group/radio-group.ios.js +6 -2
- package/lib/module/molecules/radio-group/radio-group.ios.js.map +1 -1
- package/lib/module/molecules/search-bar/search-bar.js +9 -0
- package/lib/module/molecules/search-bar/search-bar.js.map +1 -1
- package/lib/module/molecules/stepper/stepper.android.js +25 -13
- package/lib/module/molecules/stepper/stepper.android.js.map +1 -1
- package/lib/module/molecules/stepper/stepper.ios.js +12 -6
- package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
- package/lib/module/organisms/action-sheet/action-sheet.android.js +16 -8
- package/lib/module/organisms/action-sheet/action-sheet.android.js.map +1 -1
- package/lib/module/organisms/action-sheet/action-sheet.ios.js +16 -3
- package/lib/module/organisms/action-sheet/action-sheet.ios.js.map +1 -1
- package/lib/module/organisms/alert/alert.android.js +4 -3
- package/lib/module/organisms/alert/alert.android.js.map +1 -1
- package/lib/module/organisms/alert/alert.ios.js +3 -2
- package/lib/module/organisms/alert/alert.ios.js.map +1 -1
- package/lib/module/organisms/context-menu/context-menu.android.js +3 -1
- package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
- package/lib/module/organisms/context-menu/context-menu.ios.js +1 -0
- package/lib/module/organisms/context-menu/context-menu.ios.js.map +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.android.js +3 -0
- package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.ios.js +3 -0
- package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
- package/lib/module/organisms/menu/menu-parts.js +20 -0
- package/lib/module/organisms/menu/menu-parts.js.map +1 -0
- package/lib/module/organisms/menu/menu.android.js +51 -19
- package/lib/module/organisms/menu/menu.android.js.map +1 -1
- package/lib/module/organisms/menu/menu.ios.js +71 -8
- package/lib/module/organisms/menu/menu.ios.js.map +1 -1
- package/lib/module/organisms/sheet/sheet.js +2 -1
- package/lib/module/organisms/sheet/sheet.js.map +1 -1
- package/lib/typescript/atoms/badge/badge.types.d.ts +2 -0
- package/lib/typescript/atoms/button/button-tokens.d.ts +6 -2
- package/lib/typescript/atoms/button/button.types.d.ts +19 -5
- package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +2 -0
- package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +2 -0
- package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +5 -5
- package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +30 -7
- package/lib/typescript/atoms/divider/divider.ios.d.ts +4 -0
- package/lib/typescript/atoms/divider/divider.types.d.ts +13 -2
- package/lib/typescript/atoms/fab/fab.types.d.ts +21 -4
- package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +23 -4
- package/lib/typescript/atoms/list/list.types.d.ts +15 -1
- package/lib/typescript/atoms/progress/progress.types.d.ts +13 -2
- package/lib/typescript/atoms/slider/slider.ios.d.ts +4 -0
- package/lib/typescript/atoms/slider/slider.types.d.ts +7 -0
- package/lib/typescript/atoms/switch/switch.types.d.ts +2 -0
- package/lib/typescript/atoms/text/spans.d.ts +2 -0
- package/lib/typescript/atoms/text/text.types.d.ts +9 -1
- package/lib/typescript/atoms/text-field/text-field-input.d.ts +7 -0
- package/lib/typescript/atoms/text-field/text-field.types.d.ts +77 -4
- package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +2 -0
- package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +4 -0
- package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +11 -1
- package/lib/typescript/molecules/stepper/stepper.ios.d.ts +2 -1
- package/lib/typescript/molecules/stepper/stepper.types.d.ts +17 -0
- package/lib/typescript/organisms/action-sheet/action-sheet.ios.d.ts +4 -0
- package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +8 -3
- package/lib/typescript/organisms/alert/alert.types.d.ts +8 -3
- package/lib/typescript/organisms/menu/menu-parts.d.ts +14 -0
- package/lib/typescript/organisms/menu/menu.types.d.ts +35 -7
- package/lib/typescript/organisms/sheet/sheet.types.d.ts +7 -1
- package/package.json +4 -4
- package/src/atoms/badge/badge.android.tsx +9 -2
- package/src/atoms/badge/badge.ios.tsx +2 -2
- package/src/atoms/badge/badge.types.ts +2 -0
- package/src/atoms/button/button-tokens.ts +6 -3
- package/src/atoms/button/button.android.tsx +35 -11
- package/src/atoms/button/button.ios.tsx +72 -8
- package/src/atoms/button/button.types.ts +20 -5
- package/src/atoms/checkbox/checkbox.android.tsx +5 -2
- package/src/atoms/checkbox/checkbox.ios.tsx +2 -2
- package/src/atoms/checkbox/checkbox.types.ts +2 -0
- package/src/atoms/color-picker/color-picker.android.tsx +10 -2
- package/src/atoms/color-picker/color-picker.ios.tsx +24 -1
- package/src/atoms/color-picker/color-picker.types.ts +2 -0
- package/src/atoms/date-picker/date-picker.android.tsx +86 -16
- package/src/atoms/date-picker/date-picker.ios.tsx +20 -8
- package/src/atoms/date-picker/date-picker.types.ts +31 -7
- package/src/atoms/divider/divider.android.tsx +14 -3
- package/src/atoms/divider/divider.ios.tsx +4 -0
- package/src/atoms/divider/divider.types.ts +14 -2
- package/src/atoms/fab/fab.android.tsx +61 -11
- package/src/atoms/fab/fab.ios.tsx +48 -13
- package/src/atoms/fab/fab.types.ts +22 -4
- package/src/atoms/icon-button/icon-button.android.tsx +60 -10
- package/src/atoms/icon-button/icon-button.ios.tsx +77 -8
- package/src/atoms/icon-button/icon-button.types.ts +24 -4
- package/src/atoms/list/list.android.tsx +26 -4
- package/src/atoms/list/list.ios.tsx +20 -5
- package/src/atoms/list/list.types.ts +16 -1
- package/src/atoms/progress/progress.android.tsx +32 -6
- package/src/atoms/progress/progress.ios.tsx +11 -5
- package/src/atoms/progress/progress.types.ts +13 -2
- package/src/atoms/slider/slider.android.tsx +18 -2
- package/src/atoms/slider/slider.ios.tsx +38 -2
- package/src/atoms/slider/slider.types.ts +7 -0
- package/src/atoms/switch/switch.android.tsx +5 -2
- package/src/atoms/switch/switch.ios.tsx +2 -2
- package/src/atoms/switch/switch.types.ts +2 -0
- package/src/atoms/text/spans.ts +2 -1
- package/src/atoms/text/text.android.tsx +20 -7
- package/src/atoms/text/text.ios.tsx +7 -0
- package/src/atoms/text/text.types.ts +10 -1
- package/src/atoms/text-field/text-field-input.ts +17 -0
- package/src/atoms/text-field/text-field.android.tsx +132 -9
- package/src/atoms/text-field/text-field.ios.tsx +110 -6
- package/src/atoms/text-field/text-field.types.ts +98 -4
- package/src/molecules/collapsible/collapsible.android.tsx +10 -2
- package/src/molecules/collapsible/collapsible.ios.tsx +8 -2
- package/src/molecules/collapsible/collapsible.types.ts +2 -0
- package/src/molecules/radio-group/radio-group.android.tsx +18 -4
- package/src/molecules/radio-group/radio-group.ios.tsx +19 -5
- package/src/molecules/radio-group/radio-group.types.ts +4 -0
- package/src/molecules/search-bar/search-bar.tsx +9 -0
- package/src/molecules/search-bar/search-bar.types.ts +12 -1
- package/src/molecules/stepper/stepper.android.tsx +24 -4
- package/src/molecules/stepper/stepper.ios.tsx +23 -8
- package/src/molecules/stepper/stepper.types.ts +17 -0
- package/src/organisms/action-sheet/action-sheet.android.tsx +23 -11
- package/src/organisms/action-sheet/action-sheet.ios.tsx +20 -3
- package/src/organisms/action-sheet/action-sheet.types.ts +8 -3
- package/src/organisms/alert/alert.android.tsx +11 -8
- package/src/organisms/alert/alert.ios.tsx +2 -1
- package/src/organisms/alert/alert.types.ts +8 -3
- package/src/organisms/context-menu/context-menu.android.tsx +3 -2
- package/src/organisms/context-menu/context-menu.ios.tsx +1 -0
- package/src/organisms/fab-menu/fab-menu.android.tsx +3 -0
- package/src/organisms/fab-menu/fab-menu.ios.tsx +9 -1
- package/src/organisms/menu/menu-parts.ts +21 -0
- package/src/organisms/menu/menu.android.tsx +39 -10
- package/src/organisms/menu/menu.ios.tsx +55 -5
- package/src/organisms/menu/menu.types.ts +30 -7
- package/src/organisms/sheet/sheet.tsx +2 -1
- package/src/organisms/sheet/sheet.types.ts +7 -1
|
@@ -1,25 +1,140 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
Text as ComposeText,
|
|
4
|
+
type TextFieldRef as ComposeTextFieldRef,
|
|
5
|
+
OutlinedTextField,
|
|
6
|
+
type TextFieldKeyboardActions,
|
|
7
|
+
type TextFieldKeyboardOptions,
|
|
8
|
+
type TextFieldKeyboardType,
|
|
9
|
+
useNativeState,
|
|
10
|
+
} from '@expo/ui/jetpack-compose'
|
|
11
|
+
import {semantics, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
2
12
|
import {useSxModifiers} from '@hzblj/kara-sx'
|
|
3
13
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
14
|
+
import {useImperativeHandle, useMemo, useRef} from 'react'
|
|
4
15
|
import {Glyph} from '../../glyph'
|
|
5
16
|
import {useHostGuard} from '../../host-context'
|
|
6
|
-
import type {TextFieldComponent, TextFieldProps} from './text-field.types'
|
|
17
|
+
import type {TextFieldComponent, TextFieldContentType, TextFieldKeyboard, TextFieldProps} from './text-field.types'
|
|
18
|
+
import {autoCorrectFor, capitalizationFor, ignoreDetachedView} from './text-field-input'
|
|
19
|
+
|
|
20
|
+
const KEYBOARD = {
|
|
21
|
+
decimal: 'decimal',
|
|
22
|
+
default: 'text',
|
|
23
|
+
email: 'email',
|
|
24
|
+
number: 'number',
|
|
25
|
+
phone: 'phone',
|
|
26
|
+
url: 'uri',
|
|
27
|
+
} as const satisfies Record<TextFieldKeyboard, TextFieldKeyboardType>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A `secure` field asks for the password keyboards: they suggest nothing and learn nothing, and they are what Android's
|
|
31
|
+
* autofill takes for a password field.
|
|
32
|
+
*/
|
|
33
|
+
const SECURE_KEYBOARD = {
|
|
34
|
+
...KEYBOARD,
|
|
35
|
+
default: 'password',
|
|
36
|
+
number: 'numberPassword',
|
|
37
|
+
} as const satisfies Record<TextFieldKeyboard, TextFieldKeyboardType>
|
|
38
|
+
|
|
39
|
+
/** Compose's autofill `ContentType`s, by the hint names `@expo/ui`'s `semantics` modifier maps to them. */
|
|
40
|
+
const AUTOFILL_HINT = {
|
|
41
|
+
addressCity: 'postal-address-locality',
|
|
42
|
+
countryName: 'country',
|
|
43
|
+
creditCardNumber: 'cc-number',
|
|
44
|
+
emailAddress: 'email',
|
|
45
|
+
familyName: 'family-name',
|
|
46
|
+
fullStreetAddress: 'street-address',
|
|
47
|
+
givenName: 'given-name',
|
|
48
|
+
name: 'name',
|
|
49
|
+
newPassword: 'new-password',
|
|
50
|
+
oneTimeCode: 'one-time-code',
|
|
51
|
+
password: 'password',
|
|
52
|
+
postalCode: 'postal-code',
|
|
53
|
+
streetAddressLine1: 'address-line1',
|
|
54
|
+
streetAddressLine2: 'address-line2',
|
|
55
|
+
telephoneNumber: 'tel',
|
|
56
|
+
username: 'username',
|
|
57
|
+
} as const satisfies Record<TextFieldContentType, string>
|
|
7
58
|
|
|
8
59
|
export const TextField: TextFieldComponent = ({
|
|
60
|
+
autoCapitalize,
|
|
61
|
+
autoCorrect,
|
|
62
|
+
autoFocus = false,
|
|
63
|
+
contentType,
|
|
9
64
|
defaultValue = '',
|
|
10
65
|
disabled = false,
|
|
11
66
|
error = false,
|
|
67
|
+
keyboard = 'default',
|
|
12
68
|
label,
|
|
69
|
+
maxLength,
|
|
13
70
|
multiline = false,
|
|
14
71
|
onChangeText,
|
|
72
|
+
onFocusChange,
|
|
73
|
+
onSubmit,
|
|
74
|
+
placeholder,
|
|
75
|
+
ref,
|
|
15
76
|
secure = false,
|
|
77
|
+
submitLabel,
|
|
78
|
+
testID,
|
|
16
79
|
}: TextFieldProps) => {
|
|
17
80
|
useHostGuard('TextField')
|
|
18
81
|
const theme = useAppTheme()
|
|
19
82
|
const value = useNativeState(defaultValue)
|
|
20
|
-
const
|
|
83
|
+
const field = useRef<ComposeTextFieldRef>(null)
|
|
84
|
+
const sxModifiers = useSxModifiers({wide: true})
|
|
21
85
|
const invalid = Boolean(error)
|
|
22
86
|
const message = typeof error === 'string' && error ? error : undefined
|
|
87
|
+
const face = {fontFamily: theme.typography.body.fontFamily, fontWeight: theme.typography.body.fontWeight}
|
|
88
|
+
const capitalization = capitalizationFor(keyboard, autoCapitalize) ?? 'none'
|
|
89
|
+
const corrects = !secure && autoCorrectFor(keyboard, autoCorrect)
|
|
90
|
+
const singleLine = !multiline
|
|
91
|
+
const submits = singleLine && Boolean(onSubmit)
|
|
92
|
+
|
|
93
|
+
useImperativeHandle(
|
|
94
|
+
ref,
|
|
95
|
+
() => ({
|
|
96
|
+
blur: () => field.current?.blur().catch(ignoreDetachedView),
|
|
97
|
+
// The field reports no text set from code, so the change is reported here.
|
|
98
|
+
clear: () => {
|
|
99
|
+
field.current?.clear().catch(ignoreDetachedView)
|
|
100
|
+
onChangeText?.('')
|
|
101
|
+
},
|
|
102
|
+
focus: () => field.current?.focus().catch(ignoreDetachedView),
|
|
103
|
+
}),
|
|
104
|
+
[onChangeText]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const modifiers = useMemo(
|
|
108
|
+
() => [
|
|
109
|
+
...sxModifiers,
|
|
110
|
+
...(testID ? [testIdModifier(testID)] : []),
|
|
111
|
+
...(contentType ? [semantics({contentType: AUTOFILL_HINT[contentType]})] : []),
|
|
112
|
+
],
|
|
113
|
+
[contentType, sxModifiers, testID]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A multiline field keeps the default action, a return key that starts a new line. A single-line one asked for
|
|
118
|
+
* `onSubmit` without a `submitLabel` gets `done`: Compose only runs the keyboard action it was given.
|
|
119
|
+
*/
|
|
120
|
+
const keyboardOptions = useMemo<TextFieldKeyboardOptions>(
|
|
121
|
+
() => ({
|
|
122
|
+
autoCorrectEnabled: corrects,
|
|
123
|
+
capitalization,
|
|
124
|
+
imeAction: singleLine ? (submitLabel ?? (submits ? 'done' : 'default')) : 'default',
|
|
125
|
+
keyboardType: (secure ? SECURE_KEYBOARD : KEYBOARD)[keyboard],
|
|
126
|
+
}),
|
|
127
|
+
[capitalization, corrects, keyboard, secure, singleLine, submitLabel, submits]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// Only the action matching `imeAction` ever fires, so each one is the submit.
|
|
131
|
+
const keyboardActions = useMemo<TextFieldKeyboardActions | undefined>(
|
|
132
|
+
() =>
|
|
133
|
+
submits && onSubmit
|
|
134
|
+
? {onDone: onSubmit, onGo: onSubmit, onNext: onSubmit, onSearch: onSubmit, onSend: onSubmit}
|
|
135
|
+
: undefined,
|
|
136
|
+
[onSubmit, submits]
|
|
137
|
+
)
|
|
23
138
|
|
|
24
139
|
return (
|
|
25
140
|
<Column verticalArrangement={{spacedBy: 4}}>
|
|
@@ -32,19 +147,27 @@ export const TextField: TextFieldComponent = ({
|
|
|
32
147
|
</ComposeText>
|
|
33
148
|
) : null}
|
|
34
149
|
<OutlinedTextField
|
|
150
|
+
autoFocus={autoFocus}
|
|
35
151
|
enabled={!disabled}
|
|
36
152
|
isError={invalid}
|
|
153
|
+
keyboardActions={keyboardActions}
|
|
154
|
+
keyboardOptions={keyboardOptions}
|
|
155
|
+
maxLength={maxLength}
|
|
37
156
|
modifiers={modifiers}
|
|
157
|
+
onFocusChanged={onFocusChange}
|
|
38
158
|
onValueChange={onChangeText}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
fontFamily: theme.typography.body.fontFamily,
|
|
43
|
-
fontWeight: theme.typography.body.fontWeight,
|
|
44
|
-
}}
|
|
159
|
+
ref={field}
|
|
160
|
+
singleLine={singleLine}
|
|
161
|
+
textStyle={{color: theme.colorString('content.primary'), ...face}}
|
|
45
162
|
value={value}
|
|
46
163
|
visualTransformation={secure ? 'password' : 'none'}
|
|
47
164
|
>
|
|
165
|
+
{placeholder ? (
|
|
166
|
+
// No colour of its own: Material's placeholder colour comes down to it, dimmed with the field.
|
|
167
|
+
<OutlinedTextField.Placeholder>
|
|
168
|
+
<ComposeText style={face}>{placeholder}</ComposeText>
|
|
169
|
+
</OutlinedTextField.Placeholder>
|
|
170
|
+
) : null}
|
|
48
171
|
{invalid ? (
|
|
49
172
|
<OutlinedTextField.TrailingIcon>
|
|
50
173
|
<Glyph color="content.destructive" name="error" />
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
SecureField,
|
|
3
|
+
TextField as SwiftUITextField,
|
|
4
|
+
type TextFieldRef as SwiftUITextFieldRef,
|
|
5
|
+
useNativeState,
|
|
6
|
+
VStack,
|
|
7
|
+
} from '@expo/ui/swift-ui'
|
|
8
|
+
import {
|
|
9
|
+
autocorrectionDisabled,
|
|
10
|
+
disabled as disabledModifier,
|
|
11
|
+
keyboardType,
|
|
12
|
+
onSubmit as onSubmitModifier,
|
|
13
|
+
submitLabel as submitLabelModifier,
|
|
14
|
+
textContentType,
|
|
15
|
+
textFieldStyle,
|
|
16
|
+
textInputAutocapitalization,
|
|
17
|
+
} from '@expo/ui/swift-ui/modifiers'
|
|
3
18
|
import {useSxModifiers} from '@hzblj/kara-sx'
|
|
4
19
|
import {type ColorToken, spacing} from '@hzblj/kara-theme'
|
|
5
|
-
import {useMemo} from 'react'
|
|
20
|
+
import {useImperativeHandle, useMemo, useRef} from 'react'
|
|
6
21
|
import {useHostGuard} from '../../host-context'
|
|
7
22
|
import {Text} from '../text'
|
|
8
|
-
import type {TextFieldComponent, TextFieldProps} from './text-field.types'
|
|
23
|
+
import type {TextFieldAutoCapitalize, TextFieldComponent, TextFieldKeyboard, TextFieldProps} from './text-field.types'
|
|
24
|
+
import {autoCorrectFor, capitalizationFor, ignoreDetachedView} from './text-field-input'
|
|
9
25
|
|
|
10
26
|
type FieldTone = 'default' | 'disabled' | 'error'
|
|
11
27
|
|
|
@@ -21,6 +37,23 @@ const TONE = {
|
|
|
21
37
|
error: {bg: 'surface.primary', border: 'border.destructive', label: 'content.destructive'},
|
|
22
38
|
} as const satisfies Record<FieldTone, FieldToneColors>
|
|
23
39
|
|
|
40
|
+
/** UIKit's keyboards. `number` is the digits-only pad, which, like `decimal` and `phone`, has no return key. */
|
|
41
|
+
const KEYBOARD = {
|
|
42
|
+
decimal: 'decimal-pad',
|
|
43
|
+
default: 'default',
|
|
44
|
+
email: 'email-address',
|
|
45
|
+
number: 'numeric',
|
|
46
|
+
phone: 'phone-pad',
|
|
47
|
+
url: 'url',
|
|
48
|
+
} as const satisfies Record<TextFieldKeyboard, Parameters<typeof keyboardType>[0]>
|
|
49
|
+
|
|
50
|
+
const CAPITALIZATION = {
|
|
51
|
+
characters: 'characters',
|
|
52
|
+
none: 'never',
|
|
53
|
+
sentences: 'sentences',
|
|
54
|
+
words: 'words',
|
|
55
|
+
} as const satisfies Record<TextFieldAutoCapitalize, Parameters<typeof textInputAutocapitalization>[0]>
|
|
56
|
+
|
|
24
57
|
/**
|
|
25
58
|
* Disabled outranks error — a field nobody can edit should recede rather than shout. `.disabled` only
|
|
26
59
|
* dims the text, so the chrome has to carry the state itself.
|
|
@@ -34,19 +67,50 @@ const toneOf = (disabled: boolean, error: boolean): FieldTone => {
|
|
|
34
67
|
}
|
|
35
68
|
|
|
36
69
|
export const TextField: TextFieldComponent = ({
|
|
70
|
+
autoCapitalize,
|
|
71
|
+
autoCorrect,
|
|
72
|
+
autoFocus = false,
|
|
73
|
+
contentType,
|
|
37
74
|
defaultValue = '',
|
|
38
75
|
disabled = false,
|
|
39
76
|
error = false,
|
|
77
|
+
keyboard = 'default',
|
|
40
78
|
label,
|
|
79
|
+
maxLength,
|
|
41
80
|
multiline = false,
|
|
42
81
|
onChangeText,
|
|
82
|
+
onFocusChange,
|
|
83
|
+
onSubmit,
|
|
84
|
+
placeholder,
|
|
85
|
+
ref,
|
|
43
86
|
secure = false,
|
|
87
|
+
submitLabel,
|
|
44
88
|
testID,
|
|
45
89
|
}: TextFieldProps) => {
|
|
46
90
|
useHostGuard('TextField')
|
|
47
91
|
const text = useNativeState(defaultValue)
|
|
92
|
+
// One handle for either field: `SecureFieldRef` is `TextFieldRef` without the selection.
|
|
93
|
+
const field = useRef<SwiftUITextFieldRef>(null)
|
|
48
94
|
const tone = TONE[toneOf(disabled, Boolean(error))]
|
|
49
95
|
const message = tone === TONE.error && typeof error === 'string' && error ? error : undefined
|
|
96
|
+
const capitalization = capitalizationFor(keyboard, autoCapitalize)
|
|
97
|
+
const corrects = autoCorrectFor(keyboard, autoCorrect)
|
|
98
|
+
// A `SecureField` is one line whatever `multiline` says; a vertical `TextField`'s return key starts a new line.
|
|
99
|
+
const singleLine = secure || !multiline
|
|
100
|
+
|
|
101
|
+
useImperativeHandle(
|
|
102
|
+
ref,
|
|
103
|
+
() => ({
|
|
104
|
+
blur: () => field.current?.blur().catch(ignoreDetachedView),
|
|
105
|
+
// Neither field reports a text set from code, so the change is reported here.
|
|
106
|
+
clear: () => {
|
|
107
|
+
field.current?.clear().catch(ignoreDetachedView)
|
|
108
|
+
onChangeText?.('')
|
|
109
|
+
},
|
|
110
|
+
focus: () => field.current?.focus().catch(ignoreDetachedView),
|
|
111
|
+
}),
|
|
112
|
+
[onChangeText]
|
|
113
|
+
)
|
|
50
114
|
|
|
51
115
|
const sxModifiers = useSxModifiers({
|
|
52
116
|
bg: tone.bg,
|
|
@@ -65,8 +129,33 @@ export const TextField: TextFieldComponent = ({
|
|
|
65
129
|
list.push(disabledModifier(true))
|
|
66
130
|
}
|
|
67
131
|
|
|
132
|
+
if (keyboard !== 'default') {
|
|
133
|
+
list.push(keyboardType(KEYBOARD[keyboard]))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (capitalization) {
|
|
137
|
+
list.push(textInputAutocapitalization(CAPITALIZATION[capitalization]))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!corrects) {
|
|
141
|
+
list.push(autocorrectionDisabled(true))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (contentType) {
|
|
145
|
+
list.push(textContentType(contentType))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (singleLine && submitLabel) {
|
|
149
|
+
list.push(submitLabelModifier(submitLabel))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (singleLine && onSubmit) {
|
|
153
|
+
// `onSubmit` reports no text, so it is read off the field's state, which the typing has already written.
|
|
154
|
+
list.push(onSubmitModifier(() => onSubmit(text.get())))
|
|
155
|
+
}
|
|
156
|
+
|
|
68
157
|
return list
|
|
69
|
-
}, [disabled, sxModifiers])
|
|
158
|
+
}, [capitalization, contentType, corrects, disabled, keyboard, onSubmit, singleLine, submitLabel, sxModifiers, text])
|
|
70
159
|
|
|
71
160
|
return (
|
|
72
161
|
<VStack alignment="leading" spacing={spacing.s4}>
|
|
@@ -76,12 +165,27 @@ export const TextField: TextFieldComponent = ({
|
|
|
76
165
|
</Text>
|
|
77
166
|
) : null}
|
|
78
167
|
{secure ? (
|
|
79
|
-
<SecureField
|
|
168
|
+
<SecureField
|
|
169
|
+
autoFocus={autoFocus}
|
|
170
|
+
maxLength={maxLength}
|
|
171
|
+
modifiers={modifiers}
|
|
172
|
+
onFocusChange={onFocusChange}
|
|
173
|
+
onTextChange={onChangeText}
|
|
174
|
+
placeholder={placeholder}
|
|
175
|
+
ref={field}
|
|
176
|
+
testID={testID}
|
|
177
|
+
text={text}
|
|
178
|
+
/>
|
|
80
179
|
) : (
|
|
81
180
|
<SwiftUITextField
|
|
181
|
+
autoFocus={autoFocus}
|
|
82
182
|
axis={multiline ? 'vertical' : 'horizontal'}
|
|
183
|
+
maxLength={maxLength}
|
|
83
184
|
modifiers={modifiers}
|
|
185
|
+
onFocusChange={onFocusChange}
|
|
84
186
|
onTextChange={onChangeText}
|
|
187
|
+
placeholder={placeholder}
|
|
188
|
+
ref={field}
|
|
85
189
|
testID={testID}
|
|
86
190
|
text={text}
|
|
87
191
|
/>
|
|
@@ -1,10 +1,72 @@
|
|
|
1
|
-
import type {ReactNode} from 'react'
|
|
1
|
+
import type {ReactNode, Ref} from 'react'
|
|
2
|
+
|
|
3
|
+
/** The keyboard a field brings up. */
|
|
4
|
+
export type TextFieldKeyboard = 'decimal' | 'default' | 'email' | 'number' | 'phone' | 'url'
|
|
5
|
+
|
|
6
|
+
/** When the keyboard turns Shift on by itself. */
|
|
7
|
+
export type TextFieldAutoCapitalize = 'characters' | 'none' | 'sentences' | 'words'
|
|
8
|
+
|
|
9
|
+
/** What the return key says. */
|
|
10
|
+
export type TextFieldSubmitLabel = 'done' | 'go' | 'next' | 'search' | 'send'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What a field holds, so the system can offer to fill it in: an account's email or password, a new strong password,
|
|
14
|
+
* the code from a text message, the user's own name, phone number or address.
|
|
15
|
+
*/
|
|
16
|
+
export type TextFieldContentType =
|
|
17
|
+
| 'addressCity'
|
|
18
|
+
| 'countryName'
|
|
19
|
+
| 'creditCardNumber'
|
|
20
|
+
| 'emailAddress'
|
|
21
|
+
| 'familyName'
|
|
22
|
+
| 'fullStreetAddress'
|
|
23
|
+
| 'givenName'
|
|
24
|
+
| 'name'
|
|
25
|
+
| 'newPassword'
|
|
26
|
+
| 'oneTimeCode'
|
|
27
|
+
| 'password'
|
|
28
|
+
| 'postalCode'
|
|
29
|
+
| 'streetAddressLine1'
|
|
30
|
+
| 'streetAddressLine2'
|
|
31
|
+
| 'telephoneNumber'
|
|
32
|
+
| 'username'
|
|
33
|
+
|
|
34
|
+
/** Drives a `TextField` from code, as when `onSubmit` moves on to the next field. */
|
|
35
|
+
export type TextFieldRef = {
|
|
36
|
+
/** Ends editing: the field gives up focus and the keyboard goes down. */
|
|
37
|
+
blur: () => void
|
|
38
|
+
/** Empties the field and calls `onChangeText` with `''`. */
|
|
39
|
+
clear: () => void
|
|
40
|
+
/** Starts editing: the field takes focus and brings up the keyboard. */
|
|
41
|
+
focus: () => void
|
|
42
|
+
}
|
|
2
43
|
|
|
3
44
|
/**
|
|
4
45
|
* An uncontrolled text field with an optional label above it: SwiftUI `TextField` (`SecureField` when `secure`) /
|
|
5
|
-
* Compose `OutlinedTextField`.
|
|
46
|
+
* Compose `OutlinedTextField`. `keyboard`, `autoCapitalize`, `autoCorrect` and `contentType` fit it to the text it
|
|
47
|
+
* takes; `submitLabel`, `onSubmit` and `ref` chain fields so the return key moves on to the next.
|
|
6
48
|
*/
|
|
7
49
|
export type TextFieldProps = {
|
|
50
|
+
/**
|
|
51
|
+
* When the keyboard turns Shift on by itself: never (`none`), at the start of `sentences`, of `words`, or for all
|
|
52
|
+
* `characters`. Unset, it is `none` for the `email` and `url` keyboards, and otherwise each platform's own:
|
|
53
|
+
* `sentences` on iOS, `none` on Android.
|
|
54
|
+
*/
|
|
55
|
+
autoCapitalize?: TextFieldAutoCapitalize
|
|
56
|
+
/**
|
|
57
|
+
* Lets the keyboard correct words as they are typed and suggest the next. On unless the keyboard is `email` or
|
|
58
|
+
* `url`, where a correction would only break an address. A `secure` field never corrects.
|
|
59
|
+
*/
|
|
60
|
+
autoCorrect?: boolean
|
|
61
|
+
/** Takes focus, and brings up the keyboard, as soon as the field appears. Off by default. */
|
|
62
|
+
autoFocus?: boolean
|
|
63
|
+
/**
|
|
64
|
+
* What the field holds, so the system can offer to fill it in: a saved `username` or `password` from the keychain
|
|
65
|
+
* or a password manager, a strong `newPassword` for a sign-up, the `oneTimeCode` from a text message, the user's own
|
|
66
|
+
* `emailAddress`, `name`, `telephoneNumber` or address. iOS's `textContentType`; on Android a Compose autofill
|
|
67
|
+
* hint, which an autofill service (Google's, a password manager) answers.
|
|
68
|
+
*/
|
|
69
|
+
contentType?: TextFieldContentType
|
|
8
70
|
/** The text it starts with. Read once, on mount: the field is uncontrolled. */
|
|
9
71
|
defaultValue?: string
|
|
10
72
|
/** Stops editing and dims the field. On iOS it outranks `error`. */
|
|
@@ -15,15 +77,47 @@ export type TextFieldProps = {
|
|
|
15
77
|
* colour is a pale red in dark mode. `true` marks it without a message.
|
|
16
78
|
*/
|
|
17
79
|
error?: boolean | string
|
|
80
|
+
/**
|
|
81
|
+
* The keyboard it brings up. `default` by default. `email` and `url` add their `@` and `.` keys and turn
|
|
82
|
+
* capitals and corrections off unless `autoCapitalize` or `autoCorrect` say otherwise; `number` is a pad of digits,
|
|
83
|
+
* `decimal` adds the locale's decimal separator, `phone` is a dial pad. With `secure`, Android brings up its password
|
|
84
|
+
* keyboard (a numeric one for `number`).
|
|
85
|
+
*/
|
|
86
|
+
keyboard?: TextFieldKeyboard
|
|
18
87
|
/** Shown above the field, in `labelMedium`. */
|
|
19
88
|
label?: string
|
|
89
|
+
/**
|
|
90
|
+
* The most characters it takes. What is typed past it is cut off as the user types; `defaultValue` is not. No limit
|
|
91
|
+
* by default.
|
|
92
|
+
*/
|
|
93
|
+
maxLength?: number
|
|
20
94
|
/** Wraps the text and grows the field to fit it. iOS ignores it when `secure`. */
|
|
21
95
|
multiline?: boolean
|
|
22
|
-
/** Called with the whole text after every change. */
|
|
96
|
+
/** Called with the whole text after every change, `clear()` included. */
|
|
23
97
|
onChangeText?: (text: string) => void
|
|
98
|
+
/** Called with `true` when the field starts editing and `false` when it stops. */
|
|
99
|
+
onFocusChange?: (focused: boolean) => void
|
|
100
|
+
/**
|
|
101
|
+
* Called with the whole text when the return key is pressed. A `multiline` field never calls it: its return key
|
|
102
|
+
* starts a new line. Neither do iOS's `number`, `decimal` and `phone` pads, which have no return key.
|
|
103
|
+
*/
|
|
104
|
+
onSubmit?: (text: string) => void
|
|
105
|
+
/**
|
|
106
|
+
* Shown in the empty field, in the platform's placeholder colour, until the first character is typed. A hint at
|
|
107
|
+
* the format (`name@example.com`), not a replacement for `label`.
|
|
108
|
+
*/
|
|
109
|
+
placeholder?: string
|
|
110
|
+
/** `focus`, `blur` and `clear`, for moving to the next field from `onSubmit`. */
|
|
111
|
+
ref?: Ref<TextFieldRef>
|
|
24
112
|
/** Masks the text, for a password. */
|
|
25
113
|
secure?: boolean
|
|
26
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* What the return key says. Unset, each platform's own (`return` on iOS). It names the key; what pressing it does
|
|
116
|
+
* is up to `onSubmit`, so a `next` focuses the following field through its `ref`. Android's `next` also moves focus
|
|
117
|
+
* to the next field in the same host by itself. A `multiline` field ignores it: its return key starts a new line.
|
|
118
|
+
*/
|
|
119
|
+
submitLabel?: TextFieldSubmitLabel
|
|
120
|
+
/** Test identifier, forwarded to the native field (not the label above it). */
|
|
27
121
|
testID?: string
|
|
28
122
|
}
|
|
29
123
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
defaultMinSize,
|
|
14
14
|
fillMaxWidth,
|
|
15
15
|
graphicsLayer,
|
|
16
|
+
testID as testIdModifier,
|
|
16
17
|
tween,
|
|
17
18
|
} from '@expo/ui/jetpack-compose/modifiers'
|
|
18
19
|
import {useAccessibilityModifiers} from '@hzblj/kara-sx'
|
|
@@ -44,7 +45,13 @@ const ROW_HEIGHT = 48
|
|
|
44
45
|
* toward it, as a Material disclosure does — it used to jump between two glyphs. The row spans its parent, the chevron
|
|
45
46
|
* at the far end, and is never shorter than a touch target.
|
|
46
47
|
*/
|
|
47
|
-
export const Collapsible: CollapsibleComponent = ({
|
|
48
|
+
export const Collapsible: CollapsibleComponent = ({
|
|
49
|
+
children,
|
|
50
|
+
expanded,
|
|
51
|
+
label,
|
|
52
|
+
onExpandedChange,
|
|
53
|
+
testID,
|
|
54
|
+
}: CollapsibleProps) => {
|
|
48
55
|
useHostGuard('Collapsible')
|
|
49
56
|
const theme = useAppTheme()
|
|
50
57
|
const reduced = useReducedMotion()
|
|
@@ -53,10 +60,11 @@ export const Collapsible: CollapsibleComponent = ({children, expanded, label, on
|
|
|
53
60
|
() => [
|
|
54
61
|
fillMaxWidth(),
|
|
55
62
|
defaultMinSize({minHeight: ROW_HEIGHT}),
|
|
63
|
+
...(testID ? [testIdModifier(testID)] : []),
|
|
56
64
|
clickable(() => onExpandedChange(!expanded)),
|
|
57
65
|
...accessibilityModifiers,
|
|
58
66
|
],
|
|
59
|
-
[accessibilityModifiers, expanded, onExpandedChange]
|
|
67
|
+
[accessibilityModifiers, expanded, onExpandedChange, testID]
|
|
60
68
|
)
|
|
61
69
|
const turn = useMemo(
|
|
62
70
|
() => [graphicsLayer({rotationZ: animated(expanded ? 90 : 0, reduced ? SNAP : TURN)})],
|
|
@@ -3,11 +3,17 @@ import {DisclosureGroup} from '@expo/ui/swift-ui'
|
|
|
3
3
|
import {useHostGuard} from '../../host-context'
|
|
4
4
|
import type {CollapsibleComponent, CollapsibleProps} from './collapsible.types'
|
|
5
5
|
|
|
6
|
-
export const Collapsible: CollapsibleComponent = ({
|
|
6
|
+
export const Collapsible: CollapsibleComponent = ({
|
|
7
|
+
children,
|
|
8
|
+
expanded,
|
|
9
|
+
label,
|
|
10
|
+
onExpandedChange,
|
|
11
|
+
testID,
|
|
12
|
+
}: CollapsibleProps) => {
|
|
7
13
|
useHostGuard('Collapsible')
|
|
8
14
|
|
|
9
15
|
return (
|
|
10
|
-
<DisclosureGroup isExpanded={expanded} label={label} onIsExpandedChange={onExpandedChange}>
|
|
16
|
+
<DisclosureGroup isExpanded={expanded} label={label} onIsExpandedChange={onExpandedChange} testID={testID}>
|
|
11
17
|
{children}
|
|
12
18
|
</DisclosureGroup>
|
|
13
19
|
)
|
|
@@ -13,6 +13,8 @@ export type CollapsibleProps = {
|
|
|
13
13
|
label: string
|
|
14
14
|
/** The user pressed the row; it passes the state the row asks for. */
|
|
15
15
|
onExpandedChange: (expanded: boolean) => void
|
|
16
|
+
/** Test identifier: on SwiftUI's `DisclosureGroup` on iOS, on the pressable row on Android. */
|
|
17
|
+
testID?: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type CollapsibleComponent = (props: CollapsibleProps) => ReactNode
|
|
@@ -5,25 +5,39 @@ import {VStack} from '../../atoms/v-stack'
|
|
|
5
5
|
import {useHostGuard} from '../../host-context'
|
|
6
6
|
import type {RadioGroupComponent, RadioGroupProps} from './radio-group.types'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Material's radio buttons, each in a full-width row that selects it as a whole. A disabled group keeps every
|
|
10
|
+
* button's handler, so Material keeps the touch target an enabled one has; `enabled` is what stops the input.
|
|
11
|
+
*/
|
|
12
|
+
export const RadioGroup: RadioGroupComponent = ({
|
|
13
|
+
disabled = false,
|
|
14
|
+
onValueChange,
|
|
15
|
+
options,
|
|
16
|
+
testID,
|
|
17
|
+
value,
|
|
18
|
+
}: RadioGroupProps) => {
|
|
9
19
|
useHostGuard('RadioGroup')
|
|
10
20
|
|
|
11
21
|
const theme = useAppTheme()
|
|
12
22
|
|
|
13
23
|
return (
|
|
14
|
-
<VStack spacing="s4" wide>
|
|
24
|
+
<VStack spacing="s4" testID={testID} wide>
|
|
15
25
|
{options.map(option => (
|
|
16
26
|
<Row
|
|
17
27
|
key={option.value}
|
|
18
|
-
modifiers={[fillMaxWidth(), clickable(() => onValueChange?.(option.value))]}
|
|
28
|
+
modifiers={disabled ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() => onValueChange?.(option.value))]}
|
|
19
29
|
verticalAlignment="center"
|
|
20
30
|
>
|
|
21
31
|
<RadioButton
|
|
22
32
|
colors={{selectedColor: theme.color('fill.accent.primary')}}
|
|
33
|
+
enabled={!disabled}
|
|
23
34
|
onClick={() => onValueChange?.(option.value)}
|
|
24
35
|
selected={option.value === value}
|
|
25
36
|
/>
|
|
26
|
-
<ComposeText
|
|
37
|
+
<ComposeText
|
|
38
|
+
color={theme.colorString(disabled ? 'content.tertiary' : 'content.primary')}
|
|
39
|
+
style={theme.typography.body}
|
|
40
|
+
>
|
|
27
41
|
{option.label}
|
|
28
42
|
</ComposeText>
|
|
29
43
|
</Row>
|
|
@@ -6,16 +6,30 @@ import {Glyph} from '../../glyph'
|
|
|
6
6
|
import {useHostGuard} from '../../host-context'
|
|
7
7
|
import type {RadioGroupComponent, RadioGroupProps} from './radio-group.types'
|
|
8
8
|
|
|
9
|
-
export const RadioGroup: RadioGroupComponent = ({
|
|
9
|
+
export const RadioGroup: RadioGroupComponent = ({
|
|
10
|
+
disabled = false,
|
|
11
|
+
onValueChange,
|
|
12
|
+
options,
|
|
13
|
+
testID,
|
|
14
|
+
value,
|
|
15
|
+
}: RadioGroupProps) => {
|
|
10
16
|
useHostGuard('RadioGroup')
|
|
11
17
|
|
|
12
18
|
return (
|
|
13
|
-
<VStack spacing="s4" wide>
|
|
19
|
+
<VStack spacing="s4" testID={testID} wide>
|
|
14
20
|
{options.map(option => (
|
|
15
|
-
<HStack
|
|
16
|
-
|
|
21
|
+
<HStack
|
|
22
|
+
align="center"
|
|
23
|
+
key={option.value}
|
|
24
|
+
onPress={disabled ? undefined : () => onValueChange?.(option.value)}
|
|
25
|
+
py="s8"
|
|
26
|
+
wide
|
|
27
|
+
>
|
|
28
|
+
<Text color={disabled ? 'content.tertiary' : 'content.primary'}>{option.label}</Text>
|
|
17
29
|
<Spacer />
|
|
18
|
-
{option.value === value ?
|
|
30
|
+
{option.value === value ? (
|
|
31
|
+
<Glyph color={disabled ? 'content.tertiary' : 'content.accent'} name="check" size="s20" />
|
|
32
|
+
) : null}
|
|
19
33
|
</HStack>
|
|
20
34
|
))}
|
|
21
35
|
</VStack>
|
|
@@ -13,10 +13,14 @@ export type RadioOption = {
|
|
|
13
13
|
* native atom: it renders inside `<Screen>` or `<Island>`.
|
|
14
14
|
*/
|
|
15
15
|
export type RadioGroupProps = {
|
|
16
|
+
/** Stops the user changing the selection, and dims the options. */
|
|
17
|
+
disabled?: boolean
|
|
16
18
|
/** The user pressed an option; it passes that option's `value`. */
|
|
17
19
|
onValueChange?: (value: string) => void
|
|
18
20
|
/** The choices, top to bottom. */
|
|
19
21
|
options: RadioOption[]
|
|
22
|
+
/** Test identifier, on the whole group. Reach an option by its label. */
|
|
23
|
+
testID?: string
|
|
20
24
|
/** The selected option's `value`. Update it from `onValueChange`. */
|
|
21
25
|
value: string
|
|
22
26
|
}
|
|
@@ -7,7 +7,11 @@ import {RnGlyph} from '../../glyph'
|
|
|
7
7
|
import type {SearchBarComponent, SearchBarProps} from './search-bar.types'
|
|
8
8
|
|
|
9
9
|
export const SearchBar: SearchBarComponent = ({
|
|
10
|
+
autoCapitalize = 'none',
|
|
11
|
+
autoCorrect = false,
|
|
12
|
+
autoFocus = false,
|
|
10
13
|
onChangeText,
|
|
14
|
+
onSubmit,
|
|
11
15
|
placeholder = 'Search',
|
|
12
16
|
testID,
|
|
13
17
|
value,
|
|
@@ -35,9 +39,14 @@ export const SearchBar: SearchBarComponent = ({
|
|
|
35
39
|
>
|
|
36
40
|
<RnGlyph color="content.tertiary" name="search" size="s20" />
|
|
37
41
|
<TextInput
|
|
42
|
+
autoCapitalize={autoCapitalize}
|
|
43
|
+
autoCorrect={autoCorrect}
|
|
44
|
+
autoFocus={autoFocus}
|
|
38
45
|
onChangeText={onChangeText}
|
|
46
|
+
onSubmitEditing={onSubmit ? event => onSubmit(event.nativeEvent.text) : undefined}
|
|
39
47
|
placeholder={placeholder}
|
|
40
48
|
placeholderTextColor={theme.color('content.quaternary')}
|
|
49
|
+
returnKeyType="search"
|
|
41
50
|
style={[text, {color: theme.color('content.primary'), flex: 1, padding: 0}]}
|
|
42
51
|
value={value}
|
|
43
52
|
/>
|