@bacon-pos/ui 0.11.0 → 0.15.1
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/lib/commonjs/components/CodeInput/CodeInput.js +28 -9
- package/lib/commonjs/components/CodeInput/CodeInput.js.map +1 -1
- package/lib/commonjs/components/CodeInput/CodeInput.stories.js +28 -1
- package/lib/commonjs/components/CodeInput/CodeInput.stories.js.map +1 -1
- package/lib/commonjs/components/CodeInput/CodeInput.test.js +16 -0
- package/lib/commonjs/components/CodeInput/CodeInput.test.js.map +1 -1
- package/lib/commonjs/components/Dialog/Dialog.js +116 -71
- package/lib/commonjs/components/Dialog/Dialog.js.map +1 -1
- package/lib/commonjs/components/Dialog/StepDialog.js +111 -0
- package/lib/commonjs/components/Dialog/StepDialog.js.map +1 -0
- package/lib/commonjs/components/Dialog/StepDialog.stories.js +157 -0
- package/lib/commonjs/components/Dialog/StepDialog.stories.js.map +1 -0
- package/lib/commonjs/components/Dialog/StepDialog.test.js +118 -0
- package/lib/commonjs/components/Dialog/StepDialog.test.js.map +1 -0
- package/lib/commonjs/components/Dialog/index.js +7 -0
- package/lib/commonjs/components/Dialog/index.js.map +1 -1
- package/lib/commonjs/components/Icon/icons.generated.js +9 -1
- package/lib/commonjs/components/Icon/icons.generated.js.map +1 -1
- package/lib/commonjs/components/List/List.js +45 -0
- package/lib/commonjs/components/List/List.js.map +1 -0
- package/lib/commonjs/components/List/List.stories.js +215 -0
- package/lib/commonjs/components/List/List.stories.js.map +1 -0
- package/lib/commonjs/components/List/List.test.js +94 -0
- package/lib/commonjs/components/List/List.test.js.map +1 -0
- package/lib/commonjs/components/List/ListItem.js +210 -0
- package/lib/commonjs/components/List/ListItem.js.map +1 -0
- package/lib/commonjs/components/List/index.js +20 -0
- package/lib/commonjs/components/List/index.js.map +1 -0
- package/lib/commonjs/components/Menu/Menu.js +11 -3
- package/lib/commonjs/components/Menu/Menu.js.map +1 -1
- package/lib/commonjs/components/ProgressBar/ProgressBar.js +4 -3
- package/lib/commonjs/components/ProgressBar/ProgressBar.js.map +1 -1
- package/lib/commonjs/components/Sidebar/Sidebar.js +20 -3
- package/lib/commonjs/components/Sidebar/Sidebar.js.map +1 -1
- package/lib/commonjs/components/Sidebar/Sidebar.stories.js +17 -1
- package/lib/commonjs/components/Sidebar/Sidebar.stories.js.map +1 -1
- package/lib/commonjs/components/Table/Table.js +69 -6
- package/lib/commonjs/components/Table/Table.js.map +1 -1
- package/lib/commonjs/components/Table/Table.stories.js +118 -1
- package/lib/commonjs/components/Table/Table.stories.js.map +1 -1
- package/lib/commonjs/components/Table/Table.test.js +58 -0
- package/lib/commonjs/components/Table/Table.test.js.map +1 -1
- package/lib/commonjs/components/_internal/ModalHostContext.js +21 -0
- package/lib/commonjs/components/_internal/ModalHostContext.js.map +1 -0
- package/lib/commonjs/components/index.js +26 -15
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/module/components/CodeInput/CodeInput.js +28 -9
- package/lib/module/components/CodeInput/CodeInput.js.map +1 -1
- package/lib/module/components/CodeInput/CodeInput.stories.js +27 -0
- package/lib/module/components/CodeInput/CodeInput.stories.js.map +1 -1
- package/lib/module/components/CodeInput/CodeInput.test.js +16 -0
- package/lib/module/components/CodeInput/CodeInput.test.js.map +1 -1
- package/lib/module/components/Dialog/Dialog.js +116 -71
- package/lib/module/components/Dialog/Dialog.js.map +1 -1
- package/lib/module/components/Dialog/StepDialog.js +107 -0
- package/lib/module/components/Dialog/StepDialog.js.map +1 -0
- package/lib/module/components/Dialog/StepDialog.stories.js +153 -0
- package/lib/module/components/Dialog/StepDialog.stories.js.map +1 -0
- package/lib/module/components/Dialog/StepDialog.test.js +117 -0
- package/lib/module/components/Dialog/StepDialog.test.js.map +1 -0
- package/lib/module/components/Dialog/index.js +1 -0
- package/lib/module/components/Dialog/index.js.map +1 -1
- package/lib/module/components/Icon/icons.generated.js +9 -1
- package/lib/module/components/Icon/icons.generated.js.map +1 -1
- package/lib/module/components/List/List.js +41 -0
- package/lib/module/components/List/List.js.map +1 -0
- package/lib/module/components/List/List.stories.js +211 -0
- package/lib/module/components/List/List.stories.js.map +1 -0
- package/lib/module/components/List/List.test.js +94 -0
- package/lib/module/components/List/List.test.js.map +1 -0
- package/lib/module/components/List/ListItem.js +206 -0
- package/lib/module/components/List/ListItem.js.map +1 -0
- package/lib/module/components/List/index.js +5 -0
- package/lib/module/components/List/index.js.map +1 -0
- package/lib/module/components/Menu/Menu.js +12 -4
- package/lib/module/components/Menu/Menu.js.map +1 -1
- package/lib/module/components/ProgressBar/ProgressBar.js +4 -3
- package/lib/module/components/ProgressBar/ProgressBar.js.map +1 -1
- package/lib/module/components/Sidebar/Sidebar.js +20 -3
- package/lib/module/components/Sidebar/Sidebar.js.map +1 -1
- package/lib/module/components/Sidebar/Sidebar.stories.js +16 -0
- package/lib/module/components/Sidebar/Sidebar.stories.js.map +1 -1
- package/lib/module/components/Table/Table.js +70 -7
- package/lib/module/components/Table/Table.js.map +1 -1
- package/lib/module/components/Table/Table.stories.js +117 -0
- package/lib/module/components/Table/Table.stories.js.map +1 -1
- package/lib/module/components/Table/Table.test.js +58 -0
- package/lib/module/components/Table/Table.test.js.map +1 -1
- package/lib/module/components/_internal/ModalHostContext.js +17 -0
- package/lib/module/components/_internal/ModalHostContext.js.map +1 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/typescript/commonjs/components/CodeInput/CodeInput.d.ts +5 -1
- package/lib/typescript/commonjs/components/CodeInput/CodeInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts +13 -1
- package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Dialog/StepDialog.d.ts +63 -0
- package/lib/typescript/commonjs/components/Dialog/StepDialog.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Dialog/index.d.ts +1 -0
- package/lib/typescript/commonjs/components/Dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts +8 -0
- package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/List/List.d.ts +22 -0
- package/lib/typescript/commonjs/components/List/List.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/List/ListItem.d.ts +61 -0
- package/lib/typescript/commonjs/components/List/ListItem.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/List/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/List/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Menu/Menu.d.ts +4 -2
- package/lib/typescript/commonjs/components/Menu/Menu.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts +3 -1
- package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Sidebar/Sidebar.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Table/Table.d.ts +25 -3
- package/lib/typescript/commonjs/components/Table/Table.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/_internal/ModalHostContext.d.ts +11 -0
- package/lib/typescript/commonjs/components/_internal/ModalHostContext.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/index.d.ts +1 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/CodeInput/CodeInput.d.ts +5 -1
- package/lib/typescript/module/components/CodeInput/CodeInput.d.ts.map +1 -1
- package/lib/typescript/module/components/Dialog/Dialog.d.ts +13 -1
- package/lib/typescript/module/components/Dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/components/Dialog/StepDialog.d.ts +63 -0
- package/lib/typescript/module/components/Dialog/StepDialog.d.ts.map +1 -0
- package/lib/typescript/module/components/Dialog/index.d.ts +1 -0
- package/lib/typescript/module/components/Dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/components/Icon/icons.generated.d.ts +8 -0
- package/lib/typescript/module/components/Icon/icons.generated.d.ts.map +1 -1
- package/lib/typescript/module/components/List/List.d.ts +22 -0
- package/lib/typescript/module/components/List/List.d.ts.map +1 -0
- package/lib/typescript/module/components/List/ListItem.d.ts +61 -0
- package/lib/typescript/module/components/List/ListItem.d.ts.map +1 -0
- package/lib/typescript/module/components/List/index.d.ts +5 -0
- package/lib/typescript/module/components/List/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Menu/Menu.d.ts +4 -2
- package/lib/typescript/module/components/Menu/Menu.d.ts.map +1 -1
- package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts +3 -1
- package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/lib/typescript/module/components/Sidebar/Sidebar.d.ts.map +1 -1
- package/lib/typescript/module/components/Table/Table.d.ts +25 -3
- package/lib/typescript/module/components/Table/Table.d.ts.map +1 -1
- package/lib/typescript/module/components/_internal/ModalHostContext.d.ts +11 -0
- package/lib/typescript/module/components/_internal/ModalHostContext.d.ts.map +1 -0
- package/lib/typescript/module/components/index.d.ts +1 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/package.json +16 -14
- package/src/components/CodeInput/CodeInput.tsx +46 -14
- package/src/components/Dialog/Dialog.tsx +92 -31
- package/src/components/Dialog/StepDialog.tsx +173 -0
- package/src/components/Dialog/index.ts +1 -0
- package/src/components/Icon/icons.generated.ts +9 -1
- package/src/components/List/List.tsx +45 -0
- package/src/components/List/ListItem.tsx +214 -0
- package/src/components/List/index.ts +4 -0
- package/src/components/Menu/Menu.tsx +15 -6
- package/src/components/ProgressBar/ProgressBar.tsx +6 -3
- package/src/components/Sidebar/Sidebar.tsx +18 -10
- package/src/components/Table/Table.tsx +93 -8
- package/src/components/_internal/ModalHostContext.tsx +15 -0
- package/src/components/index.ts +1 -0
|
@@ -30,6 +30,10 @@ export interface CodeInputProps {
|
|
|
30
30
|
disabled?: boolean;
|
|
31
31
|
/** Focus on mount. */
|
|
32
32
|
autoFocus?: boolean;
|
|
33
|
+
/** Mark the field invalid — turns the cells red. Pass a string to show it as a message below. */
|
|
34
|
+
error?: boolean | string;
|
|
35
|
+
/** Helper/message text below the cells. Replaced by the error message when `error` is a string. */
|
|
36
|
+
helperText?: string;
|
|
33
37
|
/** Show an eye toggle to reveal/hide the entered digits. */
|
|
34
38
|
revealable?: boolean;
|
|
35
39
|
/** Controlled reveal state. Omit for uncontrolled (toggled by the eye). */
|
|
@@ -52,21 +56,28 @@ function Cell({
|
|
|
52
56
|
filled,
|
|
53
57
|
revealed,
|
|
54
58
|
disabled,
|
|
59
|
+
error,
|
|
55
60
|
theme,
|
|
56
61
|
}: {
|
|
57
62
|
char: string;
|
|
58
63
|
filled: boolean;
|
|
59
64
|
revealed: boolean;
|
|
60
65
|
disabled: boolean;
|
|
66
|
+
error: boolean;
|
|
61
67
|
theme: Theme;
|
|
62
68
|
}) {
|
|
69
|
+
const errorColor = theme.colors.feedback.error;
|
|
63
70
|
return (
|
|
64
71
|
<View
|
|
65
72
|
testID="code-input-cell"
|
|
66
73
|
style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}
|
|
67
74
|
>
|
|
68
75
|
{revealed && filled ? (
|
|
69
|
-
<Text
|
|
76
|
+
<Text
|
|
77
|
+
variant="bodyMedium"
|
|
78
|
+
weight="medium"
|
|
79
|
+
color={disabled ? 'tertiary' : error ? errorColor : 'primary'}
|
|
80
|
+
>
|
|
70
81
|
{char}
|
|
71
82
|
</Text>
|
|
72
83
|
) : (
|
|
@@ -76,8 +87,16 @@ function Cell({
|
|
|
76
87
|
height: 20,
|
|
77
88
|
borderRadius: 10,
|
|
78
89
|
borderWidth: 1.5,
|
|
79
|
-
backgroundColor: filled
|
|
80
|
-
|
|
90
|
+
backgroundColor: filled
|
|
91
|
+
? error
|
|
92
|
+
? errorColor
|
|
93
|
+
: theme.colors.text.primary
|
|
94
|
+
: theme.colors.background.default,
|
|
95
|
+
borderColor: error
|
|
96
|
+
? errorColor
|
|
97
|
+
: filled
|
|
98
|
+
? theme.colors.text.primary
|
|
99
|
+
: theme.colors.border.strong,
|
|
81
100
|
opacity: disabled ? 0.5 : 1,
|
|
82
101
|
}}
|
|
83
102
|
/>
|
|
@@ -100,6 +119,8 @@ export function CodeInput({
|
|
|
100
119
|
onComplete,
|
|
101
120
|
disabled = false,
|
|
102
121
|
autoFocus = false,
|
|
122
|
+
error,
|
|
123
|
+
helperText,
|
|
103
124
|
revealable = false,
|
|
104
125
|
revealed: revealedProp,
|
|
105
126
|
onRevealChange,
|
|
@@ -114,6 +135,8 @@ export function CodeInput({
|
|
|
114
135
|
const [internalRevealed, setInternalRevealed] = useState(false);
|
|
115
136
|
const revealed = revealedProp ?? internalRevealed;
|
|
116
137
|
const filled = Math.min(value.length, length);
|
|
138
|
+
const hasError = Boolean(error);
|
|
139
|
+
const message = typeof error === 'string' ? error : helperText;
|
|
117
140
|
|
|
118
141
|
const handleChange = (text: string) => {
|
|
119
142
|
const digits = text.replace(/[^0-9]/g, '').slice(0, length);
|
|
@@ -141,6 +164,7 @@ export function CodeInput({
|
|
|
141
164
|
filled={i < filled}
|
|
142
165
|
revealed={revealed}
|
|
143
166
|
disabled={disabled}
|
|
167
|
+
error={hasError}
|
|
144
168
|
theme={theme}
|
|
145
169
|
/>
|
|
146
170
|
))}
|
|
@@ -180,21 +204,29 @@ export function CodeInput({
|
|
|
180
204
|
const eyeFirst = revealPosition === 'top' || revealPosition === 'left';
|
|
181
205
|
|
|
182
206
|
return (
|
|
183
|
-
<View
|
|
184
|
-
|
|
185
|
-
{
|
|
207
|
+
<View style={[{ alignSelf: 'flex-start' }, style]}>
|
|
208
|
+
<View
|
|
209
|
+
style={{
|
|
186
210
|
flexDirection: vertical ? 'column' : 'row',
|
|
187
211
|
alignItems: 'center',
|
|
188
212
|
gap: 12,
|
|
189
|
-
alignSelf: 'flex-start',
|
|
190
213
|
paddingVertical: 8,
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{eyeFirst ? eyeToggle : null}
|
|
217
|
+
{cells}
|
|
218
|
+
{eyeFirst ? null : eyeToggle}
|
|
219
|
+
</View>
|
|
220
|
+
|
|
221
|
+
{message ? (
|
|
222
|
+
<Text
|
|
223
|
+
variant="bodySmall"
|
|
224
|
+
color={hasError ? theme.colors.feedback.error : 'tertiary'}
|
|
225
|
+
style={{ marginTop: 4, marginLeft: 8 }}
|
|
226
|
+
>
|
|
227
|
+
{message}
|
|
228
|
+
</Text>
|
|
229
|
+
) : null}
|
|
198
230
|
</View>
|
|
199
231
|
);
|
|
200
232
|
}
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
import { useTheme } from '../../theme';
|
|
15
15
|
import { Button, type ButtonVariant } from '../Button/Button';
|
|
16
16
|
import { Icon } from '../Icon/Icon';
|
|
17
|
+
import { IconButton } from '../IconButton/IconButton';
|
|
17
18
|
import { Text } from '../Text/Text';
|
|
19
|
+
import { ModalHostContext } from '../_internal/ModalHostContext';
|
|
18
20
|
import { ScrollArea } from '../_internal/ScrollArea';
|
|
19
21
|
|
|
20
22
|
export interface DialogAction {
|
|
@@ -44,6 +46,18 @@ export interface DialogProps {
|
|
|
44
46
|
title: string;
|
|
45
47
|
/** Optional subtitle. */
|
|
46
48
|
subtitle?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Show a round back button to the left of the title and wire its press here.
|
|
51
|
+
* Used for multi-step flows (see `StepDialog`). Omit for a plain dialog.
|
|
52
|
+
*/
|
|
53
|
+
onBack?: () => void;
|
|
54
|
+
/** Accessibility label for the back button. Defaults to "Back". */
|
|
55
|
+
backAccessibilityLabel?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Content pinned in the footer, above the action buttons (e.g. a step
|
|
58
|
+
* progress bar). Centered. Shows the footer region even without actions.
|
|
59
|
+
*/
|
|
60
|
+
footerAccessory?: ReactNode;
|
|
47
61
|
/** Any content — from text to a full form. */
|
|
48
62
|
children?: ReactNode;
|
|
49
63
|
/** Primary/secondary action buttons (right side). Keep total buttons ≤ 3. */
|
|
@@ -99,6 +113,9 @@ export function Dialog({
|
|
|
99
113
|
onClose,
|
|
100
114
|
title,
|
|
101
115
|
subtitle,
|
|
116
|
+
onBack,
|
|
117
|
+
backAccessibilityLabel = 'Back',
|
|
118
|
+
footerAccessory,
|
|
102
119
|
children,
|
|
103
120
|
actions = [],
|
|
104
121
|
cancel,
|
|
@@ -140,54 +157,94 @@ export function Dialog({
|
|
|
140
157
|
|
|
141
158
|
const header = (
|
|
142
159
|
<View style={{ paddingHorizontal: padding, paddingTop: padding, paddingBottom: contentSpacing }}>
|
|
143
|
-
<View
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
<View
|
|
161
|
+
style={{
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
gap: 20,
|
|
165
|
+
paddingRight: showClose ? 40 : 0,
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{onBack ? (
|
|
169
|
+
<IconButton
|
|
170
|
+
icon="arrow-left"
|
|
171
|
+
accessibilityLabel={backAccessibilityLabel}
|
|
172
|
+
variant="secondary"
|
|
173
|
+
size="lg"
|
|
174
|
+
onPress={onBack}
|
|
175
|
+
/>
|
|
151
176
|
) : null}
|
|
177
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
178
|
+
<Text variant="h3" weight="semibold" color="primary">
|
|
179
|
+
{title}
|
|
180
|
+
</Text>
|
|
181
|
+
{subtitle ? (
|
|
182
|
+
<Text variant="bodyLarge" color="secondary">
|
|
183
|
+
{subtitle}
|
|
184
|
+
</Text>
|
|
185
|
+
) : null}
|
|
186
|
+
</View>
|
|
152
187
|
</View>
|
|
153
188
|
</View>
|
|
154
189
|
);
|
|
155
190
|
|
|
156
|
-
const
|
|
191
|
+
const hasButtons = cancel != null || actions.length > 0;
|
|
192
|
+
const hasFooter = hasButtons || footerAccessory != null;
|
|
193
|
+
|
|
194
|
+
// The footer owns the bottom padding; without a footer the content would
|
|
195
|
+
// otherwise hug the bottom edge, so it takes over the bottom padding itself.
|
|
196
|
+
const contentBody = (
|
|
197
|
+
<View style={{ paddingHorizontal: padding, paddingBottom: hasFooter ? 0 : padding }}>
|
|
198
|
+
{children}
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
157
201
|
|
|
158
|
-
const hasFooter = cancel != null || actions.length > 0;
|
|
159
202
|
const footer = hasFooter ? (
|
|
160
203
|
<View
|
|
161
204
|
style={{
|
|
162
|
-
flexDirection: 'row',
|
|
163
|
-
gap: 12,
|
|
164
205
|
paddingHorizontal: padding,
|
|
165
206
|
paddingBottom: padding,
|
|
166
207
|
paddingTop: contentSpacing,
|
|
167
208
|
}}
|
|
168
209
|
>
|
|
169
|
-
{
|
|
170
|
-
<View
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
210
|
+
{footerAccessory ? (
|
|
211
|
+
<View
|
|
212
|
+
style={{
|
|
213
|
+
alignItems: 'center',
|
|
214
|
+
// Figma keeps the accessory (e.g. step progress) closer to the
|
|
215
|
+
// content above it than to the buttons below (~40 vs ~56).
|
|
216
|
+
marginBottom: hasButtons ? Math.round(contentSpacing * 1.4) : 0,
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
{footerAccessory}
|
|
177
220
|
</View>
|
|
178
221
|
) : null}
|
|
179
|
-
{
|
|
180
|
-
<View
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
222
|
+
{hasButtons ? (
|
|
223
|
+
<View style={{ flexDirection: 'row', gap: 12 }}>
|
|
224
|
+
{cancel ? (
|
|
225
|
+
<View style={{ flex: 1 }}>
|
|
226
|
+
<Button
|
|
227
|
+
fullWidth
|
|
228
|
+
variant={cancel.destructive ? 'destructive' : 'secondary'}
|
|
229
|
+
label={cancel.label ?? 'Cancel'}
|
|
230
|
+
onPress={cancel.onPress ?? onClose}
|
|
231
|
+
/>
|
|
232
|
+
</View>
|
|
233
|
+
) : null}
|
|
234
|
+
{actions.map((action, i) => (
|
|
235
|
+
<View key={i} style={{ flex: 1 }}>
|
|
236
|
+
<Button
|
|
237
|
+
fullWidth
|
|
238
|
+
variant={action.variant ?? 'default'}
|
|
239
|
+
label={action.label}
|
|
240
|
+
onPress={action.onPress}
|
|
241
|
+
loading={action.loading}
|
|
242
|
+
disabled={action.disabled}
|
|
243
|
+
/>
|
|
244
|
+
</View>
|
|
245
|
+
))}
|
|
189
246
|
</View>
|
|
190
|
-
)
|
|
247
|
+
) : null}
|
|
191
248
|
</View>
|
|
192
249
|
) : null;
|
|
193
250
|
|
|
@@ -213,6 +270,7 @@ export function Dialog({
|
|
|
213
270
|
<ScrollArea
|
|
214
271
|
indicatorColor={indicatorColor}
|
|
215
272
|
style={{ flex: 1 }}
|
|
273
|
+
contentContainerStyle={{ paddingVertical: 8 }}
|
|
216
274
|
keyboardShouldPersistTaps="handled"
|
|
217
275
|
keyboardDismissMode="interactive"
|
|
218
276
|
>
|
|
@@ -239,6 +297,7 @@ export function Dialog({
|
|
|
239
297
|
<ScrollArea
|
|
240
298
|
indicatorColor={indicatorColor}
|
|
241
299
|
style={{ flexShrink: 1 }}
|
|
300
|
+
contentContainerStyle={{ paddingVertical: 8 }}
|
|
242
301
|
keyboardShouldPersistTaps="handled"
|
|
243
302
|
keyboardDismissMode="interactive"
|
|
244
303
|
>
|
|
@@ -258,6 +317,7 @@ export function Dialog({
|
|
|
258
317
|
statusBarTranslucent
|
|
259
318
|
onRequestClose={onClose}
|
|
260
319
|
>
|
|
320
|
+
<ModalHostContext.Provider value={true}>
|
|
261
321
|
<KeyboardAvoidingView
|
|
262
322
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
263
323
|
style={{ flex: 1 }}
|
|
@@ -294,6 +354,7 @@ export function Dialog({
|
|
|
294
354
|
)}
|
|
295
355
|
</View>
|
|
296
356
|
</KeyboardAvoidingView>
|
|
357
|
+
</ModalHostContext.Provider>
|
|
297
358
|
</Modal>
|
|
298
359
|
);
|
|
299
360
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
2
|
+
import { ProgressBar } from '../ProgressBar/ProgressBar';
|
|
3
|
+
import { Dialog, type DialogProps } from './Dialog';
|
|
4
|
+
|
|
5
|
+
export interface StepDialogStep {
|
|
6
|
+
/** Header title for this step. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Optional subtitle under the title. */
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
/** The step body. */
|
|
11
|
+
content: ReactNode;
|
|
12
|
+
/** Primary button label. Defaults to "Continue" (or `finishLabel` on the last step). */
|
|
13
|
+
nextLabel?: string;
|
|
14
|
+
/** Disable the primary button — e.g. until this step is valid. */
|
|
15
|
+
nextDisabled?: boolean;
|
|
16
|
+
/** Show a spinner on the primary button (e.g. while submitting). */
|
|
17
|
+
nextLoading?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Run when the primary button is pressed on this step. Return (or resolve to)
|
|
20
|
+
* `false` to stay on the step — e.g. when validation fails. Anything else
|
|
21
|
+
* advances to the next step (or finishes on the last one).
|
|
22
|
+
*/
|
|
23
|
+
onNext?: () => boolean | void | Promise<boolean | void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StepDialogProps
|
|
27
|
+
extends Omit<
|
|
28
|
+
DialogProps,
|
|
29
|
+
'title' | 'subtitle' | 'children' | 'actions' | 'cancel' | 'onBack' | 'footerAccessory'
|
|
30
|
+
> {
|
|
31
|
+
/** The ordered steps of the flow. */
|
|
32
|
+
steps: StepDialogStep[];
|
|
33
|
+
/** Controlled step index. Omit for uncontrolled. */
|
|
34
|
+
step?: number;
|
|
35
|
+
/** Initial step index (uncontrolled). Defaults to 0. Resets to this on close. */
|
|
36
|
+
defaultStep?: number;
|
|
37
|
+
/** Notified whenever the step changes. */
|
|
38
|
+
onStepChange?: (index: number) => void;
|
|
39
|
+
/** Called after the last step's `onNext` resolves (unless it returned `false`). */
|
|
40
|
+
onFinish?: () => void;
|
|
41
|
+
/** Primary label on the last step. Defaults to "Finish". */
|
|
42
|
+
finishLabel?: string;
|
|
43
|
+
/** Secondary label used to go back on steps after the first. Defaults to "Back". */
|
|
44
|
+
backLabel?: string;
|
|
45
|
+
/** Secondary label on the first step (closes the dialog). Defaults to "Cancel". */
|
|
46
|
+
cancelLabel?: string;
|
|
47
|
+
/** Show the round back button in the header from step 2 onward. Defaults to true. */
|
|
48
|
+
showHeaderBack?: boolean;
|
|
49
|
+
/** Show the footer secondary button (Back / Cancel). Defaults to true. */
|
|
50
|
+
showFooterBack?: boolean;
|
|
51
|
+
/** Show the progress bar in the footer. Defaults to true. */
|
|
52
|
+
showProgress?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Width of the progress bar within the footer (centered). A number is px; a
|
|
55
|
+
* string is any RN dimension (e.g. `'62%'`). Defaults to `'62%'` — the narrower,
|
|
56
|
+
* centered bar from Figma (≈ 500 / 820 of the content width).
|
|
57
|
+
*/
|
|
58
|
+
progressWidth?: number | `${number}%`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const clamp = (n: number, min: number, max: number) => (n < min ? min : n > max ? max : n);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* StepDialog — a multi-step (wizard) flow built on `Dialog`. Give it an array of
|
|
65
|
+
* `steps`; it tracks the current one, renders a round back button in the header
|
|
66
|
+
* from step 2 onward, a thin progress bar in the footer, and a
|
|
67
|
+
* secondary (Back / Cancel) + primary (Continue / Finish) button pair. A step's
|
|
68
|
+
* `onNext` can return `false` (sync or async) to block advancing after failed
|
|
69
|
+
* validation. Controlled via `step`/`onStepChange` or fully uncontrolled.
|
|
70
|
+
* Forwards all other `Dialog` props (width, fullscreen, scrollMode, …).
|
|
71
|
+
*/
|
|
72
|
+
export function StepDialog({
|
|
73
|
+
steps,
|
|
74
|
+
step,
|
|
75
|
+
defaultStep = 0,
|
|
76
|
+
onStepChange,
|
|
77
|
+
onFinish,
|
|
78
|
+
finishLabel = 'Finish',
|
|
79
|
+
backLabel = 'Back',
|
|
80
|
+
cancelLabel = 'Cancel',
|
|
81
|
+
showHeaderBack = true,
|
|
82
|
+
showFooterBack = true,
|
|
83
|
+
showProgress = true,
|
|
84
|
+
progressWidth = '62%',
|
|
85
|
+
visible,
|
|
86
|
+
onClose,
|
|
87
|
+
...dialogProps
|
|
88
|
+
}: StepDialogProps) {
|
|
89
|
+
const total = steps.length;
|
|
90
|
+
const [uncontrolled, setUncontrolled] = useState(defaultStep);
|
|
91
|
+
const isControlled = step !== undefined;
|
|
92
|
+
const index = clamp(isControlled ? step : uncontrolled, 0, Math.max(0, total - 1));
|
|
93
|
+
|
|
94
|
+
// Reset an uncontrolled flow to its first step whenever it is closed.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!visible && !isControlled) setUncontrolled(defaultStep);
|
|
97
|
+
}, [visible, isControlled, defaultStep]);
|
|
98
|
+
|
|
99
|
+
const setIndex = (next: number) => {
|
|
100
|
+
const clamped = clamp(next, 0, total - 1);
|
|
101
|
+
if (!isControlled) setUncontrolled(clamped);
|
|
102
|
+
onStepChange?.(clamped);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const current = steps[index];
|
|
106
|
+
if (!current) return null;
|
|
107
|
+
|
|
108
|
+
const isFirst = index === 0;
|
|
109
|
+
const isLast = index === total - 1;
|
|
110
|
+
|
|
111
|
+
const goBack = () => {
|
|
112
|
+
if (!isFirst) setIndex(index - 1);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const advance = () => {
|
|
116
|
+
if (isLast) onFinish?.();
|
|
117
|
+
else setIndex(index + 1);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const goNext = () => {
|
|
121
|
+
const result = current.onNext?.();
|
|
122
|
+
// Await only a real promise, so sync steps advance synchronously.
|
|
123
|
+
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
124
|
+
void (result as Promise<boolean | void>).then((r) => {
|
|
125
|
+
if (r !== false) advance();
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (result === false) return;
|
|
130
|
+
advance();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const footerAccessory = showProgress ? (
|
|
134
|
+
<ProgressBar
|
|
135
|
+
progress={(index + 1) / total}
|
|
136
|
+
showSupporting={false}
|
|
137
|
+
bordered={false}
|
|
138
|
+
height={6}
|
|
139
|
+
style={{ width: progressWidth, alignSelf: 'center' }}
|
|
140
|
+
accessibilityLabel={`Step ${index + 1} of ${total}`}
|
|
141
|
+
/>
|
|
142
|
+
) : undefined;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Dialog
|
|
146
|
+
{...dialogProps}
|
|
147
|
+
visible={visible}
|
|
148
|
+
onClose={onClose}
|
|
149
|
+
title={current.title}
|
|
150
|
+
subtitle={current.subtitle}
|
|
151
|
+
onBack={!isFirst && showHeaderBack ? goBack : undefined}
|
|
152
|
+
backAccessibilityLabel={backLabel}
|
|
153
|
+
footerAccessory={footerAccessory}
|
|
154
|
+
cancel={
|
|
155
|
+
showFooterBack
|
|
156
|
+
? isFirst
|
|
157
|
+
? { label: cancelLabel, onPress: onClose }
|
|
158
|
+
: { label: backLabel, onPress: goBack }
|
|
159
|
+
: undefined
|
|
160
|
+
}
|
|
161
|
+
actions={[
|
|
162
|
+
{
|
|
163
|
+
label: current.nextLabel ?? (isLast ? finishLabel : 'Continue'),
|
|
164
|
+
onPress: goNext,
|
|
165
|
+
disabled: current.nextDisabled,
|
|
166
|
+
loading: current.nextLoading,
|
|
167
|
+
},
|
|
168
|
+
]}
|
|
169
|
+
>
|
|
170
|
+
{current.content}
|
|
171
|
+
</Dialog>
|
|
172
|
+
);
|
|
173
|
+
}
|