@adobe-commerce/elsie 1.5.0-alpha3000 → 1.5.0-alpha3002

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-alpha3000",
3
+ "version": "1.5.0-alpha3002",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -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">
@@ -96,6 +96,20 @@ const meta: Meta<TableProps> = {
96
96
  },
97
97
  control: 'object',
98
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
+ },
99
113
  },
100
114
  };
101
115
 
@@ -326,7 +340,7 @@ export const WideTable: Story = {
326
340
  * {
327
341
  * user: <div><strong>John Doe</strong><br/>john@example.com<br/>Senior Developer</div>,
328
342
  * 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>,
343
+ * status: <span>Active</span>,
330
344
  * actions: <div><button>Edit</button><br/><button>Delete</button><br/><button>View</button></div>
331
345
  * }
332
346
  * ]}
@@ -358,22 +372,13 @@ export const ComplexCells: Story = {
358
372
  </div>
359
373
  ),
360
374
  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>
375
+ <span>Active</span>
371
376
  ),
372
377
  actions: (
373
378
  <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>
379
+ <button>Edit</button>
380
+ <button>Delete</button>
381
+ <button>View</button>
377
382
  </div>
378
383
  ),
379
384
  },
@@ -393,22 +398,13 @@ export const ComplexCells: Story = {
393
398
  </div>
394
399
  ),
395
400
  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>
401
+ <span>Pending</span>
406
402
  ),
407
403
  actions: (
408
404
  <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>
405
+ <button>Edit</button>
406
+ <button>Approve</button>
407
+ <button>Reject</button>
412
408
  </div>
413
409
  ),
414
410
  },
@@ -428,22 +424,13 @@ export const ComplexCells: Story = {
428
424
  </div>
429
425
  ),
430
426
  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>
427
+ <span>Inactive</span>
441
428
  ),
442
429
  actions: (
443
430
  <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>
431
+ <button>Edit</button>
432
+ <button>Activate</button>
433
+ <button>Archive</button>
447
434
  </div>
448
435
  ),
449
436
  },
@@ -645,4 +632,42 @@ export const RowDetails: Story = {
645
632
  },
646
633
  };
647
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
+
648
673
 
@@ -10,7 +10,7 @@
10
10
  import { FunctionComponent, VNode, Fragment } from 'preact';
11
11
  import { HTMLAttributes } from 'preact/compat';
12
12
  import { classes, VComponent } from '@adobe-commerce/elsie/lib';
13
- import { Icon, Button } from '@adobe-commerce/elsie/components';
13
+ import { Icon, Button, Skeleton, SkeletonRow } from '@adobe-commerce/elsie/components';
14
14
  import { useText } from '@adobe-commerce/elsie/i18n';
15
15
 
16
16
  import '@adobe-commerce/elsie/components/Table/Table.css';
@@ -28,13 +28,15 @@ type RowData = {
28
28
  _rowDetails?: VNode | string; // Special property for expandable row content
29
29
  };
30
30
 
31
- export interface TableProps extends HTMLAttributes<HTMLTableElement> {
31
+ export interface TableProps extends Omit<HTMLAttributes<HTMLTableElement>, 'loading'> {
32
32
  columns: Column[];
33
33
  rowData: RowData[];
34
34
  mobileLayout?: 'stacked' | 'none';
35
35
  caption?: string;
36
- onSortChange?: (columnKey: string, direction: Sortable) => void;
37
36
  expandedRows?: Set<number>;
37
+ loading?: boolean;
38
+ skeletonRowCount?: number;
39
+ onSortChange?: (columnKey: string, direction: Sortable) => void;
38
40
  }
39
41
 
40
42
  export const Table: FunctionComponent<TableProps> = ({
@@ -44,13 +46,13 @@ export const Table: FunctionComponent<TableProps> = ({
44
46
  rowData = [],
45
47
  mobileLayout = 'none',
46
48
  caption,
47
- onSortChange,
48
49
  expandedRows = new Set(),
50
+ loading = false,
51
+ skeletonRowCount = 10,
52
+ onSortChange,
49
53
  ...props
50
54
  }) => {
51
55
  const translations = useText({
52
- ariaSortAscending: 'Dropin.Table.ariaSortAscending',
53
- ariaSortDescending: 'Dropin.Table.ariaSortDescending',
54
56
  sortedAscending: 'Dropin.Table.sortedAscending',
55
57
  sortedDescending: 'Dropin.Table.sortedDescending',
56
58
  sortBy: 'Dropin.Table.sortBy',
@@ -72,7 +74,7 @@ export const Table: FunctionComponent<TableProps> = ({
72
74
  onSortChange(column.key, nextDirection);
73
75
  };
74
76
 
75
- const getSortButton = (column: Column) => {
77
+ const renderSortButton = (column: Column) => {
76
78
  if (column.sortBy === undefined) return null;
77
79
 
78
80
  let iconSource: string;
@@ -101,6 +103,74 @@ export const Table: FunctionComponent<TableProps> = ({
101
103
  );
102
104
  };
103
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
+
104
174
  return (
105
175
  <div className={classes(['dropin-table', `dropin-table--mobile-layout-${mobileLayout}`, className])}>
106
176
  <table {...props} className="dropin-table__table">
@@ -115,59 +185,22 @@ export const Table: FunctionComponent<TableProps> = ({
115
185
  ['dropin-table__header__cell--sorted', column.sortBy === 'asc' || column.sortBy === 'desc'],
116
186
  ['dropin-table__header__cell--sortable', column.sortBy !== undefined]
117
187
  ])}
118
- role="columnheader"
188
+ aria-sort={getAriaSort(column)}
119
189
  >
120
190
  {column.label}
121
- {getSortButton(column)}
191
+ {renderSortButton(column)}
122
192
  </th>
123
193
  ))}
124
194
  </tr>
125
195
  </thead>
126
196
  <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
- })}
197
+ {loading ? (
198
+ // Render skeleton rows when loading
199
+ renderSkeletonRows()
200
+ ) : (
201
+ // Render actual data when not loading
202
+ renderDataRows()
203
+ )}
171
204
  </tbody>
172
205
  </table>
173
206
  </div>
@@ -143,8 +143,6 @@
143
143
  "picker": "Select a date"
144
144
  },
145
145
  "Table": {
146
- "ariaSortAscending": "ascending",
147
- "ariaSortDescending": "descending",
148
146
  "sortedAscending": "Sort {label} ascending",
149
147
  "sortedDescending": "Sort {label} descending",
150
148
  "sortBy": "Sort by {label}"