@adobe-commerce/elsie 1.5.0-beta3 → 1.5.0-beta4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.5.0-beta3",
3
+ "version": "1.5.0-beta4",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -25,6 +25,8 @@ export interface ButtonProps
25
25
  active?: boolean;
26
26
  activeChildren?: ComponentChildren;
27
27
  activeIcon?: VNode<HTMLAttributes<SVGSVGElement>>;
28
+ href?: string;
29
+ type?: 'button' | 'submit' | 'reset';
28
30
  }
29
31
 
30
32
  export const Button: FunctionComponent<ButtonProps> = ({
@@ -19,6 +19,12 @@
19
19
  opacity: 1;
20
20
  }
21
21
 
22
+ .dropin-incrementer__content--no-buttons {
23
+ grid-template-columns: max-content;
24
+ width: fit-content;
25
+ margin-inline: auto;
26
+ }
27
+
22
28
  .dropin-incrementer__content--disabled {
23
29
  background: var(--color-neutral-300);
24
30
  border-radius: var(--shape-border-radius-1);
@@ -79,6 +79,10 @@ const meta: Meta<IncrementerProps> = {
79
79
  description: 'Maximum length of the input field',
80
80
  type: 'number',
81
81
  },
82
+ showButtons: {
83
+ description: 'Show increase/decrease buttons',
84
+ control: 'boolean',
85
+ },
82
86
  },
83
87
  };
84
88
 
@@ -170,3 +174,17 @@ export const WithError = {
170
174
  await expect(error).toHaveTextContent('Maximum quantity is 100');
171
175
  },
172
176
  };
177
+
178
+ export const WithoutButtons: Story = {
179
+ args: {
180
+ size: 'medium',
181
+ onValue: action('onValue'),
182
+ name: 'incrementerField',
183
+ value: '1',
184
+ min: 1,
185
+ max: 100,
186
+ disabled: false,
187
+ 'aria-label': 'Quantity',
188
+ showButtons: false,
189
+ },
190
+ };
@@ -28,6 +28,7 @@ export interface IncrementerProps
28
28
  max?: number;
29
29
  disabled?: boolean;
30
30
  maxLength?: number;
31
+ showButtons?: boolean;
31
32
  }
32
33
 
33
34
  export const Incrementer: FunctionComponent<IncrementerProps> = ({
@@ -42,6 +43,7 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
42
43
  onValue,
43
44
  onUpdateError,
44
45
  size = 'medium',
46
+ showButtons = true,
45
47
  ...props
46
48
  }) => {
47
49
  const [currentValue, setCurrentValue] = useState<number>(Number(value));
@@ -99,41 +101,44 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
99
101
  className={classes([
100
102
  'dropin-incrementer__content',
101
103
  `dropin-incrementer__content--${size}`,
104
+ ['dropin-incrementer__content--no-buttons', !showButtons],
102
105
  [`dropin-incrementer__content--error`, isInvalid],
103
106
  [`dropin-incrementer__content--success`, success],
104
107
  [`dropin-incrementer__content--disabled`, disabled],
105
108
  ])}
106
109
  >
107
110
  {/* Minus Button */}
108
- <div
109
- className={classes([
110
- 'dropin-incrementer__button-container',
111
- [`dropin-incrementer__button-container--disabled`, disabled],
112
- ])}
113
- >
114
- <Localizer>
115
- <button
116
- type="button"
117
- className={classes([
118
- 'dropin-incrementer__decrease-button',
119
- [`dropin-incrementer__decrease-button--disabled`, disabled],
120
- ])}
121
- onClick={() => handleIncrementer(currentValue - 1)}
122
- disabled={disabled || currentValue < minValue + 1}
123
- aria-label={
124
- (<Text id="Dropin.Incrementer.decreaseLabel" />) as any
125
- }
126
- >
127
- <Icon
128
- source={Minus}
129
- size="16"
130
- stroke="1"
131
- viewBox="4 2 20 20"
132
- className="dropin-incrementer__down"
133
- />
134
- </button>
135
- </Localizer>
136
- </div>
111
+ {showButtons && (
112
+ <div
113
+ className={classes([
114
+ 'dropin-incrementer__button-container',
115
+ [`dropin-incrementer__button-container--disabled`, disabled],
116
+ ])}
117
+ >
118
+ <Localizer>
119
+ <button
120
+ type="button"
121
+ className={classes([
122
+ 'dropin-incrementer__decrease-button',
123
+ [`dropin-incrementer__decrease-button--disabled`, disabled],
124
+ ])}
125
+ onClick={() => handleIncrementer(currentValue - 1)}
126
+ disabled={disabled || currentValue < minValue + 1}
127
+ aria-label={
128
+ (<Text id="Dropin.Incrementer.decreaseLabel" />) as any
129
+ }
130
+ >
131
+ <Icon
132
+ source={Minus}
133
+ size="16"
134
+ stroke="1"
135
+ viewBox="4 2 20 20"
136
+ className="dropin-incrementer__down"
137
+ />
138
+ </button>
139
+ </Localizer>
140
+ </div>
141
+ )}
137
142
 
138
143
  {/* Input Field */}
139
144
  <input
@@ -157,36 +162,38 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
157
162
  {...props}
158
163
  />
159
164
 
160
- <div
161
- className={classes([
162
- 'dropin-incrementer__button-container',
163
- [`dropin-incrementer__button-container--disabled`, disabled],
164
- ])}
165
- >
166
- {/* Plus/Add button */}
167
- <Localizer>
168
- <button
169
- type="button"
170
- className={classes([
171
- 'dropin-incrementer__increase-button',
172
- [`dropin-incrementer__increase-button--disabled`, disabled],
173
- ])}
174
- onClick={() => handleIncrementer(currentValue + 1)}
175
- disabled={disabled || currentValue > maxValue - 1}
176
- aria-label={
177
- (<Text id="Dropin.Incrementer.increaseLabel" />) as any
178
- }
179
- >
180
- <Icon
181
- source={Add}
182
- size="16"
183
- stroke="1"
184
- viewBox="4 2 20 20"
185
- className="dropin-incrementer__add"
186
- />
187
- </button>
188
- </Localizer>
189
- </div>
165
+ {showButtons && (
166
+ <div
167
+ className={classes([
168
+ 'dropin-incrementer__button-container',
169
+ [`dropin-incrementer__button-container--disabled`, disabled],
170
+ ])}
171
+ >
172
+ {/* Plus/Add button */}
173
+ <Localizer>
174
+ <button
175
+ type="button"
176
+ className={classes([
177
+ 'dropin-incrementer__increase-button',
178
+ [`dropin-incrementer__increase-button--disabled`, disabled],
179
+ ])}
180
+ onClick={() => handleIncrementer(currentValue + 1)}
181
+ disabled={disabled || currentValue > maxValue - 1}
182
+ aria-label={
183
+ (<Text id="Dropin.Incrementer.increaseLabel" />) as any
184
+ }
185
+ >
186
+ <Icon
187
+ source={Add}
188
+ size="16"
189
+ stroke="1"
190
+ viewBox="4 2 20 20"
191
+ className="dropin-incrementer__add"
192
+ />
193
+ </button>
194
+ </Localizer>
195
+ </div>
196
+ )}
190
197
  </div>
191
198
  {isInvalid && (
192
199
  <p className="dropin-incrementer__content--error-message">
@@ -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,673 @@
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
+ loading: {
100
+ description: 'When true, renders skeleton rows instead of actual data. Useful for showing loading state while data is being fetched.',
101
+ table: {
102
+ type: { summary: 'boolean' },
103
+ },
104
+ control: 'boolean',
105
+ },
106
+ skeletonRowCount: {
107
+ description: 'Number of skeleton rows to render when loading is true. Defaults to 10 rows.',
108
+ table: {
109
+ type: { summary: 'number' },
110
+ },
111
+ control: 'number',
112
+ },
113
+ },
114
+ };
115
+
116
+ export default meta;
117
+
118
+ type Story = StoryObj<TableProps>;
119
+
120
+ // Wrapper component to manage sorting state
121
+ const TableWithState = (args: TableProps) => {
122
+ const [columns, setColumns] = useState(args.columns);
123
+
124
+ const handleSortChange = (columnKey: string, direction: 'asc' | 'desc' | true) => {
125
+ // call Action onSortChange
126
+ args.onSortChange?.(columnKey, direction);
127
+
128
+ // Update column sort states
129
+ setColumns(prevColumns =>
130
+ prevColumns.map(col => {
131
+ if (col.key === columnKey) {
132
+ return { ...col, sortBy: direction };
133
+ } else if (col.sortBy === 'asc' || col.sortBy === 'desc') {
134
+ return { ...col, sortBy: true }; // Reset other sorted columns to neutral
135
+ }
136
+ return col;
137
+ })
138
+ );
139
+ };
140
+
141
+ return (
142
+ <TableComponent
143
+ {...args}
144
+ columns={columns}
145
+ onSortChange={handleSortChange}
146
+ />
147
+ );
148
+ };
149
+
150
+ /**
151
+ * Simple table.
152
+ * Demonstrates basic table structure with string and number content types.
153
+ *
154
+ *
155
+ * ```tsx
156
+ * <Table
157
+ * columns={[
158
+ * { key: 'name', label: 'Name' },
159
+ * { key: 'email', label: 'Email' },
160
+ * { key: 'age', label: 'Age' }
161
+ * ]}
162
+ * rowData={[
163
+ * { name: 'John', email: 'john@example.com', age: 20 },
164
+ * { name: 'Jane', email: 'jane@example.com', age: 21 }
165
+ * ]}
166
+ * />
167
+ * ```
168
+ */
169
+ export const Table: Story = {
170
+ args: {
171
+ columns: [
172
+ { key: 'name', label: 'Name' },
173
+ { key: 'email', label: 'Email' },
174
+ { key: 'age', label: 'Age' },
175
+ { key: 'actions', label: 'Actions' },
176
+ ],
177
+ rowData: [
178
+ { name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
179
+ { name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
180
+ { name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
181
+ { name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
182
+ ],
183
+ },
184
+ };
185
+
186
+ /**
187
+ * Table where all columns are sortable. Demonstrates the three-state sorting cycle: `true` → `'asc'` → `'desc'` → `true`.
188
+ * Shows how multiple columns can be sortable simultaneously, with only one active sort at a time.
189
+ *
190
+ * ```tsx
191
+ * <Table
192
+ * columns={[
193
+ * { key: 'name', label: 'Name', sortBy: true },
194
+ * { key: 'email', label: 'Email', sortBy: true },
195
+ * { key: 'age', label: 'Age', sortBy: true }
196
+ * ]}
197
+ * rowData={[
198
+ * { name: 'John', email: 'john@example.com', age: 20 },
199
+ * { name: 'Jane', email: 'jane@example.com', age: 21 }
200
+ * ]}
201
+ * onSortChange={(columnKey, direction) => handleSort(columnKey, direction)}
202
+ * />
203
+ * ```
204
+ */
205
+ export const AllSortable: Story = {
206
+ render: TableWithState,
207
+ args: {
208
+ columns: [
209
+ { key: 'name', label: 'Name', sortBy: true },
210
+ { key: 'email', label: 'Email', sortBy: true },
211
+ { key: 'age', label: 'Age', sortBy: true },
212
+ { key: 'actions', label: 'Actions' },
213
+ ],
214
+ rowData: [
215
+ { name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
216
+ { name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
217
+ { name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
218
+ { name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
219
+ { name: 'Jack', email: 'jack@example.com', age: 24, actions: <button>Edit</button> },
220
+ ],
221
+ },
222
+ };
223
+
224
+ /**
225
+ * Wide table with 10 columns to demonstrate horizontal scrolling and container query behavior.
226
+ * This table will show how the container query responds when the table becomes too wide for its container.
227
+ *
228
+ * ```tsx
229
+ * <Table
230
+ * columns={[
231
+ * { key: 'id', label: 'ID' },
232
+ * { key: 'name', label: 'Full Name' },
233
+ * { key: 'email', label: 'Email Address' },
234
+ * { key: 'phone', label: 'Phone Number' },
235
+ * { key: 'department', label: 'Department' },
236
+ * { key: 'position', label: 'Position' },
237
+ * { key: 'salary', label: 'Salary' },
238
+ * { key: 'startDate', label: 'Start Date' },
239
+ * { key: 'status', label: 'Status' },
240
+ * { key: 'actions', label: 'Actions' }
241
+ * ]}
242
+ * rowData={[
243
+ * { 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> }
244
+ * ]}
245
+ * />
246
+ * ```
247
+ */
248
+ export const WideTable: Story = {
249
+ args: {
250
+ columns: [
251
+ { key: 'id', label: 'ID' },
252
+ { key: 'name', label: 'Full Name' },
253
+ { key: 'email', label: 'Email Address' },
254
+ { key: 'phone', label: 'Phone Number' },
255
+ { key: 'department', label: 'Department' },
256
+ { key: 'position', label: 'Position' },
257
+ { key: 'salary', label: 'Salary' },
258
+ { key: 'startDate', label: 'Start Date' },
259
+ { key: 'status', label: 'Status' },
260
+ { key: 'actions', label: 'Actions' },
261
+ ],
262
+ rowData: [
263
+ {
264
+ id: 1,
265
+ name: 'John Doe',
266
+ email: 'john.doe@company.com',
267
+ phone: '+1-555-0123',
268
+ department: 'Engineering',
269
+ position: 'Senior Developer',
270
+ salary: '$95,000',
271
+ startDate: '2022-01-15',
272
+ status: 'Active',
273
+ actions: <button>Edit</button>
274
+ },
275
+ {
276
+ id: 2,
277
+ name: 'Jane Smith',
278
+ email: 'jane.smith@company.com',
279
+ phone: '+1-555-0124',
280
+ department: 'Marketing',
281
+ position: 'Marketing Manager',
282
+ salary: '$78,000',
283
+ startDate: '2021-06-20',
284
+ status: 'Active',
285
+ actions: <button>Edit</button>
286
+ },
287
+ {
288
+ id: 3,
289
+ name: 'Bob Johnson',
290
+ email: 'bob.johnson@company.com',
291
+ phone: '+1-555-0125',
292
+ department: 'Sales',
293
+ position: 'Sales Director',
294
+ salary: '$110,000',
295
+ startDate: '2020-03-10',
296
+ status: 'Active',
297
+ actions: <button>Edit</button>
298
+ },
299
+ {
300
+ id: 4,
301
+ name: 'Alice Brown',
302
+ email: 'alice.brown@company.com',
303
+ phone: '+1-555-0126',
304
+ department: 'HR',
305
+ position: 'HR Specialist',
306
+ salary: '$65,000',
307
+ startDate: '2023-02-28',
308
+ status: 'Pending',
309
+ actions: <button>Edit</button>
310
+ },
311
+ {
312
+ id: 5,
313
+ name: 'Charlie Wilson',
314
+ email: 'charlie.wilson@company.com',
315
+ phone: '+1-555-0127',
316
+ department: 'Finance',
317
+ position: 'Financial Analyst',
318
+ salary: '$72,000',
319
+ startDate: '2022-09-12',
320
+ status: 'Active',
321
+ actions: <button>Edit</button>
322
+ },
323
+ ],
324
+ },
325
+ };
326
+
327
+ /**
328
+ * Table demonstrating complex VNode content in cells with multi-line text and interactive elements.
329
+ * This shows how the table handles rich content including buttons, badges, and formatted text.
330
+ *
331
+ * ```tsx
332
+ * <Table
333
+ * columns={[
334
+ * { key: 'user', label: 'User Info' },
335
+ * { key: 'description', label: 'Description' },
336
+ * { key: 'status', label: 'Status' },
337
+ * { key: 'actions', label: 'Actions' }
338
+ * ]}
339
+ * rowData={[
340
+ * {
341
+ * user: <div><strong>John Doe</strong><br/>john@example.com<br/>Senior Developer</div>,
342
+ * description: <div>Lead developer for the<br/>e-commerce platform<br/>with 5+ years experience</div>,
343
+ * status: <span>Active</span>,
344
+ * actions: <div><button>Edit</button><br/><button>Delete</button><br/><button>View</button></div>
345
+ * }
346
+ * ]}
347
+ * />
348
+ * ```
349
+ */
350
+ export const ComplexCells: Story = {
351
+ args: {
352
+ columns: [
353
+ { key: 'user', label: 'User Info' },
354
+ { key: 'description', label: 'Description' },
355
+ { key: 'status', label: 'Status' },
356
+ { key: 'actions', label: 'Actions' },
357
+ ],
358
+ rowData: [
359
+ {
360
+ user: (
361
+ <div>
362
+ <strong>John Doe</strong><br/>
363
+ john.doe@company.com<br/>
364
+ <em>Senior Developer</em>
365
+ </div>
366
+ ),
367
+ description: (
368
+ <div>
369
+ Lead developer for the<br/>
370
+ e-commerce platform<br/>
371
+ <small>with 5+ years experience</small>
372
+ </div>
373
+ ),
374
+ status: (
375
+ <span>Active</span>
376
+ ),
377
+ actions: (
378
+ <div>
379
+ <button>Edit</button>
380
+ <button>Delete</button>
381
+ <button>View</button>
382
+ </div>
383
+ ),
384
+ },
385
+ {
386
+ user: (
387
+ <div>
388
+ <strong>Jane Smith</strong><br/>
389
+ jane.smith@company.com<br/>
390
+ <em>Product Manager</em>
391
+ </div>
392
+ ),
393
+ description: (
394
+ <div>
395
+ Manages product roadmap<br/>
396
+ and feature planning<br/>
397
+ <small>3+ years in product</small>
398
+ </div>
399
+ ),
400
+ status: (
401
+ <span>Pending</span>
402
+ ),
403
+ actions: (
404
+ <div>
405
+ <button>Edit</button>
406
+ <button>Approve</button>
407
+ <button>Reject</button>
408
+ </div>
409
+ ),
410
+ },
411
+ {
412
+ user: (
413
+ <div>
414
+ <strong>Bob Johnson</strong><br/>
415
+ bob.johnson@company.com<br/>
416
+ <em>UX Designer</em>
417
+ </div>
418
+ ),
419
+ description: (
420
+ <div>
421
+ Designs user interfaces<br/>
422
+ and user experiences<br/>
423
+ <small>Expert in Figma & Sketch</small>
424
+ </div>
425
+ ),
426
+ status: (
427
+ <span>Inactive</span>
428
+ ),
429
+ actions: (
430
+ <div>
431
+ <button>Edit</button>
432
+ <button>Activate</button>
433
+ <button>Archive</button>
434
+ </div>
435
+ ),
436
+ },
437
+ ],
438
+ },
439
+ };
440
+
441
+ /**
442
+ * Table with stacked mobile layout that uses container queries.
443
+ * This demonstrates how the table adapts to its container width rather than viewport width.
444
+ * The table will stack vertically when its container becomes narrow (≤600px).
445
+ *
446
+ * **Container Query Behavior**: Uses `mobileLayout="stacked"` to enable responsive stacking.
447
+ * When the container width ≤ 600px:
448
+ * - Headers are hidden (`display: none`)
449
+ * - Cells stack vertically (`display: block`)
450
+ * - Column labels appear as `data-label` attributes before each cell value
451
+ * - Perfect for mobile views, sidebars, or constrained layouts
452
+ *
453
+ * ```tsx
454
+ * <Table
455
+ * mobileLayout="stacked"
456
+ * columns={[
457
+ * { key: 'name', label: 'Name' },
458
+ * { key: 'email', label: 'Email' },
459
+ * { key: 'age', label: 'Age' }
460
+ * ]}
461
+ * rowData={[
462
+ * { name: 'John', email: 'john@example.com', age: 20 },
463
+ * { name: 'Jane', email: 'jane@example.com', age: 21 }
464
+ * ]}
465
+ * />
466
+ * ```
467
+ */
468
+ export const StackedMobileLayout: Story = {
469
+ args: {
470
+ mobileLayout: 'stacked',
471
+ columns: [
472
+ { key: 'name', label: 'Name' },
473
+ { key: 'email', label: 'Email' },
474
+ { key: 'age', label: 'Age' },
475
+ { key: 'status', label: 'Status' },
476
+ { key: 'actions', label: 'Actions' },
477
+ ],
478
+ rowData: [
479
+ { name: 'John Doe', email: 'john.doe@example.com', age: 28, status: 'Active', actions: <button>Edit</button> },
480
+ { name: 'Jane Smith', email: 'jane.smith@example.com', age: 32, status: 'Inactive', actions: <button>Edit</button> },
481
+ { name: 'Bob Johnson', email: 'bob.johnson@example.com', age: 45, status: 'Active', actions: <button>Edit</button> },
482
+ { name: 'Alice Brown', email: 'alice.brown@example.com', age: 29, status: 'Pending', actions: <button>Edit</button> },
483
+ ],
484
+ },
485
+ };
486
+
487
+
488
+ /**
489
+ * Table with programmatically controlled expandable rows.
490
+ * Row expansion is controlled by buttons or other interactive elements within the column content.
491
+ * Developers must manage the `expandedRows` state themselves.
492
+ *
493
+ * **Features**:
494
+ * - Row details only render when both `_rowDetails` exists and row index is in `expandedRows`
495
+ * - Row details span the full width of the table
496
+ * - Supports any VNode content in the `_rowDetails` property
497
+ *
498
+ * ```tsx
499
+ * const [expandedRows, setExpandedRows] = useState(new Set());
500
+ *
501
+ * const toggleRow = (rowIndex: number) => {
502
+ * setExpandedRows(prev => {
503
+ * const newSet = new Set(prev);
504
+ * if (newSet.has(rowIndex)) {
505
+ * newSet.delete(rowIndex);
506
+ * } else {
507
+ * newSet.add(rowIndex);
508
+ * }
509
+ * return newSet;
510
+ * });
511
+ * };
512
+ *
513
+ * <Table
514
+ * columns={[
515
+ * { key: 'name', label: 'Name' },
516
+ * { key: 'email', label: 'Email' },
517
+ * { key: 'actions', label: 'Actions' }
518
+ * ]}
519
+ * rowData={[
520
+ * {
521
+ * name: 'John',
522
+ * email: 'john@example.com',
523
+ * actions: <button onClick={() => toggleRow(0)}>Toggle Details</button>,
524
+ * _rowDetails: <div>Additional information...</div>
525
+ * }
526
+ * ]}
527
+ * expandedRows={expandedRows}
528
+ * />
529
+ * ```
530
+ */
531
+ export const RowDetails: Story = {
532
+ render: (args) => {
533
+ const [expandedRows, setExpandedRows] = useState(new Set<number>());
534
+
535
+ const toggleRow = (rowIndex: number) => {
536
+ setExpandedRows(prev => {
537
+ const newSet = new Set(prev);
538
+ if (newSet.has(rowIndex)) {
539
+ newSet.delete(rowIndex);
540
+ } else {
541
+ newSet.add(rowIndex);
542
+ }
543
+ return newSet;
544
+ });
545
+ };
546
+
547
+ const rowData = [
548
+ {
549
+ name: 'John Doe',
550
+ email: 'john.doe@company.com',
551
+ status: 'Active',
552
+ actions: (
553
+ <button onClick={() => toggleRow(0)}>
554
+ {expandedRows.has(0) ? 'Hide' : 'Show'}
555
+ </button>
556
+ ),
557
+ _rowDetails: (
558
+ <div>
559
+ <h3>Employee Details</h3>
560
+ <p><strong>Department:</strong> Engineering</p>
561
+ <p><strong>Position:</strong> Senior Developer</p>
562
+ <p><strong>Start Date:</strong> January 15, 2022</p>
563
+ <p><strong>Notes:</strong> Excellent performance, leads the frontend team.</p>
564
+ <div style={{ marginTop: '12px' }}>
565
+ <button style={{ marginRight: '8px' }}>Update Details</button>
566
+ <button>View Full Profile</button>
567
+ </div>
568
+ </div>
569
+ )
570
+ },
571
+ {
572
+ name: 'Jane Smith',
573
+ email: 'jane.smith@company.com',
574
+ status: 'Pending',
575
+ actions: (
576
+ <button onClick={() => toggleRow(1)}>
577
+ {expandedRows.has(1) ? 'Hide' : 'Show'}
578
+ </button>
579
+ ),
580
+ _rowDetails: (
581
+ <div>
582
+ <h3>Pending Approval</h3>
583
+ <p><strong>Department:</strong> Marketing</p>
584
+ <p><strong>Position:</strong> Marketing Manager</p>
585
+ <p><strong>Application Date:</strong> December 1, 2024</p>
586
+ <p><strong>Status:</strong> Awaiting HR approval</p>
587
+ <div style={{ marginTop: '12px' }}>
588
+ <button style={{ marginRight: '8px', backgroundColor: '#22c55e', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Approve</button>
589
+ <button style={{ backgroundColor: '#ef4444', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Reject</button>
590
+ </div>
591
+ </div>
592
+ )
593
+ },
594
+ {
595
+ name: 'Bob Johnson',
596
+ email: 'bob.johnson@company.com',
597
+ status: 'Inactive',
598
+ actions: (
599
+ <button onClick={() => toggleRow(2)}>
600
+ {expandedRows.has(2) ? 'Hide' : 'Show'}
601
+ </button>
602
+ ),
603
+ _rowDetails: (
604
+ <div>
605
+ <h3>Account Information</h3>
606
+ <p><strong>Department:</strong> Sales</p>
607
+ <p><strong>Position:</strong> Sales Director</p>
608
+ <p><strong>Last Active:</strong> November 20, 2024</p>
609
+ <p><strong>Reason:</strong> On extended leave</p>
610
+ <div style={{ marginTop: '12px' }}>
611
+ <button style={{ marginRight: '8px' }}>Reactivate Account</button>
612
+ <button>Contact Employee</button>
613
+ </div>
614
+ </div>
615
+ )
616
+ },
617
+ ];
618
+
619
+ return (
620
+ <TableComponent
621
+ {...args}
622
+ columns={[
623
+ { key: 'name', label: 'Name' },
624
+ { key: 'email', label: 'Email' },
625
+ { key: 'status', label: 'Status' },
626
+ { key: 'actions', label: 'Actions' },
627
+ ]}
628
+ rowData={rowData}
629
+ expandedRows={expandedRows}
630
+ />
631
+ );
632
+ },
633
+ };
634
+
635
+ /**
636
+ * Table in loading state with skeleton rows.
637
+ * Demonstrates how the table appears while data is being fetched.
638
+ * Each cell shows a skeleton placeholder that matches the table structure.
639
+ *
640
+ * **Features**:
641
+ * - Shows skeleton rows instead of actual data when `loading` is true
642
+ * - Configurable number of skeleton rows via `skeletonRowCount` prop
643
+ * - Maintains table structure and column headers during loading
644
+ * - Each cell contains a single-line skeleton component
645
+ *
646
+ * ```tsx
647
+ * <Table
648
+ * loading={true}
649
+ * skeletonRowCount={5}
650
+ * columns={[
651
+ * { key: 'name', label: 'Name' },
652
+ * { key: 'email', label: 'Email' },
653
+ * { key: 'status', label: 'Status' }
654
+ * ]}
655
+ * rowData={[]} // Empty array when loading
656
+ * />
657
+ * ```
658
+ */
659
+ export const LoadingState: Story = {
660
+ args: {
661
+ loading: true,
662
+ skeletonRowCount: 5,
663
+ columns: [
664
+ { key: 'name', label: 'Name' },
665
+ { key: 'email', label: 'Email' },
666
+ { key: 'status', label: 'Status' },
667
+ { key: 'actions', label: 'Actions' },
668
+ ],
669
+ rowData: [], // Empty when loading
670
+ },
671
+ };
672
+
673
+
@@ -0,0 +1,208 @@
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, Skeleton, SkeletonRow } 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 Omit<HTMLAttributes<HTMLTableElement>, 'loading'> {
32
+ columns: Column[];
33
+ rowData: RowData[];
34
+ mobileLayout?: 'stacked' | 'none';
35
+ caption?: string;
36
+ expandedRows?: Set<number>;
37
+ loading?: boolean;
38
+ skeletonRowCount?: number;
39
+ onSortChange?: (columnKey: string, direction: Sortable) => void;
40
+ }
41
+
42
+ export const Table: FunctionComponent<TableProps> = ({
43
+ className,
44
+ children,
45
+ columns = [],
46
+ rowData = [],
47
+ mobileLayout = 'none',
48
+ caption,
49
+ expandedRows = new Set(),
50
+ loading = false,
51
+ skeletonRowCount = 10,
52
+ onSortChange,
53
+ ...props
54
+ }) => {
55
+ const translations = useText({
56
+ sortedAscending: 'Dropin.Table.sortedAscending',
57
+ sortedDescending: 'Dropin.Table.sortedDescending',
58
+ sortBy: 'Dropin.Table.sortBy',
59
+ });
60
+
61
+ const handleSort = (column: Column) => {
62
+ if (!onSortChange) return;
63
+
64
+ // Determine next sort direction
65
+ let nextDirection: Sortable;
66
+ if (column.sortBy === true) {
67
+ nextDirection = 'asc';
68
+ } else if (column.sortBy === 'asc') {
69
+ nextDirection = 'desc';
70
+ } else {
71
+ nextDirection = true;
72
+ }
73
+
74
+ onSortChange(column.key, nextDirection);
75
+ };
76
+
77
+ const renderSortButton = (column: Column) => {
78
+ if (column.sortBy === undefined) return null;
79
+
80
+ let iconSource: string;
81
+ let ariaLabel: string;
82
+
83
+ if (column.sortBy === 'asc') {
84
+ iconSource = 'ChevronUp';
85
+ ariaLabel = translations.sortedAscending.replace('{label}', column.label);
86
+ } else if (column.sortBy === 'desc') {
87
+ iconSource = 'ChevronDown';
88
+ ariaLabel = translations.sortedDescending.replace('{label}', column.label);
89
+ } else {
90
+ iconSource = 'Sort';
91
+ ariaLabel = translations.sortBy.replace('{label}', column.label);
92
+ }
93
+
94
+ return (
95
+ <Button
96
+ variant="tertiary"
97
+ size="medium"
98
+ className="dropin-table__header__sort-button"
99
+ icon={<Icon source={iconSource} />}
100
+ aria-label={ariaLabel}
101
+ onClick={() => handleSort(column)}
102
+ />
103
+ );
104
+ };
105
+
106
+ const renderSkeletonRows = () => {
107
+ return Array.from({ length: skeletonRowCount }, (_, rowIndex) => (
108
+ <tr key={`skeleton-${rowIndex}`} className="dropin-table__body__row">
109
+ {columns.map((column) => (
110
+ <td key={column.key} className="dropin-table__body__cell" data-label={column.label}>
111
+ <Skeleton>
112
+ <SkeletonRow variant="row" size="small" fullWidth />
113
+ </Skeleton>
114
+ </td>
115
+ ))}
116
+ </tr>
117
+ ));
118
+ };
119
+
120
+ const renderDataRows = () => {
121
+ return rowData.map((row, rowIndex) => {
122
+ const hasDetails = row._rowDetails !== undefined;
123
+ const isExpanded = expandedRows.has(rowIndex);
124
+
125
+ return (
126
+ <Fragment key={rowIndex}>
127
+ <tr className="dropin-table__body__row">
128
+ {columns.map((column) => {
129
+ const cell = row[column.key];
130
+
131
+ if (typeof cell === 'string' || typeof cell === 'number') {
132
+ return (
133
+ <td key={column.key} className="dropin-table__body__cell" data-label={column.label}>
134
+ {cell}
135
+ </td>
136
+ );
137
+ }
138
+
139
+ return (
140
+ <td key={column.key} className="dropin-table__body__cell" data-label={column.label}>
141
+ <VComponent node={cell!} />
142
+ </td>
143
+ );
144
+ })}
145
+ </tr>
146
+ {hasDetails && isExpanded && (
147
+ <tr
148
+ key={`${rowIndex}-details`}
149
+ className="dropin-table__row-details dropin-table__row-details--expanded"
150
+ id={`row-${rowIndex}-details`}
151
+ >
152
+ <td
153
+ className="dropin-table__row-details__cell"
154
+ colSpan={columns.length}
155
+ role="region"
156
+ aria-labelledby={`row-${rowIndex}-details`}
157
+ >
158
+ {typeof row._rowDetails === 'string' ? row._rowDetails : <VComponent node={row._rowDetails!} />}
159
+ </td>
160
+ </tr>
161
+ )}
162
+ </Fragment>
163
+ );
164
+ });
165
+ };
166
+
167
+ const getAriaSort = (column: Column): 'none' | 'ascending' | 'descending' | 'other' | undefined => {
168
+ if (column.sortBy === true) return 'none';
169
+ if (column.sortBy === 'asc') return 'ascending';
170
+ if (column.sortBy === 'desc') return 'descending';
171
+ return undefined;
172
+ };
173
+
174
+ return (
175
+ <div className={classes(['dropin-table', `dropin-table--mobile-layout-${mobileLayout}`, className])}>
176
+ <table {...props} className="dropin-table__table">
177
+ {caption && <caption className="dropin-table__caption">{caption}</caption>}
178
+ <thead className="dropin-table__header">
179
+ <tr className="dropin-table__header__row">
180
+ {columns.map((column) => (
181
+ <th
182
+ key={column.key}
183
+ className={classes([
184
+ 'dropin-table__header__cell',
185
+ ['dropin-table__header__cell--sorted', column.sortBy === 'asc' || column.sortBy === 'desc'],
186
+ ['dropin-table__header__cell--sortable', column.sortBy !== undefined]
187
+ ])}
188
+ aria-sort={getAriaSort(column)}
189
+ >
190
+ {column.label}
191
+ {renderSortButton(column)}
192
+ </th>
193
+ ))}
194
+ </tr>
195
+ </thead>
196
+ <tbody className="dropin-table__body">
197
+ {loading ? (
198
+ // Render skeleton rows when loading
199
+ renderSkeletonRows()
200
+ ) : (
201
+ // Render actual data when not loading
202
+ renderDataRows()
203
+ )}
204
+ </tbody>
205
+ </table>
206
+ </div>
207
+ );
208
+ };
@@ -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';
@@ -34,7 +34,19 @@
34
34
  border: var(--shape-border-width-1) solid var(--color-neutral-800);
35
35
  }
36
36
 
37
- .dropin-toggle-button:has(input:focus-visible) {
37
+ /* Disabled */
38
+ .dropin-toggle-button__disabled .dropin-toggle-button__actionButton {
39
+ cursor: default;
40
+ background-color: var(--color-neutral-300);
41
+ border: var(--shape-border-width-1) solid var(--color-neutral-500);
42
+ }
43
+
44
+ .dropin-toggle-button__disabled .dropin-toggle-button__content {
45
+ color: var(--color-neutral-500);
46
+ cursor: default;
47
+ }
48
+
49
+ .dropin-toggle-button:not(.dropin-toggle-button__disabled):has(input:focus-visible) {
38
50
  outline: 0 none;
39
51
  box-shadow: 0 0 0 var(--shape-icon-stroke-4) var(--color-neutral-400);
40
52
  -webkit-box-shadow: 0 0 0 var(--shape-icon-stroke-4) var(--color-neutral-400);
@@ -79,6 +79,15 @@ const meta: Meta<ToggleButtonProps> = {
79
79
  type: 'boolean',
80
80
  },
81
81
  },
82
+ disabled: {
83
+ description: 'Whether or not the Toggle button is disabled',
84
+ type: {
85
+ name: 'boolean',
86
+ },
87
+ control: {
88
+ type: 'boolean',
89
+ },
90
+ },
82
91
  onChange: {
83
92
  description: 'Function to be called when the Toggle button is clicked',
84
93
  type: {
@@ -103,11 +112,9 @@ export const ToggleButtonStory: Story = {
103
112
  },
104
113
  play: async ({ canvasElement }) => {
105
114
  const canvas = within(canvasElement);
106
- const toggleButton = document.querySelector('.dropin-toggle-button');
107
115
  const toggleButtonInput = await canvas.findByRole('radio');
108
- const toggleButtonText = document.querySelector(
109
- '.dropin-toggle-button__content'
110
- );
116
+ const toggleButton = toggleButtonInput.closest('.dropin-toggle-button');
117
+ const toggleButtonText = toggleButton?.querySelector('.dropin-toggle-button__content');
111
118
  await expect(toggleButton).toHaveClass('dropin-toggle-button__selected');
112
119
  await expect(toggleButtonText).toHaveTextContent('Toggle Button label');
113
120
  await expect(toggleButtonInput).toBeChecked();
@@ -124,9 +131,9 @@ export const ToggleButtonNotSelected: Story = {
124
131
  },
125
132
  play: async ({ canvasElement }) => {
126
133
  const canvas = within(canvasElement);
127
- const toggleButton = document.querySelector('.dropin-toggle-button');
128
134
  const toggleButtonInput = await canvas.findByRole('radio');
129
- const toggleButtonText = await canvas.findByText('Toggle Button label');
135
+ const toggleButton = toggleButtonInput.closest('.dropin-toggle-button');
136
+ const toggleButtonText = toggleButton?.querySelector('.dropin-toggle-button__content');
130
137
  await expect(toggleButton).not.toHaveClass(
131
138
  'dropin-toggle-button__selected'
132
139
  );
@@ -19,6 +19,7 @@ export interface ToggleButtonProps
19
19
  name: string;
20
20
  value: string;
21
21
  busy?: boolean;
22
+ disabled?: boolean;
22
23
  icon?:
23
24
  | VNode<HTMLAttributes<SVGSVGElement>>
24
25
  | VNode<HTMLAttributes<HTMLImageElement>>;
@@ -31,6 +32,7 @@ export const ToggleButton: FunctionComponent<ToggleButtonProps> = ({
31
32
  name,
32
33
  value,
33
34
  busy = false,
35
+ disabled = false,
34
36
  children,
35
37
  className,
36
38
  icon,
@@ -45,6 +47,7 @@ export const ToggleButton: FunctionComponent<ToggleButtonProps> = ({
45
47
  'dropin-toggle-button',
46
48
  className,
47
49
  ['dropin-toggle-button__selected', selected],
50
+ ['dropin-toggle-button__disabled', disabled],
48
51
  ])}
49
52
  >
50
53
  <label className="dropin-toggle-button__actionButton">
@@ -53,6 +56,7 @@ export const ToggleButton: FunctionComponent<ToggleButtonProps> = ({
53
56
  name={name}
54
57
  value={value}
55
58
  checked={selected}
59
+ disabled={disabled}
56
60
  onChange={() => onChange && onChange(value)}
57
61
  aria-label={name}
58
62
  busy={busy}
@@ -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';
@@ -141,6 +141,11 @@
141
141
  },
142
142
  "InputDate": {
143
143
  "picker": "Select a date"
144
+ },
145
+ "Table": {
146
+ "sortedAscending": "Sort {label} ascending",
147
+ "sortedDescending": "Sort {label} descending",
148
+ "sortBy": "Sort by {label}"
144
149
  }
145
150
  }
146
151
  }