@bacon-pos/ui 0.4.0 → 0.5.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/lib/commonjs/components/CodeField/CodeField.js +95 -65
- package/lib/commonjs/components/CodeField/CodeField.js.map +1 -1
- package/lib/commonjs/components/CodeField/CodeField.stories.js +24 -1
- package/lib/commonjs/components/CodeField/CodeField.stories.js.map +1 -1
- package/lib/commonjs/components/CodeField/CodeField.test.js +28 -0
- package/lib/commonjs/components/CodeField/CodeField.test.js.map +1 -1
- package/lib/commonjs/components/Keypad/Keypad.js +162 -0
- package/lib/commonjs/components/Keypad/Keypad.js.map +1 -0
- package/lib/commonjs/components/Keypad/Keypad.stories.js +210 -0
- package/lib/commonjs/components/Keypad/Keypad.stories.js.map +1 -0
- package/lib/commonjs/components/Keypad/Keypad.test.js +119 -0
- package/lib/commonjs/components/Keypad/Keypad.test.js.map +1 -0
- package/lib/commonjs/components/Keypad/index.js +13 -0
- package/lib/commonjs/components/Keypad/index.js.map +1 -0
- package/lib/commonjs/components/index.js +20 -9
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/module/components/CodeField/CodeField.js +95 -65
- package/lib/module/components/CodeField/CodeField.js.map +1 -1
- package/lib/module/components/CodeField/CodeField.stories.js +23 -0
- package/lib/module/components/CodeField/CodeField.stories.js.map +1 -1
- package/lib/module/components/CodeField/CodeField.test.js +28 -0
- package/lib/module/components/CodeField/CodeField.test.js.map +1 -1
- package/lib/module/components/Keypad/Keypad.js +158 -0
- package/lib/module/components/Keypad/Keypad.js.map +1 -0
- package/lib/module/components/Keypad/Keypad.stories.js +206 -0
- package/lib/module/components/Keypad/Keypad.stories.js.map +1 -0
- package/lib/module/components/Keypad/Keypad.test.js +119 -0
- package/lib/module/components/Keypad/Keypad.test.js.map +1 -0
- package/lib/module/components/Keypad/index.js +4 -0
- package/lib/module/components/Keypad/index.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/CodeField/CodeField.d.ts +5 -1
- package/lib/typescript/commonjs/components/CodeField/CodeField.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Keypad/Keypad.d.ts +54 -0
- package/lib/typescript/commonjs/components/Keypad/Keypad.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Keypad/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Keypad/index.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/CodeField/CodeField.d.ts +5 -1
- package/lib/typescript/module/components/CodeField/CodeField.d.ts.map +1 -1
- package/lib/typescript/module/components/Keypad/Keypad.d.ts +54 -0
- package/lib/typescript/module/components/Keypad/Keypad.d.ts.map +1 -0
- package/lib/typescript/module/components/Keypad/index.d.ts +3 -0
- package/lib/typescript/module/components/Keypad/index.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 +1 -1
- package/src/components/CodeField/CodeField.tsx +104 -64
- package/src/components/Keypad/Keypad.tsx +191 -0
- package/src/components/Keypad/index.ts +2 -0
- package/src/components/index.ts +1 -0
|
@@ -13,6 +13,16 @@ import { Text } from '../Text/Text';
|
|
|
13
13
|
/** Removes the browser's default focus outline on web (no-op on native). */
|
|
14
14
|
const WEB_NO_OUTLINE = { outlineStyle: 'none', outlineWidth: 0 } as unknown as TextStyle;
|
|
15
15
|
|
|
16
|
+
/** Convert a `#RRGGBB` (or `#RGB`) token to an `rgba()` string at the given alpha. */
|
|
17
|
+
function hexToRgba(hex: string, alpha: number): string {
|
|
18
|
+
const h = hex.replace('#', '');
|
|
19
|
+
const full = h.length === 3 ? h.replace(/./g, (c) => c + c) : h;
|
|
20
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
21
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
22
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
23
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export type CodeFieldMode = 'numeric' | 'alpha' | 'alphanumeric';
|
|
17
27
|
|
|
18
28
|
/** Chars to strip per mode (applied after upper-casing). */
|
|
@@ -33,6 +43,10 @@ export interface CodeFieldProps {
|
|
|
33
43
|
mode?: CodeFieldMode;
|
|
34
44
|
/** Called once every cell is filled. */
|
|
35
45
|
onComplete?: (value: string) => void;
|
|
46
|
+
/** Helper text below the cells. Replaced by the error message when `error` is a string. */
|
|
47
|
+
helperText?: string;
|
|
48
|
+
/** Mark the field invalid. Pass a string to show it as the error message. */
|
|
49
|
+
error?: boolean | string;
|
|
36
50
|
/** Disable input and dim the cells. */
|
|
37
51
|
disabled?: boolean;
|
|
38
52
|
/** Focus on mount. */
|
|
@@ -65,6 +79,8 @@ export function CodeField({
|
|
|
65
79
|
length = 6,
|
|
66
80
|
mode = 'numeric',
|
|
67
81
|
onComplete,
|
|
82
|
+
helperText,
|
|
83
|
+
error,
|
|
68
84
|
disabled = false,
|
|
69
85
|
autoFocus = false,
|
|
70
86
|
cellWidth = 126,
|
|
@@ -86,75 +102,99 @@ export function CodeField({
|
|
|
86
102
|
};
|
|
87
103
|
|
|
88
104
|
const activeIndex = Math.min(value.length, length - 1);
|
|
105
|
+
const hasError = Boolean(error);
|
|
106
|
+
const message = typeof error === 'string' ? error : helperText;
|
|
89
107
|
|
|
90
108
|
return (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
opacity: disabled ? 0.6 : 1,
|
|
115
|
-
...(isActive
|
|
116
|
-
? {
|
|
117
|
-
boxShadow: [
|
|
118
|
-
{ offsetX: 0, offsetY: 0, blurRadius: 0, spreadDistance: 6, color: 'rgba(201,255,0,0.15)' },
|
|
119
|
-
] as ViewStyle['boxShadow'],
|
|
120
|
-
}
|
|
121
|
-
: null),
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
<Text
|
|
125
|
-
color={disabled ? 'tertiary' : 'primary'}
|
|
109
|
+
<View style={[{ alignSelf: 'flex-start' }, style]}>
|
|
110
|
+
<Pressable
|
|
111
|
+
onPress={() => inputRef.current?.focus()}
|
|
112
|
+
disabled={disabled}
|
|
113
|
+
style={{ position: 'relative' }}
|
|
114
|
+
>
|
|
115
|
+
<View style={{ flexDirection: 'row', gap }}>
|
|
116
|
+
{Array.from({ length }).map((_, i) => {
|
|
117
|
+
const isActive = focused && i === activeIndex && !disabled;
|
|
118
|
+
// Error overrides the default/focus border on every cell; the active
|
|
119
|
+
// cell still gets the ring (red on error, lime otherwise).
|
|
120
|
+
const borderColor = hasError
|
|
121
|
+
? theme.colors.feedback.error
|
|
122
|
+
: isActive
|
|
123
|
+
? theme.colors.border.focus
|
|
124
|
+
: theme.colors.border.default;
|
|
125
|
+
const ringColor = hasError
|
|
126
|
+
? hexToRgba(theme.colors.feedback.error, 0.15)
|
|
127
|
+
: 'rgba(201,255,0,0.15)';
|
|
128
|
+
return (
|
|
129
|
+
<View
|
|
130
|
+
key={i}
|
|
131
|
+
testID="code-field-cell"
|
|
126
132
|
style={{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
width: cellWidth,
|
|
134
|
+
height: cellHeight,
|
|
135
|
+
borderRadius: theme.radii.pill,
|
|
136
|
+
borderWidth: 1,
|
|
137
|
+
borderColor,
|
|
138
|
+
backgroundColor: disabled
|
|
139
|
+
? theme.colors.disabled.background
|
|
140
|
+
: theme.colors.background.default,
|
|
141
|
+
alignItems: 'center',
|
|
142
|
+
justifyContent: 'center',
|
|
143
|
+
opacity: disabled ? 0.6 : 1,
|
|
144
|
+
...(isActive
|
|
145
|
+
? {
|
|
146
|
+
boxShadow: [
|
|
147
|
+
{ offsetX: 0, offsetY: 0, blurRadius: 0, spreadDistance: 6, color: ringColor },
|
|
148
|
+
] as ViewStyle['boxShadow'],
|
|
149
|
+
}
|
|
150
|
+
: null),
|
|
130
151
|
}}
|
|
131
152
|
>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
<Text
|
|
154
|
+
color={disabled ? 'tertiary' : 'primary'}
|
|
155
|
+
style={{
|
|
156
|
+
fontFamily: fontFamily ?? theme.fontFamily.regular,
|
|
157
|
+
fontSize,
|
|
158
|
+
lineHeight: Math.round(fontSize * 1.45),
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{value[i] ?? ''}
|
|
162
|
+
</Text>
|
|
163
|
+
</View>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</View>
|
|
167
|
+
|
|
168
|
+
<TextInput
|
|
169
|
+
ref={inputRef}
|
|
170
|
+
value={value}
|
|
171
|
+
onChangeText={handleChange}
|
|
172
|
+
onFocus={() => setFocused(true)}
|
|
173
|
+
onBlur={() => setFocused(false)}
|
|
174
|
+
keyboardType={mode === 'numeric' ? 'number-pad' : 'default'}
|
|
175
|
+
autoCapitalize="characters"
|
|
176
|
+
autoCorrect={false}
|
|
177
|
+
maxLength={length}
|
|
178
|
+
editable={!disabled}
|
|
179
|
+
caretHidden
|
|
180
|
+
autoFocus={autoFocus}
|
|
181
|
+
accessibilityLabel={accessibilityLabel}
|
|
182
|
+
style={[
|
|
183
|
+
{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0 },
|
|
184
|
+
WEB_NO_OUTLINE,
|
|
185
|
+
]}
|
|
186
|
+
/>
|
|
187
|
+
</Pressable>
|
|
138
188
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
editable={!disabled}
|
|
150
|
-
caretHidden
|
|
151
|
-
autoFocus={autoFocus}
|
|
152
|
-
accessibilityLabel={accessibilityLabel}
|
|
153
|
-
style={[
|
|
154
|
-
{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0 },
|
|
155
|
-
WEB_NO_OUTLINE,
|
|
156
|
-
]}
|
|
157
|
-
/>
|
|
158
|
-
</Pressable>
|
|
189
|
+
{message ? (
|
|
190
|
+
<Text
|
|
191
|
+
variant="bodySmall"
|
|
192
|
+
color={hasError ? theme.colors.feedback.error : 'tertiary'}
|
|
193
|
+
style={{ marginTop: 8, marginLeft: 8 }}
|
|
194
|
+
>
|
|
195
|
+
{message}
|
|
196
|
+
</Text>
|
|
197
|
+
) : null}
|
|
198
|
+
</View>
|
|
159
199
|
);
|
|
160
200
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { Pressable, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useTheme } from '../../theme';
|
|
4
|
+
import { Icon } from '../Icon/Icon';
|
|
5
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
6
|
+
import { Text } from '../Text/Text';
|
|
7
|
+
|
|
8
|
+
/** The optional key to the left of 0. Hidden unless provided. */
|
|
9
|
+
export interface KeypadExtraKey {
|
|
10
|
+
/** Short label (e.g. "."). */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Or an icon. */
|
|
13
|
+
icon?: IconName;
|
|
14
|
+
/** If set, pressing appends this to the value (e.g. "." for decimals). */
|
|
15
|
+
value?: string;
|
|
16
|
+
/** Custom action (open a modal, navigate, …). Overrides `value`. */
|
|
17
|
+
onPress?: () => void;
|
|
18
|
+
/** Content color. */
|
|
19
|
+
color?: string;
|
|
20
|
+
/** Key background color. */
|
|
21
|
+
backgroundColor?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface KeypadProps {
|
|
25
|
+
/** Controlled value being entered. Defaults to ''. */
|
|
26
|
+
value?: string;
|
|
27
|
+
/** Called with the next value on digit / backspace / extra-value press. */
|
|
28
|
+
onChangeText?: (value: string) => void;
|
|
29
|
+
/** Fired on every press with the raw key: '0'–'9', 'backspace', or 'extra'. */
|
|
30
|
+
onKeyPress?: (key: string) => void;
|
|
31
|
+
/** Max length of `value`. */
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
/** Content shown above the keys (dots, amount, anything). Swappable per use case. */
|
|
34
|
+
header?: ReactNode;
|
|
35
|
+
/** The key left of 0. Hidden by default; provide to show and customize it. */
|
|
36
|
+
extraKey?: KeypadExtraKey;
|
|
37
|
+
/** Key height. Defaults to 83. */
|
|
38
|
+
keyHeight?: number;
|
|
39
|
+
/** Gap between keys. Defaults to 8. */
|
|
40
|
+
gap?: number;
|
|
41
|
+
/** Total width. Defaults to full width; the 3 columns split evenly. */
|
|
42
|
+
width?: number;
|
|
43
|
+
/** Digit font size. Defaults to 24. */
|
|
44
|
+
digitSize?: number;
|
|
45
|
+
/** Space between header and keys. Defaults to 24. */
|
|
46
|
+
headerGap?: number;
|
|
47
|
+
/** Disable all keys. */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
style?: StyleProp<ViewStyle>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ROWS = [
|
|
53
|
+
['1', '2', '3'],
|
|
54
|
+
['4', '5', '6'],
|
|
55
|
+
['7', '8', '9'],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Numeric keypad — a 3-column pad decoupled from what it drives (bind it to a
|
|
60
|
+
* PIN display, an amount, etc. via `value`/`onChangeText`, and put that display
|
|
61
|
+
* in `header`). Backspace is fixed to the right of 0; the key left of 0 is hidden
|
|
62
|
+
* unless you pass `extraKey` (custom symbol/icon, action and colors). Width,
|
|
63
|
+
* key height, gap and digit size are all configurable.
|
|
64
|
+
*/
|
|
65
|
+
export function Keypad({
|
|
66
|
+
value = '',
|
|
67
|
+
onChangeText,
|
|
68
|
+
onKeyPress,
|
|
69
|
+
maxLength,
|
|
70
|
+
header,
|
|
71
|
+
extraKey,
|
|
72
|
+
keyHeight = 83,
|
|
73
|
+
gap = 8,
|
|
74
|
+
width,
|
|
75
|
+
digitSize = 24,
|
|
76
|
+
headerGap = 24,
|
|
77
|
+
disabled = false,
|
|
78
|
+
style,
|
|
79
|
+
}: KeypadProps) {
|
|
80
|
+
const theme = useTheme();
|
|
81
|
+
|
|
82
|
+
const keyStyle = (bg?: string): StyleProp<ViewStyle> => ({
|
|
83
|
+
flex: 1,
|
|
84
|
+
height: keyHeight,
|
|
85
|
+
borderRadius: theme.radii.xl,
|
|
86
|
+
borderWidth: 1,
|
|
87
|
+
borderColor: theme.colors.border.strong,
|
|
88
|
+
backgroundColor: bg ?? theme.colors.action.secondary,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const pressDigit = (digit: string) => {
|
|
94
|
+
onKeyPress?.(digit);
|
|
95
|
+
if (maxLength != null && value.length >= maxLength) return;
|
|
96
|
+
onChangeText?.(value + digit);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const pressBackspace = () => {
|
|
100
|
+
onKeyPress?.('backspace');
|
|
101
|
+
onChangeText?.(value.slice(0, -1));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const pressExtra = () => {
|
|
105
|
+
onKeyPress?.('extra');
|
|
106
|
+
if (extraKey?.onPress) {
|
|
107
|
+
extraKey.onPress();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (extraKey?.value != null) {
|
|
111
|
+
if (maxLength != null && value.length >= maxLength) return;
|
|
112
|
+
onChangeText?.(value + extraKey.value);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const DigitKey = ({ digit }: { digit: string }) => (
|
|
117
|
+
<Pressable
|
|
118
|
+
onPress={() => pressDigit(digit)}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
accessibilityRole="button"
|
|
121
|
+
accessibilityLabel={digit}
|
|
122
|
+
style={({ pressed }) => [
|
|
123
|
+
keyStyle(),
|
|
124
|
+
{ opacity: disabled ? 0.5 : pressed ? 0.85 : 1 },
|
|
125
|
+
]}
|
|
126
|
+
>
|
|
127
|
+
<Text
|
|
128
|
+
variant="h2"
|
|
129
|
+
color={theme.colors.action.secondaryForeground}
|
|
130
|
+
style={{ fontSize: digitSize, lineHeight: Math.round(digitSize * 1.35) }}
|
|
131
|
+
>
|
|
132
|
+
{digit}
|
|
133
|
+
</Text>
|
|
134
|
+
</Pressable>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<View style={[{ width: width ?? '100%', gap }, style]}>
|
|
139
|
+
{header != null ? <View style={{ marginBottom: headerGap - gap }}>{header}</View> : null}
|
|
140
|
+
|
|
141
|
+
{ROWS.map((row) => (
|
|
142
|
+
<View key={row[0]} style={{ flexDirection: 'row', gap }}>
|
|
143
|
+
{row.map((digit) => (
|
|
144
|
+
<DigitKey key={digit} digit={digit} />
|
|
145
|
+
))}
|
|
146
|
+
</View>
|
|
147
|
+
))}
|
|
148
|
+
|
|
149
|
+
<View style={{ flexDirection: 'row', gap }}>
|
|
150
|
+
{extraKey ? (
|
|
151
|
+
<Pressable
|
|
152
|
+
onPress={pressExtra}
|
|
153
|
+
disabled={disabled}
|
|
154
|
+
accessibilityRole="button"
|
|
155
|
+
accessibilityLabel={extraKey.label ?? 'action'}
|
|
156
|
+
style={({ pressed }) => [
|
|
157
|
+
keyStyle(extraKey.backgroundColor),
|
|
158
|
+
{ opacity: disabled ? 0.5 : pressed ? 0.85 : 1 },
|
|
159
|
+
]}
|
|
160
|
+
>
|
|
161
|
+
{extraKey.icon ? (
|
|
162
|
+
<Icon name={extraKey.icon} size={24} color={extraKey.color ?? theme.colors.action.secondaryForeground} />
|
|
163
|
+
) : (
|
|
164
|
+
<Text
|
|
165
|
+
variant="h2"
|
|
166
|
+
color={extraKey.color ?? theme.colors.action.secondaryForeground}
|
|
167
|
+
style={{ fontSize: digitSize, lineHeight: Math.round(digitSize * 1.35) }}
|
|
168
|
+
>
|
|
169
|
+
{extraKey.label ?? ''}
|
|
170
|
+
</Text>
|
|
171
|
+
)}
|
|
172
|
+
</Pressable>
|
|
173
|
+
) : (
|
|
174
|
+
<View style={{ flex: 1 }} />
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<DigitKey digit="0" />
|
|
178
|
+
|
|
179
|
+
<Pressable
|
|
180
|
+
onPress={pressBackspace}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
accessibilityRole="button"
|
|
183
|
+
accessibilityLabel="Backspace"
|
|
184
|
+
style={({ pressed }) => [keyStyle(), { opacity: disabled ? 0.5 : pressed ? 0.85 : 1 }]}
|
|
185
|
+
>
|
|
186
|
+
<Icon name="arrow-left" size={24} color={theme.colors.action.secondaryForeground} />
|
|
187
|
+
</Pressable>
|
|
188
|
+
</View>
|
|
189
|
+
</View>
|
|
190
|
+
);
|
|
191
|
+
}
|
package/src/components/index.ts
CHANGED