@adobe-commerce/elsie 1.1.0 → 1.2.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.
@@ -3,8 +3,7 @@ const path = require('path');
3
3
  module.exports = async function generateResourceBuilder({ argv }) {
4
4
  const { build } = await import('vite');
5
5
 
6
- const configFile =
7
- argv?.config ?? path.resolve(__dirname, '../../../config/vite.mjs');
6
+ const configFile = argv?.config ?? path.resolve(__dirname, '../../../config/vite.mjs');
8
7
 
9
8
  const outDir = argv?.outDir ?? 'dist';
10
9
 
@@ -1,5 +1,5 @@
1
1
  const cli = require('../../lib/cli');
2
2
 
3
3
  module.exports = function generateResourceBuilder() {
4
- cli('eslint "*/**/*.{ts,tsx}"');
4
+ return cli('eslint "*/**/*.{ts,tsx}"');
5
5
  };
@@ -1,5 +1,5 @@
1
1
  const cli = require('../../lib/cli');
2
2
 
3
3
  module.exports = function generateResourceBuilder() {
4
- cli('storybook dev -h localhost -p 6006 --disable-telemetry --quiet');
4
+ return cli('storybook dev -h localhost -p 6006 --disable-telemetry --quiet');
5
5
  };
@@ -1,5 +1,5 @@
1
1
  const cli = require('../../lib/cli');
2
2
 
3
3
  module.exports = function generateResourceBuilder() {
4
- cli('jest');
4
+ return cli('jest');
5
5
  };
package/bin/lib/cli.js CHANGED
@@ -4,5 +4,20 @@ module.exports = function cli(command) {
4
4
  let cmd = command;
5
5
  const argvs = process.argv.slice(3).join(' ');
6
6
  if (argvs) cmd += ` ${argvs}`;
7
- return spawn(cmd, { shell: true, stdio: 'inherit' });
7
+
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn(cmd, { shell: true, stdio: 'inherit' });
10
+
11
+ child.on('close', (code) => {
12
+ if (code !== 0) {
13
+ reject(new Error(`Command failed with exit code ${code}`));
14
+ } else {
15
+ resolve(child);
16
+ }
17
+ });
18
+
19
+ child.on('error', (err) => {
20
+ reject(err);
21
+ });
22
+ });
8
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -15,7 +15,7 @@
15
15
  "dev": "concurrently 'yarn storybook' 'yarn serve'",
16
16
  "storybook": "elsie storybook",
17
17
  "serve": "elsie serve --config vite.config.mjs",
18
- "lint": "elsie lint --max-warnings=0",
18
+ "lint": "elsie lint",
19
19
  "test": "elsie test",
20
20
  "test:ci": "jest --config jest.config.js --passWithNoTests --coverage",
21
21
  "build": "elsie build --config vite.config.mjs",
@@ -7,13 +7,13 @@
7
7
  * accompanying it.
8
8
  *******************************************************************/
9
9
 
10
- import { ComponentChildren, FunctionComponent, VNode } from 'preact';
10
+ import { ComponentChildren, FunctionComponent, VNode, JSX } from 'preact';
11
11
  import { HTMLAttributes } from 'preact/compat';
12
12
  import { classes, VComponent } from '@adobe-commerce/elsie/lib';
13
13
  import { Divider } from '@adobe-commerce/elsie/components';
14
14
  import '@adobe-commerce/elsie/components/Header/Header.css';
15
15
 
16
- export interface HeaderProps extends HTMLAttributes<HTMLDivElement> {
16
+ export interface HeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'size'> {
17
17
  title: string;
18
18
  size?: 'medium' | 'large';
19
19
  divider?: boolean;
@@ -33,6 +33,7 @@
33
33
  overflow: hidden;
34
34
  }
35
35
 
36
+ .dropin-image-swatch__span img,
36
37
  .dropin-image-swatch__content {
37
38
  width: inherit;
38
39
  position: absolute;
@@ -121,10 +122,19 @@
121
122
  }
122
123
 
123
124
  .dropin-image-swatch__span--out-of-stock > .dropin-image-swatch__content,
125
+ .dropin-image-swatch__span--out-of-stock img,
126
+ .dropin-image-swatch__container
127
+ input[type='radio']:disabled
128
+ ~ .dropin-image-swatch__span
129
+ img,
124
130
  .dropin-image-swatch__container
125
131
  input[type='radio']:disabled
126
132
  ~ .dropin-image-swatch__span
127
133
  > .dropin-image-swatch__content,
134
+ .dropin-image-swatch__container
135
+ input[type='checkbox']:disabled
136
+ ~ .dropin-image-swatch__span
137
+ img,
128
138
  .dropin-image-swatch__container
129
139
  input[type='checkbox']:disabled
130
140
  ~ .dropin-image-swatch__span
@@ -260,11 +260,11 @@ export const MultiImageSwatch: Story = {
260
260
  ),
261
261
  };
262
262
 
263
- export const CustomImageNodeSwatch: Story = {
263
+ export const CustomImageNodeVNodeSwatch: Story = {
264
264
  args: {
265
265
  name: 'customImageSwatch',
266
266
  id: 'customImageSwatch1',
267
- label: 'Custom Image Node Example',
267
+ label: 'Custom Image Node VNode Example',
268
268
  groupAriaLabel: 'Custom Image Swatches',
269
269
  value: 'customImageNode',
270
270
  src: `https://picsum.photos/${defaultWidth}/${defaultHeight}`, // fallback, not used with imageNode
@@ -277,7 +277,7 @@ export const CustomImageNodeSwatch: Story = {
277
277
  <div style="position: relative; width: 100%; height: 100%;">
278
278
  <img
279
279
  src={`https://picsum.photos/${defaultWidth}/${defaultHeight}?grayscale`}
280
- alt="Custom grayscale image"
280
+ alt="Custom grayscale image - VNode"
281
281
  style="width: 100%; height: 100%; object-fit: cover;"
282
282
  />
283
283
  <div style="position: absolute; top: 0; left: 0; background: rgba(255,255,255,0.7); padding: 4px 8px; border-radius: 0 0 8px 0;">
@@ -295,7 +295,56 @@ export const CustomImageNodeSwatch: Story = {
295
295
  'div[style*="position: relative"]'
296
296
  );
297
297
  const customImage = canvasElement.querySelector(
298
- 'img[alt="Custom grayscale image"]'
298
+ 'img[alt="Custom grayscale image - VNode"]'
299
+ );
300
+ const customLabel = canvasElement.querySelector(
301
+ 'span[style*="font-weight: bold"]'
302
+ );
303
+
304
+ expect(imageSwatch).toBeInTheDocument();
305
+ expect(customImageContainer).toBeInTheDocument();
306
+ expect(customImage).toBeInTheDocument();
307
+ expect(customLabel).toBeInTheDocument();
308
+ expect(customLabel?.textContent).toBe('Custom');
309
+ },
310
+ };
311
+
312
+ export const CustomImageNodeRenderFunctionSwatch: Story = {
313
+ args: {
314
+ name: 'customImageSwatch',
315
+ id: 'customImageSwatch2',
316
+ label: 'Custom Image Node Render Function Example',
317
+ groupAriaLabel: 'Custom Image Swatches',
318
+ value: 'customImageNode',
319
+ src: `https://picsum.photos/${defaultWidth}/${defaultHeight}`, // fallback, not used with imageNode
320
+ alt: 'Custom Image Node',
321
+ selected: false,
322
+ disabled: false,
323
+ outOfStock: false,
324
+ onValue: action('onValue'),
325
+ imageNode: () => (
326
+ <div style="position: relative; width: 100%; height: 100%;">
327
+ <img
328
+ src={`https://picsum.photos/${defaultWidth}/${defaultHeight}?grayscale`}
329
+ alt="Custom grayscale image - Render Function"
330
+ style="width: 100%; height: 100%; object-fit: cover;"
331
+ />
332
+ <div style="position: absolute; top: 0; left: 0; background: rgba(255,255,255,0.7); padding: 4px 8px; border-radius: 0 0 8px 0;">
333
+ <span style="font-size: 12px; font-weight: bold; color: #333;">
334
+ Custom
335
+ </span>
336
+ </div>
337
+ </div>
338
+ ),
339
+ },
340
+ play: async ({ canvasElement }) => {
341
+ const canvas = within(canvasElement);
342
+ const imageSwatch = await canvas.findByRole('radio');
343
+ const customImageContainer = canvasElement.querySelector(
344
+ 'div[style*="position: relative"]'
345
+ );
346
+ const customImage = canvasElement.querySelector(
347
+ 'img[alt="Custom grayscale image - Render Function"]'
299
348
  );
300
349
  const customLabel = canvasElement.querySelector(
301
350
  'span[style*="font-weight: bold"]'
@@ -8,11 +8,26 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { FunctionComponent, VNode } from 'preact';
11
- import { HTMLAttributes, useCallback } from 'preact/compat';
11
+ import { HTMLAttributes, useCallback, JSX, useMemo } from 'preact/compat';
12
12
  import { classes } from '@adobe-commerce/elsie/lib';
13
13
  import '@adobe-commerce/elsie/components/ImageSwatch/ImageSwatch.css';
14
- import { Image } from '@adobe-commerce/elsie/components/Image';
14
+ import { Image, ImageProps } from '@adobe-commerce/elsie/components/Image';
15
15
  import { useText } from '@adobe-commerce/elsie/i18n';
16
+
17
+ export interface ImageNodeRenderProps extends ImageProps {
18
+ imageSwatchContext: {
19
+ disabled?: boolean;
20
+ outOfStock?: boolean;
21
+ multi?: boolean;
22
+ selected?: boolean;
23
+ value?: string;
24
+ label?: string;
25
+ groupAriaLabel?: string;
26
+ name?: string;
27
+ id?: string;
28
+ };
29
+ }
30
+
16
31
  export interface ImageSwatchProps
17
32
  extends Omit<HTMLAttributes<HTMLInputElement>, 'label'> {
18
33
  name?: string;
@@ -26,7 +41,7 @@ export interface ImageSwatchProps
26
41
  selected?: boolean;
27
42
  outOfStock?: boolean;
28
43
  multi?: boolean;
29
- imageNode?: VNode;
44
+ imageNode?: VNode | ((props: ImageNodeRenderProps) => JSX.Element);
30
45
  onValue?: (value: any) => void;
31
46
  onUpdateError?: (error: Error) => void;
32
47
  }
@@ -80,6 +95,16 @@ export const ImageSwatch: FunctionComponent<ImageSwatchProps> = ({
80
95
  return `${groupAriaLabel}: ${label} ${swatchLabel}`;
81
96
  };
82
97
 
98
+ const imageProps: ImageProps = useMemo(() => {
99
+ return {
100
+ src,
101
+ alt,
102
+ loading: 'lazy',
103
+ params: { width: 100, fit: 'bounds', crop: true },
104
+ onError: (e: any) => (e.target.style.display = 'none'),
105
+ };
106
+ }, [src, alt]);
107
+
83
108
  return (
84
109
  <label className={classes(['dropin-image-swatch__container', className])}>
85
110
  <input
@@ -107,14 +132,24 @@ export const ImageSwatch: FunctionComponent<ImageSwatchProps> = ({
107
132
  className,
108
133
  ])}
109
134
  >
110
- {imageNode || (
135
+ {typeof imageNode === 'function' ? (
136
+ imageNode({
137
+ ...imageProps,
138
+ imageSwatchContext: {
139
+ disabled,
140
+ outOfStock,
141
+ selected,
142
+ value,
143
+ label,
144
+ groupAriaLabel,
145
+ name,
146
+ id,
147
+ },
148
+ })
149
+ ) : imageNode || (
111
150
  <Image
112
- src={src}
151
+ {...imageProps}
113
152
  className={classes(['dropin-image-swatch__content'])}
114
- params={{ width: 100, fit: 'bounds', crop: true }}
115
- alt={alt}
116
- loading={'lazy'}
117
- onError={(e: any) => (e.target.style.display = 'none')}
118
153
  />
119
154
  )}
120
155
  </span>
@@ -8,7 +8,7 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { FunctionComponent } from 'preact';
11
- import { useState, useCallback } from 'preact/hooks';
11
+ import { useState, useCallback, useEffect } from 'preact/hooks';
12
12
  import { HTMLAttributes } from 'preact/compat';
13
13
  import { classes, debounce } from '@adobe-commerce/elsie/lib';
14
14
  import { Add, Minus } from '@adobe-commerce/elsie/icons';
@@ -56,6 +56,15 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
56
56
  ? 'Dropin.Incrementer.maxQuantityMessage'
57
57
  : 'Dropin.Incrementer.errorMessage';
58
58
 
59
+ // Add this effect to synchronize internal state with external value prop
60
+ useEffect(() => {
61
+ const propValue = Number(value);
62
+ if (propValue !== currentValue) {
63
+ setCurrentValue(propValue);
64
+ }
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [value]);
67
+
59
68
  // eslint-disable-next-line react-hooks/exhaustive-deps
60
69
  const debouncedOnValueHandler = useCallback(
61
70
  debounce(async (newValue: any) => {
@@ -48,6 +48,7 @@ const Template: StoryObj<TagProps> = {
48
48
  <Tag {...args}>
49
49
  {/* This workaround allows children to be edited as plain text in Storybook */}
50
50
  {args.children && typeof args.children === 'string' ? (
51
+ // eslint-disable-next-line react/no-danger
51
52
  <span dangerouslySetInnerHTML={{ __html: args.children }} />
52
53
  ) : undefined}
53
54
  </Tag>
package/src/lib/slot.tsx CHANGED
@@ -25,6 +25,7 @@ import '@adobe-commerce/elsie/components/UIProvider/debugger.css';
25
25
 
26
26
  type MutateElement = (elem: HTMLElement) => void;
27
27
 
28
+
28
29
  interface State {
29
30
  get: (key: string) => void;
30
31
  set: (key: string, value: any) => void;
@@ -42,6 +43,7 @@ interface PrivateContext<T> {
42
43
  _registerMethod: (
43
44
  cb: (next: T & DefaultSlotContext<T>, state: State) => void
44
45
  ) => void;
46
+ // eslint-disable-next-line no-undef
45
47
  _htmlElementToVNode: (element: HTMLElement, tag: keyof HTMLElementTagNameMap) => VNode;
46
48
  }
47
49
 
@@ -76,6 +78,7 @@ export function useSlot<K, V extends HTMLElement>(
76
78
  callback?: SlotProps<K>,
77
79
  children?: ComponentChildren,
78
80
  render?: Function,
81
+ // eslint-disable-next-line no-undef
79
82
  contentTag: keyof HTMLElementTagNameMap = 'div'
80
83
  ): [RefObject<V>, Record<string, any>] {
81
84
  const slotsQueue = useContext(SlotQueueContext);
@@ -319,7 +322,7 @@ export function useSlot<K, V extends HTMLElement>(
319
322
  status.current = 'loading';
320
323
 
321
324
  log(`🟩 "${name}" Slot Initialized`);
322
- await callback(context as K & DefaultSlotContext<K>, elementRef.current);
325
+ await callback(context as K & DefaultSlotContext<K>, elementRef.current as HTMLDivElement | null);
323
326
  } catch (error) {
324
327
  console.error(`Error in "${callback.name}" Slot callback`, error);
325
328
  } finally {
@@ -359,7 +362,9 @@ interface SlotPropsComponent<T>
359
362
  slot?: SlotProps<T>;
360
363
  context?: Context<T>;
361
364
  render?: (props: Record<string, any>) => VNode | VNode[];
365
+ // eslint-disable-next-line no-undef
362
366
  slotTag?: keyof HTMLElementTagNameMap; // The tag for the slot wrapper itself
367
+ // eslint-disable-next-line no-undef
363
368
  contentTag?: keyof HTMLElementTagNameMap; // The tag for dynamically inserted content
364
369
  children?: ComponentChildren;
365
370
  }
@@ -373,7 +378,11 @@ export function Slot<T>({
373
378
  slotTag = 'div',
374
379
  contentTag = 'div',
375
380
  ...props
376
- }: Readonly<SlotPropsComponent<T>>) {
381
+ }: Readonly<SlotPropsComponent<T>>): VNode<{
382
+ ref: RefObject<HTMLElement>;
383
+ 'data-slot': string;
384
+ [key: string]: any;
385
+ }> {
377
386
  const slotsQueue = useContext(SlotQueueContext);
378
387
 
379
388
  const [elementRef, slotProps] = useSlot<T, HTMLElement>(