@adobe-commerce/elsie 1.6.0-alpha999 → 1.6.0-beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/jest.js +3 -3
- package/package.json +3 -3
- package/src/components/Button/Button.tsx +2 -0
- package/src/components/Field/Field.tsx +19 -14
- package/src/components/Incrementer/Incrementer.css +6 -0
- package/src/components/Incrementer/Incrementer.stories.tsx +18 -0
- package/src/components/Incrementer/Incrementer.tsx +66 -59
- package/src/components/MultiSelect/MultiSelect.css +273 -0
- package/src/components/MultiSelect/MultiSelect.stories.tsx +457 -0
- package/src/components/MultiSelect/MultiSelect.tsx +763 -0
- package/src/components/MultiSelect/index.ts +11 -0
- package/src/components/Price/Price.tsx +8 -41
- package/src/components/Table/Table.css +110 -0
- package/src/components/Table/Table.stories.tsx +761 -0
- package/src/components/Table/Table.tsx +249 -0
- package/src/components/Table/index.ts +11 -0
- package/src/components/ToggleButton/ToggleButton.css +13 -1
- package/src/components/ToggleButton/ToggleButton.stories.tsx +13 -6
- package/src/components/ToggleButton/ToggleButton.tsx +4 -0
- package/src/components/index.ts +5 -3
- package/src/docs/slots.mdx +2 -0
- package/src/i18n/en_US.json +38 -0
- package/src/lib/aem/configs.ts +10 -4
- package/src/lib/get-price-formatter.ts +69 -0
- package/src/lib/index.ts +4 -3
- package/src/lib/slot.tsx +61 -5
|
@@ -0,0 +1,249 @@
|
|
|
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 {
|
|
14
|
+
Icon,
|
|
15
|
+
Button,
|
|
16
|
+
Skeleton,
|
|
17
|
+
SkeletonRow,
|
|
18
|
+
} from '@adobe-commerce/elsie/components';
|
|
19
|
+
import { useText } from '@adobe-commerce/elsie/i18n';
|
|
20
|
+
|
|
21
|
+
import '@adobe-commerce/elsie/components/Table/Table.css';
|
|
22
|
+
|
|
23
|
+
type Sortable = 'asc' | 'desc' | true;
|
|
24
|
+
|
|
25
|
+
type Column =
|
|
26
|
+
| { label: string; key: string; ariaLabel?: string; sortBy?: Sortable }
|
|
27
|
+
| {
|
|
28
|
+
label: VNode<HTMLAttributes<HTMLElement>>;
|
|
29
|
+
key: string;
|
|
30
|
+
ariaLabel: string;
|
|
31
|
+
sortBy?: Sortable;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type RowData = {
|
|
35
|
+
[key: string]: VNode | string | number | undefined;
|
|
36
|
+
_rowDetails?: VNode | string; // Special property for expandable row content
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface TableProps
|
|
40
|
+
extends Omit<HTMLAttributes<HTMLTableElement>, 'loading'> {
|
|
41
|
+
columns: Column[];
|
|
42
|
+
rowData: RowData[];
|
|
43
|
+
mobileLayout?: 'stacked' | 'none';
|
|
44
|
+
caption?: string;
|
|
45
|
+
expandedRows?: Set<number>;
|
|
46
|
+
loading?: boolean;
|
|
47
|
+
skeletonRowCount?: number;
|
|
48
|
+
onSortChange?: (columnKey: string, direction: Sortable) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Table: FunctionComponent<TableProps> = ({
|
|
52
|
+
className,
|
|
53
|
+
children,
|
|
54
|
+
columns = [],
|
|
55
|
+
rowData = [],
|
|
56
|
+
mobileLayout = 'none',
|
|
57
|
+
caption,
|
|
58
|
+
expandedRows = new Set(),
|
|
59
|
+
loading = false,
|
|
60
|
+
skeletonRowCount = 10,
|
|
61
|
+
onSortChange,
|
|
62
|
+
...props
|
|
63
|
+
}) => {
|
|
64
|
+
const translations = useText({
|
|
65
|
+
sortedAscending: 'Dropin.Table.sortedAscending',
|
|
66
|
+
sortedDescending: 'Dropin.Table.sortedDescending',
|
|
67
|
+
sortBy: 'Dropin.Table.sortBy',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const handleSort = (column: Column) => {
|
|
71
|
+
if (!onSortChange) return;
|
|
72
|
+
|
|
73
|
+
// Determine next sort direction
|
|
74
|
+
let nextDirection: Sortable;
|
|
75
|
+
if (column.sortBy === true) {
|
|
76
|
+
nextDirection = 'asc';
|
|
77
|
+
} else if (column.sortBy === 'asc') {
|
|
78
|
+
nextDirection = 'desc';
|
|
79
|
+
} else {
|
|
80
|
+
nextDirection = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onSortChange(column.key, nextDirection);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const renderSortButton = (column: Column) => {
|
|
87
|
+
if (column.sortBy === undefined) return null;
|
|
88
|
+
const label = column.ariaLabel ?? (column.label as string);
|
|
89
|
+
|
|
90
|
+
let iconSource: string;
|
|
91
|
+
let ariaLabel: string;
|
|
92
|
+
|
|
93
|
+
if (column.sortBy === 'asc') {
|
|
94
|
+
iconSource = 'ChevronUp';
|
|
95
|
+
ariaLabel = translations.sortedAscending.replace('{label}', label);
|
|
96
|
+
} else if (column.sortBy === 'desc') {
|
|
97
|
+
iconSource = 'ChevronDown';
|
|
98
|
+
ariaLabel = translations.sortedDescending.replace('{label}', label);
|
|
99
|
+
} else {
|
|
100
|
+
iconSource = 'Sort';
|
|
101
|
+
ariaLabel = translations.sortBy.replace('{label}', label);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Button
|
|
106
|
+
variant="tertiary"
|
|
107
|
+
size="medium"
|
|
108
|
+
className="dropin-table__header__sort-button"
|
|
109
|
+
icon={<Icon source={iconSource} />}
|
|
110
|
+
aria-label={ariaLabel}
|
|
111
|
+
onClick={() => handleSort(column)}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const renderSkeletonRows = () => {
|
|
117
|
+
return Array.from({ length: skeletonRowCount }, (_, rowIndex) => (
|
|
118
|
+
<tr key={`skeleton-${rowIndex}`} className="dropin-table__body__row">
|
|
119
|
+
{columns.map((column) => (
|
|
120
|
+
<td
|
|
121
|
+
key={column.key}
|
|
122
|
+
className="dropin-table__body__cell"
|
|
123
|
+
data-label={column.ariaLabel ?? column.label}
|
|
124
|
+
>
|
|
125
|
+
<Skeleton>
|
|
126
|
+
<SkeletonRow variant="row" size="small" fullWidth />
|
|
127
|
+
</Skeleton>
|
|
128
|
+
</td>
|
|
129
|
+
))}
|
|
130
|
+
</tr>
|
|
131
|
+
));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const renderDataRows = () => {
|
|
135
|
+
return rowData.map((row, rowIndex) => {
|
|
136
|
+
const hasDetails = row._rowDetails !== undefined;
|
|
137
|
+
const isExpanded = expandedRows.has(rowIndex);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Fragment key={rowIndex}>
|
|
141
|
+
<tr className="dropin-table__body__row">
|
|
142
|
+
{columns.map((column) => {
|
|
143
|
+
const cell = row[column.key];
|
|
144
|
+
const label = column.ariaLabel ?? column.label;
|
|
145
|
+
|
|
146
|
+
if (typeof cell === 'string' || typeof cell === 'number') {
|
|
147
|
+
return (
|
|
148
|
+
<td
|
|
149
|
+
key={column.key}
|
|
150
|
+
className="dropin-table__body__cell"
|
|
151
|
+
data-label={label}
|
|
152
|
+
>
|
|
153
|
+
{cell}
|
|
154
|
+
</td>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<td
|
|
160
|
+
key={column.key}
|
|
161
|
+
className="dropin-table__body__cell"
|
|
162
|
+
data-label={label}
|
|
163
|
+
>
|
|
164
|
+
<VComponent node={cell!} />
|
|
165
|
+
</td>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</tr>
|
|
169
|
+
{hasDetails && isExpanded && (
|
|
170
|
+
<tr
|
|
171
|
+
key={`${rowIndex}-details`}
|
|
172
|
+
className="dropin-table__row-details dropin-table__row-details--expanded"
|
|
173
|
+
id={`row-${rowIndex}-details`}
|
|
174
|
+
>
|
|
175
|
+
<td
|
|
176
|
+
className="dropin-table__row-details__cell"
|
|
177
|
+
colSpan={columns.length}
|
|
178
|
+
role="region"
|
|
179
|
+
aria-labelledby={`row-${rowIndex}-details`}
|
|
180
|
+
>
|
|
181
|
+
{typeof row._rowDetails === 'string' ? (
|
|
182
|
+
row._rowDetails
|
|
183
|
+
) : (
|
|
184
|
+
<VComponent node={row._rowDetails!} />
|
|
185
|
+
)}
|
|
186
|
+
</td>
|
|
187
|
+
</tr>
|
|
188
|
+
)}
|
|
189
|
+
</Fragment>
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const getAriaSort = (
|
|
195
|
+
column: Column
|
|
196
|
+
): 'none' | 'ascending' | 'descending' | 'other' | undefined => {
|
|
197
|
+
if (column.sortBy === true) return 'none';
|
|
198
|
+
if (column.sortBy === 'asc') return 'ascending';
|
|
199
|
+
if (column.sortBy === 'desc') return 'descending';
|
|
200
|
+
return undefined;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
className={classes([
|
|
206
|
+
'dropin-table',
|
|
207
|
+
`dropin-table--mobile-layout-${mobileLayout}`,
|
|
208
|
+
className,
|
|
209
|
+
])}
|
|
210
|
+
>
|
|
211
|
+
<table {...props} className="dropin-table__table">
|
|
212
|
+
{caption && (
|
|
213
|
+
<caption className="dropin-table__caption">{caption}</caption>
|
|
214
|
+
)}
|
|
215
|
+
<thead className="dropin-table__header">
|
|
216
|
+
<tr className="dropin-table__header__row">
|
|
217
|
+
{columns.map((column) => (
|
|
218
|
+
<th
|
|
219
|
+
key={column.key}
|
|
220
|
+
className={classes([
|
|
221
|
+
'dropin-table__header__cell',
|
|
222
|
+
[
|
|
223
|
+
'dropin-table__header__cell--sorted',
|
|
224
|
+
column.sortBy === 'asc' || column.sortBy === 'desc',
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
'dropin-table__header__cell--sortable',
|
|
228
|
+
column.sortBy !== undefined,
|
|
229
|
+
],
|
|
230
|
+
])}
|
|
231
|
+
aria-sort={getAriaSort(column)}
|
|
232
|
+
>
|
|
233
|
+
{column.label}
|
|
234
|
+
{renderSortButton(column)}
|
|
235
|
+
</th>
|
|
236
|
+
))}
|
|
237
|
+
</tr>
|
|
238
|
+
</thead>
|
|
239
|
+
<tbody className="dropin-table__body">
|
|
240
|
+
{loading
|
|
241
|
+
? // Render skeleton rows when loading
|
|
242
|
+
renderSkeletonRows()
|
|
243
|
+
: // Render actual data when not loading
|
|
244
|
+
renderDataRows()}
|
|
245
|
+
</tbody>
|
|
246
|
+
</table>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
@@ -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
|
-
|
|
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
|
|
109
|
-
|
|
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
|
|
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}
|
package/src/components/index.ts
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
|
export * from '@adobe-commerce/elsie/components/Skeleton';
|
|
@@ -49,3 +49,5 @@ 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';
|
|
53
|
+
export * from '@adobe-commerce/elsie/components/MultiSelect';
|
package/src/docs/slots.mdx
CHANGED
|
@@ -19,6 +19,7 @@ The context is defined during implementation of a drop-in and can be used to pas
|
|
|
19
19
|
- **prependChild**: A function to prepend a new HTML element to the slot's content.
|
|
20
20
|
- **appendSibling**: A function to append a new HTML element after the slot's content.
|
|
21
21
|
- **prependSibling**: A function to prepend a new HTML element **before** the slot's content.
|
|
22
|
+
- **remove**: A function to remove the slot element from the DOM.
|
|
22
23
|
- **getSlotElement**: A function to get a slot element.
|
|
23
24
|
- **onChange**: A function to listen to changes in the slot's context.
|
|
24
25
|
|
|
@@ -150,6 +151,7 @@ provider.render(MyContainer, {
|
|
|
150
151
|
// ctx.prependChild(element);
|
|
151
152
|
// ctx.appendSibling(element);
|
|
152
153
|
// ctx.prependSibling(element);
|
|
154
|
+
// ctx.remove();
|
|
153
155
|
|
|
154
156
|
// to listen and react to changes in the slot's context (lifecycle)
|
|
155
157
|
ctx.onChange((next) => {
|
package/src/i18n/en_US.json
CHANGED
|
@@ -141,6 +141,44 @@
|
|
|
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}"
|
|
149
|
+
},
|
|
150
|
+
"MultiSelect": {
|
|
151
|
+
"selectAll": "Select All",
|
|
152
|
+
"deselectAll": "Deselect All",
|
|
153
|
+
"placeholder": "Select options",
|
|
154
|
+
"noResultsText": "No options available",
|
|
155
|
+
"ariaLabel":{
|
|
156
|
+
"removed": "removed",
|
|
157
|
+
"added": "added",
|
|
158
|
+
"itemsSelected": "items selected",
|
|
159
|
+
"itemsAdded": "items added",
|
|
160
|
+
"itemsRemoved": "items removed",
|
|
161
|
+
"selectedTotal": "selected total",
|
|
162
|
+
"noResultsFor": "No results found for",
|
|
163
|
+
"optionsAvailable": "options available",
|
|
164
|
+
"dropdownExpanded": "Dropdown expanded",
|
|
165
|
+
"useArrowKeys": "Use arrow keys to navigate",
|
|
166
|
+
"removeFromSelection": "Remove",
|
|
167
|
+
"fromSelection": "from selection",
|
|
168
|
+
"selectedItem": "Selected item:",
|
|
169
|
+
"inField": " in {floatingLabel}",
|
|
170
|
+
"selectedItems": "Selected items",
|
|
171
|
+
"scrollableOptionsList": "Scrollable options list",
|
|
172
|
+
"selectOptions": "Select options",
|
|
173
|
+
"itemAction": "{label} {action}. {count} items selected.",
|
|
174
|
+
"bulkAdded": "{count} items added. {total} items selected total.",
|
|
175
|
+
"bulkRemoved": "{count} items removed. {total} items selected total.",
|
|
176
|
+
"dropdownExpandedWithOptions": "Dropdown expanded. {count} option{s} available. Use arrow keys to navigate.",
|
|
177
|
+
"selectedItemInField": "Selected item: {label} in {field}",
|
|
178
|
+
"removeFromSelectionWithText": "Remove {label} from selection. {text}",
|
|
179
|
+
"itemsSelectedDescription": "{count} item{s} selected: {labels}",
|
|
180
|
+
"noItemsSelected": "No items selected"
|
|
181
|
+
}
|
|
144
182
|
}
|
|
145
183
|
}
|
|
146
184
|
}
|
package/src/lib/aem/configs.ts
CHANGED
|
@@ -23,6 +23,7 @@ interface Config {
|
|
|
23
23
|
|
|
24
24
|
// Private state
|
|
25
25
|
let config: Config | null = null;
|
|
26
|
+
let options: { match?: (key: string) => boolean } | null = null;
|
|
26
27
|
let rootPath: string | null = null;
|
|
27
28
|
let rootConfig: ConfigRoot | null = null;
|
|
28
29
|
|
|
@@ -31,6 +32,7 @@ let rootConfig: ConfigRoot | null = null;
|
|
|
31
32
|
*/
|
|
32
33
|
function resetConfig() {
|
|
33
34
|
config = null;
|
|
35
|
+
options = null;
|
|
34
36
|
rootPath = null;
|
|
35
37
|
rootConfig = null;
|
|
36
38
|
}
|
|
@@ -40,7 +42,7 @@ function resetConfig() {
|
|
|
40
42
|
* @param {Object} [configObj=config] - The config object.
|
|
41
43
|
* @returns {string} - The root path.
|
|
42
44
|
*/
|
|
43
|
-
function getRootPath(configObj: Config | null = config): string {
|
|
45
|
+
function getRootPath(configObj: Config | null = config, optionsObj: { match?: (key: string) => boolean } | null = options): string {
|
|
44
46
|
if (!configObj) {
|
|
45
47
|
console.warn('No config found. Please call initializeConfig() first.');
|
|
46
48
|
return '/';
|
|
@@ -56,7 +58,7 @@ function getRootPath(configObj: Config | null = config): string {
|
|
|
56
58
|
.find(
|
|
57
59
|
(key) =>
|
|
58
60
|
window.location.pathname === key ||
|
|
59
|
-
window.location.pathname.startsWith(key)
|
|
61
|
+
(optionsObj?.match?.(key) ?? window.location.pathname.startsWith(key))
|
|
60
62
|
);
|
|
61
63
|
|
|
62
64
|
return value ?? '/';
|
|
@@ -126,11 +128,15 @@ function applyConfigOverrides(
|
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* Initializes the configuration system.
|
|
131
|
+
* @param {Object} configObj - The config object.
|
|
132
|
+
* @param {Object} [options] - The options object.
|
|
133
|
+
* @param {Function} [options.match] - The function to match the path to the config.
|
|
129
134
|
* @returns {Object} The initialized root configuration
|
|
130
135
|
*/
|
|
131
|
-
function initializeConfig(configObj: Config): ConfigRoot {
|
|
136
|
+
function initializeConfig(configObj: Config, optionsObj?: { match?: (key: string) => boolean }): ConfigRoot {
|
|
132
137
|
config = configObj;
|
|
133
|
-
|
|
138
|
+
options = optionsObj ?? null;
|
|
139
|
+
rootPath = getRootPath(config, { match: optionsObj?.match });
|
|
134
140
|
rootConfig = applyConfigOverrides(config, rootPath);
|
|
135
141
|
return rootConfig;
|
|
136
142
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { getGlobalLocale } from '@adobe-commerce/elsie/lib';
|
|
11
|
+
|
|
12
|
+
export interface PriceFormatterOptions {
|
|
13
|
+
currency?: string | null;
|
|
14
|
+
locale?: string;
|
|
15
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Determines the effective locale to use for price formatting
|
|
20
|
+
* Priority: prop locale > global locale > browser locale > default 'en-US'
|
|
21
|
+
*/
|
|
22
|
+
export function getEffectiveLocale(locale?: string): string {
|
|
23
|
+
if (locale) {
|
|
24
|
+
return locale;
|
|
25
|
+
}
|
|
26
|
+
const globalLocale = getGlobalLocale();
|
|
27
|
+
if (globalLocale) {
|
|
28
|
+
return globalLocale;
|
|
29
|
+
}
|
|
30
|
+
// Fallback to browser locale or default
|
|
31
|
+
return process.env.LOCALE && process.env.LOCALE !== 'undefined' ? process.env.LOCALE : 'en-US';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gets an Intl.NumberFormat instance for price formatting
|
|
36
|
+
* Uses getEffectiveLocale internally to determine the best locale
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Single price formatting
|
|
40
|
+
* const formatter = getPriceFormatter({ currency: 'USD', locale: 'en-US' });
|
|
41
|
+
* const price = formatter.format(10.99); // "$10.99"
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Bulk price formatting (more efficient)
|
|
45
|
+
* const formatter = getPriceFormatter({ currency: 'EUR', locale: 'fr-FR' });
|
|
46
|
+
* const prices = [10.99, 25.50, 99.99].map(amount => formatter.format(amount));
|
|
47
|
+
*/
|
|
48
|
+
export function getPriceFormatter(
|
|
49
|
+
options: PriceFormatterOptions = {}
|
|
50
|
+
): Intl.NumberFormat {
|
|
51
|
+
const { currency, locale, formatOptions = {} } = options;
|
|
52
|
+
const effectiveLocale = getEffectiveLocale(locale);
|
|
53
|
+
|
|
54
|
+
const params: Intl.NumberFormatOptions = {
|
|
55
|
+
style: 'currency',
|
|
56
|
+
currency: currency || 'USD',
|
|
57
|
+
// These options are needed to round to whole numbers if that's what you want.
|
|
58
|
+
minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
|
|
59
|
+
maximumFractionDigits: 2, // (causes 2500.99 to be printed as $2,501)
|
|
60
|
+
...formatOptions,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
return new Intl.NumberFormat(effectiveLocale, params);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`Error creating Intl.NumberFormat instance for locale ${effectiveLocale}. Falling back to en-US.`, error);
|
|
67
|
+
return new Intl.NumberFormat('en-US', params);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/lib/index.ts
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
|
export * from '@adobe-commerce/elsie/lib/form-values';
|
|
@@ -25,3 +25,4 @@ export * from '@adobe-commerce/elsie/lib/is-number';
|
|
|
25
25
|
export * from '@adobe-commerce/elsie/lib/deviceUtils';
|
|
26
26
|
export * from '@adobe-commerce/elsie/lib/get-path-value';
|
|
27
27
|
export * from '@adobe-commerce/elsie/lib/get-cookie';
|
|
28
|
+
export * from '@adobe-commerce/elsie/lib/get-price-formatter';
|