@adobe-commerce/elsie 1.5.0-beta4 → 1.5.0
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/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/Table/Table.stories.tsx +88 -0
- package/src/components/Table/Table.tsx +70 -29
- package/src/components/index.ts +4 -3
- package/src/docs/slots.mdx +2 -0
- package/src/i18n/en_US.json +33 -0
- package/src/lib/aem/configs.ts +7 -4
- package/src/lib/slot.tsx +61 -5
|
@@ -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/MultiSelect/MultiSelect';
|
|
11
|
+
export { MultiSelect as default } from '@adobe-commerce/elsie/components/MultiSelect/MultiSelect';
|
|
@@ -670,4 +670,92 @@ export const LoadingState: Story = {
|
|
|
670
670
|
},
|
|
671
671
|
};
|
|
672
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Table with VNode labels in column headers.
|
|
675
|
+
* Demonstrates how column labels can be VNode elements instead of simple strings.
|
|
676
|
+
* This allows for rich header content like icons, formatted text, or custom components.
|
|
677
|
+
*
|
|
678
|
+
* **Features**:
|
|
679
|
+
* - Column labels can be VNode elements (JSX components)
|
|
680
|
+
* - Supports any valid Preact VNode content in headers
|
|
681
|
+
* - Maintains all table functionality with custom header content
|
|
682
|
+
* - Useful for adding icons, tooltips, or formatted text to headers
|
|
683
|
+
*
|
|
684
|
+
* ```tsx
|
|
685
|
+
* <Table
|
|
686
|
+
* columns={[
|
|
687
|
+
* { key: 'name', label: <span><strong>👤 User Name</strong></span> },
|
|
688
|
+
* { key: 'email', label: <span style={{ color: '#0066cc' }}>📧 Email</span> },
|
|
689
|
+
* { key: 'status', label: <em>Status Info</em> }
|
|
690
|
+
* ]}
|
|
691
|
+
* rowData={[
|
|
692
|
+
* { name: 'John', email: 'john@example.com', status: 'Active' }
|
|
693
|
+
* ]}
|
|
694
|
+
* />
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
697
|
+
export const VNodeLabels: Story = {
|
|
698
|
+
args: {
|
|
699
|
+
columns: [
|
|
700
|
+
{
|
|
701
|
+
key: 'name',
|
|
702
|
+
label: (
|
|
703
|
+
<span>
|
|
704
|
+
<strong>👤 User Name</strong>
|
|
705
|
+
</span>
|
|
706
|
+
),
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
key: 'email',
|
|
710
|
+
label: <span style={{ color: '#0066cc' }}>📧 Email Address</span>,
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
key: 'role',
|
|
714
|
+
label: <em style={{ color: '#666' }}>Role & Department</em>,
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
key: 'status',
|
|
718
|
+
label: (
|
|
719
|
+
<span
|
|
720
|
+
style={{
|
|
721
|
+
padding: '4px 8px',
|
|
722
|
+
backgroundColor: '#f0f9ff',
|
|
723
|
+
borderRadius: '4px',
|
|
724
|
+
fontSize: '12px',
|
|
725
|
+
}}
|
|
726
|
+
>
|
|
727
|
+
📊 Status
|
|
728
|
+
</span>
|
|
729
|
+
),
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
key: 'actions',
|
|
733
|
+
label: <span>⚙️ Actions</span>,
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
rowData: [
|
|
737
|
+
{
|
|
738
|
+
name: 'John Doe',
|
|
739
|
+
email: 'john.doe@company.com',
|
|
740
|
+
role: 'Senior Developer',
|
|
741
|
+
status: 'Active',
|
|
742
|
+
actions: <button>Edit</button>,
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: 'Jane Smith',
|
|
746
|
+
email: 'jane.smith@company.com',
|
|
747
|
+
role: 'Product Manager',
|
|
748
|
+
status: 'Active',
|
|
749
|
+
actions: <button>Edit</button>,
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: 'Bob Johnson',
|
|
753
|
+
email: 'bob.johnson@company.com',
|
|
754
|
+
role: 'UX Designer',
|
|
755
|
+
status: 'Inactive',
|
|
756
|
+
actions: <button>Edit</button>,
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
};
|
|
673
761
|
|
|
@@ -2,33 +2,42 @@
|
|
|
2
2
|
* Copyright 2025 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 { FunctionComponent, VNode, Fragment } from 'preact';
|
|
11
11
|
import { HTMLAttributes } from 'preact/compat';
|
|
12
12
|
import { classes, VComponent } from '@adobe-commerce/elsie/lib';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Icon,
|
|
15
|
+
Button,
|
|
16
|
+
Skeleton,
|
|
17
|
+
SkeletonRow,
|
|
18
|
+
} from '@adobe-commerce/elsie/components';
|
|
14
19
|
import { useText } from '@adobe-commerce/elsie/i18n';
|
|
15
20
|
|
|
16
21
|
import '@adobe-commerce/elsie/components/Table/Table.css';
|
|
17
22
|
|
|
18
23
|
type Sortable = 'asc' | 'desc' | true;
|
|
19
24
|
|
|
20
|
-
type Column =
|
|
21
|
-
key: string;
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
};
|
|
25
33
|
|
|
26
34
|
type RowData = {
|
|
27
35
|
[key: string]: VNode | string | number | undefined;
|
|
28
36
|
_rowDetails?: VNode | string; // Special property for expandable row content
|
|
29
37
|
};
|
|
30
38
|
|
|
31
|
-
export interface TableProps
|
|
39
|
+
export interface TableProps
|
|
40
|
+
extends Omit<HTMLAttributes<HTMLTableElement>, 'loading'> {
|
|
32
41
|
columns: Column[];
|
|
33
42
|
rowData: RowData[];
|
|
34
43
|
mobileLayout?: 'stacked' | 'none';
|
|
@@ -76,19 +85,20 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
76
85
|
|
|
77
86
|
const renderSortButton = (column: Column) => {
|
|
78
87
|
if (column.sortBy === undefined) return null;
|
|
88
|
+
const label = column.ariaLabel ?? (column.label as string);
|
|
79
89
|
|
|
80
90
|
let iconSource: string;
|
|
81
91
|
let ariaLabel: string;
|
|
82
92
|
|
|
83
93
|
if (column.sortBy === 'asc') {
|
|
84
94
|
iconSource = 'ChevronUp';
|
|
85
|
-
ariaLabel = translations.sortedAscending.replace('{label}',
|
|
95
|
+
ariaLabel = translations.sortedAscending.replace('{label}', label);
|
|
86
96
|
} else if (column.sortBy === 'desc') {
|
|
87
97
|
iconSource = 'ChevronDown';
|
|
88
|
-
ariaLabel = translations.sortedDescending.replace('{label}',
|
|
98
|
+
ariaLabel = translations.sortedDescending.replace('{label}', label);
|
|
89
99
|
} else {
|
|
90
100
|
iconSource = 'Sort';
|
|
91
|
-
ariaLabel = translations.sortBy.replace('{label}',
|
|
101
|
+
ariaLabel = translations.sortBy.replace('{label}', label);
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
return (
|
|
@@ -107,7 +117,11 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
107
117
|
return Array.from({ length: skeletonRowCount }, (_, rowIndex) => (
|
|
108
118
|
<tr key={`skeleton-${rowIndex}`} className="dropin-table__body__row">
|
|
109
119
|
{columns.map((column) => (
|
|
110
|
-
<td
|
|
120
|
+
<td
|
|
121
|
+
key={column.key}
|
|
122
|
+
className="dropin-table__body__cell"
|
|
123
|
+
data-label={column.ariaLabel ?? column.label}
|
|
124
|
+
>
|
|
111
125
|
<Skeleton>
|
|
112
126
|
<SkeletonRow variant="row" size="small" fullWidth />
|
|
113
127
|
</Skeleton>
|
|
@@ -127,17 +141,26 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
127
141
|
<tr className="dropin-table__body__row">
|
|
128
142
|
{columns.map((column) => {
|
|
129
143
|
const cell = row[column.key];
|
|
144
|
+
const label = column.ariaLabel ?? column.label;
|
|
130
145
|
|
|
131
146
|
if (typeof cell === 'string' || typeof cell === 'number') {
|
|
132
147
|
return (
|
|
133
|
-
<td
|
|
148
|
+
<td
|
|
149
|
+
key={column.key}
|
|
150
|
+
className="dropin-table__body__cell"
|
|
151
|
+
data-label={label}
|
|
152
|
+
>
|
|
134
153
|
{cell}
|
|
135
154
|
</td>
|
|
136
155
|
);
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
return (
|
|
140
|
-
<td
|
|
159
|
+
<td
|
|
160
|
+
key={column.key}
|
|
161
|
+
className="dropin-table__body__cell"
|
|
162
|
+
data-label={label}
|
|
163
|
+
>
|
|
141
164
|
<VComponent node={cell!} />
|
|
142
165
|
</td>
|
|
143
166
|
);
|
|
@@ -155,7 +178,11 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
155
178
|
role="region"
|
|
156
179
|
aria-labelledby={`row-${rowIndex}-details`}
|
|
157
180
|
>
|
|
158
|
-
{typeof row._rowDetails === 'string' ?
|
|
181
|
+
{typeof row._rowDetails === 'string' ? (
|
|
182
|
+
row._rowDetails
|
|
183
|
+
) : (
|
|
184
|
+
<VComponent node={row._rowDetails!} />
|
|
185
|
+
)}
|
|
159
186
|
</td>
|
|
160
187
|
</tr>
|
|
161
188
|
)}
|
|
@@ -164,7 +191,9 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
164
191
|
});
|
|
165
192
|
};
|
|
166
193
|
|
|
167
|
-
const getAriaSort = (
|
|
194
|
+
const getAriaSort = (
|
|
195
|
+
column: Column
|
|
196
|
+
): 'none' | 'ascending' | 'descending' | 'other' | undefined => {
|
|
168
197
|
if (column.sortBy === true) return 'none';
|
|
169
198
|
if (column.sortBy === 'asc') return 'ascending';
|
|
170
199
|
if (column.sortBy === 'desc') return 'descending';
|
|
@@ -172,9 +201,17 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
172
201
|
};
|
|
173
202
|
|
|
174
203
|
return (
|
|
175
|
-
<div
|
|
204
|
+
<div
|
|
205
|
+
className={classes([
|
|
206
|
+
'dropin-table',
|
|
207
|
+
`dropin-table--mobile-layout-${mobileLayout}`,
|
|
208
|
+
className,
|
|
209
|
+
])}
|
|
210
|
+
>
|
|
176
211
|
<table {...props} className="dropin-table__table">
|
|
177
|
-
{caption &&
|
|
212
|
+
{caption && (
|
|
213
|
+
<caption className="dropin-table__caption">{caption}</caption>
|
|
214
|
+
)}
|
|
178
215
|
<thead className="dropin-table__header">
|
|
179
216
|
<tr className="dropin-table__header__row">
|
|
180
217
|
{columns.map((column) => (
|
|
@@ -182,8 +219,14 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
182
219
|
key={column.key}
|
|
183
220
|
className={classes([
|
|
184
221
|
'dropin-table__header__cell',
|
|
185
|
-
[
|
|
186
|
-
|
|
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
|
+
],
|
|
187
230
|
])}
|
|
188
231
|
aria-sort={getAriaSort(column)}
|
|
189
232
|
>
|
|
@@ -194,13 +237,11 @@ export const Table: FunctionComponent<TableProps> = ({
|
|
|
194
237
|
</tr>
|
|
195
238
|
</thead>
|
|
196
239
|
<tbody className="dropin-table__body">
|
|
197
|
-
{loading
|
|
198
|
-
// Render skeleton rows when loading
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
renderDataRows()
|
|
203
|
-
)}
|
|
240
|
+
{loading
|
|
241
|
+
? // Render skeleton rows when loading
|
|
242
|
+
renderSkeletonRows()
|
|
243
|
+
: // Render actual data when not loading
|
|
244
|
+
renderDataRows()}
|
|
204
245
|
</tbody>
|
|
205
246
|
</table>
|
|
206
247
|
</div>
|
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';
|
|
@@ -50,3 +50,4 @@ 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
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
|
@@ -146,6 +146,39 @@
|
|
|
146
146
|
"sortedAscending": "Sort {label} ascending",
|
|
147
147
|
"sortedDescending": "Sort {label} descending",
|
|
148
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
|
+
}
|
|
149
182
|
}
|
|
150
183
|
}
|
|
151
184
|
}
|
package/src/lib/aem/configs.ts
CHANGED
|
@@ -40,7 +40,7 @@ function resetConfig() {
|
|
|
40
40
|
* @param {Object} [configObj=config] - The config object.
|
|
41
41
|
* @returns {string} - The root path.
|
|
42
42
|
*/
|
|
43
|
-
function getRootPath(configObj: Config | null = config): string {
|
|
43
|
+
function getRootPath(configObj: Config | null = config, options?: { match?: (key: string) => boolean }): string {
|
|
44
44
|
if (!configObj) {
|
|
45
45
|
console.warn('No config found. Please call initializeConfig() first.');
|
|
46
46
|
return '/';
|
|
@@ -56,7 +56,7 @@ function getRootPath(configObj: Config | null = config): string {
|
|
|
56
56
|
.find(
|
|
57
57
|
(key) =>
|
|
58
58
|
window.location.pathname === key ||
|
|
59
|
-
window.location.pathname.startsWith(key)
|
|
59
|
+
(options?.match?.(key) ?? window.location.pathname.startsWith(key))
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
return value ?? '/';
|
|
@@ -126,11 +126,14 @@ function applyConfigOverrides(
|
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
128
|
* Initializes the configuration system.
|
|
129
|
+
* @param {Object} configObj - The config object.
|
|
130
|
+
* @param {Object} [options] - The options object.
|
|
131
|
+
* @param {Function} [options.match] - The function to match the path to the config.
|
|
129
132
|
* @returns {Object} The initialized root configuration
|
|
130
133
|
*/
|
|
131
|
-
function initializeConfig(configObj: Config): ConfigRoot {
|
|
134
|
+
function initializeConfig(configObj: Config, options?: { match?: (key: string) => boolean }): ConfigRoot {
|
|
132
135
|
config = configObj;
|
|
133
|
-
rootPath = getRootPath(config);
|
|
136
|
+
rootPath = getRootPath(config, { match: options?.match });
|
|
134
137
|
rootConfig = applyConfigOverrides(config, rootPath);
|
|
135
138
|
return rootConfig;
|
|
136
139
|
}
|
package/src/lib/slot.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
RefObject,
|
|
16
16
|
VNode,
|
|
17
17
|
} from 'preact';
|
|
18
|
-
import { HTMLAttributes } from 'preact/compat';
|
|
18
|
+
import { Children, HTMLAttributes, isValidElement } from 'preact/compat';
|
|
19
19
|
import {
|
|
20
20
|
StateUpdater,
|
|
21
21
|
useCallback,
|
|
@@ -41,6 +41,7 @@ interface SlotElement {
|
|
|
41
41
|
prependChild: MutateElement;
|
|
42
42
|
appendSibling: MutateElement;
|
|
43
43
|
prependSibling: MutateElement;
|
|
44
|
+
remove: () => void;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
interface PrivateContext<T> {
|
|
@@ -60,6 +61,7 @@ interface DefaultSlotContext<T> extends PrivateContext<T> {
|
|
|
60
61
|
prependChild: MutateElement;
|
|
61
62
|
appendSibling: MutateElement;
|
|
62
63
|
prependSibling: MutateElement;
|
|
64
|
+
remove: () => void;
|
|
63
65
|
onRender: (cb: (next: T & DefaultSlotContext<T>) => void) => void;
|
|
64
66
|
onChange: (cb: (next: T & DefaultSlotContext<T>) => void) => void;
|
|
65
67
|
}
|
|
@@ -85,7 +87,7 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
85
87
|
render?: Function,
|
|
86
88
|
// eslint-disable-next-line no-undef
|
|
87
89
|
contentTag: keyof HTMLElementTagNameMap = 'div'
|
|
88
|
-
): [RefObject<V>, Record<string, any
|
|
90
|
+
): [RefObject<V>, Record<string, any>, 'loading' | 'pending' | 'ready'] {
|
|
89
91
|
const slotsQueue = useContext(SlotQueueContext);
|
|
90
92
|
|
|
91
93
|
// HTML Element
|
|
@@ -207,6 +209,10 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
207
209
|
const parent = element.parentNode;
|
|
208
210
|
parent?.insertBefore(elem, element);
|
|
209
211
|
},
|
|
212
|
+
|
|
213
|
+
remove: () => {
|
|
214
|
+
element.remove();
|
|
215
|
+
},
|
|
210
216
|
};
|
|
211
217
|
},
|
|
212
218
|
[name]
|
|
@@ -301,6 +307,14 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
301
307
|
[_registerMethod]
|
|
302
308
|
);
|
|
303
309
|
|
|
310
|
+
// @ts-ignore
|
|
311
|
+
context.remove = useCallback(() => {
|
|
312
|
+
// @ts-ignore
|
|
313
|
+
_registerMethod(() => {
|
|
314
|
+
elementRef.current?.remove();
|
|
315
|
+
});
|
|
316
|
+
}, [_registerMethod]);
|
|
317
|
+
|
|
304
318
|
const handleLifeCycleRender = useCallback(async () => {
|
|
305
319
|
if (status.current === 'loading') return;
|
|
306
320
|
|
|
@@ -363,9 +377,51 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
363
377
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
364
378
|
}, [JSON.stringify(context), JSON.stringify(_state)]);
|
|
365
379
|
|
|
366
|
-
return [elementRef, props];
|
|
380
|
+
return [elementRef, props, status.current];
|
|
367
381
|
}
|
|
368
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Recursively processes children elements to conditionally prevent image loading.
|
|
385
|
+
*
|
|
386
|
+
* This function traverses the children tree and modifies img elements to prevent
|
|
387
|
+
* premature loading during slot initialization. When isReady is false, img src
|
|
388
|
+
* attributes are set to empty string to prevent network requests, while preserving
|
|
389
|
+
* the original src in a data attribute for debugging purposes.
|
|
390
|
+
*
|
|
391
|
+
* @param children - The children elements to process (can be any React/Preact children)
|
|
392
|
+
* @param isReady - Whether the slot is ready to load images (true) or should prevent loading (false)
|
|
393
|
+
* @returns Processed children with conditional image src attributes
|
|
394
|
+
*/
|
|
395
|
+
const processChildren = (children: any, isReady: boolean): any => {
|
|
396
|
+
return Children.map(children, child => {
|
|
397
|
+
// Handle text nodes, numbers, etc.
|
|
398
|
+
if (!isValidElement(child)) {
|
|
399
|
+
return child;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Handle img elements - conditionally set src
|
|
403
|
+
if (child.props.src) {
|
|
404
|
+
return cloneElement(child, {
|
|
405
|
+
...child.props,
|
|
406
|
+
src: isReady ? child.props.src : "",
|
|
407
|
+
// Optionally preserve original src in data attribute for debugging
|
|
408
|
+
'data-original-src': child.props.src,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Handle elements with children - recursively process them
|
|
413
|
+
if (child.props && child.props.children) {
|
|
414
|
+
return cloneElement(child, {
|
|
415
|
+
...child.props,
|
|
416
|
+
children: processChildren(child.props.children, isReady),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Return other elements as-is
|
|
421
|
+
return child;
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
|
|
369
425
|
// Slot Component
|
|
370
426
|
interface SlotPropsComponent<T>
|
|
371
427
|
extends Omit<HTMLAttributes<HTMLElement>, 'slot'> {
|
|
@@ -398,7 +454,7 @@ export function Slot<T>({
|
|
|
398
454
|
}> {
|
|
399
455
|
const slotsQueue = useContext(SlotQueueContext);
|
|
400
456
|
|
|
401
|
-
const [elementRef, slotProps] = useSlot<T, HTMLElement>(
|
|
457
|
+
const [elementRef, slotProps, status] = useSlot<T, HTMLElement>(
|
|
402
458
|
name,
|
|
403
459
|
context,
|
|
404
460
|
slot,
|
|
@@ -426,7 +482,7 @@ export function Slot<T>({
|
|
|
426
482
|
ref: elementRef,
|
|
427
483
|
'data-slot': name,
|
|
428
484
|
},
|
|
429
|
-
slotProps.children
|
|
485
|
+
processChildren(slotProps.children, status === 'ready')
|
|
430
486
|
);
|
|
431
487
|
}
|
|
432
488
|
|