@utilitywarehouse/hearth-react-native 0.27.2 → 0.28.0-testid-fix-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/.turbo/turbo-build.log +5 -4
- package/.turbo/turbo-lint.log +70 -69
- package/CHANGELOG.md +149 -0
- package/build/components/Button/ButtonRoot.js +8 -0
- package/build/components/Combobox/Combobox.context.d.ts +13 -0
- package/build/components/Combobox/Combobox.context.js +9 -0
- package/build/components/Combobox/Combobox.d.ts +6 -0
- package/build/components/Combobox/Combobox.js +246 -0
- package/build/components/Combobox/Combobox.props.d.ts +180 -0
- package/build/components/Combobox/Combobox.props.js +1 -0
- package/build/components/Combobox/ComboboxOption.d.ts +6 -0
- package/build/components/Combobox/ComboboxOption.js +56 -0
- package/build/components/Combobox/index.d.ts +4 -0
- package/build/components/Combobox/index.js +3 -0
- package/build/components/DatePicker/TimePicker.d.ts +3 -0
- package/build/components/DatePicker/TimePicker.js +84 -0
- package/build/components/DatePicker/time-picker/animated-math.d.ts +4 -0
- package/build/components/DatePicker/time-picker/animated-math.js +19 -0
- package/build/components/DatePicker/time-picker/period-native.d.ts +6 -0
- package/build/components/DatePicker/time-picker/period-native.js +17 -0
- package/build/components/DatePicker/time-picker/period-picker.d.ts +6 -0
- package/build/components/DatePicker/time-picker/period-picker.js +10 -0
- package/build/components/DatePicker/time-picker/period-web.d.ts +6 -0
- package/build/components/DatePicker/time-picker/period-web.js +21 -0
- package/build/components/DatePicker/time-picker/wheel-native.d.ts +8 -0
- package/build/components/DatePicker/time-picker/wheel-native.js +19 -0
- package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +2 -0
- package/build/components/DatePicker/time-picker/wheel-picker/index.js +2 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +16 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +97 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +21 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +88 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +23 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +21 -0
- package/build/components/DatePicker/time-picker/wheel-web.d.ts +8 -0
- package/build/components/DatePicker/time-picker/wheel-web.js +146 -0
- package/build/components/DatePicker/time-picker/wheel.d.ts +8 -0
- package/build/components/DatePicker/time-picker/wheel.js +10 -0
- package/build/components/List/List.js +2 -2
- package/build/components/Modal/Modal.js +31 -42
- package/build/components/Modal/Modal.web.js +3 -3
- package/build/components/Pagination/Pagination.d.ts +6 -0
- package/build/components/Pagination/Pagination.js +125 -0
- package/build/components/Pagination/Pagination.props.d.ts +26 -0
- package/build/components/Pagination/Pagination.props.js +1 -0
- package/build/components/Pagination/Pagination.utils.d.ts +2 -0
- package/build/components/Pagination/Pagination.utils.js +20 -0
- package/build/components/Pagination/Pagination.utils.test.d.ts +1 -0
- package/build/components/Pagination/Pagination.utils.test.js +16 -0
- package/build/components/Pagination/index.d.ts +2 -0
- package/build/components/Pagination/index.js +1 -0
- package/build/components/SafeAreaView/SafeAreaView.d.ts +5 -0
- package/build/components/SafeAreaView/SafeAreaView.js +117 -0
- package/build/components/SafeAreaView/SafeAreaView.props.d.ts +17 -0
- package/build/components/SafeAreaView/SafeAreaView.props.js +1 -0
- package/build/components/SafeAreaView/index.d.ts +2 -0
- package/build/components/SafeAreaView/index.js +1 -0
- package/build/components/Select/Select.d.ts +1 -1
- package/build/components/Select/Select.js +6 -5
- package/build/components/Select/Select.props.d.ts +4 -0
- package/build/components/Select/SelectOption.d.ts +1 -1
- package/build/components/Select/SelectOption.js +2 -2
- package/build/components/Table/Table.context.d.ts +12 -0
- package/build/components/Table/Table.context.js +9 -0
- package/build/components/Table/Table.d.ts +6 -0
- package/build/components/Table/Table.js +71 -0
- package/build/components/Table/Table.props.d.ts +56 -0
- package/build/components/Table/Table.props.js +1 -0
- package/build/components/Table/Table.utils.d.ts +5 -0
- package/build/components/Table/Table.utils.js +48 -0
- package/build/components/Table/Table.utils.test.d.ts +1 -0
- package/build/components/Table/Table.utils.test.js +71 -0
- package/build/components/Table/TableBody.d.ts +6 -0
- package/build/components/Table/TableBody.js +16 -0
- package/build/components/Table/TableCell.d.ts +10 -0
- package/build/components/Table/TableCell.js +44 -0
- package/build/components/Table/TableHeader.d.ts +6 -0
- package/build/components/Table/TableHeader.js +24 -0
- package/build/components/Table/TableHeaderCell.d.ts +10 -0
- package/build/components/Table/TableHeaderCell.js +97 -0
- package/build/components/Table/TablePagination.d.ts +6 -0
- package/build/components/Table/TablePagination.js +7 -0
- package/build/components/Table/TableRow.d.ts +8 -0
- package/build/components/Table/TableRow.js +25 -0
- package/build/components/Table/index.d.ts +8 -0
- package/build/components/Table/index.js +7 -0
- package/build/components/Timeline/Timeline.d.ts +6 -0
- package/build/components/Timeline/Timeline.js +34 -0
- package/build/components/Timeline/Timeline.props.d.ts +47 -0
- package/build/components/Timeline/Timeline.props.js +1 -0
- package/build/components/Timeline/TimelineItem.d.ts +6 -0
- package/build/components/Timeline/TimelineItem.js +235 -0
- package/build/components/Timeline/index.d.ts +3 -0
- package/build/components/Timeline/index.js +2 -0
- package/build/components/VerificationInput/VerificationInput.js +3 -3
- package/build/components/index.d.ts +5 -0
- package/build/components/index.js +5 -0
- package/build/tokens/components/dark/timeline.d.ts +2 -2
- package/build/tokens/components/dark/timeline.js +2 -2
- package/docs/components/AllComponents.web.tsx +106 -23
- package/docs/llm-docs/unistyles-llms-full.txt +1132 -534
- package/docs/llm-docs/unistyles-llms-small.txt +37 -37
- package/package.json +4 -4
- package/src/components/Button/Button.stories.tsx +43 -7
- package/src/components/Button/ButtonRoot.tsx +8 -0
- package/src/components/Combobox/Combobox.context.ts +26 -0
- package/src/components/Combobox/Combobox.docs.mdx +277 -0
- package/src/components/Combobox/Combobox.figma.tsx +60 -0
- package/src/components/Combobox/Combobox.props.ts +187 -0
- package/src/components/Combobox/Combobox.stories.tsx +233 -0
- package/src/components/Combobox/Combobox.tsx +446 -0
- package/src/components/Combobox/ComboboxOption.tsx +100 -0
- package/src/components/Combobox/index.ts +9 -0
- package/src/components/List/List.tsx +5 -4
- package/src/components/Modal/Modal.tsx +67 -74
- package/src/components/Modal/Modal.web.tsx +3 -3
- package/src/components/Pagination/Pagination.docs.mdx +99 -0
- package/src/components/Pagination/Pagination.figma.tsx +20 -0
- package/src/components/Pagination/Pagination.props.ts +28 -0
- package/src/components/Pagination/Pagination.stories.tsx +88 -0
- package/src/components/Pagination/Pagination.tsx +248 -0
- package/src/components/Pagination/Pagination.utils.test.ts +20 -0
- package/src/components/Pagination/Pagination.utils.ts +37 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/SafeAreaView/SafeAreaView.props.ts +20 -0
- package/src/components/SafeAreaView/SafeAreaView.tsx +173 -0
- package/src/components/SafeAreaView/index.ts +2 -0
- package/src/components/Select/Select.props.ts +4 -0
- package/src/components/Select/Select.tsx +35 -28
- package/src/components/Select/SelectOption.tsx +2 -0
- package/src/components/Table/Table.context.tsx +23 -0
- package/src/components/Table/Table.docs.mdx +239 -0
- package/src/components/Table/Table.figma.tsx +65 -0
- package/src/components/Table/Table.props.ts +65 -0
- package/src/components/Table/Table.stories.tsx +399 -0
- package/src/components/Table/Table.tsx +127 -0
- package/src/components/Table/Table.utils.test.ts +82 -0
- package/src/components/Table/Table.utils.ts +72 -0
- package/src/components/Table/TableBody.tsx +25 -0
- package/src/components/Table/TableCell.tsx +67 -0
- package/src/components/Table/TableHeader.tsx +41 -0
- package/src/components/Table/TableHeaderCell.tsx +136 -0
- package/src/components/Table/TablePagination.tsx +10 -0
- package/src/components/Table/TableRow.tsx +42 -0
- package/src/components/Table/index.ts +16 -0
- package/src/components/Timeline/Timeline.docs.mdx +177 -0
- package/src/components/Timeline/Timeline.figma.tsx +89 -0
- package/src/components/Timeline/Timeline.props.ts +51 -0
- package/src/components/Timeline/Timeline.stories.tsx +102 -0
- package/src/components/Timeline/Timeline.tsx +48 -0
- package/src/components/Timeline/TimelineItem.tsx +293 -0
- package/src/components/Timeline/index.ts +9 -0
- package/src/components/VerificationInput/VerificationInput.tsx +3 -0
- package/src/components/index.ts +5 -0
- package/src/tokens/components/dark/timeline.ts +2 -2
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
|
+
import { ExpandSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Box } from '../Box';
|
|
5
|
+
import { Flex } from '../Flex';
|
|
6
|
+
import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
7
|
+
import {
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableHeaderCell,
|
|
13
|
+
TablePagination,
|
|
14
|
+
TableRow,
|
|
15
|
+
} from './';
|
|
16
|
+
|
|
17
|
+
type SortDirection = 'asc' | 'desc';
|
|
18
|
+
type SortStatus = 'Active' | 'Pending' | 'Paused' | 'Cancelled';
|
|
19
|
+
|
|
20
|
+
const rows = [
|
|
21
|
+
{
|
|
22
|
+
id: '1',
|
|
23
|
+
name: 'Alex Morgan',
|
|
24
|
+
email: 'alex@example.com',
|
|
25
|
+
phone: '020 7946 0931',
|
|
26
|
+
city: 'London',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: '2',
|
|
30
|
+
name: 'Priya Shah',
|
|
31
|
+
email: 'priya@example.com',
|
|
32
|
+
phone: '0161 496 0124',
|
|
33
|
+
city: 'Manchester',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: '3',
|
|
37
|
+
name: 'Chris Brown',
|
|
38
|
+
email: 'chris@example.com',
|
|
39
|
+
phone: '0117 496 1820',
|
|
40
|
+
city: 'Bristol',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: '4',
|
|
44
|
+
name: 'Nina Evans',
|
|
45
|
+
email: 'nina@example.com',
|
|
46
|
+
phone: '0141 496 2048',
|
|
47
|
+
city: 'Glasgow',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: '5',
|
|
51
|
+
name: 'Luca Rossi',
|
|
52
|
+
email: 'luca@example.com',
|
|
53
|
+
phone: '029 2199 4412',
|
|
54
|
+
city: 'Cardiff',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: '6',
|
|
58
|
+
name: 'Sarah Kent',
|
|
59
|
+
email: 'sarah@example.com',
|
|
60
|
+
phone: '0131 496 2755',
|
|
61
|
+
city: 'Edinburgh',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: '7',
|
|
65
|
+
name: 'Tom Hall',
|
|
66
|
+
email: 'tom@example.com',
|
|
67
|
+
phone: '0121 496 3901',
|
|
68
|
+
city: 'Birmingham',
|
|
69
|
+
},
|
|
70
|
+
{ id: '8', name: 'Emma Dale', email: 'emma@example.com', phone: '0113 496 1140', city: 'Leeds' },
|
|
71
|
+
{
|
|
72
|
+
id: '9',
|
|
73
|
+
name: 'James Cole',
|
|
74
|
+
email: 'james@example.com',
|
|
75
|
+
phone: '0191 496 4502',
|
|
76
|
+
city: 'Newcastle',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: '10',
|
|
80
|
+
name: 'Mia White',
|
|
81
|
+
email: 'mia@example.com',
|
|
82
|
+
phone: '0151 496 5620',
|
|
83
|
+
city: 'Liverpool',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const sortableRows = [
|
|
88
|
+
{ id: '1', customer: 'Alex Morgan', plan: 'Full Fibre 900', status: 'Pending' as SortStatus },
|
|
89
|
+
{ id: '2', customer: 'Priya Shah', plan: 'Energy Saver', status: 'Active' as SortStatus },
|
|
90
|
+
{ id: '3', customer: 'Chris Brown', plan: 'Mobile Unlimited', status: 'Paused' as SortStatus },
|
|
91
|
+
{ id: '4', customer: 'Nina Evans', plan: 'Home Insurance', status: 'Cancelled' as SortStatus },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const statusOrder = {
|
|
95
|
+
Active: 0,
|
|
96
|
+
Pending: 1,
|
|
97
|
+
Paused: 2,
|
|
98
|
+
Cancelled: 3,
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
const sortRowsByStatus = (items: typeof sortableRows, direction: SortDirection = 'asc') => {
|
|
102
|
+
const multiplier = direction === 'asc' ? 1 : -1;
|
|
103
|
+
|
|
104
|
+
return [...items].sort((left, right) => {
|
|
105
|
+
const statusDifference = (statusOrder[left.status] - statusOrder[right.status]) * multiplier;
|
|
106
|
+
|
|
107
|
+
if (statusDifference !== 0) {
|
|
108
|
+
return statusDifference;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return left.customer.localeCompare(right.customer) * multiplier;
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const meta = {
|
|
116
|
+
title: 'Stories / Table',
|
|
117
|
+
component: Table,
|
|
118
|
+
args: {
|
|
119
|
+
container: 'subtle',
|
|
120
|
+
},
|
|
121
|
+
argTypes: {
|
|
122
|
+
container: {
|
|
123
|
+
control: 'radio',
|
|
124
|
+
options: ['none', 'subtle', 'emphasis'],
|
|
125
|
+
},
|
|
126
|
+
pagination: {
|
|
127
|
+
control: false,
|
|
128
|
+
},
|
|
129
|
+
children: {
|
|
130
|
+
control: false,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
} satisfies Meta<typeof Table>;
|
|
134
|
+
|
|
135
|
+
export default meta;
|
|
136
|
+
type Story = StoryObj<typeof meta>;
|
|
137
|
+
|
|
138
|
+
const HeaderSortButton = ({ inverted, label }: { inverted: boolean; label: string }) => (
|
|
139
|
+
<UnstyledIconButton
|
|
140
|
+
accessibilityLabel={label}
|
|
141
|
+
icon={ExpandSmallIcon}
|
|
142
|
+
inverted={inverted}
|
|
143
|
+
size="sm"
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export const Playground: Story = {
|
|
148
|
+
render: (args: StoryObj<typeof meta.args>) => {
|
|
149
|
+
const headerColor = args.container === 'none' ? 'white' : 'purple';
|
|
150
|
+
const headerInverted = headerColor === 'purple';
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Table {...args}>
|
|
154
|
+
<TableHeader color={headerColor}>
|
|
155
|
+
<TableHeaderCell
|
|
156
|
+
trailingContent={<HeaderSortButton inverted={headerInverted} label="Sort by name" />}
|
|
157
|
+
>
|
|
158
|
+
Name
|
|
159
|
+
</TableHeaderCell>
|
|
160
|
+
<TableHeaderCell
|
|
161
|
+
trailingContent={<HeaderSortButton inverted={headerInverted} label="Sort by email" />}
|
|
162
|
+
>
|
|
163
|
+
Email
|
|
164
|
+
</TableHeaderCell>
|
|
165
|
+
<TableHeaderCell
|
|
166
|
+
trailingContent={<HeaderSortButton inverted={headerInverted} label="Sort by phone" />}
|
|
167
|
+
>
|
|
168
|
+
Phone
|
|
169
|
+
</TableHeaderCell>
|
|
170
|
+
<TableHeaderCell
|
|
171
|
+
trailingContent={<HeaderSortButton inverted={headerInverted} label="Sort by city" />}
|
|
172
|
+
>
|
|
173
|
+
City
|
|
174
|
+
</TableHeaderCell>
|
|
175
|
+
</TableHeader>
|
|
176
|
+
<TableBody>
|
|
177
|
+
{rows.slice(0, 5).map(person => (
|
|
178
|
+
<TableRow key={person.id}>
|
|
179
|
+
<TableHeaderCell row>{person.name}</TableHeaderCell>
|
|
180
|
+
<TableCell>{person.email}</TableCell>
|
|
181
|
+
<TableCell>{person.phone}</TableCell>
|
|
182
|
+
<TableCell>{person.city}</TableCell>
|
|
183
|
+
</TableRow>
|
|
184
|
+
))}
|
|
185
|
+
</TableBody>
|
|
186
|
+
</Table>
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const Variants: Story = {
|
|
192
|
+
render: () => (
|
|
193
|
+
<Flex direction="column" spacing="xl" style={{ width: '100%' }}>
|
|
194
|
+
{[
|
|
195
|
+
{ container: 'none' as const, color: 'white' as const },
|
|
196
|
+
{ container: 'subtle' as const, color: 'purple' as const },
|
|
197
|
+
{ container: 'emphasis' as const, color: 'purple' as const },
|
|
198
|
+
].map(({ container, color }) => (
|
|
199
|
+
<Table key={`${container}-${color}`} container={container}>
|
|
200
|
+
<TableHeader color={color}>
|
|
201
|
+
<TableHeaderCell
|
|
202
|
+
trailingContent={
|
|
203
|
+
<HeaderSortButton inverted={color === 'purple'} label="Sort by name" />
|
|
204
|
+
}
|
|
205
|
+
>
|
|
206
|
+
Name
|
|
207
|
+
</TableHeaderCell>
|
|
208
|
+
<TableHeaderCell
|
|
209
|
+
trailingContent={
|
|
210
|
+
<HeaderSortButton inverted={color === 'purple'} label="Sort by plan" />
|
|
211
|
+
}
|
|
212
|
+
>
|
|
213
|
+
Plan
|
|
214
|
+
</TableHeaderCell>
|
|
215
|
+
<TableHeaderCell
|
|
216
|
+
trailingContent={
|
|
217
|
+
<HeaderSortButton inverted={color === 'purple'} label="Sort by status" />
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
Status
|
|
221
|
+
</TableHeaderCell>
|
|
222
|
+
</TableHeader>
|
|
223
|
+
<TableBody>
|
|
224
|
+
<TableRow>
|
|
225
|
+
<TableHeaderCell row>Alex Morgan</TableHeaderCell>
|
|
226
|
+
<TableCell>Full Fibre 900</TableCell>
|
|
227
|
+
<TableCell>Active</TableCell>
|
|
228
|
+
</TableRow>
|
|
229
|
+
<TableRow>
|
|
230
|
+
<TableHeaderCell row>Priya Shah</TableHeaderCell>
|
|
231
|
+
<TableCell>Energy Saver</TableCell>
|
|
232
|
+
<TableCell>Pending</TableCell>
|
|
233
|
+
</TableRow>
|
|
234
|
+
</TableBody>
|
|
235
|
+
</Table>
|
|
236
|
+
))}
|
|
237
|
+
</Flex>
|
|
238
|
+
),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const WithPagination: Story = {
|
|
242
|
+
render: (args: StoryObj<typeof meta.args>) => {
|
|
243
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
244
|
+
const itemsPerPage = 4;
|
|
245
|
+
const totalPages = Math.ceil(rows.length / itemsPerPage);
|
|
246
|
+
const pageRows = rows.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
|
247
|
+
const headerColor = args.container === 'none' ? 'white' : 'purple';
|
|
248
|
+
const headerInverted = headerColor === 'purple';
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<Table
|
|
252
|
+
{...args}
|
|
253
|
+
pagination={
|
|
254
|
+
<TablePagination
|
|
255
|
+
currentPage={currentPage}
|
|
256
|
+
onPageChange={setCurrentPage}
|
|
257
|
+
totalPages={totalPages}
|
|
258
|
+
/>
|
|
259
|
+
}
|
|
260
|
+
>
|
|
261
|
+
<TableHeader color={headerColor}>
|
|
262
|
+
<TableHeaderCell
|
|
263
|
+
trailingContent={<HeaderSortButton inverted={headerInverted} label="Sort by name" />}
|
|
264
|
+
>
|
|
265
|
+
Name
|
|
266
|
+
</TableHeaderCell>
|
|
267
|
+
<TableHeaderCell
|
|
268
|
+
trailingContent={<HeaderSortButton inverted={headerInverted} label="Sort by email" />}
|
|
269
|
+
>
|
|
270
|
+
Email
|
|
271
|
+
</TableHeaderCell>
|
|
272
|
+
<TableHeaderCell
|
|
273
|
+
trailingContent={<HeaderSortButton inverted={headerInverted} label="Sort by city" />}
|
|
274
|
+
>
|
|
275
|
+
City
|
|
276
|
+
</TableHeaderCell>
|
|
277
|
+
</TableHeader>
|
|
278
|
+
<TableBody>
|
|
279
|
+
{pageRows.map(person => (
|
|
280
|
+
<TableRow key={person.id}>
|
|
281
|
+
<TableHeaderCell row>{person.name}</TableHeaderCell>
|
|
282
|
+
<TableCell>{person.email}</TableCell>
|
|
283
|
+
<TableCell>{person.city}</TableCell>
|
|
284
|
+
</TableRow>
|
|
285
|
+
))}
|
|
286
|
+
</TableBody>
|
|
287
|
+
</Table>
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export const NarrowViewport: Story = {
|
|
293
|
+
render: () => (
|
|
294
|
+
<Box style={{ width: 320 }}>
|
|
295
|
+
<Table
|
|
296
|
+
container="subtle"
|
|
297
|
+
pagination={
|
|
298
|
+
<TablePagination condensed currentPage={1} onPageChange={() => {}} totalPages={10} />
|
|
299
|
+
}
|
|
300
|
+
>
|
|
301
|
+
<TableHeader color="purple">
|
|
302
|
+
<TableHeaderCell trailingContent={<HeaderSortButton inverted label="Sort by account" />}>
|
|
303
|
+
Account
|
|
304
|
+
</TableHeaderCell>
|
|
305
|
+
<TableHeaderCell trailingContent={<HeaderSortButton inverted label="Sort by type" />}>
|
|
306
|
+
Type
|
|
307
|
+
</TableHeaderCell>
|
|
308
|
+
<TableHeaderCell
|
|
309
|
+
trailingContent={<HeaderSortButton inverted label="Sort by monthly cost" />}
|
|
310
|
+
>
|
|
311
|
+
Monthly cost
|
|
312
|
+
</TableHeaderCell>
|
|
313
|
+
<TableHeaderCell trailingContent={<HeaderSortButton inverted label="Sort by renewal" />}>
|
|
314
|
+
Renewal
|
|
315
|
+
</TableHeaderCell>
|
|
316
|
+
</TableHeader>
|
|
317
|
+
<TableBody>
|
|
318
|
+
<TableRow>
|
|
319
|
+
<TableHeaderCell row>Energy</TableHeaderCell>
|
|
320
|
+
<TableCell>Dual fuel</TableCell>
|
|
321
|
+
<TableCell>£132.50</TableCell>
|
|
322
|
+
<TableCell>12 Sep</TableCell>
|
|
323
|
+
</TableRow>
|
|
324
|
+
<TableRow>
|
|
325
|
+
<TableHeaderCell row>Broadband</TableHeaderCell>
|
|
326
|
+
<TableCell>Full Fibre 900</TableCell>
|
|
327
|
+
<TableCell>£44.00</TableCell>
|
|
328
|
+
<TableCell>22 Nov</TableCell>
|
|
329
|
+
</TableRow>
|
|
330
|
+
</TableBody>
|
|
331
|
+
</Table>
|
|
332
|
+
</Box>
|
|
333
|
+
),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export const ConfiguredColumnWidths: Story = {
|
|
337
|
+
render: () => (
|
|
338
|
+
<Table columnWidths={[180, '2fr', '1fr', 140]} container="subtle">
|
|
339
|
+
<TableHeader color="purple">
|
|
340
|
+
<TableHeaderCell>Name</TableHeaderCell>
|
|
341
|
+
<TableHeaderCell>Email</TableHeaderCell>
|
|
342
|
+
<TableHeaderCell>Plan</TableHeaderCell>
|
|
343
|
+
<TableHeaderCell>Status</TableHeaderCell>
|
|
344
|
+
</TableHeader>
|
|
345
|
+
<TableBody>
|
|
346
|
+
<TableRow>
|
|
347
|
+
<TableHeaderCell row>Alex Morgan</TableHeaderCell>
|
|
348
|
+
<TableCell>alex.longer-email@example.com</TableCell>
|
|
349
|
+
<TableCell>Full Fibre 900</TableCell>
|
|
350
|
+
<TableCell>Active</TableCell>
|
|
351
|
+
</TableRow>
|
|
352
|
+
<TableRow>
|
|
353
|
+
<TableHeaderCell row>Priya Shah</TableHeaderCell>
|
|
354
|
+
<TableCell>priya@example.com</TableCell>
|
|
355
|
+
<TableCell>Energy Saver</TableCell>
|
|
356
|
+
<TableCell>Pending</TableCell>
|
|
357
|
+
</TableRow>
|
|
358
|
+
</TableBody>
|
|
359
|
+
</Table>
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export const WithCustomSortFunction: Story = {
|
|
364
|
+
render: () => {
|
|
365
|
+
const [direction, setDirection] = useState<SortDirection>('asc');
|
|
366
|
+
const sortedRows = sortRowsByStatus(sortableRows, direction);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<Table columnWidths={[180, '2fr', 120]} container="subtle">
|
|
370
|
+
<TableHeader color="purple">
|
|
371
|
+
<TableHeaderCell>Customer</TableHeaderCell>
|
|
372
|
+
<TableHeaderCell>Plan</TableHeaderCell>
|
|
373
|
+
<TableHeaderCell
|
|
374
|
+
trailingContent={
|
|
375
|
+
<UnstyledIconButton
|
|
376
|
+
accessibilityLabel="Sort by custom status order"
|
|
377
|
+
icon={ExpandSmallIcon}
|
|
378
|
+
inverted
|
|
379
|
+
onPress={() => setDirection(current => (current === 'asc' ? 'desc' : 'asc'))}
|
|
380
|
+
size="sm"
|
|
381
|
+
/>
|
|
382
|
+
}
|
|
383
|
+
>
|
|
384
|
+
Status
|
|
385
|
+
</TableHeaderCell>
|
|
386
|
+
</TableHeader>
|
|
387
|
+
<TableBody>
|
|
388
|
+
{sortedRows.map(item => (
|
|
389
|
+
<TableRow key={item.id}>
|
|
390
|
+
<TableHeaderCell row>{item.customer}</TableHeaderCell>
|
|
391
|
+
<TableCell>{item.plan}</TableCell>
|
|
392
|
+
<TableCell>{item.status}</TableCell>
|
|
393
|
+
</TableRow>
|
|
394
|
+
))}
|
|
395
|
+
</TableBody>
|
|
396
|
+
</Table>
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Children, isValidElement, useState } from 'react';
|
|
2
|
+
import { type LayoutChangeEvent, View } from 'react-native';
|
|
3
|
+
import { ScrollView } from 'react-native-gesture-handler';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { Card } from '../Card';
|
|
6
|
+
import { TableContextProvider } from './Table.context';
|
|
7
|
+
import type { TableProps } from './Table.props';
|
|
8
|
+
import { getMinTableWidth } from './Table.utils';
|
|
9
|
+
|
|
10
|
+
const MIN_COLUMN_WIDTH = 120;
|
|
11
|
+
|
|
12
|
+
const getChildCount = (node: React.ReactNode): number => {
|
|
13
|
+
if (!isValidElement(node)) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const childProps = node.props as { children?: React.ReactNode };
|
|
18
|
+
return Children.count(childProps.children);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getMaxColumnCount = (children: React.ReactNode): number => {
|
|
22
|
+
let maxColumns = 1;
|
|
23
|
+
|
|
24
|
+
Children.forEach(children, child => {
|
|
25
|
+
if (!isValidElement(child)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const childType = child.type as { displayName?: string };
|
|
30
|
+
const childProps = child.props as { children?: React.ReactNode };
|
|
31
|
+
|
|
32
|
+
if (childType.displayName === 'TableHeader') {
|
|
33
|
+
maxColumns = Math.max(maxColumns, getChildCount(child));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (childType.displayName === 'TableBody') {
|
|
38
|
+
Children.forEach(childProps.children, rowChild => {
|
|
39
|
+
maxColumns = Math.max(maxColumns, getChildCount(rowChild));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return maxColumns;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const Table = ({
|
|
48
|
+
children,
|
|
49
|
+
columnWidths = [],
|
|
50
|
+
container = 'none',
|
|
51
|
+
pagination,
|
|
52
|
+
style,
|
|
53
|
+
...props
|
|
54
|
+
}: TableProps) => {
|
|
55
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
56
|
+
const columnCount = getMaxColumnCount(children);
|
|
57
|
+
const minTableWidth = getMinTableWidth(columnCount, columnWidths, MIN_COLUMN_WIDTH);
|
|
58
|
+
const tableWidth = Math.max(containerWidth, minTableWidth);
|
|
59
|
+
const isScrollable = tableWidth > containerWidth + 1;
|
|
60
|
+
|
|
61
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
62
|
+
const nextWidth = event.nativeEvent.layout.width;
|
|
63
|
+
setContainerWidth(currentWidth => (currentWidth === nextWidth ? currentWidth : nextWidth));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const content = (
|
|
67
|
+
<View onLayout={handleLayout} style={styles.wrapper}>
|
|
68
|
+
<ScrollView
|
|
69
|
+
alwaysBounceHorizontal={false}
|
|
70
|
+
alwaysBounceVertical={false}
|
|
71
|
+
bounces={false}
|
|
72
|
+
canCancelContentTouches
|
|
73
|
+
contentContainerStyle={styles.scrollContent}
|
|
74
|
+
horizontal
|
|
75
|
+
keyboardShouldPersistTaps="handled"
|
|
76
|
+
nestedScrollEnabled
|
|
77
|
+
overScrollMode="never"
|
|
78
|
+
scrollEnabled={isScrollable}
|
|
79
|
+
scrollEventThrottle={16}
|
|
80
|
+
showsHorizontalScrollIndicator
|
|
81
|
+
>
|
|
82
|
+
<View style={[styles.content, { width: tableWidth || minTableWidth }]}>{children}</View>
|
|
83
|
+
</ScrollView>
|
|
84
|
+
{pagination ? <View style={styles.pagination}>{pagination}</View> : null}
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const hasPagination = Boolean(pagination);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<TableContextProvider value={{ columnCount, columnWidths, container, hasPagination }}>
|
|
92
|
+
{container !== 'none' ? (
|
|
93
|
+
<Card {...props} colorScheme="neutralStrong" noPadding style={style} variant={container}>
|
|
94
|
+
{content}
|
|
95
|
+
</Card>
|
|
96
|
+
) : (
|
|
97
|
+
<View {...props} style={[styles.plainTable, style]}>
|
|
98
|
+
{content}
|
|
99
|
+
</View>
|
|
100
|
+
)}
|
|
101
|
+
</TableContextProvider>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
Table.displayName = 'Table';
|
|
106
|
+
|
|
107
|
+
const styles = StyleSheet.create(theme => ({
|
|
108
|
+
wrapper: {
|
|
109
|
+
width: '100%',
|
|
110
|
+
},
|
|
111
|
+
plainTable: {
|
|
112
|
+
width: '100%',
|
|
113
|
+
overflow: 'hidden',
|
|
114
|
+
},
|
|
115
|
+
scrollContent: {
|
|
116
|
+
minWidth: '100%',
|
|
117
|
+
},
|
|
118
|
+
content: {
|
|
119
|
+
alignSelf: 'flex-start',
|
|
120
|
+
},
|
|
121
|
+
pagination: {
|
|
122
|
+
paddingHorizontal: theme.components.table.cell.padding,
|
|
123
|
+
paddingVertical: theme.space[200],
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
export default Table;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getColumnMinWidth, getColumnStyle, getMinTableWidth } from './Table.utils';
|
|
3
|
+
|
|
4
|
+
describe('getColumnMinWidth', () => {
|
|
5
|
+
it('returns the configured fixed width when it is larger than the minimum', () => {
|
|
6
|
+
expect(getColumnMinWidth(180, 120)).toBe(180);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('clamps fixed widths to the minimum column width', () => {
|
|
10
|
+
expect(getColumnMinWidth(80, 120)).toBe(120);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns the minimum width for flexible and unspecified columns', () => {
|
|
14
|
+
expect(getColumnMinWidth('2fr', 120)).toBe(120);
|
|
15
|
+
expect(getColumnMinWidth({ flex: 3 }, 120)).toBe(120);
|
|
16
|
+
expect(getColumnMinWidth(undefined, 120)).toBe(120);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('getMinTableWidth', () => {
|
|
21
|
+
it('adds fixed widths and minimum widths for flexible columns', () => {
|
|
22
|
+
expect(getMinTableWidth(4, [180, '2fr', { flex: 3 }], 120)).toBe(540);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('uses the minimum width for unspecified trailing columns', () => {
|
|
26
|
+
expect(getMinTableWidth(3, [200], 120)).toBe(440);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('getColumnStyle', () => {
|
|
31
|
+
it('returns a fixed-width style for numeric column widths', () => {
|
|
32
|
+
expect(getColumnStyle(180, 120)).toEqual({
|
|
33
|
+
width: 180,
|
|
34
|
+
minWidth: 180,
|
|
35
|
+
flexGrow: 0,
|
|
36
|
+
flexShrink: 0,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('maps fr values to flexGrow', () => {
|
|
41
|
+
expect(getColumnStyle('2fr', 120)).toEqual({
|
|
42
|
+
minWidth: 120,
|
|
43
|
+
flexBasis: 0,
|
|
44
|
+
flexGrow: 2,
|
|
45
|
+
flexShrink: 0,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('supports fractional fr values', () => {
|
|
50
|
+
expect(getColumnStyle('0.5fr', 120)).toEqual({
|
|
51
|
+
minWidth: 120,
|
|
52
|
+
flexBasis: 0,
|
|
53
|
+
flexGrow: 0.5,
|
|
54
|
+
flexShrink: 0,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('uses flex weights from object column widths', () => {
|
|
59
|
+
expect(getColumnStyle({ flex: 3 }, 120)).toEqual({
|
|
60
|
+
minWidth: 120,
|
|
61
|
+
flexBasis: 0,
|
|
62
|
+
flexGrow: 3,
|
|
63
|
+
flexShrink: 0,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('falls back to a single flexible column for invalid flexible values', () => {
|
|
68
|
+
expect(getColumnStyle('invalid' as `${number}fr`, 120)).toEqual({
|
|
69
|
+
minWidth: 120,
|
|
70
|
+
flexBasis: 0,
|
|
71
|
+
flexGrow: 1,
|
|
72
|
+
flexShrink: 0,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(getColumnStyle({ flex: 0 }, 120)).toEqual({
|
|
76
|
+
minWidth: 120,
|
|
77
|
+
flexBasis: 0,
|
|
78
|
+
flexGrow: 1,
|
|
79
|
+
flexShrink: 0,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ViewStyle } from 'react-native';
|
|
2
|
+
import type { TableColumnWidth } from './Table.props';
|
|
3
|
+
|
|
4
|
+
const FR_UNIT_PATTERN = /^(\d*\.?\d+)fr$/;
|
|
5
|
+
|
|
6
|
+
const parseFractionUnit = (columnWidth?: TableColumnWidth): number | null => {
|
|
7
|
+
if (typeof columnWidth !== 'string') {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const match = columnWidth.match(FR_UNIT_PATTERN);
|
|
12
|
+
if (!match) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parsedValue = Number(match[1]);
|
|
17
|
+
return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getFlexGrow = (columnWidth?: TableColumnWidth): number => {
|
|
21
|
+
if (!columnWidth || typeof columnWidth === 'number') {
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof columnWidth === 'string') {
|
|
26
|
+
return parseFractionUnit(columnWidth) ?? 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Number.isFinite(columnWidth.flex) && columnWidth.flex > 0 ? columnWidth.flex : 1;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getColumnMinWidth = (
|
|
33
|
+
columnWidth: TableColumnWidth | undefined,
|
|
34
|
+
minColumnWidth: number
|
|
35
|
+
): number => {
|
|
36
|
+
if (typeof columnWidth === 'number') {
|
|
37
|
+
return Math.max(columnWidth, minColumnWidth);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return minColumnWidth;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getMinTableWidth = (
|
|
44
|
+
columnCount: number,
|
|
45
|
+
columnWidths: TableColumnWidth[],
|
|
46
|
+
minColumnWidth: number
|
|
47
|
+
): number => {
|
|
48
|
+
return Array.from({ length: columnCount }).reduce<number>((totalWidth, _, index) => {
|
|
49
|
+
return totalWidth + getColumnMinWidth(columnWidths[index], minColumnWidth);
|
|
50
|
+
}, 0);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const getColumnStyle = (
|
|
54
|
+
columnWidth: TableColumnWidth | undefined,
|
|
55
|
+
minColumnWidth: number
|
|
56
|
+
): ViewStyle => {
|
|
57
|
+
if (typeof columnWidth === 'number') {
|
|
58
|
+
return {
|
|
59
|
+
width: columnWidth,
|
|
60
|
+
minWidth: columnWidth,
|
|
61
|
+
flexGrow: 0,
|
|
62
|
+
flexShrink: 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
minWidth: minColumnWidth,
|
|
68
|
+
flexBasis: 0,
|
|
69
|
+
flexGrow: getFlexGrow(columnWidth),
|
|
70
|
+
flexShrink: 0,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Children, cloneElement, isValidElement } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { TableBodyProps } from './Table.props';
|
|
4
|
+
|
|
5
|
+
const TableBody = ({ children, ...props }: TableBodyProps) => {
|
|
6
|
+
const items = Children.toArray(children);
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<View {...props}>
|
|
10
|
+
{items.map((child, index) => {
|
|
11
|
+
if (!isValidElement(child)) {
|
|
12
|
+
return child;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return cloneElement(child as React.ReactElement<{ isLastRow?: boolean }>, {
|
|
16
|
+
isLastRow: index === items.length - 1,
|
|
17
|
+
});
|
|
18
|
+
})}
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
TableBody.displayName = 'TableBody';
|
|
24
|
+
|
|
25
|
+
export default TableBody;
|