@adobe-commerce/elsie 1.6.0-alpha999 → 1.6.0-beta2
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/config/jest.js +3 -3
- package/package.json +3 -3
- package/src/components/Button/Button.tsx +2 -0
- package/src/components/Field/Field.tsx +19 -14
- package/src/components/Icon/Icon.tsx +29 -24
- package/src/components/Incrementer/Incrementer.css +6 -0
- package/src/components/Incrementer/Incrementer.stories.tsx +18 -0
- package/src/components/Incrementer/Incrementer.tsx +66 -59
- package/src/components/MultiSelect/MultiSelect.css +273 -0
- package/src/components/MultiSelect/MultiSelect.stories.tsx +459 -0
- package/src/components/MultiSelect/MultiSelect.tsx +763 -0
- package/src/components/MultiSelect/index.ts +11 -0
- package/src/components/Pagination/Pagination.css +14 -5
- package/src/components/Pagination/Pagination.stories.tsx +32 -3
- package/src/components/Pagination/Pagination.tsx +28 -22
- package/src/components/Pagination/PaginationButton.tsx +46 -0
- package/src/components/Price/Price.tsx +8 -41
- package/src/components/Table/Table.css +183 -0
- package/src/components/Table/Table.stories.tsx +1024 -0
- package/src/components/Table/Table.tsx +253 -0
- package/src/components/Table/index.ts +11 -0
- package/src/components/ToggleButton/ToggleButton.css +13 -1
- package/src/components/ToggleButton/ToggleButton.stories.tsx +13 -6
- package/src/components/ToggleButton/ToggleButton.tsx +4 -0
- package/src/components/index.ts +5 -3
- package/src/docs/slots.mdx +2 -0
- package/src/i18n/en_US.json +38 -0
- package/src/icons/Business.svg +3 -0
- package/src/icons/List.svg +3 -0
- package/src/icons/Quote.svg +3 -0
- package/src/icons/Structure.svg +8 -0
- package/src/icons/Team.svg +5 -0
- package/src/icons/index.ts +29 -24
- package/src/lib/aem/configs.ts +10 -4
- package/src/lib/get-price-formatter.ts +69 -0
- package/src/lib/index.ts +4 -3
- package/src/lib/slot.tsx +61 -5
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* Copyright 2025 Adobe
|
|
3
|
+
* All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
|
+
*******************************************************************/
|
|
9
|
+
|
|
10
|
+
// https://storybook.js.org/docs/7.0/preact/writing-stories/introduction
|
|
11
|
+
import type { Meta, StoryObj } from '@storybook/preact';
|
|
12
|
+
import { useState } from 'preact/hooks';
|
|
13
|
+
import { Table as TableComponent, TableProps } from '@adobe-commerce/elsie/components/Table';
|
|
14
|
+
import { ActionButton } from '@adobe-commerce/elsie/components/ActionButton';
|
|
15
|
+
import { ActionButtonGroup } from '@adobe-commerce/elsie/components/ActionButtonGroup';
|
|
16
|
+
import { Pagination } from '@adobe-commerce/elsie/components/Pagination';
|
|
17
|
+
import { Picker, type PickerOption } from '@adobe-commerce/elsie/components/Picker';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Use the `Table` component to render data in a structured table.
|
|
21
|
+
*
|
|
22
|
+
* ## Column Structure
|
|
23
|
+
* Each column in the `columns` array defines a table column with:
|
|
24
|
+
* - **`key`**: Unique identifier that matches the property names in `rowData` objects
|
|
25
|
+
* - **`label`**: Display text shown in the column header
|
|
26
|
+
* - **`sortBy`**: Optional sorting state (`true` for sortable but neutral, `'asc'` for ascending, `'desc'` for descending)
|
|
27
|
+
*
|
|
28
|
+
* ## Row Data Structure
|
|
29
|
+
* Each object in the `rowData` array represents a table row where:
|
|
30
|
+
* - **Keys** must match the `key` values from the `columns` array
|
|
31
|
+
* - **Values** can be:
|
|
32
|
+
* - **Strings**: Plain text content
|
|
33
|
+
* - **Numbers**: Numeric values (automatically converted to strings for display)
|
|
34
|
+
* - **VNode**: Preact `VNode` for complex content (buttons, icons, formatted text, etc.)
|
|
35
|
+
*
|
|
36
|
+
* ## Mobile Layout & Container Queries
|
|
37
|
+
* The table uses **container queries** instead of media queries for responsive behavior:
|
|
38
|
+
* - **`mobileLayout`**: Optional prop that controls mobile behavior
|
|
39
|
+
* - `'none'` (default): No special mobile layout
|
|
40
|
+
* - `'stacked'`: Stacks cells vertically when container width ≤ 600px
|
|
41
|
+
* - **Container Query Breakpoint**: 600px - triggers when the table's container becomes narrow
|
|
42
|
+
* - **`data-label`**: Automatically added to cells for accessibility in stacked layout
|
|
43
|
+
* - **Responsive Behavior**: Table adapts to its container width, not viewport width
|
|
44
|
+
*
|
|
45
|
+
* ## All props
|
|
46
|
+
* The table below shows all the props for the `Table` component.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const meta: Meta<TableProps> = {
|
|
50
|
+
title: 'Components/Table',
|
|
51
|
+
component: TableComponent,
|
|
52
|
+
parameters: {
|
|
53
|
+
layout: 'padded',
|
|
54
|
+
},
|
|
55
|
+
argTypes: {
|
|
56
|
+
columns: {
|
|
57
|
+
description: 'Array of column definitions for the table. Each column defines the structure and behavior of a table column.',
|
|
58
|
+
table: {
|
|
59
|
+
type: { summary: 'Column[]' },
|
|
60
|
+
},
|
|
61
|
+
control: 'object',
|
|
62
|
+
},
|
|
63
|
+
rowData: {
|
|
64
|
+
description: 'Array of data objects to display in table rows. Each object represents a table row where keys match column keys and values contain the cell content.',
|
|
65
|
+
table: {
|
|
66
|
+
type: { summary: 'RowData[]' },
|
|
67
|
+
},
|
|
68
|
+
control: 'object',
|
|
69
|
+
},
|
|
70
|
+
mobileLayout: {
|
|
71
|
+
description: 'Controls responsive layout behavior using container queries. When set to "stacked", cells stack vertically when the table container width ≤ 600px. The `data-label` attribute is automatically added to cells for accessibility.',
|
|
72
|
+
table: {
|
|
73
|
+
type: { summary: 'none | stacked' },
|
|
74
|
+
},
|
|
75
|
+
control: 'select',
|
|
76
|
+
options: ['none', 'stacked'],
|
|
77
|
+
mapping: {
|
|
78
|
+
none: 'none',
|
|
79
|
+
stacked: 'stacked',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
caption: {
|
|
83
|
+
description: 'Optional table caption that provides context and description. Displays above the table and is announced by screen readers.',
|
|
84
|
+
table: {
|
|
85
|
+
type: { summary: 'string' },
|
|
86
|
+
},
|
|
87
|
+
control: 'text',
|
|
88
|
+
},
|
|
89
|
+
onSortChange: {
|
|
90
|
+
description: 'Callback function triggered when column sorting changes.',
|
|
91
|
+
table: {
|
|
92
|
+
type: { summary: '(columnKey: string, direction: Sortable) => void' },
|
|
93
|
+
},
|
|
94
|
+
action: 'onSortChange',
|
|
95
|
+
},
|
|
96
|
+
expandedRows: {
|
|
97
|
+
description: 'Set of row indices that are currently expanded. Used to control which rows are shown in expanded state. Row details will only render for rows that have `_rowDetails` content and are included in this set.',
|
|
98
|
+
table: {
|
|
99
|
+
type: { summary: 'Set<number>' },
|
|
100
|
+
},
|
|
101
|
+
control: 'object',
|
|
102
|
+
},
|
|
103
|
+
loading: {
|
|
104
|
+
description: 'When true, renders skeleton rows instead of actual data. Useful for showing loading state while data is being fetched.',
|
|
105
|
+
table: {
|
|
106
|
+
type: { summary: 'boolean' },
|
|
107
|
+
},
|
|
108
|
+
control: 'boolean',
|
|
109
|
+
},
|
|
110
|
+
skeletonRowCount: {
|
|
111
|
+
description: 'Number of skeleton rows to render when loading is true. Defaults to 10 rows.',
|
|
112
|
+
table: {
|
|
113
|
+
type: { summary: 'number' },
|
|
114
|
+
},
|
|
115
|
+
control: 'number',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default meta;
|
|
121
|
+
|
|
122
|
+
type Story = StoryObj<TableProps>;
|
|
123
|
+
|
|
124
|
+
// Wrapper component to manage sorting state
|
|
125
|
+
const TableWithState = (args: TableProps) => {
|
|
126
|
+
const [columns, setColumns] = useState(args.columns);
|
|
127
|
+
|
|
128
|
+
const handleSortChange = (columnKey: string, direction: 'asc' | 'desc' | true) => {
|
|
129
|
+
// call Action onSortChange
|
|
130
|
+
args.onSortChange?.(columnKey, direction);
|
|
131
|
+
|
|
132
|
+
// Update column sort states
|
|
133
|
+
setColumns(prevColumns =>
|
|
134
|
+
prevColumns.map(col => {
|
|
135
|
+
if (col.key === columnKey) {
|
|
136
|
+
return { ...col, sortBy: direction };
|
|
137
|
+
} else if (col.sortBy === 'asc' || col.sortBy === 'desc') {
|
|
138
|
+
return { ...col, sortBy: true }; // Reset other sorted columns to neutral
|
|
139
|
+
}
|
|
140
|
+
return col;
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<TableComponent
|
|
147
|
+
{...args}
|
|
148
|
+
columns={columns}
|
|
149
|
+
onSortChange={handleSortChange}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Simple table.
|
|
156
|
+
* Demonstrates basic table structure with string and number content types.
|
|
157
|
+
*
|
|
158
|
+
*
|
|
159
|
+
* ```tsx
|
|
160
|
+
* <Table
|
|
161
|
+
* columns={[
|
|
162
|
+
* { key: 'name', label: 'Name' },
|
|
163
|
+
* { key: 'email', label: 'Email' },
|
|
164
|
+
* { key: 'age', label: 'Age' }
|
|
165
|
+
* ]}
|
|
166
|
+
* rowData={[
|
|
167
|
+
* { name: 'John', email: 'john@example.com', age: 20 },
|
|
168
|
+
* { name: 'Jane', email: 'jane@example.com', age: 21 }
|
|
169
|
+
* ]}
|
|
170
|
+
* />
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export const Table: Story = {
|
|
174
|
+
args: {
|
|
175
|
+
columns: [
|
|
176
|
+
{ key: 'name', label: 'Name' },
|
|
177
|
+
{ key: 'email', label: 'Email' },
|
|
178
|
+
{ key: 'age', label: 'Age' },
|
|
179
|
+
{ key: 'actions', label: 'Actions' },
|
|
180
|
+
],
|
|
181
|
+
rowData: [
|
|
182
|
+
{ name: 'John', email: 'john@example.com', age: 20, actions: <ActionButton>Edit</ActionButton> },
|
|
183
|
+
{ name: 'Jane', email: 'jane@example.com', age: 21, actions: <ActionButton>Edit</ActionButton> },
|
|
184
|
+
{ name: 'Jim', email: 'jim@example.com', age: 22, actions: <ActionButton>Edit</ActionButton> },
|
|
185
|
+
{ name: 'Jill', email: 'jill@example.com', age: 23, actions: <ActionButton>Edit</ActionButton> },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Table where all columns are sortable. Demonstrates the three-state sorting cycle: `true` → `'asc'` → `'desc'` → `true`.
|
|
192
|
+
* Shows how multiple columns can be sortable simultaneously, with only one active sort at a time.
|
|
193
|
+
*
|
|
194
|
+
* ```tsx
|
|
195
|
+
* <Table
|
|
196
|
+
* columns={[
|
|
197
|
+
* { key: 'name', label: 'Name', sortBy: true },
|
|
198
|
+
* { key: 'email', label: 'Email', sortBy: true },
|
|
199
|
+
* { key: 'age', label: 'Age', sortBy: true }
|
|
200
|
+
* ]}
|
|
201
|
+
* rowData={[
|
|
202
|
+
* { name: 'John', email: 'john@example.com', age: 20 },
|
|
203
|
+
* { name: 'Jane', email: 'jane@example.com', age: 21 }
|
|
204
|
+
* ]}
|
|
205
|
+
* onSortChange={(columnKey, direction) => handleSort(columnKey, direction)}
|
|
206
|
+
* />
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export const AllSortable: Story = {
|
|
210
|
+
render: TableWithState,
|
|
211
|
+
args: {
|
|
212
|
+
columns: [
|
|
213
|
+
{ key: 'name', label: 'Name', sortBy: true },
|
|
214
|
+
{ key: 'email', label: 'Email', sortBy: true },
|
|
215
|
+
{ key: 'age', label: 'Age', sortBy: true },
|
|
216
|
+
{ key: 'actions', label: 'Actions' },
|
|
217
|
+
],
|
|
218
|
+
rowData: [
|
|
219
|
+
{ name: 'John', email: 'john@example.com', age: 20, actions: <ActionButton>Edit</ActionButton> },
|
|
220
|
+
{ name: 'Jane', email: 'jane@example.com', age: 21, actions: <ActionButton>Edit</ActionButton> },
|
|
221
|
+
{ name: 'Jim', email: 'jim@example.com', age: 22, actions: <ActionButton>Edit</ActionButton> },
|
|
222
|
+
{ name: 'Jill', email: 'jill@example.com', age: 23, actions: <ActionButton>Edit</ActionButton> },
|
|
223
|
+
{ name: 'Jack', email: 'jack@example.com', age: 24, actions: <ActionButton>Edit</ActionButton> },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Wide table with 10 columns to demonstrate horizontal scrolling and container query behavior.
|
|
230
|
+
* This table will show how the container query responds when the table becomes too wide for its container.
|
|
231
|
+
*
|
|
232
|
+
* ```tsx
|
|
233
|
+
* <Table
|
|
234
|
+
* columns={[
|
|
235
|
+
* { key: 'id', label: 'ID' },
|
|
236
|
+
* { key: 'name', label: 'Full Name' },
|
|
237
|
+
* { key: 'email', label: 'Email Address' },
|
|
238
|
+
* { key: 'phone', label: 'Phone Number' },
|
|
239
|
+
* { key: 'department', label: 'Department' },
|
|
240
|
+
* { key: 'position', label: 'Position' },
|
|
241
|
+
* { key: 'salary', label: 'Salary' },
|
|
242
|
+
* { key: 'startDate', label: 'Start Date' },
|
|
243
|
+
* { key: 'status', label: 'Status' },
|
|
244
|
+
* { key: 'actions', label: 'Actions' }
|
|
245
|
+
* ]}
|
|
246
|
+
* rowData={[
|
|
247
|
+
* { id: 1, name: 'John Doe', email: 'john@company.com', phone: '+1-555-0123', department: 'Engineering', position: 'Senior Developer', salary: '$95,000', startDate: '2022-01-15', status: 'Active', actions: <ActionButton>Edit</ActionButton> }
|
|
248
|
+
* ]}
|
|
249
|
+
* />
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export const WideTable: Story = {
|
|
253
|
+
args: {
|
|
254
|
+
columns: [
|
|
255
|
+
{ key: 'id', label: 'ID' },
|
|
256
|
+
{ key: 'name', label: 'Full Name' },
|
|
257
|
+
{ key: 'email', label: 'Email Address' },
|
|
258
|
+
{ key: 'phone', label: 'Phone Number' },
|
|
259
|
+
{ key: 'department', label: 'Department' },
|
|
260
|
+
{ key: 'position', label: 'Position' },
|
|
261
|
+
{ key: 'salary', label: 'Salary' },
|
|
262
|
+
{ key: 'startDate', label: 'Start Date' },
|
|
263
|
+
{ key: 'status', label: 'Status' },
|
|
264
|
+
{ key: 'actions', label: 'Actions' },
|
|
265
|
+
],
|
|
266
|
+
rowData: [
|
|
267
|
+
{
|
|
268
|
+
id: 1,
|
|
269
|
+
name: 'John Doe',
|
|
270
|
+
email: 'john.doe@company.com',
|
|
271
|
+
phone: '+1-555-0123',
|
|
272
|
+
department: 'Engineering',
|
|
273
|
+
position: 'Senior Developer',
|
|
274
|
+
salary: '$95,000',
|
|
275
|
+
startDate: '2022-01-15',
|
|
276
|
+
status: 'Active',
|
|
277
|
+
actions: <ActionButton>Edit</ActionButton>
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 2,
|
|
281
|
+
name: 'Jane Smith',
|
|
282
|
+
email: 'jane.smith@company.com',
|
|
283
|
+
phone: '+1-555-0124',
|
|
284
|
+
department: 'Marketing',
|
|
285
|
+
position: 'Marketing Manager',
|
|
286
|
+
salary: '$78,000',
|
|
287
|
+
startDate: '2021-06-20',
|
|
288
|
+
status: 'Active',
|
|
289
|
+
actions: <ActionButton>Edit</ActionButton>
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: 3,
|
|
293
|
+
name: 'Bob Johnson',
|
|
294
|
+
email: 'bob.johnson@company.com',
|
|
295
|
+
phone: '+1-555-0125',
|
|
296
|
+
department: 'Sales',
|
|
297
|
+
position: 'Sales Director',
|
|
298
|
+
salary: '$110,000',
|
|
299
|
+
startDate: '2020-03-10',
|
|
300
|
+
status: 'Active',
|
|
301
|
+
actions: <ActionButton>Edit</ActionButton>
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: 4,
|
|
305
|
+
name: 'Alice Brown',
|
|
306
|
+
email: 'alice.brown@company.com',
|
|
307
|
+
phone: '+1-555-0126',
|
|
308
|
+
department: 'HR',
|
|
309
|
+
position: 'HR Specialist',
|
|
310
|
+
salary: '$65,000',
|
|
311
|
+
startDate: '2023-02-28',
|
|
312
|
+
status: 'Pending',
|
|
313
|
+
actions: <ActionButton>Edit</ActionButton>
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 5,
|
|
317
|
+
name: 'Charlie Wilson',
|
|
318
|
+
email: 'charlie.wilson@company.com',
|
|
319
|
+
phone: '+1-555-0127',
|
|
320
|
+
department: 'Finance',
|
|
321
|
+
position: 'Financial Analyst',
|
|
322
|
+
salary: '$72,000',
|
|
323
|
+
startDate: '2022-09-12',
|
|
324
|
+
status: 'Active',
|
|
325
|
+
actions: <ActionButton>Edit</ActionButton>
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Table demonstrating complex VNode content in cells with multi-line text and interactive elements.
|
|
333
|
+
* This shows how the table handles rich content including buttons, badges, and formatted text.
|
|
334
|
+
*
|
|
335
|
+
* ```tsx
|
|
336
|
+
* <Table
|
|
337
|
+
* columns={[
|
|
338
|
+
* { key: 'user', label: 'User Info' },
|
|
339
|
+
* { key: 'description', label: 'Description' },
|
|
340
|
+
* { key: 'status', label: 'Status' },
|
|
341
|
+
* { key: 'actions', label: 'Actions' }
|
|
342
|
+
* ]}
|
|
343
|
+
* rowData={[
|
|
344
|
+
* {
|
|
345
|
+
* user: <div><strong>John Doe</strong><br/>john@example.com<br/>Senior Developer</div>,
|
|
346
|
+
* description: <div>Lead developer for the<br/>e-commerce platform<br/>with 5+ years experience</div>,
|
|
347
|
+
* status: <span>Active</span>,
|
|
348
|
+
* actions: <div><ActionButton>Edit</ActionButton><br/><ActionButton>Delete</ActionButton><br/><ActionButton>View</ActionButton></div>
|
|
349
|
+
* }
|
|
350
|
+
* ]}
|
|
351
|
+
* />
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
export const ComplexCells: Story = {
|
|
355
|
+
args: {
|
|
356
|
+
columns: [
|
|
357
|
+
{ key: 'user', label: 'User Info' },
|
|
358
|
+
{ key: 'description', label: 'Description' },
|
|
359
|
+
{ key: 'status', label: 'Status' },
|
|
360
|
+
{ key: 'actions', label: 'Actions' },
|
|
361
|
+
],
|
|
362
|
+
rowData: [
|
|
363
|
+
{
|
|
364
|
+
user: (
|
|
365
|
+
<div>
|
|
366
|
+
<strong>John Doe</strong><br/>
|
|
367
|
+
john.doe@company.com<br/>
|
|
368
|
+
<em>Senior Developer</em>
|
|
369
|
+
</div>
|
|
370
|
+
),
|
|
371
|
+
description: (
|
|
372
|
+
<div>
|
|
373
|
+
Lead developer for the<br/>
|
|
374
|
+
e-commerce platform<br/>
|
|
375
|
+
<small>with 5+ years experience</small>
|
|
376
|
+
</div>
|
|
377
|
+
),
|
|
378
|
+
status: (
|
|
379
|
+
<span>Active</span>
|
|
380
|
+
),
|
|
381
|
+
actions: (
|
|
382
|
+
<ActionButtonGroup>
|
|
383
|
+
<ActionButton value="edit">Edit</ActionButton>
|
|
384
|
+
<ActionButton value="delete">Delete</ActionButton>
|
|
385
|
+
<ActionButton value="view">View</ActionButton>
|
|
386
|
+
</ActionButtonGroup>
|
|
387
|
+
),
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
user: (
|
|
391
|
+
<div>
|
|
392
|
+
<strong>Jane Smith</strong><br/>
|
|
393
|
+
jane.smith@company.com<br/>
|
|
394
|
+
<em>Product Manager</em>
|
|
395
|
+
</div>
|
|
396
|
+
),
|
|
397
|
+
description: (
|
|
398
|
+
<div>
|
|
399
|
+
Manages product roadmap<br/>
|
|
400
|
+
and feature planning<br/>
|
|
401
|
+
<small>3+ years in product</small>
|
|
402
|
+
</div>
|
|
403
|
+
),
|
|
404
|
+
status: (
|
|
405
|
+
<span>Pending</span>
|
|
406
|
+
),
|
|
407
|
+
actions: (
|
|
408
|
+
<ActionButtonGroup>
|
|
409
|
+
<ActionButton value="edit">Edit</ActionButton>
|
|
410
|
+
<ActionButton value="approve">Approve</ActionButton>
|
|
411
|
+
<ActionButton value="reject">Reject</ActionButton>
|
|
412
|
+
</ActionButtonGroup>
|
|
413
|
+
),
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
user: (
|
|
417
|
+
<div>
|
|
418
|
+
<strong>Bob Johnson</strong><br/>
|
|
419
|
+
bob.johnson@company.com<br/>
|
|
420
|
+
<em>UX Designer</em>
|
|
421
|
+
</div>
|
|
422
|
+
),
|
|
423
|
+
description: (
|
|
424
|
+
<div>
|
|
425
|
+
Designs user interfaces<br/>
|
|
426
|
+
and user experiences<br/>
|
|
427
|
+
<small>Expert in Figma & Sketch</small>
|
|
428
|
+
</div>
|
|
429
|
+
),
|
|
430
|
+
status: (
|
|
431
|
+
<span>Inactive</span>
|
|
432
|
+
),
|
|
433
|
+
actions: (
|
|
434
|
+
<ActionButtonGroup>
|
|
435
|
+
<ActionButton value="edit">Edit</ActionButton>
|
|
436
|
+
<ActionButton value="activate">Activate</ActionButton>
|
|
437
|
+
<ActionButton value="archive">Archive</ActionButton>
|
|
438
|
+
</ActionButtonGroup>
|
|
439
|
+
),
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Table with stacked mobile layout that uses container queries.
|
|
447
|
+
* This demonstrates how the table adapts to its container width rather than viewport width.
|
|
448
|
+
* The table will stack vertically when its container becomes narrow (≤600px).
|
|
449
|
+
*
|
|
450
|
+
* **Container Query Behavior**: Uses `mobileLayout="stacked"` to enable responsive stacking.
|
|
451
|
+
* When the container width ≤ 600px:
|
|
452
|
+
* - Headers are hidden (`display: none`)
|
|
453
|
+
* - Cells stack vertically (`display: block`)
|
|
454
|
+
* - Column labels appear as `data-label` attributes before each cell value
|
|
455
|
+
* - Perfect for mobile views, sidebars, or constrained layouts
|
|
456
|
+
*
|
|
457
|
+
* ```tsx
|
|
458
|
+
* <Table
|
|
459
|
+
* mobileLayout="stacked"
|
|
460
|
+
* columns={[
|
|
461
|
+
* { key: 'name', label: 'Name' },
|
|
462
|
+
* { key: 'email', label: 'Email' },
|
|
463
|
+
* { key: 'age', label: 'Age' }
|
|
464
|
+
* ]}
|
|
465
|
+
* rowData={[
|
|
466
|
+
* { name: 'John', email: 'john@example.com', age: 20 },
|
|
467
|
+
* { name: 'Jane', email: 'jane@example.com', age: 21 }
|
|
468
|
+
* ]}
|
|
469
|
+
* />
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
export const StackedMobileLayout: Story = {
|
|
473
|
+
args: {
|
|
474
|
+
mobileLayout: 'stacked',
|
|
475
|
+
columns: [
|
|
476
|
+
{ key: 'name', label: 'Name' },
|
|
477
|
+
{ key: 'email', label: 'Email' },
|
|
478
|
+
{ key: 'age', label: 'Age' },
|
|
479
|
+
{ key: 'status', label: 'Status' },
|
|
480
|
+
{ key: 'actions', label: 'Actions' },
|
|
481
|
+
],
|
|
482
|
+
rowData: [
|
|
483
|
+
{ name: 'John Doe', email: 'john.doe@example.com', age: 28, status: 'Active', actions: <ActionButton>Edit</ActionButton> },
|
|
484
|
+
{ name: 'Jane Smith', email: 'jane.smith@example.com', age: 32, status: 'Inactive', actions: <ActionButton>Edit</ActionButton> },
|
|
485
|
+
{ name: 'Bob Johnson', email: 'bob.johnson@example.com', age: 45, status: 'Active', actions: <ActionButton>Edit</ActionButton> },
|
|
486
|
+
{ name: 'Alice Brown', email: 'alice.brown@example.com', age: 29, status: 'Pending', actions: <ActionButton>Edit</ActionButton> },
|
|
487
|
+
],
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Table with programmatically controlled expandable rows.
|
|
494
|
+
* Row expansion is controlled by buttons or other interactive elements within the column content.
|
|
495
|
+
* Developers must manage the `expandedRows` state themselves.
|
|
496
|
+
*
|
|
497
|
+
* **Features**:
|
|
498
|
+
* - Row details only render when both `_rowDetails` exists and row index is in `expandedRows`
|
|
499
|
+
* - Row details span the full width of the table
|
|
500
|
+
* - Supports any VNode content in the `_rowDetails` property
|
|
501
|
+
*
|
|
502
|
+
* ```tsx
|
|
503
|
+
* const [expandedRows, setExpandedRows] = useState(new Set());
|
|
504
|
+
*
|
|
505
|
+
* const toggleRow = (rowIndex: number) => {
|
|
506
|
+
* setExpandedRows(prev => {
|
|
507
|
+
* const newSet = new Set(prev);
|
|
508
|
+
* if (newSet.has(rowIndex)) {
|
|
509
|
+
* newSet.delete(rowIndex);
|
|
510
|
+
* } else {
|
|
511
|
+
* newSet.add(rowIndex);
|
|
512
|
+
* }
|
|
513
|
+
* return newSet;
|
|
514
|
+
* });
|
|
515
|
+
* };
|
|
516
|
+
*
|
|
517
|
+
* <Table
|
|
518
|
+
* columns={[
|
|
519
|
+
* { key: 'name', label: 'Name' },
|
|
520
|
+
* { key: 'email', label: 'Email' },
|
|
521
|
+
* { key: 'actions', label: 'Actions' }
|
|
522
|
+
* ]}
|
|
523
|
+
* rowData={[
|
|
524
|
+
* {
|
|
525
|
+
* name: 'John',
|
|
526
|
+
* email: 'john@example.com',
|
|
527
|
+
* actions: <ActionButton onClick={() => toggleRow(0)}>Toggle Details</ActionButton>,
|
|
528
|
+
* _rowDetails: <div>Additional information...</div>
|
|
529
|
+
* }
|
|
530
|
+
* ]}
|
|
531
|
+
* expandedRows={expandedRows}
|
|
532
|
+
* />
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
export const RowDetails: Story = {
|
|
536
|
+
render: (args) => {
|
|
537
|
+
const [expandedRows, setExpandedRows] = useState(new Set<number>());
|
|
538
|
+
|
|
539
|
+
const toggleRow = (rowIndex: number) => {
|
|
540
|
+
setExpandedRows(prev => {
|
|
541
|
+
const newSet = new Set(prev);
|
|
542
|
+
if (newSet.has(rowIndex)) {
|
|
543
|
+
newSet.delete(rowIndex);
|
|
544
|
+
} else {
|
|
545
|
+
newSet.add(rowIndex);
|
|
546
|
+
}
|
|
547
|
+
return newSet;
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const rowData = [
|
|
552
|
+
{
|
|
553
|
+
name: 'John Doe',
|
|
554
|
+
email: 'john.doe@company.com',
|
|
555
|
+
status: 'Active',
|
|
556
|
+
actions: (
|
|
557
|
+
<ActionButton onClick={() => toggleRow(0)}>
|
|
558
|
+
{expandedRows.has(0) ? 'Hide' : 'Show'}
|
|
559
|
+
</ActionButton>
|
|
560
|
+
),
|
|
561
|
+
_rowDetails: (
|
|
562
|
+
<>
|
|
563
|
+
<h3>Employee Details</h3>
|
|
564
|
+
<p><strong>Department:</strong> Engineering</p>
|
|
565
|
+
<p><strong>Position:</strong> Senior Developer</p>
|
|
566
|
+
<p><strong>Start Date:</strong> January 15, 2022</p>
|
|
567
|
+
<p><strong>Notes:</strong> Excellent performance, leads the frontend team.</p>
|
|
568
|
+
<div style={{ marginTop: '12px' }}>
|
|
569
|
+
<ActionButton style={{ marginRight: '8px' }}>Update Details</ActionButton>
|
|
570
|
+
<ActionButton>View Full Profile</ActionButton>
|
|
571
|
+
</div>
|
|
572
|
+
</>
|
|
573
|
+
)
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'Jane Smith',
|
|
577
|
+
email: 'jane.smith@company.com',
|
|
578
|
+
status: 'Pending',
|
|
579
|
+
actions: (
|
|
580
|
+
<ActionButton onClick={() => toggleRow(1)}>
|
|
581
|
+
{expandedRows.has(1) ? 'Hide' : 'Show'}
|
|
582
|
+
</ActionButton>
|
|
583
|
+
),
|
|
584
|
+
_rowDetails: (
|
|
585
|
+
<>
|
|
586
|
+
<h3>Pending Approval</h3>
|
|
587
|
+
<p><strong>Department:</strong> Marketing</p>
|
|
588
|
+
<p><strong>Position:</strong> Marketing Manager</p>
|
|
589
|
+
<p><strong>Application Date:</strong> December 1, 2024</p>
|
|
590
|
+
<p><strong>Status:</strong> Awaiting HR approval</p>
|
|
591
|
+
<div style={{ marginTop: '12px' }}>
|
|
592
|
+
<ActionButton style={{ marginRight: '8px', backgroundColor: '#22c55e', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Approve</ActionButton>
|
|
593
|
+
<ActionButton style={{ backgroundColor: '#ef4444', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Reject</ActionButton>
|
|
594
|
+
</div>
|
|
595
|
+
</>
|
|
596
|
+
)
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: 'Bob Johnson',
|
|
600
|
+
email: 'bob.johnson@company.com',
|
|
601
|
+
status: 'Inactive',
|
|
602
|
+
actions: (
|
|
603
|
+
<ActionButton onClick={() => toggleRow(2)}>
|
|
604
|
+
{expandedRows.has(2) ? 'Hide' : 'Show'}
|
|
605
|
+
</ActionButton>
|
|
606
|
+
),
|
|
607
|
+
_rowDetails: (
|
|
608
|
+
<>
|
|
609
|
+
<h3>Account Information</h3>
|
|
610
|
+
<p><strong>Department:</strong> Sales</p>
|
|
611
|
+
<p><strong>Position:</strong> Sales Director</p>
|
|
612
|
+
<p><strong>Last Active:</strong> November 20, 2024</p>
|
|
613
|
+
<p><strong>Reason:</strong> On extended leave</p>
|
|
614
|
+
<div style={{ marginTop: '12px' }}>
|
|
615
|
+
<ActionButton style={{ marginRight: '8px' }}>Reactivate Account</ActionButton>
|
|
616
|
+
<ActionButton>Contact Employee</ActionButton>
|
|
617
|
+
</div>
|
|
618
|
+
</>
|
|
619
|
+
)
|
|
620
|
+
},
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
return (
|
|
624
|
+
<TableComponent
|
|
625
|
+
{...args}
|
|
626
|
+
columns={[
|
|
627
|
+
{ key: 'name', label: 'Name' },
|
|
628
|
+
{ key: 'email', label: 'Email' },
|
|
629
|
+
{ key: 'status', label: 'Status' },
|
|
630
|
+
{ key: 'actions', label: 'Actions' },
|
|
631
|
+
]}
|
|
632
|
+
rowData={rowData}
|
|
633
|
+
expandedRows={expandedRows}
|
|
634
|
+
/>
|
|
635
|
+
);
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Table in loading state with skeleton rows.
|
|
641
|
+
* Demonstrates how the table appears while data is being fetched.
|
|
642
|
+
* Each cell shows a skeleton placeholder that matches the table structure.
|
|
643
|
+
*
|
|
644
|
+
* **Features**:
|
|
645
|
+
* - Shows skeleton rows instead of actual data when `loading` is true
|
|
646
|
+
* - Configurable number of skeleton rows via `skeletonRowCount` prop
|
|
647
|
+
* - Maintains table structure and column headers during loading
|
|
648
|
+
* - Each cell contains a single-line skeleton component
|
|
649
|
+
*
|
|
650
|
+
* ```tsx
|
|
651
|
+
* <Table
|
|
652
|
+
* loading={true}
|
|
653
|
+
* skeletonRowCount={5}
|
|
654
|
+
* columns={[
|
|
655
|
+
* { key: 'name', label: 'Name' },
|
|
656
|
+
* { key: 'email', label: 'Email' },
|
|
657
|
+
* { key: 'status', label: 'Status' }
|
|
658
|
+
* ]}
|
|
659
|
+
* rowData={[]} // Empty array when loading
|
|
660
|
+
* />
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
663
|
+
export const LoadingState: Story = {
|
|
664
|
+
args: {
|
|
665
|
+
loading: true,
|
|
666
|
+
skeletonRowCount: 5,
|
|
667
|
+
columns: [
|
|
668
|
+
{ key: 'name', label: 'Name' },
|
|
669
|
+
{ key: 'email', label: 'Email' },
|
|
670
|
+
{ key: 'status', label: 'Status' },
|
|
671
|
+
{ key: 'actions', label: 'Actions' },
|
|
672
|
+
],
|
|
673
|
+
rowData: [], // Empty when loading
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Table with VNode labels in column headers.
|
|
679
|
+
* Demonstrates how column labels can be VNode elements instead of simple strings.
|
|
680
|
+
* This allows for rich header content like icons, formatted text, or custom components.
|
|
681
|
+
*
|
|
682
|
+
* **Features**:
|
|
683
|
+
* - Column labels can be VNode elements (JSX components)
|
|
684
|
+
* - Supports any valid Preact VNode content in headers
|
|
685
|
+
* - Maintains all table functionality with custom header content
|
|
686
|
+
* - Useful for adding icons, tooltips, or formatted text to headers
|
|
687
|
+
*
|
|
688
|
+
* ```tsx
|
|
689
|
+
* <Table
|
|
690
|
+
* columns={[
|
|
691
|
+
* { key: 'name', label: <span><strong>👤 User Name</strong></span> },
|
|
692
|
+
* { key: 'email', label: <span style={{ color: '#0066cc' }}>📧 Email</span> },
|
|
693
|
+
* { key: 'status', label: <em>Status Info</em> }
|
|
694
|
+
* ]}
|
|
695
|
+
* rowData={[
|
|
696
|
+
* { name: 'John', email: 'john@example.com', status: 'Active' }
|
|
697
|
+
* ]}
|
|
698
|
+
* />
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
export const VNodeLabels: Story = {
|
|
702
|
+
args: {
|
|
703
|
+
columns: [
|
|
704
|
+
{
|
|
705
|
+
key: 'name',
|
|
706
|
+
label: (
|
|
707
|
+
<span>
|
|
708
|
+
<strong>👤 User Name</strong>
|
|
709
|
+
</span>
|
|
710
|
+
),
|
|
711
|
+
ariaLabel: 'User Name',
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
key: 'email',
|
|
715
|
+
label: <span style={{ color: '#0066cc' }}>📧 Email Address</span>,
|
|
716
|
+
ariaLabel: 'Email Address',
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
key: 'role',
|
|
720
|
+
label: <em style={{ color: '#666' }}>Role & Department</em>,
|
|
721
|
+
ariaLabel: 'Role & Department',
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
key: 'status',
|
|
725
|
+
label: (
|
|
726
|
+
<span
|
|
727
|
+
style={{
|
|
728
|
+
padding: '4px 8px',
|
|
729
|
+
backgroundColor: '#f0f9ff',
|
|
730
|
+
borderRadius: '4px',
|
|
731
|
+
fontSize: '12px',
|
|
732
|
+
}}
|
|
733
|
+
>
|
|
734
|
+
📊 Status
|
|
735
|
+
</span>
|
|
736
|
+
),
|
|
737
|
+
ariaLabel: 'Status',
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
key: 'actions',
|
|
741
|
+
label: <span>⚙️ Actions</span>,
|
|
742
|
+
ariaLabel: 'Actions',
|
|
743
|
+
},
|
|
744
|
+
],
|
|
745
|
+
rowData: [
|
|
746
|
+
{
|
|
747
|
+
name: 'John Doe',
|
|
748
|
+
email: 'john.doe@company.com',
|
|
749
|
+
role: 'Senior Developer',
|
|
750
|
+
status: 'Active',
|
|
751
|
+
actions: <ActionButton>Edit</ActionButton>,
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: 'Jane Smith',
|
|
755
|
+
email: 'jane.smith@company.com',
|
|
756
|
+
role: 'Product Manager',
|
|
757
|
+
status: 'Active',
|
|
758
|
+
actions: <ActionButton>Edit</ActionButton>,
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: 'Bob Johnson',
|
|
762
|
+
email: 'bob.johnson@company.com',
|
|
763
|
+
role: 'UX Designer',
|
|
764
|
+
status: 'Inactive',
|
|
765
|
+
actions: <ActionButton>Edit</ActionButton>,
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Table with pagination to navigate through multiple pages of data.
|
|
773
|
+
* This demonstrates how to integrate the Pagination component with the Table component.
|
|
774
|
+
*
|
|
775
|
+
* **Features**:
|
|
776
|
+
* - Pagination controls below the table
|
|
777
|
+
* - Page state management with useState
|
|
778
|
+
* - Dynamic row data based on current page
|
|
779
|
+
* - Items per page configuration
|
|
780
|
+
*
|
|
781
|
+
* ```tsx
|
|
782
|
+
* const [currentPage, setCurrentPage] = useState(1);
|
|
783
|
+
* const itemsPerPage = 5;
|
|
784
|
+
* const totalItems = 50;
|
|
785
|
+
* const totalPages = Math.ceil(totalItems / itemsPerPage);
|
|
786
|
+
*
|
|
787
|
+
* const paginatedData = allData.slice(
|
|
788
|
+
* (currentPage - 1) * itemsPerPage,
|
|
789
|
+
* currentPage * itemsPerPage
|
|
790
|
+
* );
|
|
791
|
+
*
|
|
792
|
+
* <div>
|
|
793
|
+
* <Table
|
|
794
|
+
* columns={columns}
|
|
795
|
+
* rowData={paginatedData}
|
|
796
|
+
* />
|
|
797
|
+
* <Pagination
|
|
798
|
+
* currentPage={currentPage}
|
|
799
|
+
* totalPages={totalPages}
|
|
800
|
+
* onChange={setCurrentPage}
|
|
801
|
+
* />
|
|
802
|
+
* </div>
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
export const WithPagination: Story = {
|
|
806
|
+
render: (args) => {
|
|
807
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
808
|
+
const [pageSize, setPageSize] = useState(5);
|
|
809
|
+
|
|
810
|
+
// Page size options
|
|
811
|
+
const pageSizeOptions: PickerOption[] = [
|
|
812
|
+
{ value: '5', text: '5' },
|
|
813
|
+
{ value: '10', text: '10' },
|
|
814
|
+
{ value: '15', text: '15' },
|
|
815
|
+
];
|
|
816
|
+
|
|
817
|
+
// Generate sample data (287 items to match screenshot)
|
|
818
|
+
const allData = Array.from({ length: 287 }, (_, index) => ({
|
|
819
|
+
id: index + 1,
|
|
820
|
+
name: `User ${index + 1}`,
|
|
821
|
+
email: `user${index + 1}@example.com`,
|
|
822
|
+
department: ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'][index % 5],
|
|
823
|
+
status: ['Active', 'Inactive', 'Pending'][index % 3],
|
|
824
|
+
actions: <ActionButton>Edit</ActionButton>,
|
|
825
|
+
}));
|
|
826
|
+
|
|
827
|
+
const totalPages = Math.ceil(allData.length / pageSize);
|
|
828
|
+
|
|
829
|
+
// Calculate item range
|
|
830
|
+
const startItem = (currentPage - 1) * pageSize + 1;
|
|
831
|
+
const endItem = Math.min(currentPage * pageSize, allData.length);
|
|
832
|
+
const totalItems = allData.length;
|
|
833
|
+
|
|
834
|
+
// Get current page data
|
|
835
|
+
const paginatedData = allData.slice(
|
|
836
|
+
(currentPage - 1) * pageSize,
|
|
837
|
+
currentPage * pageSize
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// Handle page size change
|
|
841
|
+
const handlePageSizeChange = (event: Event) => {
|
|
842
|
+
const target = event.target as HTMLSelectElement;
|
|
843
|
+
const newPageSize = Number(target.value);
|
|
844
|
+
setPageSize(newPageSize);
|
|
845
|
+
setCurrentPage(1); // Reset to first page when page size changes
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
return (
|
|
849
|
+
<div>
|
|
850
|
+
<TableComponent
|
|
851
|
+
{...args}
|
|
852
|
+
columns={[
|
|
853
|
+
{ key: 'id', label: 'ID' },
|
|
854
|
+
{ key: 'name', label: 'Name' },
|
|
855
|
+
{ key: 'email', label: 'Email' },
|
|
856
|
+
{ key: 'department', label: 'Department' },
|
|
857
|
+
{ key: 'status', label: 'Status' },
|
|
858
|
+
{ key: 'actions', label: 'Actions' },
|
|
859
|
+
]}
|
|
860
|
+
rowData={paginatedData}
|
|
861
|
+
/>
|
|
862
|
+
<div style={{ marginTop: 'var(--spacing-small)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
863
|
+
<span style={{
|
|
864
|
+
font: 'var(--type-body-1-default-font)',
|
|
865
|
+
letterSpacing: 'var(--type-body-1-default-letter-spacing)',
|
|
866
|
+
color: 'var(--color-neutral-800)'
|
|
867
|
+
}}>
|
|
868
|
+
Items {startItem} to {endItem} of {totalItems} total
|
|
869
|
+
</span>
|
|
870
|
+
<Pagination
|
|
871
|
+
currentPage={currentPage}
|
|
872
|
+
totalPages={totalPages}
|
|
873
|
+
onChange={setCurrentPage}
|
|
874
|
+
/>
|
|
875
|
+
<div style={{
|
|
876
|
+
display: 'flex',
|
|
877
|
+
alignItems: 'center',
|
|
878
|
+
gap: 'var(--spacing-xsmall)',
|
|
879
|
+
font: 'var(--type-body-1-default-font)',
|
|
880
|
+
letterSpacing: 'var(--type-body-1-default-letter-spacing)',
|
|
881
|
+
color: 'var(--color-neutral-800)'
|
|
882
|
+
}}>
|
|
883
|
+
<span>Show</span>
|
|
884
|
+
<Picker
|
|
885
|
+
variant="primary"
|
|
886
|
+
size="medium"
|
|
887
|
+
value={String(pageSize)}
|
|
888
|
+
options={pageSizeOptions}
|
|
889
|
+
handleSelect={handlePageSizeChange}
|
|
890
|
+
aria-label="Items per page"
|
|
891
|
+
/>
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
);
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Table with expandable row details and stacked mobile layout.
|
|
901
|
+
* Combines the expandable rows feature with responsive mobile behavior using container queries.
|
|
902
|
+
*
|
|
903
|
+
* **Features**:
|
|
904
|
+
* - Expandable rows with toggle buttons
|
|
905
|
+
* - Stacked mobile layout when container width ≤ 600px
|
|
906
|
+
* - Row details expand in both desktop and mobile views
|
|
907
|
+
* - Mobile view shows labels above each cell value
|
|
908
|
+
*
|
|
909
|
+
* ```tsx
|
|
910
|
+
* const [expandedRows, setExpandedRows] = useState(new Set());
|
|
911
|
+
*
|
|
912
|
+
* const toggleRow = (rowIndex: number) => {
|
|
913
|
+
* setExpandedRows(prev => {
|
|
914
|
+
* const newSet = new Set(prev);
|
|
915
|
+
* if (newSet.has(rowIndex)) {
|
|
916
|
+
* newSet.delete(rowIndex);
|
|
917
|
+
* } else {
|
|
918
|
+
* newSet.add(rowIndex);
|
|
919
|
+
* }
|
|
920
|
+
* return newSet;
|
|
921
|
+
* });
|
|
922
|
+
* };
|
|
923
|
+
*
|
|
924
|
+
* <Table
|
|
925
|
+
* mobileLayout="stacked"
|
|
926
|
+
* columns={columns}
|
|
927
|
+
* rowData={rowDataWithDetails}
|
|
928
|
+
* expandedRows={expandedRows}
|
|
929
|
+
* />
|
|
930
|
+
* ```
|
|
931
|
+
*/
|
|
932
|
+
export const ExpandableRowsWithMobileLayout: Story = {
|
|
933
|
+
render: (args) => {
|
|
934
|
+
const [expandedRows, setExpandedRows] = useState(new Set<number>());
|
|
935
|
+
|
|
936
|
+
const toggleRow = (rowIndex: number) => {
|
|
937
|
+
setExpandedRows((prev) => {
|
|
938
|
+
const newSet = new Set(prev);
|
|
939
|
+
if (newSet.has(rowIndex)) {
|
|
940
|
+
newSet.delete(rowIndex);
|
|
941
|
+
} else {
|
|
942
|
+
newSet.add(rowIndex);
|
|
943
|
+
}
|
|
944
|
+
return newSet;
|
|
945
|
+
});
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
const rowData = [
|
|
949
|
+
{
|
|
950
|
+
name: 'John Doe',
|
|
951
|
+
email: 'john.doe@company.com',
|
|
952
|
+
status: 'Active',
|
|
953
|
+
actions: (
|
|
954
|
+
<ActionButton onClick={() => toggleRow(0)}>
|
|
955
|
+
{expandedRows.has(0) ? 'Hide' : 'Show'}
|
|
956
|
+
</ActionButton>
|
|
957
|
+
),
|
|
958
|
+
_rowDetails: (
|
|
959
|
+
<>
|
|
960
|
+
<h3>Employee Details</h3>
|
|
961
|
+
<p><strong>Department:</strong> Engineering</p>
|
|
962
|
+
<p><strong>Position:</strong> Senior Developer</p>
|
|
963
|
+
<p><strong>Start Date:</strong> January 15, 2022</p>
|
|
964
|
+
<p><strong>Notes:</strong> Excellent performance, leads the frontend team.</p>
|
|
965
|
+
</>
|
|
966
|
+
)
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: 'Jane Smith',
|
|
970
|
+
email: 'jane.smith@company.com',
|
|
971
|
+
status: 'Pending',
|
|
972
|
+
actions: (
|
|
973
|
+
<ActionButton onClick={() => toggleRow(1)}>
|
|
974
|
+
{expandedRows.has(1) ? 'Hide' : 'Show'}
|
|
975
|
+
</ActionButton>
|
|
976
|
+
),
|
|
977
|
+
_rowDetails: (
|
|
978
|
+
<>
|
|
979
|
+
<h3>Pending Approval</h3>
|
|
980
|
+
<p><strong>Department:</strong> Marketing</p>
|
|
981
|
+
<p><strong>Position:</strong> Marketing Manager</p>
|
|
982
|
+
<p><strong>Application Date:</strong> December 1, 2024</p>
|
|
983
|
+
<p><strong>Status:</strong> Awaiting HR approval</p>
|
|
984
|
+
</>
|
|
985
|
+
)
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
name: 'Bob Johnson',
|
|
989
|
+
email: 'bob.johnson@company.com',
|
|
990
|
+
status: 'Inactive',
|
|
991
|
+
actions: (
|
|
992
|
+
<ActionButton onClick={() => toggleRow(2)}>
|
|
993
|
+
{expandedRows.has(2) ? 'Hide' : 'Show'}
|
|
994
|
+
</ActionButton>
|
|
995
|
+
),
|
|
996
|
+
_rowDetails: (
|
|
997
|
+
<>
|
|
998
|
+
<h3>Account Information</h3>
|
|
999
|
+
<p><strong>Department:</strong> Sales</p>
|
|
1000
|
+
<p><strong>Position:</strong> Sales Director</p>
|
|
1001
|
+
<p><strong>Last Active:</strong> November 20, 2024</p>
|
|
1002
|
+
<p><strong>Reason:</strong> On extended leave</p>
|
|
1003
|
+
</>
|
|
1004
|
+
)
|
|
1005
|
+
},
|
|
1006
|
+
];
|
|
1007
|
+
|
|
1008
|
+
return (
|
|
1009
|
+
<TableComponent
|
|
1010
|
+
{...args}
|
|
1011
|
+
mobileLayout="stacked"
|
|
1012
|
+
columns={[
|
|
1013
|
+
{ key: 'name', label: 'Name', ariaLabel: 'Name' },
|
|
1014
|
+
{ key: 'email', label: 'Email', ariaLabel: 'Email' },
|
|
1015
|
+
{ key: 'status', label: 'Status', ariaLabel: 'Status' },
|
|
1016
|
+
{ key: 'actions', label: 'Actions', ariaLabel: 'Actions' },
|
|
1017
|
+
]}
|
|
1018
|
+
rowData={rowData}
|
|
1019
|
+
expandedRows={expandedRows}
|
|
1020
|
+
/>
|
|
1021
|
+
);
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
1024
|
+
|