@hzblj/kara-components 0.16.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 +62 -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/picker/picker.ios.js +27 -15
- package/lib/module/atoms/picker/picker.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/picker/picker.ios.d.ts +5 -3
- package/lib/typescript/atoms/picker/picker.types.d.ts +11 -3
- 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/picker/picker.ios.tsx +46 -16
- package/src/atoms/picker/picker.types.ts +12 -3
- 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,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
|
/>
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
|
|
3
|
+
import type {TextFieldAutoCapitalize} from '../../atoms/text-field'
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* A search field drawn the same on both platforms: a rounded box with a magnifier, a `TextInput` and a clear button
|
|
5
|
-
* while there is text. The
|
|
7
|
+
* while there is text. The keyboard's return key reads Search, and a query is taken as typed: no capitals and no
|
|
8
|
+
* corrections unless asked for. The React Native tier: it needs no host.
|
|
6
9
|
*/
|
|
7
10
|
export type SearchBarProps = {
|
|
11
|
+
/** When the keyboard turns Shift on by itself. `none` by default: a query is not a sentence. */
|
|
12
|
+
autoCapitalize?: TextFieldAutoCapitalize
|
|
13
|
+
/** Lets the keyboard correct words as they are typed. Off by default, so a query is searched as typed. */
|
|
14
|
+
autoCorrect?: boolean
|
|
15
|
+
/** Takes focus, and brings up the keyboard, as soon as the field appears. Off by default. */
|
|
16
|
+
autoFocus?: boolean
|
|
8
17
|
/** Every edit, and `''` when the clear button is pressed. */
|
|
9
18
|
onChangeText?: (text: string) => void
|
|
19
|
+
/** Called with the query when the keyboard's Search key is pressed. The keyboard goes down after it. */
|
|
20
|
+
onSubmit?: (text: string) => void
|
|
10
21
|
/** Shown while the field is empty. `Search` by default. */
|
|
11
22
|
placeholder?: string
|
|
12
23
|
/** Test identifier, put on the field's container view rather than on the `TextInput`. */
|
|
@@ -7,25 +7,45 @@ import {useHostGuard} from '../../host-context'
|
|
|
7
7
|
import type {StepperComponent, StepperProps} from './stepper.types'
|
|
8
8
|
|
|
9
9
|
export const Stepper: StepperComponent = ({
|
|
10
|
+
decrementLabel = 'Decrement',
|
|
11
|
+
disabled = false,
|
|
12
|
+
incrementLabel = 'Increment',
|
|
10
13
|
label,
|
|
11
14
|
max = Number.MAX_SAFE_INTEGER,
|
|
12
15
|
min = Number.MIN_SAFE_INTEGER,
|
|
13
16
|
onValueChange,
|
|
14
17
|
step = 1,
|
|
18
|
+
testID,
|
|
15
19
|
value,
|
|
16
20
|
}: StepperProps) => {
|
|
17
21
|
useHostGuard('Stepper')
|
|
18
22
|
|
|
19
23
|
const onDecrement = useCallback(() => onValueChange(Math.max(min, value - step)), [min, onValueChange, step, value])
|
|
20
24
|
const onIncrement = useCallback(() => onValueChange(Math.min(max, value + step)), [max, onValueChange, step, value])
|
|
25
|
+
const color = disabled ? 'content.tertiary' : 'content.primary'
|
|
21
26
|
|
|
22
27
|
return (
|
|
23
28
|
<HStack align="center" spacing="s8" wide>
|
|
24
|
-
<Text>{label}</Text>
|
|
29
|
+
<Text color={color}>{label}</Text>
|
|
25
30
|
<Spacer />
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
31
|
+
{/* The control in a row of its own, the same gap apart, so the id lands on it and not on the label. */}
|
|
32
|
+
<HStack align="center" spacing="s8" testID={testID}>
|
|
33
|
+
<GlyphButton
|
|
34
|
+
accessibilityLabel={decrementLabel}
|
|
35
|
+
disabled={disabled || value <= min}
|
|
36
|
+
name="minus"
|
|
37
|
+
onPress={onDecrement}
|
|
38
|
+
/>
|
|
39
|
+
<Text color={color} variant="labelMedium">
|
|
40
|
+
{String(value)}
|
|
41
|
+
</Text>
|
|
42
|
+
<GlyphButton
|
|
43
|
+
accessibilityLabel={incrementLabel}
|
|
44
|
+
disabled={disabled || value >= max}
|
|
45
|
+
name="plus"
|
|
46
|
+
onPress={onIncrement}
|
|
47
|
+
/>
|
|
48
|
+
</HStack>
|
|
29
49
|
</HStack>
|
|
30
50
|
)
|
|
31
51
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {Stepper as SwiftUIStepper} from '@expo/ui/swift-ui'
|
|
2
|
-
import {labelsHidden} from '@expo/ui/swift-ui/modifiers'
|
|
2
|
+
import {disabled as disabledModifier, labelsHidden} from '@expo/ui/swift-ui/modifiers'
|
|
3
3
|
import {useReducedMotion} from '@hzblj/kara-theme'
|
|
4
4
|
import {useRef} from 'react'
|
|
5
5
|
|
|
@@ -12,12 +12,24 @@ import type {StepperComponent, StepperProps} from './stepper.types'
|
|
|
12
12
|
/** The label stays VoiceOver's, on the control; on screen it is the row's own text. */
|
|
13
13
|
const LABEL_HIDDEN = [labelsHidden()]
|
|
14
14
|
|
|
15
|
+
const LABEL_HIDDEN_DISABLED = [labelsHidden(), disabledModifier(true)]
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* SwiftUI's `Stepper` at the end of a row, the label leading and the value just before the control — the way iOS's own
|
|
17
19
|
* settings show a stepped value. The value rolls to the next one (`numericText`), up or down with the change, in
|
|
18
|
-
* monospaced digits so the row does not jump. With motion reduced it cross-fades instead.
|
|
20
|
+
* monospaced digits so the row does not jump. With motion reduced it cross-fades instead. Its buttons are labelled by
|
|
21
|
+
* the system, so `decrementLabel` and `incrementLabel` are Android's alone.
|
|
19
22
|
*/
|
|
20
|
-
export const Stepper: StepperComponent = ({
|
|
23
|
+
export const Stepper: StepperComponent = ({
|
|
24
|
+
disabled = false,
|
|
25
|
+
label,
|
|
26
|
+
max,
|
|
27
|
+
min,
|
|
28
|
+
onValueChange,
|
|
29
|
+
step = 1,
|
|
30
|
+
testID,
|
|
31
|
+
value,
|
|
32
|
+
}: StepperProps) => {
|
|
21
33
|
useHostGuard('Stepper')
|
|
22
34
|
const reduced = useReducedMotion()
|
|
23
35
|
const previous = useRef(value)
|
|
@@ -26,10 +38,10 @@ export const Stepper: StepperComponent = ({label, max, min, onValueChange, step
|
|
|
26
38
|
|
|
27
39
|
return (
|
|
28
40
|
<HStack align="center" spacing="s12" wide>
|
|
29
|
-
<Text>{label}</Text>
|
|
41
|
+
<Text color={disabled ? 'content.tertiary' : 'content.primary'}>{label}</Text>
|
|
30
42
|
<Spacer />
|
|
31
43
|
<Text
|
|
32
|
-
color=
|
|
44
|
+
color={disabled ? 'content.tertiary' : 'content.secondary'}
|
|
33
45
|
ios={{
|
|
34
46
|
animation: {value},
|
|
35
47
|
contentTransition: reduced ? {type: 'opacity'} : {countsDown, type: 'numericText'},
|
|
@@ -38,13 +50,16 @@ export const Stepper: StepperComponent = ({label, max, min, onValueChange, step
|
|
|
38
50
|
>
|
|
39
51
|
{String(value)}
|
|
40
52
|
</Text>
|
|
53
|
+
{/* iOS 27 stops a disabled stepper but draws its buttons at full strength. Pinned to its value, UIKit greys
|
|
54
|
+
both buttons, as it greys one at a limit. */}
|
|
41
55
|
<SwiftUIStepper
|
|
42
56
|
label={label}
|
|
43
|
-
max={max}
|
|
44
|
-
min={min}
|
|
45
|
-
modifiers={LABEL_HIDDEN}
|
|
57
|
+
max={disabled ? value : max}
|
|
58
|
+
min={disabled ? value : min}
|
|
59
|
+
modifiers={disabled ? LABEL_HIDDEN_DISABLED : LABEL_HIDDEN}
|
|
46
60
|
onValueChange={onValueChange}
|
|
47
61
|
step={step}
|
|
62
|
+
testID={testID}
|
|
48
63
|
value={value}
|
|
49
64
|
/>
|
|
50
65
|
</HStack>
|
|
@@ -5,6 +5,18 @@ import type {ReactNode} from 'react'
|
|
|
5
5
|
* two icon buttons in a row. A native atom: it renders inside `<Screen>` or `<Island>`.
|
|
6
6
|
*/
|
|
7
7
|
export type StepperProps = {
|
|
8
|
+
/**
|
|
9
|
+
* What TalkBack says for the minus button. `Decrement` by default: pass the app's own words, in its language.
|
|
10
|
+
* Android only. iOS ignores it: its native stepper labels its buttons itself, in the device's language.
|
|
11
|
+
*/
|
|
12
|
+
decrementLabel?: string
|
|
13
|
+
/** Stops the user changing the value, and dims the row. */
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* What TalkBack says for the plus button. `Increment` by default: pass the app's own words, in its language. Android
|
|
17
|
+
* only. iOS ignores it: its native stepper labels its buttons itself, in the device's language.
|
|
18
|
+
*/
|
|
19
|
+
incrementLabel?: string
|
|
8
20
|
/** The leading text. The value is shown too: before the control on iOS, between the buttons on Android. */
|
|
9
21
|
label: string
|
|
10
22
|
/** The highest value. When unset, Android has no upper limit and iOS stops at `100`. */
|
|
@@ -15,6 +27,11 @@ export type StepperProps = {
|
|
|
15
27
|
onValueChange: (value: number) => void
|
|
16
28
|
/** How much one press adds or takes away. `1` by default. */
|
|
17
29
|
step?: number
|
|
30
|
+
/**
|
|
31
|
+
* Test identifier, on the control rather than the row: SwiftUI's `Stepper` on iOS, the minus and plus buttons with
|
|
32
|
+
* the value between them on Android. Reach a button by what screen readers say for it.
|
|
33
|
+
*/
|
|
34
|
+
testID?: string
|
|
18
35
|
/** The current value. Update it from `onValueChange`. iOS's native stepper counts in whole numbers. */
|
|
19
36
|
value: number
|
|
20
37
|
}
|