@adobe-commerce/elsie 1.5.0-alpha200 → 1.5.0-alpha3000
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/bin/builders/serve/index.js +3 -1
- package/config/vite.mjs +13 -8
- package/package.json +3 -3
- package/src/components/Table/Table.css +110 -0
- package/src/components/Table/Table.stories.tsx +648 -0
- package/src/components/Table/Table.tsx +175 -0
- package/src/components/Table/index.ts +11 -0
- package/src/components/index.ts +1 -0
- package/src/docs/slots.mdx +7 -1
- package/src/i18n/en_US.json +7 -0
- package/src/lib/slot.tsx +39 -26
|
@@ -4,7 +4,9 @@ const path = require('path');
|
|
|
4
4
|
module.exports = async function generateResourceBuilder({ argv }) {
|
|
5
5
|
const { build, preview } = await import('vite');
|
|
6
6
|
|
|
7
|
-
const configFile =
|
|
7
|
+
const configFile =
|
|
8
|
+
argv?.config ??
|
|
9
|
+
path.resolve(...[__dirname, '..', '..', '..', 'config', 'vite.mjs']);
|
|
8
10
|
|
|
9
11
|
let built = false;
|
|
10
12
|
|
package/config/vite.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Copyright 2024 Adobe
|
|
3
3
|
* All Rights Reserved.
|
|
4
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.
|
|
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
8
|
*******************************************************************/
|
|
9
9
|
|
|
10
10
|
import { glob } from 'glob';
|
|
@@ -25,7 +25,12 @@ import banner from 'vite-plugin-banner';
|
|
|
25
25
|
const env = loadEnv('', process.cwd());
|
|
26
26
|
|
|
27
27
|
// Load Elsie Config
|
|
28
|
-
const
|
|
28
|
+
const elsieConfigPath = path.resolve(process.cwd(), './.elsie.js');
|
|
29
|
+
// Convert Windows paths to file:// URLs for ES module imports
|
|
30
|
+
const elsieConfigUrl = elsieConfigPath.startsWith('file://')
|
|
31
|
+
? elsieConfigPath
|
|
32
|
+
: `file://${elsieConfigPath.replace(/\\/g, '/')}`;
|
|
33
|
+
const elsieConfig = await import(elsieConfigUrl).then((m) => m.default);
|
|
29
34
|
|
|
30
35
|
// Read package.json using createRequire (compatible with Node 20 and 22)
|
|
31
36
|
const require = createRequire(import.meta.url);
|
|
@@ -293,19 +298,19 @@ export default {
|
|
|
293
298
|
generateBundle(options, bundle) {
|
|
294
299
|
for (const fileName in bundle) {
|
|
295
300
|
const chunk = bundle[fileName];
|
|
296
|
-
|
|
301
|
+
|
|
297
302
|
// Process both .map files and JS/TS files with sourcemaps
|
|
298
|
-
if ((chunk.type === 'asset' && fileName.endsWith('.map')) ||
|
|
303
|
+
if ((chunk.type === 'asset' && fileName.endsWith('.map')) ||
|
|
299
304
|
(chunk.type === 'chunk' && chunk.map)) {
|
|
300
305
|
try {
|
|
301
306
|
// Get the sourcemap object - either from the asset source or the chunk's map
|
|
302
307
|
const map = chunk.type === 'asset' ? JSON.parse(chunk.source) : chunk.map;
|
|
303
|
-
|
|
308
|
+
|
|
304
309
|
if (map.sources) {
|
|
305
310
|
map.sources = map.sources.map((input) => {
|
|
306
311
|
return input.replace(/(?:\.\.?\/)+src\//, `/${packageJSON.name}/src/`);
|
|
307
312
|
});
|
|
308
|
-
|
|
313
|
+
|
|
309
314
|
// Update the sourcemap in the appropriate place
|
|
310
315
|
if (chunk.type === 'asset') {
|
|
311
316
|
chunk.source = JSON.stringify(map);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe-commerce/elsie",
|
|
3
|
-
"version": "1.5.0-
|
|
3
|
+
"version": "1.5.0-alpha3000",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"description": "Domain Package SDK",
|
|
6
6
|
"engines": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@adobe-commerce/event-bus": "~1.0.1",
|
|
30
|
-
"@adobe-commerce/fetch-graphql": "
|
|
31
|
-
"@adobe-commerce/recaptcha": "
|
|
30
|
+
"@adobe-commerce/fetch-graphql": "1.2.0-beta1",
|
|
31
|
+
"@adobe-commerce/recaptcha": "1.0.2-beta1",
|
|
32
32
|
"@adobe-commerce/storefront-design": "~1.0.0",
|
|
33
33
|
"@dropins/build-tools": "~1.0.1",
|
|
34
34
|
"preact": "~10.22.1",
|
|
@@ -0,0 +1,110 @@
|
|
|
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://cssguidelin.es/#bem-like-naming */
|
|
11
|
+
|
|
12
|
+
.dropin-table {
|
|
13
|
+
container-type: inline-size;
|
|
14
|
+
overflow-x: auto;
|
|
15
|
+
font: var(--type-body-1-default-font);
|
|
16
|
+
letter-spacing: var(--type-body-1-default-letter-spacing);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.dropin-table__table {
|
|
20
|
+
border-collapse: collapse;
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.dropin-table__caption {
|
|
25
|
+
font: var(--type-details-caption-1-font);
|
|
26
|
+
letter-spacing: var(--type-details-caption-1-letter-spacing);
|
|
27
|
+
text-align: left;
|
|
28
|
+
margin-bottom: var(--spacing-small);
|
|
29
|
+
caption-side: top;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.dropin-table__header__cell {
|
|
33
|
+
font: var(--type-body-1-strong-font);
|
|
34
|
+
letter-spacing: var(--type-body-1-strong-letter-spacing);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dropin-table__header__cell,
|
|
38
|
+
.dropin-table__body__cell {
|
|
39
|
+
padding: var(--spacing-xsmall);
|
|
40
|
+
text-align: left;
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.dropin-table__header__cell--sortable {
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dropin-table__header__row {
|
|
49
|
+
border-bottom: 2px solid var(--color-neutral-400);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dropin-table__body__row {
|
|
53
|
+
border-bottom: 1px solid var(--color-neutral-400);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dropin-table__header__sort-button {
|
|
57
|
+
margin-left: var(--spacing-xsmall);
|
|
58
|
+
vertical-align: middle;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dropin-table__row-details {
|
|
62
|
+
display: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.dropin-table__row-details--expanded {
|
|
66
|
+
display: table-row;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.dropin-table__row-details__cell {
|
|
70
|
+
padding: var(--spacing-small);
|
|
71
|
+
background-color: var(--color-neutral-100);
|
|
72
|
+
border-top: 1px solid var(--color-neutral-300);
|
|
73
|
+
border-bottom: 1px solid var(--color-neutral-400);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Container query for mobile layout */
|
|
77
|
+
@container (max-width: 600px) {
|
|
78
|
+
/* Mobile layout Stacked */
|
|
79
|
+
.dropin-table--mobile-layout-stacked .dropin-table__header {
|
|
80
|
+
display: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.dropin-table--mobile-layout-stacked .dropin-table__body__cell {
|
|
84
|
+
display: block;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dropin-table--mobile-layout-stacked .dropin-table__body__cell::before {
|
|
88
|
+
content: attr(data-label);
|
|
89
|
+
font-weight: bold;
|
|
90
|
+
display: block;
|
|
91
|
+
margin-bottom: var(--spacing-xxsmall);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.dropin-table--mobile-layout-stacked .dropin-table__row-details__cell {
|
|
95
|
+
display: block;
|
|
96
|
+
padding: var(--spacing-small);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Medium (portrait tablets and large phones, 768px and up) */
|
|
101
|
+
/* @media only screen and (min-width: 768px) { } */
|
|
102
|
+
|
|
103
|
+
/* Large (landscape tablets, 1024px and up) */
|
|
104
|
+
/* @media only screen and (min-width: 1024px) { } */
|
|
105
|
+
|
|
106
|
+
/* XLarge (laptops/desktops, 1366px and up) */
|
|
107
|
+
/* @media only screen and (min-width: 1366px) { } */
|
|
108
|
+
|
|
109
|
+
/* XXlarge (large laptops and desktops, 1920px and up) */
|
|
110
|
+
/* @media only screen and (min-width: 1920px) { } */
|
|
@@ -0,0 +1,648 @@
|
|
|
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
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Use the `Table` component to render data in a structured table.
|
|
17
|
+
*
|
|
18
|
+
* ## Column Structure
|
|
19
|
+
* Each column in the `columns` array defines a table column with:
|
|
20
|
+
* - **`key`**: Unique identifier that matches the property names in `rowData` objects
|
|
21
|
+
* - **`label`**: Display text shown in the column header
|
|
22
|
+
* - **`sortBy`**: Optional sorting state (`true` for sortable but neutral, `'asc'` for ascending, `'desc'` for descending)
|
|
23
|
+
*
|
|
24
|
+
* ## Row Data Structure
|
|
25
|
+
* Each object in the `rowData` array represents a table row where:
|
|
26
|
+
* - **Keys** must match the `key` values from the `columns` array
|
|
27
|
+
* - **Values** can be:
|
|
28
|
+
* - **Strings**: Plain text content
|
|
29
|
+
* - **Numbers**: Numeric values (automatically converted to strings for display)
|
|
30
|
+
* - **VNode**: Preact `VNode` for complex content (buttons, icons, formatted text, etc.)
|
|
31
|
+
*
|
|
32
|
+
* ## Mobile Layout & Container Queries
|
|
33
|
+
* The table uses **container queries** instead of media queries for responsive behavior:
|
|
34
|
+
* - **`mobileLayout`**: Optional prop that controls mobile behavior
|
|
35
|
+
* - `'none'` (default): No special mobile layout
|
|
36
|
+
* - `'stacked'`: Stacks cells vertically when container width ≤ 600px
|
|
37
|
+
* - **Container Query Breakpoint**: 600px - triggers when the table's container becomes narrow
|
|
38
|
+
* - **`data-label`**: Automatically added to cells for accessibility in stacked layout
|
|
39
|
+
* - **Responsive Behavior**: Table adapts to its container width, not viewport width
|
|
40
|
+
*
|
|
41
|
+
* ## All props
|
|
42
|
+
* The table below shows all the props for the `Table` component.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const meta: Meta<TableProps> = {
|
|
46
|
+
title: 'Components/Table',
|
|
47
|
+
component: TableComponent,
|
|
48
|
+
parameters: {
|
|
49
|
+
layout: 'padded',
|
|
50
|
+
},
|
|
51
|
+
argTypes: {
|
|
52
|
+
columns: {
|
|
53
|
+
description: 'Array of column definitions for the table. Each column defines the structure and behavior of a table column.',
|
|
54
|
+
table: {
|
|
55
|
+
type: { summary: 'Column[]' },
|
|
56
|
+
},
|
|
57
|
+
control: 'object',
|
|
58
|
+
},
|
|
59
|
+
rowData: {
|
|
60
|
+
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.',
|
|
61
|
+
table: {
|
|
62
|
+
type: { summary: 'RowData[]' },
|
|
63
|
+
},
|
|
64
|
+
control: 'object',
|
|
65
|
+
},
|
|
66
|
+
mobileLayout: {
|
|
67
|
+
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.',
|
|
68
|
+
table: {
|
|
69
|
+
type: { summary: 'none | stacked' },
|
|
70
|
+
},
|
|
71
|
+
control: 'select',
|
|
72
|
+
options: ['none', 'stacked'],
|
|
73
|
+
mapping: {
|
|
74
|
+
none: 'none',
|
|
75
|
+
stacked: 'stacked',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
caption: {
|
|
79
|
+
description: 'Optional table caption that provides context and description. Displays above the table and is announced by screen readers.',
|
|
80
|
+
table: {
|
|
81
|
+
type: { summary: 'string' },
|
|
82
|
+
},
|
|
83
|
+
control: 'text',
|
|
84
|
+
},
|
|
85
|
+
onSortChange: {
|
|
86
|
+
description: 'Callback function triggered when column sorting changes.',
|
|
87
|
+
table: {
|
|
88
|
+
type: { summary: '(columnKey: string, direction: Sortable) => void' },
|
|
89
|
+
},
|
|
90
|
+
action: 'onSortChange',
|
|
91
|
+
},
|
|
92
|
+
expandedRows: {
|
|
93
|
+
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.',
|
|
94
|
+
table: {
|
|
95
|
+
type: { summary: 'Set<number>' },
|
|
96
|
+
},
|
|
97
|
+
control: 'object',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default meta;
|
|
103
|
+
|
|
104
|
+
type Story = StoryObj<TableProps>;
|
|
105
|
+
|
|
106
|
+
// Wrapper component to manage sorting state
|
|
107
|
+
const TableWithState = (args: TableProps) => {
|
|
108
|
+
const [columns, setColumns] = useState(args.columns);
|
|
109
|
+
|
|
110
|
+
const handleSortChange = (columnKey: string, direction: 'asc' | 'desc' | true) => {
|
|
111
|
+
// call Action onSortChange
|
|
112
|
+
args.onSortChange?.(columnKey, direction);
|
|
113
|
+
|
|
114
|
+
// Update column sort states
|
|
115
|
+
setColumns(prevColumns =>
|
|
116
|
+
prevColumns.map(col => {
|
|
117
|
+
if (col.key === columnKey) {
|
|
118
|
+
return { ...col, sortBy: direction };
|
|
119
|
+
} else if (col.sortBy === 'asc' || col.sortBy === 'desc') {
|
|
120
|
+
return { ...col, sortBy: true }; // Reset other sorted columns to neutral
|
|
121
|
+
}
|
|
122
|
+
return col;
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<TableComponent
|
|
129
|
+
{...args}
|
|
130
|
+
columns={columns}
|
|
131
|
+
onSortChange={handleSortChange}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Simple table.
|
|
138
|
+
* Demonstrates basic table structure with string and number content types.
|
|
139
|
+
*
|
|
140
|
+
*
|
|
141
|
+
* ```tsx
|
|
142
|
+
* <Table
|
|
143
|
+
* columns={[
|
|
144
|
+
* { key: 'name', label: 'Name' },
|
|
145
|
+
* { key: 'email', label: 'Email' },
|
|
146
|
+
* { key: 'age', label: 'Age' }
|
|
147
|
+
* ]}
|
|
148
|
+
* rowData={[
|
|
149
|
+
* { name: 'John', email: 'john@example.com', age: 20 },
|
|
150
|
+
* { name: 'Jane', email: 'jane@example.com', age: 21 }
|
|
151
|
+
* ]}
|
|
152
|
+
* />
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export const Table: Story = {
|
|
156
|
+
args: {
|
|
157
|
+
columns: [
|
|
158
|
+
{ key: 'name', label: 'Name' },
|
|
159
|
+
{ key: 'email', label: 'Email' },
|
|
160
|
+
{ key: 'age', label: 'Age' },
|
|
161
|
+
{ key: 'actions', label: 'Actions' },
|
|
162
|
+
],
|
|
163
|
+
rowData: [
|
|
164
|
+
{ name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
|
|
165
|
+
{ name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
|
|
166
|
+
{ name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
|
|
167
|
+
{ name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Table where all columns are sortable. Demonstrates the three-state sorting cycle: `true` → `'asc'` → `'desc'` → `true`.
|
|
174
|
+
* Shows how multiple columns can be sortable simultaneously, with only one active sort at a time.
|
|
175
|
+
*
|
|
176
|
+
* ```tsx
|
|
177
|
+
* <Table
|
|
178
|
+
* columns={[
|
|
179
|
+
* { key: 'name', label: 'Name', sortBy: true },
|
|
180
|
+
* { key: 'email', label: 'Email', sortBy: true },
|
|
181
|
+
* { key: 'age', label: 'Age', sortBy: true }
|
|
182
|
+
* ]}
|
|
183
|
+
* rowData={[
|
|
184
|
+
* { name: 'John', email: 'john@example.com', age: 20 },
|
|
185
|
+
* { name: 'Jane', email: 'jane@example.com', age: 21 }
|
|
186
|
+
* ]}
|
|
187
|
+
* onSortChange={(columnKey, direction) => handleSort(columnKey, direction)}
|
|
188
|
+
* />
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export const AllSortable: Story = {
|
|
192
|
+
render: TableWithState,
|
|
193
|
+
args: {
|
|
194
|
+
columns: [
|
|
195
|
+
{ key: 'name', label: 'Name', sortBy: true },
|
|
196
|
+
{ key: 'email', label: 'Email', sortBy: true },
|
|
197
|
+
{ key: 'age', label: 'Age', sortBy: true },
|
|
198
|
+
{ key: 'actions', label: 'Actions' },
|
|
199
|
+
],
|
|
200
|
+
rowData: [
|
|
201
|
+
{ name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
|
|
202
|
+
{ name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
|
|
203
|
+
{ name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
|
|
204
|
+
{ name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
|
|
205
|
+
{ name: 'Jack', email: 'jack@example.com', age: 24, actions: <button>Edit</button> },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Wide table with 10 columns to demonstrate horizontal scrolling and container query behavior.
|
|
212
|
+
* This table will show how the container query responds when the table becomes too wide for its container.
|
|
213
|
+
*
|
|
214
|
+
* ```tsx
|
|
215
|
+
* <Table
|
|
216
|
+
* columns={[
|
|
217
|
+
* { key: 'id', label: 'ID' },
|
|
218
|
+
* { key: 'name', label: 'Full Name' },
|
|
219
|
+
* { key: 'email', label: 'Email Address' },
|
|
220
|
+
* { key: 'phone', label: 'Phone Number' },
|
|
221
|
+
* { key: 'department', label: 'Department' },
|
|
222
|
+
* { key: 'position', label: 'Position' },
|
|
223
|
+
* { key: 'salary', label: 'Salary' },
|
|
224
|
+
* { key: 'startDate', label: 'Start Date' },
|
|
225
|
+
* { key: 'status', label: 'Status' },
|
|
226
|
+
* { key: 'actions', label: 'Actions' }
|
|
227
|
+
* ]}
|
|
228
|
+
* rowData={[
|
|
229
|
+
* { 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: <button>Edit</button> }
|
|
230
|
+
* ]}
|
|
231
|
+
* />
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export const WideTable: Story = {
|
|
235
|
+
args: {
|
|
236
|
+
columns: [
|
|
237
|
+
{ key: 'id', label: 'ID' },
|
|
238
|
+
{ key: 'name', label: 'Full Name' },
|
|
239
|
+
{ key: 'email', label: 'Email Address' },
|
|
240
|
+
{ key: 'phone', label: 'Phone Number' },
|
|
241
|
+
{ key: 'department', label: 'Department' },
|
|
242
|
+
{ key: 'position', label: 'Position' },
|
|
243
|
+
{ key: 'salary', label: 'Salary' },
|
|
244
|
+
{ key: 'startDate', label: 'Start Date' },
|
|
245
|
+
{ key: 'status', label: 'Status' },
|
|
246
|
+
{ key: 'actions', label: 'Actions' },
|
|
247
|
+
],
|
|
248
|
+
rowData: [
|
|
249
|
+
{
|
|
250
|
+
id: 1,
|
|
251
|
+
name: 'John Doe',
|
|
252
|
+
email: 'john.doe@company.com',
|
|
253
|
+
phone: '+1-555-0123',
|
|
254
|
+
department: 'Engineering',
|
|
255
|
+
position: 'Senior Developer',
|
|
256
|
+
salary: '$95,000',
|
|
257
|
+
startDate: '2022-01-15',
|
|
258
|
+
status: 'Active',
|
|
259
|
+
actions: <button>Edit</button>
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 2,
|
|
263
|
+
name: 'Jane Smith',
|
|
264
|
+
email: 'jane.smith@company.com',
|
|
265
|
+
phone: '+1-555-0124',
|
|
266
|
+
department: 'Marketing',
|
|
267
|
+
position: 'Marketing Manager',
|
|
268
|
+
salary: '$78,000',
|
|
269
|
+
startDate: '2021-06-20',
|
|
270
|
+
status: 'Active',
|
|
271
|
+
actions: <button>Edit</button>
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 3,
|
|
275
|
+
name: 'Bob Johnson',
|
|
276
|
+
email: 'bob.johnson@company.com',
|
|
277
|
+
phone: '+1-555-0125',
|
|
278
|
+
department: 'Sales',
|
|
279
|
+
position: 'Sales Director',
|
|
280
|
+
salary: '$110,000',
|
|
281
|
+
startDate: '2020-03-10',
|
|
282
|
+
status: 'Active',
|
|
283
|
+
actions: <button>Edit</button>
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: 4,
|
|
287
|
+
name: 'Alice Brown',
|
|
288
|
+
email: 'alice.brown@company.com',
|
|
289
|
+
phone: '+1-555-0126',
|
|
290
|
+
department: 'HR',
|
|
291
|
+
position: 'HR Specialist',
|
|
292
|
+
salary: '$65,000',
|
|
293
|
+
startDate: '2023-02-28',
|
|
294
|
+
status: 'Pending',
|
|
295
|
+
actions: <button>Edit</button>
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 5,
|
|
299
|
+
name: 'Charlie Wilson',
|
|
300
|
+
email: 'charlie.wilson@company.com',
|
|
301
|
+
phone: '+1-555-0127',
|
|
302
|
+
department: 'Finance',
|
|
303
|
+
position: 'Financial Analyst',
|
|
304
|
+
salary: '$72,000',
|
|
305
|
+
startDate: '2022-09-12',
|
|
306
|
+
status: 'Active',
|
|
307
|
+
actions: <button>Edit</button>
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Table demonstrating complex VNode content in cells with multi-line text and interactive elements.
|
|
315
|
+
* This shows how the table handles rich content including buttons, badges, and formatted text.
|
|
316
|
+
*
|
|
317
|
+
* ```tsx
|
|
318
|
+
* <Table
|
|
319
|
+
* columns={[
|
|
320
|
+
* { key: 'user', label: 'User Info' },
|
|
321
|
+
* { key: 'description', label: 'Description' },
|
|
322
|
+
* { key: 'status', label: 'Status' },
|
|
323
|
+
* { key: 'actions', label: 'Actions' }
|
|
324
|
+
* ]}
|
|
325
|
+
* rowData={[
|
|
326
|
+
* {
|
|
327
|
+
* user: <div><strong>John Doe</strong><br/>john@example.com<br/>Senior Developer</div>,
|
|
328
|
+
* description: <div>Lead developer for the<br/>e-commerce platform<br/>with 5+ years experience</div>,
|
|
329
|
+
* status: <span style="background: green; color: white; padding: 2px 8px; border-radius: 4px;">Active</span>,
|
|
330
|
+
* actions: <div><button>Edit</button><br/><button>Delete</button><br/><button>View</button></div>
|
|
331
|
+
* }
|
|
332
|
+
* ]}
|
|
333
|
+
* />
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export const ComplexCells: Story = {
|
|
337
|
+
args: {
|
|
338
|
+
columns: [
|
|
339
|
+
{ key: 'user', label: 'User Info' },
|
|
340
|
+
{ key: 'description', label: 'Description' },
|
|
341
|
+
{ key: 'status', label: 'Status' },
|
|
342
|
+
{ key: 'actions', label: 'Actions' },
|
|
343
|
+
],
|
|
344
|
+
rowData: [
|
|
345
|
+
{
|
|
346
|
+
user: (
|
|
347
|
+
<div>
|
|
348
|
+
<strong>John Doe</strong><br/>
|
|
349
|
+
john.doe@company.com<br/>
|
|
350
|
+
<em>Senior Developer</em>
|
|
351
|
+
</div>
|
|
352
|
+
),
|
|
353
|
+
description: (
|
|
354
|
+
<div>
|
|
355
|
+
Lead developer for the<br/>
|
|
356
|
+
e-commerce platform<br/>
|
|
357
|
+
<small>with 5+ years experience</small>
|
|
358
|
+
</div>
|
|
359
|
+
),
|
|
360
|
+
status: (
|
|
361
|
+
<span style={{
|
|
362
|
+
background: '#22c55e',
|
|
363
|
+
color: 'white',
|
|
364
|
+
padding: '2px 8px',
|
|
365
|
+
borderRadius: '4px',
|
|
366
|
+
fontSize: '12px',
|
|
367
|
+
fontWeight: 'bold'
|
|
368
|
+
}}>
|
|
369
|
+
Active
|
|
370
|
+
</span>
|
|
371
|
+
),
|
|
372
|
+
actions: (
|
|
373
|
+
<div>
|
|
374
|
+
<button style={{ marginBottom: '4px', display: 'block' }}>Edit</button>
|
|
375
|
+
<button style={{ marginBottom: '4px', display: 'block' }}>Delete</button>
|
|
376
|
+
<button style={{ display: 'block' }}>View</button>
|
|
377
|
+
</div>
|
|
378
|
+
),
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
user: (
|
|
382
|
+
<div>
|
|
383
|
+
<strong>Jane Smith</strong><br/>
|
|
384
|
+
jane.smith@company.com<br/>
|
|
385
|
+
<em>Product Manager</em>
|
|
386
|
+
</div>
|
|
387
|
+
),
|
|
388
|
+
description: (
|
|
389
|
+
<div>
|
|
390
|
+
Manages product roadmap<br/>
|
|
391
|
+
and feature planning<br/>
|
|
392
|
+
<small>3+ years in product</small>
|
|
393
|
+
</div>
|
|
394
|
+
),
|
|
395
|
+
status: (
|
|
396
|
+
<span style={{
|
|
397
|
+
background: '#f59e0b',
|
|
398
|
+
color: 'white',
|
|
399
|
+
padding: '2px 8px',
|
|
400
|
+
borderRadius: '4px',
|
|
401
|
+
fontSize: '12px',
|
|
402
|
+
fontWeight: 'bold'
|
|
403
|
+
}}>
|
|
404
|
+
Pending
|
|
405
|
+
</span>
|
|
406
|
+
),
|
|
407
|
+
actions: (
|
|
408
|
+
<div>
|
|
409
|
+
<button style={{ marginBottom: '4px', display: 'block' }}>Edit</button>
|
|
410
|
+
<button style={{ marginBottom: '4px', display: 'block' }}>Approve</button>
|
|
411
|
+
<button style={{ display: 'block' }}>Reject</button>
|
|
412
|
+
</div>
|
|
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 style={{
|
|
432
|
+
background: '#ef4444',
|
|
433
|
+
color: 'white',
|
|
434
|
+
padding: '2px 8px',
|
|
435
|
+
borderRadius: '4px',
|
|
436
|
+
fontSize: '12px',
|
|
437
|
+
fontWeight: 'bold'
|
|
438
|
+
}}>
|
|
439
|
+
Inactive
|
|
440
|
+
</span>
|
|
441
|
+
),
|
|
442
|
+
actions: (
|
|
443
|
+
<div>
|
|
444
|
+
<button style={{ marginBottom: '4px', display: 'block' }}>Edit</button>
|
|
445
|
+
<button style={{ marginBottom: '4px', display: 'block' }}>Activate</button>
|
|
446
|
+
<button style={{ display: 'block' }}>Archive</button>
|
|
447
|
+
</div>
|
|
448
|
+
),
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Table with stacked mobile layout that uses container queries.
|
|
456
|
+
* This demonstrates how the table adapts to its container width rather than viewport width.
|
|
457
|
+
* The table will stack vertically when its container becomes narrow (≤600px).
|
|
458
|
+
*
|
|
459
|
+
* **Container Query Behavior**: Uses `mobileLayout="stacked"` to enable responsive stacking.
|
|
460
|
+
* When the container width ≤ 600px:
|
|
461
|
+
* - Headers are hidden (`display: none`)
|
|
462
|
+
* - Cells stack vertically (`display: block`)
|
|
463
|
+
* - Column labels appear as `data-label` attributes before each cell value
|
|
464
|
+
* - Perfect for mobile views, sidebars, or constrained layouts
|
|
465
|
+
*
|
|
466
|
+
* ```tsx
|
|
467
|
+
* <Table
|
|
468
|
+
* mobileLayout="stacked"
|
|
469
|
+
* columns={[
|
|
470
|
+
* { key: 'name', label: 'Name' },
|
|
471
|
+
* { key: 'email', label: 'Email' },
|
|
472
|
+
* { key: 'age', label: 'Age' }
|
|
473
|
+
* ]}
|
|
474
|
+
* rowData={[
|
|
475
|
+
* { name: 'John', email: 'john@example.com', age: 20 },
|
|
476
|
+
* { name: 'Jane', email: 'jane@example.com', age: 21 }
|
|
477
|
+
* ]}
|
|
478
|
+
* />
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
export const StackedMobileLayout: Story = {
|
|
482
|
+
args: {
|
|
483
|
+
mobileLayout: 'stacked',
|
|
484
|
+
columns: [
|
|
485
|
+
{ key: 'name', label: 'Name' },
|
|
486
|
+
{ key: 'email', label: 'Email' },
|
|
487
|
+
{ key: 'age', label: 'Age' },
|
|
488
|
+
{ key: 'status', label: 'Status' },
|
|
489
|
+
{ key: 'actions', label: 'Actions' },
|
|
490
|
+
],
|
|
491
|
+
rowData: [
|
|
492
|
+
{ name: 'John Doe', email: 'john.doe@example.com', age: 28, status: 'Active', actions: <button>Edit</button> },
|
|
493
|
+
{ name: 'Jane Smith', email: 'jane.smith@example.com', age: 32, status: 'Inactive', actions: <button>Edit</button> },
|
|
494
|
+
{ name: 'Bob Johnson', email: 'bob.johnson@example.com', age: 45, status: 'Active', actions: <button>Edit</button> },
|
|
495
|
+
{ name: 'Alice Brown', email: 'alice.brown@example.com', age: 29, status: 'Pending', actions: <button>Edit</button> },
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Table with programmatically controlled expandable rows.
|
|
503
|
+
* Row expansion is controlled by buttons or other interactive elements within the column content.
|
|
504
|
+
* Developers must manage the `expandedRows` state themselves.
|
|
505
|
+
*
|
|
506
|
+
* **Features**:
|
|
507
|
+
* - Row details only render when both `_rowDetails` exists and row index is in `expandedRows`
|
|
508
|
+
* - Row details span the full width of the table
|
|
509
|
+
* - Supports any VNode content in the `_rowDetails` property
|
|
510
|
+
*
|
|
511
|
+
* ```tsx
|
|
512
|
+
* const [expandedRows, setExpandedRows] = useState(new Set());
|
|
513
|
+
*
|
|
514
|
+
* const toggleRow = (rowIndex: number) => {
|
|
515
|
+
* setExpandedRows(prev => {
|
|
516
|
+
* const newSet = new Set(prev);
|
|
517
|
+
* if (newSet.has(rowIndex)) {
|
|
518
|
+
* newSet.delete(rowIndex);
|
|
519
|
+
* } else {
|
|
520
|
+
* newSet.add(rowIndex);
|
|
521
|
+
* }
|
|
522
|
+
* return newSet;
|
|
523
|
+
* });
|
|
524
|
+
* };
|
|
525
|
+
*
|
|
526
|
+
* <Table
|
|
527
|
+
* columns={[
|
|
528
|
+
* { key: 'name', label: 'Name' },
|
|
529
|
+
* { key: 'email', label: 'Email' },
|
|
530
|
+
* { key: 'actions', label: 'Actions' }
|
|
531
|
+
* ]}
|
|
532
|
+
* rowData={[
|
|
533
|
+
* {
|
|
534
|
+
* name: 'John',
|
|
535
|
+
* email: 'john@example.com',
|
|
536
|
+
* actions: <button onClick={() => toggleRow(0)}>Toggle Details</button>,
|
|
537
|
+
* _rowDetails: <div>Additional information...</div>
|
|
538
|
+
* }
|
|
539
|
+
* ]}
|
|
540
|
+
* expandedRows={expandedRows}
|
|
541
|
+
* />
|
|
542
|
+
* ```
|
|
543
|
+
*/
|
|
544
|
+
export const RowDetails: Story = {
|
|
545
|
+
render: (args) => {
|
|
546
|
+
const [expandedRows, setExpandedRows] = useState(new Set<number>());
|
|
547
|
+
|
|
548
|
+
const toggleRow = (rowIndex: number) => {
|
|
549
|
+
setExpandedRows(prev => {
|
|
550
|
+
const newSet = new Set(prev);
|
|
551
|
+
if (newSet.has(rowIndex)) {
|
|
552
|
+
newSet.delete(rowIndex);
|
|
553
|
+
} else {
|
|
554
|
+
newSet.add(rowIndex);
|
|
555
|
+
}
|
|
556
|
+
return newSet;
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const rowData = [
|
|
561
|
+
{
|
|
562
|
+
name: 'John Doe',
|
|
563
|
+
email: 'john.doe@company.com',
|
|
564
|
+
status: 'Active',
|
|
565
|
+
actions: (
|
|
566
|
+
<button onClick={() => toggleRow(0)}>
|
|
567
|
+
{expandedRows.has(0) ? 'Hide' : 'Show'}
|
|
568
|
+
</button>
|
|
569
|
+
),
|
|
570
|
+
_rowDetails: (
|
|
571
|
+
<div>
|
|
572
|
+
<h3>Employee Details</h3>
|
|
573
|
+
<p><strong>Department:</strong> Engineering</p>
|
|
574
|
+
<p><strong>Position:</strong> Senior Developer</p>
|
|
575
|
+
<p><strong>Start Date:</strong> January 15, 2022</p>
|
|
576
|
+
<p><strong>Notes:</strong> Excellent performance, leads the frontend team.</p>
|
|
577
|
+
<div style={{ marginTop: '12px' }}>
|
|
578
|
+
<button style={{ marginRight: '8px' }}>Update Details</button>
|
|
579
|
+
<button>View Full Profile</button>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
)
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: 'Jane Smith',
|
|
586
|
+
email: 'jane.smith@company.com',
|
|
587
|
+
status: 'Pending',
|
|
588
|
+
actions: (
|
|
589
|
+
<button onClick={() => toggleRow(1)}>
|
|
590
|
+
{expandedRows.has(1) ? 'Hide' : 'Show'}
|
|
591
|
+
</button>
|
|
592
|
+
),
|
|
593
|
+
_rowDetails: (
|
|
594
|
+
<div>
|
|
595
|
+
<h3>Pending Approval</h3>
|
|
596
|
+
<p><strong>Department:</strong> Marketing</p>
|
|
597
|
+
<p><strong>Position:</strong> Marketing Manager</p>
|
|
598
|
+
<p><strong>Application Date:</strong> December 1, 2024</p>
|
|
599
|
+
<p><strong>Status:</strong> Awaiting HR approval</p>
|
|
600
|
+
<div style={{ marginTop: '12px' }}>
|
|
601
|
+
<button style={{ marginRight: '8px', backgroundColor: '#22c55e', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Approve</button>
|
|
602
|
+
<button style={{ backgroundColor: '#ef4444', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Reject</button>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
)
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: 'Bob Johnson',
|
|
609
|
+
email: 'bob.johnson@company.com',
|
|
610
|
+
status: 'Inactive',
|
|
611
|
+
actions: (
|
|
612
|
+
<button onClick={() => toggleRow(2)}>
|
|
613
|
+
{expandedRows.has(2) ? 'Hide' : 'Show'}
|
|
614
|
+
</button>
|
|
615
|
+
),
|
|
616
|
+
_rowDetails: (
|
|
617
|
+
<div>
|
|
618
|
+
<h3>Account Information</h3>
|
|
619
|
+
<p><strong>Department:</strong> Sales</p>
|
|
620
|
+
<p><strong>Position:</strong> Sales Director</p>
|
|
621
|
+
<p><strong>Last Active:</strong> November 20, 2024</p>
|
|
622
|
+
<p><strong>Reason:</strong> On extended leave</p>
|
|
623
|
+
<div style={{ marginTop: '12px' }}>
|
|
624
|
+
<button style={{ marginRight: '8px' }}>Reactivate Account</button>
|
|
625
|
+
<button>Contact Employee</button>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
)
|
|
629
|
+
},
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
return (
|
|
633
|
+
<TableComponent
|
|
634
|
+
{...args}
|
|
635
|
+
columns={[
|
|
636
|
+
{ key: 'name', label: 'Name' },
|
|
637
|
+
{ key: 'email', label: 'Email' },
|
|
638
|
+
{ key: 'status', label: 'Status' },
|
|
639
|
+
{ key: 'actions', label: 'Actions' },
|
|
640
|
+
]}
|
|
641
|
+
rowData={rowData}
|
|
642
|
+
expandedRows={expandedRows}
|
|
643
|
+
/>
|
|
644
|
+
);
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
import { FunctionComponent, VNode, Fragment } from 'preact';
|
|
11
|
+
import { HTMLAttributes } from 'preact/compat';
|
|
12
|
+
import { classes, VComponent } from '@adobe-commerce/elsie/lib';
|
|
13
|
+
import { Icon, Button } from '@adobe-commerce/elsie/components';
|
|
14
|
+
import { useText } from '@adobe-commerce/elsie/i18n';
|
|
15
|
+
|
|
16
|
+
import '@adobe-commerce/elsie/components/Table/Table.css';
|
|
17
|
+
|
|
18
|
+
type Sortable = 'asc' | 'desc' | true;
|
|
19
|
+
|
|
20
|
+
type Column = {
|
|
21
|
+
key: string;
|
|
22
|
+
label: string;
|
|
23
|
+
sortBy?: Sortable;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type RowData = {
|
|
27
|
+
[key: string]: VNode | string | number | undefined;
|
|
28
|
+
_rowDetails?: VNode | string; // Special property for expandable row content
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface TableProps extends HTMLAttributes<HTMLTableElement> {
|
|
32
|
+
columns: Column[];
|
|
33
|
+
rowData: RowData[];
|
|
34
|
+
mobileLayout?: 'stacked' | 'none';
|
|
35
|
+
caption?: string;
|
|
36
|
+
onSortChange?: (columnKey: string, direction: Sortable) => void;
|
|
37
|
+
expandedRows?: Set<number>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Table: FunctionComponent<TableProps> = ({
|
|
41
|
+
className,
|
|
42
|
+
children,
|
|
43
|
+
columns = [],
|
|
44
|
+
rowData = [],
|
|
45
|
+
mobileLayout = 'none',
|
|
46
|
+
caption,
|
|
47
|
+
onSortChange,
|
|
48
|
+
expandedRows = new Set(),
|
|
49
|
+
...props
|
|
50
|
+
}) => {
|
|
51
|
+
const translations = useText({
|
|
52
|
+
ariaSortAscending: 'Dropin.Table.ariaSortAscending',
|
|
53
|
+
ariaSortDescending: 'Dropin.Table.ariaSortDescending',
|
|
54
|
+
sortedAscending: 'Dropin.Table.sortedAscending',
|
|
55
|
+
sortedDescending: 'Dropin.Table.sortedDescending',
|
|
56
|
+
sortBy: 'Dropin.Table.sortBy',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const handleSort = (column: Column) => {
|
|
60
|
+
if (!onSortChange) return;
|
|
61
|
+
|
|
62
|
+
// Determine next sort direction
|
|
63
|
+
let nextDirection: Sortable;
|
|
64
|
+
if (column.sortBy === true) {
|
|
65
|
+
nextDirection = 'asc';
|
|
66
|
+
} else if (column.sortBy === 'asc') {
|
|
67
|
+
nextDirection = 'desc';
|
|
68
|
+
} else {
|
|
69
|
+
nextDirection = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onSortChange(column.key, nextDirection);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getSortButton = (column: Column) => {
|
|
76
|
+
if (column.sortBy === undefined) return null;
|
|
77
|
+
|
|
78
|
+
let iconSource: string;
|
|
79
|
+
let ariaLabel: string;
|
|
80
|
+
|
|
81
|
+
if (column.sortBy === 'asc') {
|
|
82
|
+
iconSource = 'ChevronUp';
|
|
83
|
+
ariaLabel = translations.sortedAscending.replace('{label}', column.label);
|
|
84
|
+
} else if (column.sortBy === 'desc') {
|
|
85
|
+
iconSource = 'ChevronDown';
|
|
86
|
+
ariaLabel = translations.sortedDescending.replace('{label}', column.label);
|
|
87
|
+
} else {
|
|
88
|
+
iconSource = 'Sort';
|
|
89
|
+
ariaLabel = translations.sortBy.replace('{label}', column.label);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Button
|
|
94
|
+
variant="tertiary"
|
|
95
|
+
size="medium"
|
|
96
|
+
className="dropin-table__header__sort-button"
|
|
97
|
+
icon={<Icon source={iconSource} />}
|
|
98
|
+
aria-label={ariaLabel}
|
|
99
|
+
onClick={() => handleSort(column)}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className={classes(['dropin-table', `dropin-table--mobile-layout-${mobileLayout}`, className])}>
|
|
106
|
+
<table {...props} className="dropin-table__table">
|
|
107
|
+
{caption && <caption className="dropin-table__caption">{caption}</caption>}
|
|
108
|
+
<thead className="dropin-table__header">
|
|
109
|
+
<tr className="dropin-table__header__row">
|
|
110
|
+
{columns.map((column) => (
|
|
111
|
+
<th
|
|
112
|
+
key={column.key}
|
|
113
|
+
className={classes([
|
|
114
|
+
'dropin-table__header__cell',
|
|
115
|
+
['dropin-table__header__cell--sorted', column.sortBy === 'asc' || column.sortBy === 'desc'],
|
|
116
|
+
['dropin-table__header__cell--sortable', column.sortBy !== undefined]
|
|
117
|
+
])}
|
|
118
|
+
role="columnheader"
|
|
119
|
+
>
|
|
120
|
+
{column.label}
|
|
121
|
+
{getSortButton(column)}
|
|
122
|
+
</th>
|
|
123
|
+
))}
|
|
124
|
+
</tr>
|
|
125
|
+
</thead>
|
|
126
|
+
<tbody className="dropin-table__body">
|
|
127
|
+
{rowData.map((row, rowIndex) => {
|
|
128
|
+
const hasDetails = row._rowDetails !== undefined;
|
|
129
|
+
const isExpanded = expandedRows.has(rowIndex);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Fragment key={rowIndex}>
|
|
133
|
+
<tr className="dropin-table__body__row">
|
|
134
|
+
{columns.map((column) => {
|
|
135
|
+
const cell = row[column.key];
|
|
136
|
+
|
|
137
|
+
if (typeof cell === 'string' || typeof cell === 'number') {
|
|
138
|
+
return (
|
|
139
|
+
<td key={column.key} className="dropin-table__body__cell" data-label={column.label}>
|
|
140
|
+
{cell}
|
|
141
|
+
</td>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<td key={column.key} className="dropin-table__body__cell" data-label={column.label}>
|
|
147
|
+
<VComponent node={cell!} />
|
|
148
|
+
</td>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
</tr>
|
|
152
|
+
{hasDetails && isExpanded && (
|
|
153
|
+
<tr
|
|
154
|
+
key={`${rowIndex}-details`}
|
|
155
|
+
className="dropin-table__row-details dropin-table__row-details--expanded"
|
|
156
|
+
id={`row-${rowIndex}-details`}
|
|
157
|
+
>
|
|
158
|
+
<td
|
|
159
|
+
className="dropin-table__row-details__cell"
|
|
160
|
+
colSpan={columns.length}
|
|
161
|
+
role="region"
|
|
162
|
+
aria-labelledby={`row-${rowIndex}-details`}
|
|
163
|
+
>
|
|
164
|
+
{typeof row._rowDetails === 'string' ? row._rowDetails : <VComponent node={row._rowDetails!} />}
|
|
165
|
+
</td>
|
|
166
|
+
</tr>
|
|
167
|
+
)}
|
|
168
|
+
</Fragment>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export * from '@adobe-commerce/elsie/components/Table/Table';
|
|
11
|
+
export { Table as default } from '@adobe-commerce/elsie/components/Table/Table';
|
package/src/components/index.ts
CHANGED
|
@@ -49,3 +49,4 @@ export * from '@adobe-commerce/elsie/components/ContentGrid';
|
|
|
49
49
|
export * from '@adobe-commerce/elsie/components/Pagination';
|
|
50
50
|
export * from '@adobe-commerce/elsie/components/ProductItemCard';
|
|
51
51
|
export * from '@adobe-commerce/elsie/components/InputFile';
|
|
52
|
+
export * from '@adobe-commerce/elsie/components/Table';
|
package/src/docs/slots.mdx
CHANGED
|
@@ -32,6 +32,12 @@ The `<Slot />` component is used to define a slot in a container. It receives a
|
|
|
32
32
|
|
|
33
33
|
The name of the slot in _PascalCase_. `string` (required).
|
|
34
34
|
|
|
35
|
+
### lazy
|
|
36
|
+
|
|
37
|
+
Controls whether the slot should be loaded immediately or deferred for later initialization. `boolean` (optional).
|
|
38
|
+
|
|
39
|
+
When `lazy={false}`, the slot is initialized as soon as the container mounts. When `lazy={true}`, the slot can be initialized later on when it is needed. This is useful for performance optimization, especially when the slot's content is not immediately required.
|
|
40
|
+
|
|
35
41
|
### slotTag
|
|
36
42
|
|
|
37
43
|
The HTML tag to use for the slot's wrapper element. This allows you to change the wrapper element from the default `div` to any valid HTML tag (e.g., 'span', 'p', 'a', etc.). When using specific tags like 'a', you can also provide their respective HTML attributes (e.g., 'href', 'target', etc.).
|
|
@@ -71,7 +77,7 @@ Example:
|
|
|
71
77
|
|
|
72
78
|
- `ctx`: An object representing the context of the slot, including methods for manipulating the slot's content.
|
|
73
79
|
|
|
74
|
-
The slot property, which is implemented as a promise function, provides developers with the flexibility to dynamically generate and manipulate content within slots.
|
|
80
|
+
The slot property, which is implemented as a promise function, provides developers with the flexibility to dynamically generate and manipulate content within slots.
|
|
75
81
|
However, it's important to note that this promise is render-blocking, meaning that the component will not render until the promise is resolved.
|
|
76
82
|
|
|
77
83
|
### context
|
package/src/i18n/en_US.json
CHANGED
|
@@ -141,6 +141,13 @@
|
|
|
141
141
|
},
|
|
142
142
|
"InputDate": {
|
|
143
143
|
"picker": "Select a date"
|
|
144
|
+
},
|
|
145
|
+
"Table": {
|
|
146
|
+
"ariaSortAscending": "ascending",
|
|
147
|
+
"ariaSortDescending": "descending",
|
|
148
|
+
"sortedAscending": "Sort {label} ascending",
|
|
149
|
+
"sortedDescending": "Sort {label} descending",
|
|
150
|
+
"sortBy": "Sort by {label}"
|
|
144
151
|
}
|
|
145
152
|
}
|
|
146
153
|
}
|
package/src/lib/slot.tsx
CHANGED
|
@@ -2,30 +2,35 @@
|
|
|
2
2
|
* Copyright 2024 Adobe
|
|
3
3
|
* All Rights Reserved.
|
|
4
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.
|
|
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
8
|
*******************************************************************/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { IntlContext, Lang } from '@adobe-commerce/elsie/i18n';
|
|
11
|
+
import {
|
|
12
|
+
cloneElement,
|
|
13
|
+
ComponentChildren,
|
|
14
|
+
createElement,
|
|
15
|
+
RefObject,
|
|
16
|
+
VNode,
|
|
17
|
+
} from 'preact';
|
|
18
|
+
import { HTMLAttributes } from 'preact/compat';
|
|
11
19
|
import {
|
|
12
20
|
StateUpdater,
|
|
21
|
+
useCallback,
|
|
13
22
|
useContext,
|
|
14
|
-
useState,
|
|
15
|
-
useRef,
|
|
16
23
|
useEffect,
|
|
17
24
|
useMemo,
|
|
18
|
-
|
|
25
|
+
useRef,
|
|
26
|
+
useState,
|
|
19
27
|
} from 'preact/hooks';
|
|
20
|
-
import { IntlContext, Lang } from '@adobe-commerce/elsie/i18n';
|
|
21
|
-
import { HTMLAttributes } from 'preact/compat';
|
|
22
28
|
import { SlotQueueContext } from './render';
|
|
23
29
|
|
|
24
30
|
import '@adobe-commerce/elsie/components/UIProvider/debugger.css';
|
|
25
31
|
|
|
26
32
|
type MutateElement = (elem: HTMLElement) => void;
|
|
27
33
|
|
|
28
|
-
|
|
29
34
|
interface State {
|
|
30
35
|
get: (key: string) => void;
|
|
31
36
|
set: (key: string, value: any) => void;
|
|
@@ -149,18 +154,21 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
149
154
|
// @ts-ignore
|
|
150
155
|
context._registerMethod = _registerMethod;
|
|
151
156
|
|
|
152
|
-
const _htmlElementToVNode = useCallback(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
refElem
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
const _htmlElementToVNode = useCallback(
|
|
158
|
+
(elem: HTMLElement) => {
|
|
159
|
+
return createElement(
|
|
160
|
+
contentTag,
|
|
161
|
+
{
|
|
162
|
+
'data-slot-html-element': elem.tagName.toLowerCase(),
|
|
163
|
+
ref: (refElem: HTMLElement | null): void => {
|
|
164
|
+
refElem?.appendChild(elem);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
null
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
[contentTag]
|
|
171
|
+
);
|
|
164
172
|
|
|
165
173
|
// @ts-ignore
|
|
166
174
|
context._htmlElementToVNode = _htmlElementToVNode;
|
|
@@ -322,7 +330,10 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
322
330
|
status.current = 'loading';
|
|
323
331
|
|
|
324
332
|
log(`🟩 "${name}" Slot Initialized`);
|
|
325
|
-
await callback(
|
|
333
|
+
await callback(
|
|
334
|
+
context as K & DefaultSlotContext<K>,
|
|
335
|
+
elementRef.current as HTMLDivElement | null
|
|
336
|
+
);
|
|
326
337
|
} catch (error) {
|
|
327
338
|
console.error(`Error in "${callback.name}" Slot callback`, error);
|
|
328
339
|
} finally {
|
|
@@ -336,7 +347,7 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
336
347
|
// Initialization
|
|
337
348
|
useEffect(() => {
|
|
338
349
|
handleLifeCycleInit().finally(() => {
|
|
339
|
-
if (slotsQueue) {
|
|
350
|
+
if (slotsQueue && slotsQueue.value.has(name)) {
|
|
340
351
|
slotsQueue.value.delete(name);
|
|
341
352
|
slotsQueue.value = new Set(slotsQueue.value);
|
|
342
353
|
}
|
|
@@ -359,6 +370,7 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
359
370
|
interface SlotPropsComponent<T>
|
|
360
371
|
extends Omit<HTMLAttributes<HTMLElement>, 'slot'> {
|
|
361
372
|
name: string;
|
|
373
|
+
lazy?: boolean;
|
|
362
374
|
slot?: SlotProps<T>;
|
|
363
375
|
context?: Context<T>;
|
|
364
376
|
render?: (props: Record<string, any>) => VNode | VNode[];
|
|
@@ -371,6 +383,7 @@ interface SlotPropsComponent<T>
|
|
|
371
383
|
|
|
372
384
|
export function Slot<T>({
|
|
373
385
|
name,
|
|
386
|
+
lazy = false,
|
|
374
387
|
context,
|
|
375
388
|
slot,
|
|
376
389
|
children,
|
|
@@ -400,11 +413,11 @@ export function Slot<T>({
|
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
// add slot to queue
|
|
403
|
-
if (slotsQueue) {
|
|
416
|
+
if (slotsQueue && lazy === false) {
|
|
404
417
|
slotsQueue.value.add(name);
|
|
405
418
|
slotsQueue.value = new Set(slotsQueue.value);
|
|
406
419
|
}
|
|
407
|
-
}, [name, slotsQueue]);
|
|
420
|
+
}, [name, lazy, slotsQueue]);
|
|
408
421
|
|
|
409
422
|
return createElement(
|
|
410
423
|
slotTag,
|