@utilitywarehouse/hearth-react-native 0.9.0 → 0.11.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +16 -0
- package/build/components/Avatar/Avatar.d.ts +6 -0
- package/build/components/Avatar/Avatar.js +80 -0
- package/build/components/Avatar/Avatar.props.d.ts +28 -0
- package/build/components/Avatar/Avatar.props.js +1 -0
- package/build/components/Avatar/index.d.ts +2 -0
- package/build/components/Avatar/index.js +1 -0
- package/build/components/DateInput/DateInput.d.ts +6 -0
- package/build/components/DateInput/DateInput.js +19 -0
- package/build/components/DateInput/DateInput.props.d.ts +79 -0
- package/build/components/DateInput/DateInput.props.js +1 -0
- package/build/components/DateInput/DateInputSegment.d.ts +20 -0
- package/build/components/DateInput/DateInputSegment.js +31 -0
- package/build/components/DateInput/index.d.ts +2 -0
- package/build/components/DateInput/index.js +1 -0
- package/build/components/PillGroup/Pill.d.ts +16 -0
- package/build/components/PillGroup/Pill.js +94 -0
- package/build/components/PillGroup/Pill.props.d.ts +10 -0
- package/build/components/PillGroup/Pill.props.js +1 -0
- package/build/components/PillGroup/PillGroup.context.d.ts +6 -0
- package/build/components/PillGroup/PillGroup.context.js +5 -0
- package/build/components/PillGroup/PillGroup.d.ts +5 -0
- package/build/components/PillGroup/PillGroup.js +34 -0
- package/build/components/PillGroup/PillGroup.props.d.ts +15 -0
- package/build/components/PillGroup/PillGroup.props.js +1 -0
- package/build/components/PillGroup/index.d.ts +4 -0
- package/build/components/PillGroup/index.js +2 -0
- package/build/components/Select/Select.js +2 -1
- package/build/components/Toast/Toast.context.d.ts +9 -0
- package/build/components/Toast/Toast.context.js +90 -0
- package/build/components/Toast/Toast.props.d.ts +29 -0
- package/build/components/Toast/Toast.props.js +1 -0
- package/build/components/Toast/ToastItem.d.ts +10 -0
- package/build/components/Toast/ToastItem.js +129 -0
- package/build/components/Toast/index.d.ts +3 -0
- package/build/components/Toast/index.js +2 -0
- package/build/components/index.d.ts +4 -0
- package/build/components/index.js +4 -0
- package/build/tokens/components/dark/checkbox.d.ts +3 -0
- package/build/tokens/components/dark/checkbox.js +3 -0
- package/build/tokens/components/dark/input.d.ts +6 -0
- package/build/tokens/components/dark/input.js +6 -0
- package/build/tokens/components/dark/radio.d.ts +3 -0
- package/build/tokens/components/dark/radio.js +3 -0
- package/build/tokens/components/dark/table.d.ts +2 -0
- package/build/tokens/components/dark/table.js +2 -0
- package/build/tokens/components/dark/toast.d.ts +6 -2
- package/build/tokens/components/dark/toast.js +6 -2
- package/build/tokens/components/light/checkbox.d.ts +3 -0
- package/build/tokens/components/light/checkbox.js +3 -0
- package/build/tokens/components/light/input.d.ts +6 -0
- package/build/tokens/components/light/input.js +6 -0
- package/build/tokens/components/light/radio.d.ts +3 -0
- package/build/tokens/components/light/radio.js +3 -0
- package/build/tokens/components/light/table.d.ts +2 -0
- package/build/tokens/components/light/table.js +2 -0
- package/build/tokens/components/light/toast.d.ts +6 -2
- package/build/tokens/components/light/toast.js +6 -2
- package/build/utils/getInitials.d.ts +1 -0
- package/build/utils/getInitials.js +8 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.js +1 -0
- package/docs/assets/toast-ios.MP4 +0 -0
- package/docs/components/AllComponents.web.tsx +43 -0
- package/package.json +3 -3
- package/src/components/Avatar/Avatar.docs.mdx +105 -0
- package/src/components/Avatar/Avatar.props.ts +31 -0
- package/src/components/Avatar/Avatar.stories.tsx +77 -0
- package/src/components/Avatar/Avatar.tsx +136 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/DateInput/DateInput.docs.mdx +163 -0
- package/src/components/DateInput/DateInput.props.ts +80 -0
- package/src/components/DateInput/DateInput.stories.tsx +269 -0
- package/src/components/DateInput/DateInput.tsx +117 -0
- package/src/components/DateInput/DateInputSegment.tsx +83 -0
- package/src/components/DateInput/index.ts +2 -0
- package/src/components/PillGroup/Pill.props.ts +13 -0
- package/src/components/PillGroup/Pill.tsx +120 -0
- package/src/components/PillGroup/PillGroup.context.tsx +12 -0
- package/src/components/PillGroup/PillGroup.docs.mdx +96 -0
- package/src/components/PillGroup/PillGroup.props.ts +22 -0
- package/src/components/PillGroup/PillGroup.stories.tsx +159 -0
- package/src/components/PillGroup/PillGroup.tsx +66 -0
- package/src/components/PillGroup/index.ts +4 -0
- package/src/components/Select/Select.tsx +2 -0
- package/src/components/Toast/Toast.context.tsx +118 -0
- package/src/components/Toast/Toast.docs.mdx +164 -0
- package/src/components/Toast/Toast.props.ts +33 -0
- package/src/components/Toast/Toast.stories.tsx +356 -0
- package/src/components/Toast/ToastItem.tsx +200 -0
- package/src/components/Toast/index.ts +3 -0
- package/src/components/index.ts +4 -0
- package/src/tokens/components/dark/checkbox.ts +3 -0
- package/src/tokens/components/dark/input.ts +6 -0
- package/src/tokens/components/dark/radio.ts +3 -0
- package/src/tokens/components/dark/table.ts +2 -0
- package/src/tokens/components/dark/toast.ts +6 -2
- package/src/tokens/components/light/checkbox.ts +3 -0
- package/src/tokens/components/light/input.ts +6 -0
- package/src/tokens/components/light/radio.ts +3 -0
- package/src/tokens/components/light/table.ts +2 -0
- package/src/tokens/components/light/toast.ts +6 -2
- package/src/utils/getInitials.ts +7 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { TextInputProps } from 'react-native';
|
|
2
|
+
import type { FormFieldBaseProps } from '../FormField/FormField.props';
|
|
3
|
+
|
|
4
|
+
export interface DateInputProps extends FormFieldBaseProps {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the day segment is hidden.
|
|
7
|
+
* @default false
|
|
8
|
+
*/
|
|
9
|
+
hideDay?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the month segment is hidden.
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
hideMonth?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the year segment is hidden.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
hideYear?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Placeholder text for the day segment.
|
|
22
|
+
*/
|
|
23
|
+
dayPlaceholder?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Placeholder text for the month segment.
|
|
26
|
+
*/
|
|
27
|
+
monthPlaceholder?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Placeholder text for the year segment.
|
|
30
|
+
*/
|
|
31
|
+
yearPlaceholder?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The controlled value for the day segment. Must be used with an `onDayChange` handler.
|
|
34
|
+
*/
|
|
35
|
+
dayValue?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The controlled value for the month segment. Must be used with an `onMonthChange` handler.
|
|
38
|
+
*/
|
|
39
|
+
monthValue?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The controlled value for the year segment. Must be used with an `onYearChange` handler.
|
|
42
|
+
*/
|
|
43
|
+
yearValue?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Callback fired when the day value changes.
|
|
46
|
+
*/
|
|
47
|
+
onDayChange?: (text: string) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Callback fired when the month value changes.
|
|
50
|
+
*/
|
|
51
|
+
onMonthChange?: (text: string) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Callback fired when the year value changes.
|
|
54
|
+
*/
|
|
55
|
+
onYearChange?: (text: string) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Callback fired when the day segment receives focus.
|
|
58
|
+
*/
|
|
59
|
+
onDayFocus?: TextInputProps['onFocus'];
|
|
60
|
+
/**
|
|
61
|
+
* Callback fired when the month segment receives focus.
|
|
62
|
+
*/
|
|
63
|
+
onMonthFocus?: TextInputProps['onFocus'];
|
|
64
|
+
/**
|
|
65
|
+
* Callback fired when the year segment receives focus.
|
|
66
|
+
*/
|
|
67
|
+
onYearFocus?: TextInputProps['onFocus'];
|
|
68
|
+
/**
|
|
69
|
+
* Callback fired when the day segment loses focus.
|
|
70
|
+
*/
|
|
71
|
+
onDayBlur?: TextInputProps['onBlur'];
|
|
72
|
+
/**
|
|
73
|
+
* Callback fired when the month segment loses focus.
|
|
74
|
+
*/
|
|
75
|
+
onMonthBlur?: TextInputProps['onBlur'];
|
|
76
|
+
/**
|
|
77
|
+
* Callback fired when the year segment loses focus.
|
|
78
|
+
*/
|
|
79
|
+
onYearBlur?: TextInputProps['onBlur'];
|
|
80
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native';
|
|
2
|
+
import { TickSmallIcon, WarningSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { View } from 'react-native';
|
|
5
|
+
import { Button, Card, Flex, Heading } from '../../components';
|
|
6
|
+
import { DateInput } from './';
|
|
7
|
+
|
|
8
|
+
const DateInputMeta: Meta<typeof DateInput> = {
|
|
9
|
+
title: 'Stories / DateInput',
|
|
10
|
+
component: DateInput,
|
|
11
|
+
parameters: {
|
|
12
|
+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
label: {
|
|
17
|
+
control: 'text',
|
|
18
|
+
},
|
|
19
|
+
helperText: {
|
|
20
|
+
control: 'text',
|
|
21
|
+
},
|
|
22
|
+
validationStatus: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['initial', 'valid', 'invalid'],
|
|
25
|
+
},
|
|
26
|
+
disabled: {
|
|
27
|
+
control: 'boolean',
|
|
28
|
+
},
|
|
29
|
+
required: {
|
|
30
|
+
control: 'boolean',
|
|
31
|
+
},
|
|
32
|
+
hideDay: {
|
|
33
|
+
control: 'boolean',
|
|
34
|
+
},
|
|
35
|
+
hideMonth: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
},
|
|
38
|
+
hideYear: {
|
|
39
|
+
control: 'boolean',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default DateInputMeta;
|
|
45
|
+
|
|
46
|
+
type Story = StoryObj<typeof DateInput>;
|
|
47
|
+
|
|
48
|
+
export const Playground: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
label: 'Date',
|
|
51
|
+
helperText: 'Helper text',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const DateOfBirth: Story = {
|
|
56
|
+
render: () => {
|
|
57
|
+
return <DateInput label="Date of birth" helperText="Enter your date of birth" required />;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const CardExpiry: Story = {
|
|
62
|
+
render: () => {
|
|
63
|
+
const [month, setMonth] = useState('');
|
|
64
|
+
const [year, setYear] = useState('');
|
|
65
|
+
return (
|
|
66
|
+
<DateInput
|
|
67
|
+
label="Card expiry"
|
|
68
|
+
helperText="Enter the expiry month and year"
|
|
69
|
+
monthValue={month}
|
|
70
|
+
yearValue={year}
|
|
71
|
+
onMonthChange={setMonth}
|
|
72
|
+
onYearChange={setYear}
|
|
73
|
+
hideDay
|
|
74
|
+
required
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const YearOnly: Story = {
|
|
81
|
+
render: () => {
|
|
82
|
+
return <DateInput label="Year" helperText="Enter the year" hideDay hideMonth />;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const Validation: Story = {
|
|
87
|
+
render: () => {
|
|
88
|
+
const [day, setDay] = useState('01');
|
|
89
|
+
const [month, setMonth] = useState('02');
|
|
90
|
+
const [year, setYear] = useState('2025');
|
|
91
|
+
return (
|
|
92
|
+
<View style={{ gap: 16 }}>
|
|
93
|
+
<DateInput
|
|
94
|
+
label="Valid date"
|
|
95
|
+
dayValue={day}
|
|
96
|
+
monthValue={month}
|
|
97
|
+
yearValue={year}
|
|
98
|
+
onDayChange={setDay}
|
|
99
|
+
onMonthChange={setMonth}
|
|
100
|
+
onYearChange={setYear}
|
|
101
|
+
validationStatus="valid"
|
|
102
|
+
validText="Date is valid"
|
|
103
|
+
helperIcon={TickSmallIcon}
|
|
104
|
+
required
|
|
105
|
+
/>
|
|
106
|
+
<DateInput
|
|
107
|
+
label="Invalid date"
|
|
108
|
+
dayValue="32"
|
|
109
|
+
monthValue="13"
|
|
110
|
+
yearValue="2025"
|
|
111
|
+
validationStatus="invalid"
|
|
112
|
+
invalidText="Please enter a valid date"
|
|
113
|
+
helperIcon={WarningSmallIcon}
|
|
114
|
+
required
|
|
115
|
+
/>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const Disabled: Story = {
|
|
122
|
+
render: () => {
|
|
123
|
+
return (
|
|
124
|
+
<DateInput
|
|
125
|
+
label="Date of birth"
|
|
126
|
+
helperText="This field is disabled"
|
|
127
|
+
dayValue="15"
|
|
128
|
+
monthValue="06"
|
|
129
|
+
yearValue="1990"
|
|
130
|
+
disabled
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const DefaultValue: Story = {
|
|
137
|
+
render: () => {
|
|
138
|
+
return <DateInput label="Date of birth" dayValue="01" monthValue="01" yearValue="2000" />;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const WithCustomValidation: Story = {
|
|
143
|
+
render: () => {
|
|
144
|
+
const [day, setDay] = useState('');
|
|
145
|
+
const [month, setMonth] = useState('');
|
|
146
|
+
const [year, setYear] = useState('');
|
|
147
|
+
|
|
148
|
+
const validateDate = () => {
|
|
149
|
+
if (!day || !month || !year) return { status: 'initial' as const, message: '' };
|
|
150
|
+
|
|
151
|
+
const dayNum = parseInt(day, 10);
|
|
152
|
+
const monthNum = parseInt(month, 10);
|
|
153
|
+
const yearNum = parseInt(year, 10);
|
|
154
|
+
|
|
155
|
+
// Basic validation
|
|
156
|
+
if (dayNum < 1 || dayNum > 31) {
|
|
157
|
+
return { status: 'invalid' as const, message: 'Day must be between 1 and 31' };
|
|
158
|
+
}
|
|
159
|
+
if (monthNum < 1 || monthNum > 12) {
|
|
160
|
+
return { status: 'invalid' as const, message: 'Month must be between 1 and 12' };
|
|
161
|
+
}
|
|
162
|
+
if (yearNum < 1900 || yearNum > new Date().getFullYear()) {
|
|
163
|
+
return {
|
|
164
|
+
status: 'invalid' as const,
|
|
165
|
+
message: `Year must be between 1900 and ${new Date().getFullYear()}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check valid date
|
|
170
|
+
const date = new Date(yearNum, monthNum - 1, dayNum);
|
|
171
|
+
if (
|
|
172
|
+
date.getDate() !== dayNum ||
|
|
173
|
+
date.getMonth() !== monthNum - 1 ||
|
|
174
|
+
date.getFullYear() !== yearNum
|
|
175
|
+
) {
|
|
176
|
+
return { status: 'invalid' as const, message: 'Please enter a valid date' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { status: 'valid' as const, message: 'Valid date' };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const validation = validateDate();
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<DateInput
|
|
186
|
+
label="Date of birth"
|
|
187
|
+
helperText="Enter a valid date between 1900 and today"
|
|
188
|
+
dayValue={day}
|
|
189
|
+
monthValue={month}
|
|
190
|
+
yearValue={year}
|
|
191
|
+
onDayChange={setDay}
|
|
192
|
+
onMonthChange={setMonth}
|
|
193
|
+
onYearChange={setYear}
|
|
194
|
+
validationStatus={validation.status}
|
|
195
|
+
validText={validation.status === 'valid' ? validation.message : undefined}
|
|
196
|
+
invalidText={validation.status === 'invalid' ? validation.message : undefined}
|
|
197
|
+
required
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const FlexibleSegments: Story = {
|
|
204
|
+
render: () => (
|
|
205
|
+
<View style={{ gap: 16 }}>
|
|
206
|
+
<DateInput label="Full date" helperText="DD/MM/YYYY" />
|
|
207
|
+
<DateInput label="Month and year" helperText="MM/YYYY" hideDay required />
|
|
208
|
+
<DateInput label="Year only" helperText="YYYY" hideDay hideMonth required />
|
|
209
|
+
</View>
|
|
210
|
+
),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const GroupingInputs: Story = {
|
|
214
|
+
render: () => (
|
|
215
|
+
<Flex space="sm">
|
|
216
|
+
<Heading size="lg">Event Registration</Heading>
|
|
217
|
+
<Card variant="subtle" gap="250">
|
|
218
|
+
<DateInput label="Date of birth" helperText="Enter your date of birth" required />
|
|
219
|
+
<DateInput
|
|
220
|
+
label="Event date preference"
|
|
221
|
+
helperText="Select your preferred date"
|
|
222
|
+
required={false}
|
|
223
|
+
/>
|
|
224
|
+
</Card>
|
|
225
|
+
</Flex>
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const WithState: Story = {
|
|
230
|
+
render: () => {
|
|
231
|
+
const [day, setDay] = useState('');
|
|
232
|
+
const [month, setMonth] = useState('');
|
|
233
|
+
const [year, setYear] = useState('');
|
|
234
|
+
|
|
235
|
+
const handleReset = () => {
|
|
236
|
+
setDay('');
|
|
237
|
+
setMonth('');
|
|
238
|
+
setYear('');
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const handleSetToday = () => {
|
|
242
|
+
const today = new Date();
|
|
243
|
+
setDay(String(today.getDate()).padStart(2, '0'));
|
|
244
|
+
setMonth(String(today.getMonth() + 1).padStart(2, '0'));
|
|
245
|
+
setYear(String(today.getFullYear()));
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<Flex space="md">
|
|
250
|
+
<DateInput
|
|
251
|
+
label="Date"
|
|
252
|
+
helperText="Select or enter a date"
|
|
253
|
+
dayValue={day}
|
|
254
|
+
monthValue={month}
|
|
255
|
+
yearValue={year}
|
|
256
|
+
onDayChange={setDay}
|
|
257
|
+
onMonthChange={setMonth}
|
|
258
|
+
onYearChange={setYear}
|
|
259
|
+
/>
|
|
260
|
+
<Flex space="xs">
|
|
261
|
+
<Button onPress={handleSetToday}>Set to Today</Button>
|
|
262
|
+
<Button onPress={handleReset} variant="solid">
|
|
263
|
+
Reset
|
|
264
|
+
</Button>
|
|
265
|
+
</Flex>
|
|
266
|
+
</Flex>
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
3
|
+
import { FormField } from '../FormField';
|
|
4
|
+
import type { DateInputProps } from './DateInput.props';
|
|
5
|
+
import DateInputSegment from './DateInputSegment';
|
|
6
|
+
|
|
7
|
+
const DateInput = ({
|
|
8
|
+
label,
|
|
9
|
+
helperText,
|
|
10
|
+
helperIcon,
|
|
11
|
+
validationStatus = 'initial',
|
|
12
|
+
validText,
|
|
13
|
+
invalidText,
|
|
14
|
+
disabled,
|
|
15
|
+
readonly,
|
|
16
|
+
required,
|
|
17
|
+
hideDay = false,
|
|
18
|
+
hideMonth = false,
|
|
19
|
+
hideYear = false,
|
|
20
|
+
dayPlaceholder = 'DD',
|
|
21
|
+
monthPlaceholder = 'MM',
|
|
22
|
+
yearPlaceholder = 'YYYY',
|
|
23
|
+
dayValue,
|
|
24
|
+
monthValue,
|
|
25
|
+
yearValue,
|
|
26
|
+
onDayChange,
|
|
27
|
+
onMonthChange,
|
|
28
|
+
onYearChange,
|
|
29
|
+
onDayFocus,
|
|
30
|
+
onMonthFocus,
|
|
31
|
+
onYearFocus,
|
|
32
|
+
onDayBlur,
|
|
33
|
+
onMonthBlur,
|
|
34
|
+
onYearBlur,
|
|
35
|
+
...props
|
|
36
|
+
}: DateInputProps) => {
|
|
37
|
+
return (
|
|
38
|
+
<FormField
|
|
39
|
+
label={label}
|
|
40
|
+
helperText={helperText}
|
|
41
|
+
helperIcon={helperIcon}
|
|
42
|
+
validationStatus={validationStatus}
|
|
43
|
+
validText={validText}
|
|
44
|
+
invalidText={invalidText}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
readonly={readonly}
|
|
47
|
+
required={required}
|
|
48
|
+
style={styles.wrap}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
<View style={styles.container}>
|
|
52
|
+
{!hideDay && (
|
|
53
|
+
<DateInputSegment
|
|
54
|
+
label="Day"
|
|
55
|
+
placeholder={dayPlaceholder}
|
|
56
|
+
value={dayValue}
|
|
57
|
+
onChange={onDayChange}
|
|
58
|
+
onFocus={onDayFocus}
|
|
59
|
+
onBlur={onDayBlur}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
required={required}
|
|
62
|
+
readonly={readonly}
|
|
63
|
+
validationStatus={validationStatus}
|
|
64
|
+
maxLength={2}
|
|
65
|
+
testID="date-input-day"
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
{!hideMonth && (
|
|
69
|
+
<DateInputSegment
|
|
70
|
+
label="Month"
|
|
71
|
+
placeholder={monthPlaceholder}
|
|
72
|
+
value={monthValue}
|
|
73
|
+
onChange={onMonthChange}
|
|
74
|
+
onFocus={onMonthFocus}
|
|
75
|
+
onBlur={onMonthBlur}
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
required={required}
|
|
78
|
+
readonly={readonly}
|
|
79
|
+
validationStatus={validationStatus}
|
|
80
|
+
maxLength={2}
|
|
81
|
+
testID="date-input-month"
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
{!hideYear && (
|
|
85
|
+
<DateInputSegment
|
|
86
|
+
label="Year"
|
|
87
|
+
placeholder={yearPlaceholder}
|
|
88
|
+
value={yearValue}
|
|
89
|
+
onChange={onYearChange}
|
|
90
|
+
onFocus={onYearFocus}
|
|
91
|
+
onBlur={onYearBlur}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
required={required}
|
|
94
|
+
readonly={readonly}
|
|
95
|
+
validationStatus={validationStatus}
|
|
96
|
+
maxLength={4}
|
|
97
|
+
testID="date-input-year"
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</View>
|
|
101
|
+
</FormField>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
DateInput.displayName = 'DateInput';
|
|
106
|
+
|
|
107
|
+
const styles = StyleSheet.create(theme => ({
|
|
108
|
+
wrap: {
|
|
109
|
+
gap: theme.components.input.gap,
|
|
110
|
+
},
|
|
111
|
+
container: {
|
|
112
|
+
flexDirection: 'row',
|
|
113
|
+
gap: theme.components.input.date.gap,
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
export default DateInput;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
3
|
+
import { BodyText } from '../BodyText';
|
|
4
|
+
import { Input } from '../Input';
|
|
5
|
+
import type { DateInputProps } from './DateInput.props';
|
|
6
|
+
|
|
7
|
+
interface DateInputSegmentProps {
|
|
8
|
+
label: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
onChange?: (text: string) => void;
|
|
12
|
+
onFocus?: DateInputProps['onDayFocus'];
|
|
13
|
+
onBlur?: DateInputProps['onDayBlur'];
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
validationStatus?: DateInputProps['validationStatus'];
|
|
17
|
+
maxLength?: number;
|
|
18
|
+
readonly?: boolean;
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DateInputSegment = ({
|
|
23
|
+
label,
|
|
24
|
+
placeholder,
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
onFocus,
|
|
28
|
+
onBlur,
|
|
29
|
+
disabled,
|
|
30
|
+
validationStatus,
|
|
31
|
+
maxLength,
|
|
32
|
+
readonly,
|
|
33
|
+
testID,
|
|
34
|
+
}: DateInputSegmentProps) => {
|
|
35
|
+
styles.useVariants({ disabled });
|
|
36
|
+
return (
|
|
37
|
+
<View style={styles.container}>
|
|
38
|
+
<BodyText size="md" style={styles.label}>
|
|
39
|
+
{label}
|
|
40
|
+
</BodyText>
|
|
41
|
+
<Input
|
|
42
|
+
value={value}
|
|
43
|
+
onChangeText={onChange}
|
|
44
|
+
onFocus={onFocus}
|
|
45
|
+
onBlur={onBlur}
|
|
46
|
+
placeholder={disabled ? undefined : placeholder}
|
|
47
|
+
keyboardType="number-pad"
|
|
48
|
+
maxLength={maxLength}
|
|
49
|
+
testID={testID}
|
|
50
|
+
accessibilityLabel={label}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
validationStatus={validationStatus}
|
|
53
|
+
readonly={readonly}
|
|
54
|
+
style={styles.input}
|
|
55
|
+
/>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
DateInputSegment.displayName = 'DateInputSegment';
|
|
61
|
+
|
|
62
|
+
const styles = StyleSheet.create(theme => ({
|
|
63
|
+
container: {
|
|
64
|
+
flex: 1,
|
|
65
|
+
gap: theme.components.input.gap,
|
|
66
|
+
maxWidth: 96,
|
|
67
|
+
},
|
|
68
|
+
label: {
|
|
69
|
+
variants: {
|
|
70
|
+
disabled: {
|
|
71
|
+
true: {
|
|
72
|
+
opacity: theme.opacity.disabled,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
input: {
|
|
78
|
+
flex: 1,
|
|
79
|
+
maxWidth: 96,
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
export default DateInputSegment;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PressableProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface PillProps extends Omit<PressableProps, 'children'> {
|
|
5
|
+
/** Value returned when selected */
|
|
6
|
+
value: string;
|
|
7
|
+
|
|
8
|
+
/** Text label shown inside the pill */
|
|
9
|
+
label: string;
|
|
10
|
+
|
|
11
|
+
/** Left icon */
|
|
12
|
+
icon?: React.ComponentType<any>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createPressable } from '@gluestack-ui/pressable';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
4
|
+
import { Icon } from '../Icon';
|
|
5
|
+
import { BodyText } from '../BodyText';
|
|
6
|
+
import { usePillGroupContext } from './PillGroup.context';
|
|
7
|
+
import type { PillProps } from './Pill.props';
|
|
8
|
+
|
|
9
|
+
const PillRoot = ({
|
|
10
|
+
value,
|
|
11
|
+
label,
|
|
12
|
+
icon,
|
|
13
|
+
states = {},
|
|
14
|
+
...props
|
|
15
|
+
}: PillProps & { states?: { active?: boolean } }) => {
|
|
16
|
+
const { active } = states;
|
|
17
|
+
const context = usePillGroupContext();
|
|
18
|
+
const isSelected = context?.value.includes(value) ?? false;
|
|
19
|
+
|
|
20
|
+
styles.useVariants({ selected: isSelected, active });
|
|
21
|
+
|
|
22
|
+
const handlePress = () => {
|
|
23
|
+
context?.onChange(value);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Pressable
|
|
28
|
+
{...props}
|
|
29
|
+
style={styles.pill}
|
|
30
|
+
accessibilityRole="button"
|
|
31
|
+
accessibilityState={{ selected: isSelected }}
|
|
32
|
+
onPress={handlePress}
|
|
33
|
+
>
|
|
34
|
+
{icon && <Icon as={icon} size="sm" style={styles.icon} />}
|
|
35
|
+
<BodyText weight="semibold" style={styles.text}>
|
|
36
|
+
{label}
|
|
37
|
+
</BodyText>
|
|
38
|
+
</Pressable>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Pill = createPressable({ Root: PillRoot });
|
|
43
|
+
|
|
44
|
+
Pill.displayName = 'Pill';
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create(theme => ({
|
|
47
|
+
pill: {
|
|
48
|
+
flexDirection: 'row',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
height: theme.components.pill.height,
|
|
52
|
+
minWidth: theme.components.pill.minWidth,
|
|
53
|
+
gap: theme.components.pill.gap,
|
|
54
|
+
paddingHorizontal: theme.components.pill.paddingHorizontal,
|
|
55
|
+
paddingVertical: theme.components.pill.paddingVertical,
|
|
56
|
+
borderRadius: theme.components.pill.borderRadius,
|
|
57
|
+
borderWidth: theme.components.pill.borderWidth,
|
|
58
|
+
borderColor: theme.color.interactive.neutral.border.subtle,
|
|
59
|
+
backgroundColor: 'transparent',
|
|
60
|
+
_web: {
|
|
61
|
+
_hover: {
|
|
62
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.hover,
|
|
63
|
+
},
|
|
64
|
+
'_focus-visible': theme.helpers.focusVisible,
|
|
65
|
+
},
|
|
66
|
+
variants: {
|
|
67
|
+
active: {
|
|
68
|
+
true: {
|
|
69
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
selected: {
|
|
73
|
+
true: {
|
|
74
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.default,
|
|
75
|
+
borderColor: theme.color.interactive.brand.surface.strong.default,
|
|
76
|
+
_web: {
|
|
77
|
+
_hover: {
|
|
78
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.hover,
|
|
79
|
+
borderColor: theme.color.interactive.brand.surface.strong.hover,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
compoundVariants: [
|
|
86
|
+
{
|
|
87
|
+
selected: true,
|
|
88
|
+
active: true,
|
|
89
|
+
styles: {
|
|
90
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.active,
|
|
91
|
+
borderColor: theme.color.interactive.brand.surface.strong.active,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
text: {
|
|
97
|
+
variants: {
|
|
98
|
+
selected: {
|
|
99
|
+
true: {
|
|
100
|
+
color: theme.color.text.inverted,
|
|
101
|
+
},
|
|
102
|
+
false: {
|
|
103
|
+
color: theme.color.text.primary,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
icon: {
|
|
109
|
+
variants: {
|
|
110
|
+
selected: {
|
|
111
|
+
true: {
|
|
112
|
+
color: theme.color.icon.inverted,
|
|
113
|
+
},
|
|
114
|
+
false: {
|
|
115
|
+
color: theme.color.icon.primary,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
}));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface PillGroupContextValue {
|
|
4
|
+
value: string[];
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const PillGroupContext = createContext<PillGroupContextValue | null>(null);
|
|
9
|
+
|
|
10
|
+
export const usePillGroupContext = () => {
|
|
11
|
+
return useContext(PillGroupContext);
|
|
12
|
+
};
|