@adobe-commerce/elsie 1.5.1-alpha003 → 1.6.0-alpha999

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.
@@ -4,7 +4,9 @@ const path = require('path');
4
4
  module.exports = async function generateResourceBuilder({ argv }) {
5
5
  const { build, preview } = await import('vite');
6
6
 
7
- const configFile = argv?.config ?? path.resolve(__dirname, '../../../config/vite.mjs');
7
+ const configFile =
8
+ argv?.config ??
9
+ path.resolve(...[__dirname, '..', '..', '..', 'config', 'vite.mjs']);
8
10
 
9
11
  let built = false;
10
12
 
package/config/vite.mjs CHANGED
@@ -2,9 +2,9 @@
2
2
  * Copyright 2024 Adobe
3
3
  * All Rights Reserved.
4
4
  *
5
- * NOTICE: Adobe permits you to use, modify, and distribute this
6
- * file in accordance with the terms of the Adobe license agreement
7
- * accompanying it.
5
+ * NOTICE: Adobe permits you to use, modify, and distribute this
6
+ * file in accordance with the terms of the Adobe license agreement
7
+ * accompanying it.
8
8
  *******************************************************************/
9
9
 
10
10
  import { glob } from 'glob';
@@ -25,7 +25,12 @@ import banner from 'vite-plugin-banner';
25
25
  const env = loadEnv('', process.cwd());
26
26
 
27
27
  // Load Elsie Config
28
- const elsieConfig = await import(path.resolve(process.cwd(), './.elsie.js')).then((m) => m.default);
28
+ const elsieConfigPath = path.resolve(process.cwd(), './.elsie.js');
29
+ // Convert Windows paths to file:// URLs for ES module imports
30
+ const elsieConfigUrl = elsieConfigPath.startsWith('file://')
31
+ ? elsieConfigPath
32
+ : `file://${elsieConfigPath.replace(/\\/g, '/')}`;
33
+ const elsieConfig = await import(elsieConfigUrl).then((m) => m.default);
29
34
 
30
35
  // Read package.json using createRequire (compatible with Node 20 and 22)
31
36
  const require = createRequire(import.meta.url);
@@ -293,19 +298,19 @@ export default {
293
298
  generateBundle(options, bundle) {
294
299
  for (const fileName in bundle) {
295
300
  const chunk = bundle[fileName];
296
-
301
+
297
302
  // Process both .map files and JS/TS files with sourcemaps
298
- if ((chunk.type === 'asset' && fileName.endsWith('.map')) ||
303
+ if ((chunk.type === 'asset' && fileName.endsWith('.map')) ||
299
304
  (chunk.type === 'chunk' && chunk.map)) {
300
305
  try {
301
306
  // Get the sourcemap object - either from the asset source or the chunk's map
302
307
  const map = chunk.type === 'asset' ? JSON.parse(chunk.source) : chunk.map;
303
-
308
+
304
309
  if (map.sources) {
305
310
  map.sources = map.sources.map((input) => {
306
311
  return input.replace(/(?:\.\.?\/)+src\//, `/${packageJSON.name}/src/`);
307
312
  });
308
-
313
+
309
314
  // Update the sourcemap in the appropriate place
310
315
  if (chunk.type === 'asset') {
311
316
  chunk.source = JSON.stringify(map);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.5.1-alpha003",
3
+ "version": "1.6.0-alpha999",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -8,12 +8,13 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { Icon } from '@adobe-commerce/elsie/components';
11
- import '@adobe-commerce/elsie/components/Picker/Picker.css';
12
11
  import { ChevronDown } from '@adobe-commerce/elsie/icons';
13
12
  import { classes } from '@adobe-commerce/elsie/lib';
14
13
  import { FunctionComponent, VNode } from 'preact';
15
14
  import { HTMLAttributes, useEffect, useState } from 'preact/compat';
16
15
 
16
+ import '@adobe-commerce/elsie/components/Picker/Picker.css';
17
+
17
18
  type PickerValue = string | null;
18
19
 
19
20
  export interface PickerOption {
@@ -68,9 +69,10 @@ export const Picker: FunctionComponent<PickerProps> = ({
68
69
  defaultOption,
69
70
  icon,
70
71
  className,
72
+ id,
71
73
  ...props
72
74
  }) => {
73
- const id = props?.id || name || `dropin-picker-${Math.random().toString(36)}`;
75
+ const uniqueId = id ?? name ?? `dropin-picker-${Math.random().toString(36)}`;
74
76
  const isRequired = !!props?.required;
75
77
 
76
78
  // find the first option that is not disabled
@@ -154,7 +156,7 @@ export const Picker: FunctionComponent<PickerProps> = ({
154
156
  )}
155
157
 
156
158
  <select
157
- id={id}
159
+ id={uniqueId}
158
160
  className={classes([
159
161
  'dropin-picker__select',
160
162
  `dropin-picker__select--${variant}`,
@@ -23,16 +23,14 @@
23
23
  }
24
24
 
25
25
  .dropin-product-item-card__image-container {
26
- overflow: visible;
26
+ overflow: hidden;
27
27
  width: 100%;
28
28
  height: auto;
29
29
  }
30
30
 
31
31
  .dropin-product-item-card__image img {
32
- display: block;
33
32
  width: 100%;
34
33
  max-height: 375px;
35
- border-radius: var(--border-radius-xsmall, 4px);
36
34
  }
37
35
 
38
36
  .dropin-product-item-card__content {
@@ -76,42 +74,6 @@
76
74
  width: 100%;
77
75
  }
78
76
 
79
- .dropin-product-item-card__image a:focus {
80
- outline: 1px solid var(--color-neutral-400);
81
- outline-offset: 1px;
82
- }
83
-
84
- .dropin-product-item-card__image {
85
- display: inline-block;
86
- border-radius: var(--border-radius-xsmall, 4px);
87
- outline: none;
88
- }
89
-
90
- .dropin-product-item-card__image:focus-visible {
91
- outline: 2px solid var(--color-interactive-focus, #005fcc);
92
- outline-offset: 2px;
93
- z-index: 2;
94
- }
95
-
96
- .dropin-product-item-card__image-container {
97
- overflow: visible;
98
- width: 100%;
99
- height: auto;
100
- }
101
-
102
- .dropin-product-item-card__image-link {
103
- display: block;
104
- border-radius: var(--border-radius-xsmall, 4px);
105
- outline: none;
106
- }
107
-
108
- .dropin-product-item-card__image-link:focus-visible {
109
- outline: 3px solid var(--color-interactive-focus, #333333);
110
- outline-offset: 3px;
111
- position: relative;
112
- z-index: 1;
113
- }
114
-
115
77
  /* Medium (portrait tablets and large phones, 768px and up) */
116
78
  /* @media only screen and (min-width: 768px) { } */
117
79
 
@@ -47,15 +47,13 @@ const meta: Meta<ProductItemCardProps> = {
47
47
  options: ['DefaultImage', 'Empty'],
48
48
  mapping: {
49
49
  DefaultImage: (
50
- <a href="#" className="dropin-product-item-card__image-link">
51
- <Image
52
- src="https://picsum.photos/300/375"
53
- width="300"
54
- height="375"
55
- alt="Product Image"
56
- loading="lazy"
57
- />
58
- </a>
50
+ <Image
51
+ src="https://picsum.photos/300/375"
52
+ width="300"
53
+ height="375"
54
+ alt="Product Image"
55
+ loading="lazy"
56
+ />
59
57
  ),
60
58
  Empty: null,
61
59
  },
@@ -255,7 +253,6 @@ export const Default: Story = {
255
253
  sku: 'DefaultSku' as any,
256
254
  swatches: 'DefaultSwatches' as any,
257
255
  actionButton: 'DefaultButton' as any,
258
- imageHref: '#',
259
256
  },
260
257
  };
261
258
 
@@ -22,8 +22,6 @@ export interface ProductItemCardProps
22
22
  actionButton?: VNode;
23
23
  swatches?: VNode;
24
24
  initialized?: boolean;
25
- imageHref?: string;
26
- imageLinkLabel?: string;
27
25
  }
28
26
 
29
27
  export const ProductItemCard: FunctionComponent<ProductItemCardProps> = ({
@@ -35,8 +33,6 @@ export const ProductItemCard: FunctionComponent<ProductItemCardProps> = ({
35
33
  actionButton,
36
34
  swatches,
37
35
  initialized = false,
38
- imageHref,
39
- imageLinkLabel,
40
36
  ...props
41
37
  }) => {
42
38
  if (!initialized) {
@@ -49,23 +45,10 @@ export const ProductItemCard: FunctionComponent<ProductItemCardProps> = ({
49
45
  >
50
46
  <div className="dropin-product-item-card__image-container">
51
47
  {image && (
52
- imageHref ? (
53
- <a
54
- href={imageHref}
55
- className="dropin-product-item-card__image-link"
56
- aria-label={imageLinkLabel}
57
- >
58
- <VComponent
59
- node={image}
60
- className={classes(['dropin-product-item-card__image'])}
61
- />
62
- </a>
63
- ) : (
64
- <VComponent
65
- node={image}
66
- className={classes(['dropin-product-item-card__image'])}
67
- />
68
- )
48
+ <VComponent
49
+ node={image}
50
+ className={classes(['dropin-product-item-card__image'])}
51
+ />
69
52
  )}
70
53
  </div>
71
54
  <div className="dropin-product-item-card__content">
@@ -14,6 +14,7 @@ import {
14
14
  useEffect,
15
15
  useRef,
16
16
  useCallback,
17
+ useMemo,
17
18
  } from 'preact/compat';
18
19
  import { classes } from '@adobe-commerce/elsie/lib';
19
20
  import '@adobe-commerce/elsie/components/TextSwatch/TextSwatch.css';
@@ -93,6 +94,8 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
93
94
  }
94
95
  }, [label]);
95
96
 
97
+ const uniqueId = useMemo(() => id ?? `${name}_${id}_${Math.random().toString(36)}`, [name, id]);
98
+
96
99
  return (
97
100
  <div
98
101
  className="dropin-text-swatch__container"
@@ -101,7 +104,7 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
101
104
  <input
102
105
  type={multi ? 'checkbox' : 'radio'}
103
106
  name={name}
104
- id={id}
107
+ id={uniqueId}
105
108
  value={value}
106
109
  aria-label={handleAriaLabel()}
107
110
  checked={selected}
@@ -116,7 +119,7 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
116
119
  ])}
117
120
  />
118
121
  <label
119
- htmlFor={id}
122
+ htmlFor={uniqueId}
120
123
  ref={spanRef}
121
124
  className={classes([
122
125
  'dropin-text-swatch__label',
@@ -121,5 +121,21 @@ button.addEventListener('click', () => {
121
121
  });
122
122
  ```
123
123
 
124
+ ### Unmounting components without instance access
125
+
126
+ The `Render.unmount` static method provides a way to unmount components from the DOM when you don't have direct access to the component instance.
127
+ This is particularly useful in scenarios where components are rendered inside modals, dialogs, or other temporary containers that need to be cleaned up.
128
+
129
+ #### Example
130
+
131
+ ```js
132
+ // Close the dialog
133
+ dialog.close();
134
+
135
+ // Unmount any dropin containers rendered in the modal
136
+ dialog.querySelectorAll('[data-dropin-container]').forEach(Render.unmount);
137
+ ```
138
+
139
+ This approach ensures that all dropin components are properly cleaned up when their container elements are removed from the DOM, preventing memory leaks and maintaining application performance.
124
140
  </Unstyled>
125
141
 
@@ -32,6 +32,12 @@ The `<Slot />` component is used to define a slot in a container. It receives a
32
32
 
33
33
  The name of the slot in _PascalCase_. `string` (required).
34
34
 
35
+ ### lazy
36
+
37
+ Controls whether the slot should be loaded immediately or deferred for later initialization. `boolean` (optional).
38
+
39
+ When `lazy={false}`, the slot is initialized as soon as the container mounts. When `lazy={true}`, the slot can be initialized later on when it is needed. This is useful for performance optimization, especially when the slot's content is not immediately required.
40
+
35
41
  ### slotTag
36
42
 
37
43
  The HTML tag to use for the slot's wrapper element. This allows you to change the wrapper element from the default `div` to any valid HTML tag (e.g., 'span', 'p', 'a', etc.). When using specific tags like 'a', you can also provide their respective HTML attributes (e.g., 'href', 'target', etc.).
@@ -71,7 +77,7 @@ Example:
71
77
 
72
78
  - `ctx`: An object representing the context of the slot, including methods for manipulating the slot's content.
73
79
 
74
- The slot property, which is implemented as a promise function, provides developers with the flexibility to dynamically generate and manipulate content within slots.
80
+ The slot property, which is implemented as a promise function, provides developers with the flexibility to dynamically generate and manipulate content within slots.
75
81
  However, it's important to note that this promise is render-blocking, meaning that the component will not render until the promise is resolved.
76
82
 
77
83
  ### context
@@ -71,17 +71,21 @@ export class Render {
71
71
  rootElement.innerHTML = '';
72
72
 
73
73
  // clone the root element to initialize rendering on the background
74
- const tmp = document.createElement('div');
74
+ const root = document.createElement('div');
75
75
 
76
76
  // apply base design tokens and global styles to the root element
77
77
  rootElement.classList.add('dropin-design');
78
+ rootElement.setAttribute('data-dropin-container', Component.name);
78
79
 
79
- render(<Root next={state} />, tmp);
80
+ // store the virtual root element
81
+ (rootElement as any).__rootElement = root;
82
+
83
+ render(<Root next={state} />, root);
80
84
 
81
85
  // API object to control the rendered component
82
86
  const API: RenderAPI = {
83
87
  remove: () => {
84
- render(null, tmp);
88
+ render(null, root);
85
89
  },
86
90
  setProps: (cb: (prev: T) => T) => {
87
91
  const next = cb(state.peek());
@@ -97,7 +101,7 @@ export class Render {
97
101
  rootElement.classList.add('dropin-design');
98
102
 
99
103
  // append the rendered component to the DOM only when all slots are resolved
100
- rootElement.appendChild(tmp.firstChild ?? tmp);
104
+ rootElement.appendChild(root.firstChild ?? root);
101
105
 
102
106
  return resolve(API);
103
107
  }
@@ -106,14 +110,24 @@ export class Render {
106
110
  };
107
111
  }
108
112
 
113
+ /**
114
+ * Unmounts a container from a root element.
115
+ * @param rootElement - The root element to unmount the container from.
116
+ */
117
+ static unmount(rootElement: HTMLElement) {
118
+ const root = (rootElement as any)?.__rootElement;
119
+ if (!root) throw new Error('Root element is not defined');
120
+ render(null, root);
121
+ }
122
+
109
123
  /**
110
124
  * UnRenders a component from a root element.
111
125
  * @param rootElement - The root element to unmount the component from.
112
- * @deprecated Use `remove` method from the returned object of the `mount` method instead.
126
+ * @deprecated Use `remove` method from the returned object of the `mount` method instead or `unmount` method from the `Render` class.
113
127
  */
114
128
  unmount(rootElement: HTMLElement) {
115
129
  if (!rootElement) throw new Error('Root element is not defined');
116
- rootElement.firstChild?.remove();
130
+ rootElement.firstChild?.remove();
117
131
  }
118
132
 
119
133
  /**
@@ -135,4 +149,4 @@ export class Render {
135
149
  { ...options }
136
150
  );
137
151
  }
138
- }
152
+ }
package/src/lib/slot.tsx CHANGED
@@ -2,30 +2,35 @@
2
2
  * Copyright 2024 Adobe
3
3
  * All Rights Reserved.
4
4
  *
5
- * NOTICE: Adobe permits you to use, modify, and distribute this
6
- * file in accordance with the terms of the Adobe license agreement
7
- * accompanying it.
5
+ * NOTICE: Adobe permits you to use, modify, and distribute this
6
+ * file in accordance with the terms of the Adobe license agreement
7
+ * accompanying it.
8
8
  *******************************************************************/
9
9
 
10
- import { cloneElement, ComponentChildren, RefObject, VNode, createElement } from 'preact';
10
+ import { IntlContext, Lang } from '@adobe-commerce/elsie/i18n';
11
+ import {
12
+ cloneElement,
13
+ ComponentChildren,
14
+ createElement,
15
+ RefObject,
16
+ VNode,
17
+ } from 'preact';
18
+ import { HTMLAttributes } from 'preact/compat';
11
19
  import {
12
20
  StateUpdater,
21
+ useCallback,
13
22
  useContext,
14
- useState,
15
- useRef,
16
23
  useEffect,
17
24
  useMemo,
18
- useCallback,
25
+ useRef,
26
+ useState,
19
27
  } from 'preact/hooks';
20
- import { IntlContext, Lang } from '@adobe-commerce/elsie/i18n';
21
- import { HTMLAttributes } from 'preact/compat';
22
28
  import { SlotQueueContext } from './render';
23
29
 
24
30
  import '@adobe-commerce/elsie/components/UIProvider/debugger.css';
25
31
 
26
32
  type MutateElement = (elem: HTMLElement) => void;
27
33
 
28
-
29
34
  interface State {
30
35
  get: (key: string) => void;
31
36
  set: (key: string, value: any) => void;
@@ -149,18 +154,21 @@ export function useSlot<K, V extends HTMLElement>(
149
154
  // @ts-ignore
150
155
  context._registerMethod = _registerMethod;
151
156
 
152
- const _htmlElementToVNode = useCallback((elem: HTMLElement) => {
153
- return createElement(
154
- contentTag,
155
- {
156
- 'data-slot-html-element': elem.tagName.toLowerCase(),
157
- ref: (refElem: HTMLElement | null): void => {
158
- refElem?.appendChild(elem);
159
- }
160
- },
161
- null
162
- );
163
- }, [contentTag]);
157
+ const _htmlElementToVNode = useCallback(
158
+ (elem: HTMLElement) => {
159
+ return createElement(
160
+ contentTag,
161
+ {
162
+ 'data-slot-html-element': elem.tagName.toLowerCase(),
163
+ ref: (refElem: HTMLElement | null): void => {
164
+ refElem?.appendChild(elem);
165
+ },
166
+ },
167
+ null
168
+ );
169
+ },
170
+ [contentTag]
171
+ );
164
172
 
165
173
  // @ts-ignore
166
174
  context._htmlElementToVNode = _htmlElementToVNode;
@@ -322,7 +330,10 @@ export function useSlot<K, V extends HTMLElement>(
322
330
  status.current = 'loading';
323
331
 
324
332
  log(`🟩 "${name}" Slot Initialized`);
325
- await callback(context as K & DefaultSlotContext<K>, elementRef.current as HTMLDivElement | null);
333
+ await callback(
334
+ context as K & DefaultSlotContext<K>,
335
+ elementRef.current as HTMLDivElement | null
336
+ );
326
337
  } catch (error) {
327
338
  console.error(`Error in "${callback.name}" Slot callback`, error);
328
339
  } finally {
@@ -336,7 +347,7 @@ export function useSlot<K, V extends HTMLElement>(
336
347
  // Initialization
337
348
  useEffect(() => {
338
349
  handleLifeCycleInit().finally(() => {
339
- if (slotsQueue) {
350
+ if (slotsQueue && slotsQueue.value.has(name)) {
340
351
  slotsQueue.value.delete(name);
341
352
  slotsQueue.value = new Set(slotsQueue.value);
342
353
  }
@@ -359,6 +370,7 @@ export function useSlot<K, V extends HTMLElement>(
359
370
  interface SlotPropsComponent<T>
360
371
  extends Omit<HTMLAttributes<HTMLElement>, 'slot'> {
361
372
  name: string;
373
+ lazy?: boolean;
362
374
  slot?: SlotProps<T>;
363
375
  context?: Context<T>;
364
376
  render?: (props: Record<string, any>) => VNode | VNode[];
@@ -371,6 +383,7 @@ interface SlotPropsComponent<T>
371
383
 
372
384
  export function Slot<T>({
373
385
  name,
386
+ lazy = false,
374
387
  context,
375
388
  slot,
376
389
  children,
@@ -400,11 +413,11 @@ export function Slot<T>({
400
413
  }
401
414
 
402
415
  // add slot to queue
403
- if (slotsQueue) {
416
+ if (slotsQueue && lazy === false) {
404
417
  slotsQueue.value.add(name);
405
418
  slotsQueue.value = new Set(slotsQueue.value);
406
419
  }
407
- }, [name, slotsQueue]);
420
+ }, [name, lazy, slotsQueue]);
408
421
 
409
422
  return createElement(
410
423
  slotTag,