@adobe-commerce/elsie 1.6.0 → 1.7.0-alpha1

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.6.0",
3
+ "version": "1.7.0-alpha1",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -58,7 +58,6 @@
58
58
  "@storybook/preact": "^8.2.3",
59
59
  "@storybook/preact-vite": "^8.2.3",
60
60
  "@storybook/preact-webpack5": "^8.2.3",
61
- "@storybook/storybook-deployer": "^2.8.16",
62
61
  "@storybook/test": "^8.2.3",
63
62
  "@storybook/test-runner": "^0.19.1",
64
63
  "@storybook/theming": "^8.2.3",
@@ -266,6 +266,13 @@
266
266
  color: var(--color-neutral-800);
267
267
  }
268
268
 
269
+ .dropin-cart-item__row-total-footer {
270
+ margin-top: var(--spacing-xsmall);
271
+ font: var(--type-body-2-emphasized-font);
272
+ letter-spacing: var(--type-body-2-emphasized-letter-spacing);
273
+ color: var(--color-neutral-800);
274
+ }
275
+
269
276
  .dropin-cart-item__total-tax-message {
270
277
  margin-left: var(--spacing-xxsmall);
271
278
  }
@@ -17,7 +17,8 @@ import {
17
17
  CartItem as component,
18
18
  CartItemProps,
19
19
  CartItemSkeleton,
20
- Icon, Button,
20
+ Icon,
21
+ Button,
21
22
  } from '@adobe-commerce/elsie/components';
22
23
  import { WarningWithCircle } from '@adobe-commerce/elsie/icons';
23
24
 
@@ -43,13 +44,13 @@ const meta: Meta<CartItemProps> = {
43
44
  options: ['Image', 'None'],
44
45
  mapping: {
45
46
  Image: (
46
- <Image
47
- src="https://picsum.photos/132/184"
48
- width="132"
49
- height="184"
50
- alt="Some alternative text"
51
- loading="lazy"
52
- />
47
+ <Image
48
+ src="https://picsum.photos/132/184"
49
+ width="132"
50
+ height="184"
51
+ alt="Some alternative text"
52
+ loading="lazy"
53
+ />
53
54
  ),
54
55
  None: undefined,
55
56
  },
@@ -83,9 +84,9 @@ const meta: Meta<CartItemProps> = {
83
84
  options: ['Description', 'None'],
84
85
  mapping: {
85
86
  Description: (
86
- <div>
87
- Secondary product information such as brand name, description, etc.
88
- </div>
87
+ <div>
88
+ Secondary product information such as brand name, description, etc.
89
+ </div>
89
90
  ),
90
91
  None: undefined,
91
92
  },
@@ -118,12 +119,12 @@ const meta: Meta<CartItemProps> = {
118
119
  options: ['Attributes', 'none'],
119
120
  mapping: {
120
121
  Attributes: (
122
+ <div>
121
123
  <div>
122
- <div>
123
- Activity: Gym, Hiking, Overnight, School, Trail, Travel, Urban
124
- </div>
125
- <div>Material: Nylon, Polyester</div>
124
+ Activity: Gym, Hiking, Overnight, School, Trail, Travel, Urban
126
125
  </div>
126
+ <div>Material: Nylon, Polyester</div>
127
+ </div>
127
128
  ),
128
129
  none: undefined,
129
130
  },
@@ -170,10 +171,10 @@ const meta: Meta<CartItemProps> = {
170
171
  options: ['Total', 'Final', 'None'],
171
172
  mapping: {
172
173
  Total: (
173
- <>
174
- <Price amount={59.98} variant="strikethrough" />
175
- <Price amount={55.95} sale />
176
- </>
174
+ <>
175
+ <Price amount={59.98} variant="strikethrough" />
176
+ <Price amount={55.95} sale />
177
+ </>
177
178
  ),
178
179
  Final: <Price amount={55.95} sale />,
179
180
  None: undefined,
@@ -192,9 +193,9 @@ const meta: Meta<CartItemProps> = {
192
193
  options: ['totalExcludingTax', 'None'],
193
194
  mapping: {
194
195
  totalExcludingTax: (
195
- <>
196
- <Price amount={53.99} weight="normal" />
197
- </>
196
+ <>
197
+ <Price amount={53.99} weight="normal" />
198
+ </>
198
199
  ),
199
200
  None: undefined,
200
201
  },
@@ -277,9 +278,9 @@ const meta: Meta<CartItemProps> = {
277
278
  mapping: {
278
279
  none: null,
279
280
  RunningOut: (
280
- <span>
281
+ <span>
281
282
  <Icon source={WarningWithCircle} size={'16'} aria-hidden={true} />{' '}
282
- Out of stock!
283
+ Out of stock!
283
284
  </span>
284
285
  ),
285
286
  },
@@ -325,26 +326,42 @@ const meta: Meta<CartItemProps> = {
325
326
  type: 'select',
326
327
  labels: {
327
328
  Button: 'Button',
328
- None: 'None'
329
+ None: 'None',
329
330
  },
330
331
  },
331
332
  description: 'Wishlist control.',
332
333
  options: ['Button', 'None'],
333
334
  mapping: {
334
335
  Button: (
335
- <Button
336
- size="medium"
337
- type="submit"
338
- icon={<Icon source="Heart" />}
339
- variant="tertiary"
340
- >
341
- Move to wishlist
342
- </Button>
336
+ <Button
337
+ size="medium"
338
+ type="submit"
339
+ icon={<Icon source="Heart" />}
340
+ variant="tertiary"
341
+ >
342
+ Move to wishlist
343
+ </Button>
343
344
  ),
344
345
  None: undefined,
345
346
  },
346
347
  table: { defaultValue: { summary: 'null' } },
347
348
  },
349
+ rowTotalFooter: {
350
+ control: {
351
+ type: 'check',
352
+ labels: ['Super Offer Badge'],
353
+ },
354
+ description: 'Content displayed right below the total row price.',
355
+ options: ['SuperOffer'],
356
+ mapping: {
357
+ SuperOffer: (
358
+ <div style={{ color: 'var(--color-alert-800)', fontWeight: 'bold' }}>
359
+ Super offer
360
+ </div>
361
+ ),
362
+ },
363
+ table: { defaultValue: { summary: 'null' } },
364
+ },
348
365
  footer: {
349
366
  control: {
350
367
  type: 'check',
@@ -354,24 +371,24 @@ const meta: Meta<CartItemProps> = {
354
371
  'Final Sales and Returns Policy',
355
372
  ],
356
373
  },
357
- description: 'Footer content.',
374
+ description: 'Footer content displayed at the bottom of the cart item.',
358
375
  options: ['Promotions', 'Delivery', 'Returns'],
359
376
  mapping: {
360
377
  Promotions: <div>Extra 20% Off Clearance with Code: EXTRA20</div>,
361
378
  Delivery: (
379
+ <div>
380
+ <div>Free Shipping</div>
362
381
  <div>
363
- <div>Free Shipping</div>
364
- <div>
365
- Delivery Estimate
366
- <p>Order now for delivery Aug 26 - Aug 28 to ZIP code: 80201</p>
367
- </div>
382
+ Delivery Estimate
383
+ <p>Order now for delivery Aug 26 - Aug 28 to ZIP code: 80201</p>
368
384
  </div>
385
+ </div>
369
386
  ),
370
387
  Returns: (
371
- <div>
372
- Final-sale items, identified by a price ending in .99 or .97, cannot
373
- be canceled or returned.
374
- </div>
388
+ <div>
389
+ Final-sale items, identified by a price ending in .99 or .97, cannot
390
+ be canceled or returned.
391
+ </div>
375
392
  ),
376
393
  },
377
394
  table: { defaultValue: { summary: 'null' } },
@@ -440,7 +457,6 @@ export const CartItem: Story = {
440
457
  discount: 'none' as any,
441
458
  savings: 'none' as any,
442
459
  actions: 'Button' as any,
443
- footer: null as any,
444
460
  warning: 'none' as any,
445
461
  alert: 'none' as any,
446
462
  loading: false,
@@ -453,48 +469,48 @@ export const CartItem: Story = {
453
469
  },
454
470
  play: async () => {
455
471
  const canvasElement = document.querySelector(
456
- '#storybook-root'
472
+ '#storybook-root'
457
473
  ) as HTMLElement;
458
474
  const canvas = within(canvasElement);
459
475
 
460
476
  const itemImage = document.querySelector(
461
- '.dropin-cart-item__image'
477
+ '.dropin-cart-item__image'
462
478
  ) as HTMLElement;
463
479
  expect(itemImage).toBeVisible();
464
480
  const itemTitle = document.querySelector(
465
- '.dropin-cart-item__title'
481
+ '.dropin-cart-item__title'
466
482
  ) as HTMLElement;
467
483
  expect(itemTitle).toBeVisible();
468
484
  const itemSku = document.querySelector(
469
- '.dropin-cart-item__sku'
485
+ '.dropin-cart-item__sku'
470
486
  ) as HTMLElement;
471
487
  expect(itemSku).toBeVisible();
472
488
  const itemConfigurations = document.querySelector(
473
- '.dropin-cart-item__configurations'
489
+ '.dropin-cart-item__configurations'
474
490
  ) as HTMLElement;
475
491
  expect(itemConfigurations).toBeVisible();
476
492
  const itemPrice = document.querySelector(
477
- '.dropin-cart-item__price'
493
+ '.dropin-cart-item__price'
478
494
  ) as HTMLElement;
479
495
  expect(itemPrice).toBeVisible();
480
496
  const quantityStepper = document.querySelector(
481
- '.dropin-cart-item__quantity'
497
+ '.dropin-cart-item__quantity'
482
498
  ) as HTMLElement;
483
499
  expect(quantityStepper).toBeVisible();
484
500
  const itemTotal = document.querySelector(
485
- '.dropin-cart-item__total'
501
+ '.dropin-cart-item__total'
486
502
  ) as HTMLElement;
487
503
  expect(itemTotal).toBeVisible();
488
504
  const actions = document.querySelector(
489
- '.dropin-cart-item__buttons'
505
+ '.dropin-cart-item__buttons'
490
506
  ) as HTMLElement;
491
507
  expect(actions).toBeVisible();
492
508
 
493
509
  const increaseButton = document.querySelector(
494
- 'button[aria-label="Increase Quantity"]'
510
+ 'button[aria-label="Increase Quantity"]'
495
511
  ) as HTMLElement;
496
512
  const decreaseButton = document.querySelector(
497
- 'button[aria-label="Decrease Quantity"]'
513
+ 'button[aria-label="Decrease Quantity"]'
498
514
  ) as HTMLElement;
499
515
 
500
516
  // Without this wait test failing intermittently as click event is triggering before even element fully loaded
@@ -542,7 +558,6 @@ export const ReadOnly: Story = {
542
558
  attributes: 'none' as any,
543
559
  quantity: 1,
544
560
  description: 'Description' as any,
545
- footer: null as any,
546
561
  warning: 'none' as any,
547
562
  alert: 'none' as any,
548
563
  discount: 'none' as any,
@@ -608,7 +623,6 @@ export const DropdownQuantity: Story = {
608
623
  description: 'Description' as any,
609
624
  discount: 'none' as any,
610
625
  savings: 'none' as any,
611
- footer: null as any,
612
626
  warning: 'none' as any,
613
627
  alert: 'none' as any,
614
628
  loading: false,
@@ -626,3 +640,34 @@ export const DropdownQuantity: Story = {
626
640
  quantityType: 'dropdown',
627
641
  },
628
642
  };
643
+
644
+ export const WithPromotionalBadge: Story = {
645
+ args: {
646
+ ariaLabel: 'Short Name',
647
+ image: 'Image' as any,
648
+ title: 'Short' as any,
649
+ price: 'Price' as any,
650
+ rowTotalFooter: 'SuperOffer' as any,
651
+ total: 'Final' as any,
652
+ sku: 'Sku' as any,
653
+ quantity: 1,
654
+ warning: 'none' as any,
655
+ alert: 'none' as any,
656
+ discount: 'none' as any,
657
+ savings: 'none' as any,
658
+ loading: false,
659
+ updating: false,
660
+ configurations: {
661
+ Color: 'Blue',
662
+ Size: 'L',
663
+ },
664
+ },
665
+ play: async ({ canvasElement }) => {
666
+ // Verify row total footer is rendered below the total
667
+ const rowTotalFooter = canvasElement.querySelector(
668
+ '.dropin-cart-item__row-total-footer'
669
+ ) as HTMLElement;
670
+ expect(rowTotalFooter).toBeTruthy();
671
+ expect(rowTotalFooter?.textContent).toContain('Super offer');
672
+ },
673
+ };
@@ -30,6 +30,7 @@ export interface CartItemProps
30
30
  image?: VNode;
31
31
  title?: VNode;
32
32
  price?: VNode;
33
+ rowTotalFooter?: VNode;
33
34
  taxIncluded?: boolean;
34
35
  taxExcluded?: boolean;
35
36
  total?: VNode;
@@ -62,6 +63,7 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
62
63
  image,
63
64
  title,
64
65
  price,
66
+ rowTotalFooter,
65
67
  taxIncluded = false,
66
68
  taxExcluded = false,
67
69
  total,
@@ -434,6 +436,14 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
434
436
  className={classes(['dropin-cart-item__savings'])}
435
437
  />
436
438
  )}
439
+
440
+ {/* Row Total Footer */}
441
+ {rowTotalFooter && (
442
+ <VComponent
443
+ node={rowTotalFooter}
444
+ className={classes(['dropin-cart-item__row-total-footer'])}
445
+ />
446
+ )}
437
447
  </div>
438
448
 
439
449
  {/* Footer */}
@@ -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
  // https://storybook.js.org/docs/7.0/preact/writing-stories/introduction
@@ -67,6 +67,12 @@ export const CartList: Story = {
67
67
  tags: ['skip'],
68
68
  };
69
69
 
70
+ export const WithPromotionalBadges: Story = {
71
+ args: {
72
+ children: renderItemsWithPromos(3),
73
+ },
74
+ };
75
+
70
76
  function renderItems(count: number) {
71
77
  return Array.from({ length: count }, (_, i) => i + 1).map((key) => (
72
78
  <CartItem
@@ -109,3 +115,60 @@ function renderItems(count: number) {
109
115
  />
110
116
  ));
111
117
  }
118
+
119
+ function renderItemsWithPromos(count: number) {
120
+ return Array.from({ length: count }, (_, i) => i + 1).map((key) => {
121
+ // Show promo badge on first and third items as an example
122
+ const showPromo = key === 1 || key === 3;
123
+
124
+ return (
125
+ <CartItem
126
+ key={key}
127
+ image={
128
+ <Image
129
+ src="https://picsum.photos/132/184"
130
+ width="132"
131
+ height="184"
132
+ alt="Some alternative text"
133
+ loading="lazy"
134
+ />
135
+ }
136
+ title={<div>Product Name {key}</div>}
137
+ description={
138
+ <div>
139
+ Secondary product information such as brand name, description, etc.
140
+ </div>
141
+ }
142
+ sku={<div>SKU: 59YK{key}</div>}
143
+ quantity={1}
144
+ price={
145
+ <span>
146
+ <Price
147
+ amount={53.99}
148
+ style={{ fontWeight: 'inherit', color: 'inherit' }}
149
+ />
150
+ </span>
151
+ }
152
+ rowTotalFooter={
153
+ showPromo ? (
154
+ <div
155
+ style={{ color: 'var(--color-alert-800)', fontWeight: 'bold' }}
156
+ >
157
+ Super offer
158
+ </div>
159
+ ) : undefined
160
+ }
161
+ total={
162
+ <span>
163
+ <Price amount={59.98} variant="strikethrough" />{' '}
164
+ <Price amount={55.95} sale />
165
+ </span>
166
+ }
167
+ configurations={{ Color: 'Blue', Size: 'L' }}
168
+ onRemove={() => {}}
169
+ onQuantity={() => {}}
170
+ taxIncluded={true}
171
+ />
172
+ );
173
+ });
174
+ }
@@ -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
  // https://storybook.js.org/docs/7.0/preact/writing-stories/introduction
@@ -316,3 +316,21 @@ export const MandatoryFieldFloatingLabelWithValue: Story = {
316
316
  ],
317
317
  },
318
318
  };
319
+
320
+ export const SingleOption: Story = {
321
+ name: 'Single option',
322
+ args: {
323
+ name: 'pickerField',
324
+ variant: 'primary',
325
+ defaultOption: {
326
+ value: 'option1',
327
+ text: 'Only Option',
328
+ },
329
+ options: [
330
+ {
331
+ value: 'option1',
332
+ text: 'Only Option',
333
+ },
334
+ ],
335
+ },
336
+ };
@@ -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 { Icon } from '@adobe-commerce/elsie/components';
@@ -74,6 +74,7 @@ export const Picker: FunctionComponent<PickerProps> = ({
74
74
  }) => {
75
75
  const uniqueId = id ?? name ?? `dropin-picker-${Math.random().toString(36)}`;
76
76
  const isRequired = !!props?.required;
77
+ const isDisabled = disabled || options?.length === 1;
77
78
 
78
79
  // find the first option that is not disabled
79
80
  const firstAvailableOption = options?.find((option) => !option.disabled);
@@ -143,7 +144,7 @@ export const Picker: FunctionComponent<PickerProps> = ({
143
144
  ['dropin-picker__floating', !!floatingLabel],
144
145
  ['dropin-picker__selected', isSelected],
145
146
  ['dropin-picker__error', error],
146
- ['dropin-picker__disabled', disabled],
147
+ ['dropin-picker__disabled', isDisabled],
147
148
  ['dropin-picker__icon', icon],
148
149
  ])}
149
150
  >
@@ -165,7 +166,7 @@ export const Picker: FunctionComponent<PickerProps> = ({
165
166
  ])}
166
167
  name={name}
167
168
  aria-label={name}
168
- disabled={disabled}
169
+ disabled={isDisabled}
169
170
  onChange={handleOptionClick}
170
171
  {...props}
171
172
  >
@@ -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
  /* https://cssguidelin.es/#bem-like-naming */
@@ -64,6 +64,11 @@
64
64
  box-shadow: 0 0 0 var(--shape-icon-stroke-4) var(--color-neutral-400);
65
65
  }
66
66
 
67
+ .dropin-radio-button__icon {
68
+ margin-right: var(--spacing-xsmall);
69
+ flex-shrink: 0;
70
+ }
71
+
67
72
  .dropin-radio-button__description {
68
73
  clear: both;
69
74
  color: var(--color-neutral-700);
@@ -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
  // https://storybook.js.org/docs/7.0/preact/writing-stories/introduction
@@ -14,6 +14,7 @@ import {
14
14
  RadioButtonProps,
15
15
  } from '@adobe-commerce/elsie/components/RadioButton';
16
16
  import { expect, userEvent, within } from '@storybook/test';
17
+ import { IconsList } from '@adobe-commerce/elsie/components/Icon/Icon.stories.helpers';
17
18
 
18
19
  /**
19
20
  * Use Radio Buttons to let users select one option from a set of mutually exclusive choices.
@@ -91,6 +92,15 @@ const meta: Meta<RadioButtonProps> = {
91
92
  name: 'boolean',
92
93
  },
93
94
  },
95
+ icon: {
96
+ description:
97
+ 'Optional icon to display before the label (SVG or img element)',
98
+ options: Object.keys(IconsList),
99
+ mapping: IconsList,
100
+ control: {
101
+ type: 'select',
102
+ },
103
+ },
94
104
  },
95
105
  };
96
106
 
@@ -124,3 +134,27 @@ export const RadioButtonStory: Story = {
124
134
  await expect(radioButton).toBeChecked();
125
135
  },
126
136
  };
137
+
138
+ export const RadioButtonWithIcon: Story = {
139
+ name: 'Radio button with icon',
140
+ args: {
141
+ name: 'shipping',
142
+ label: 'Free Shipping',
143
+ value: 'free-shipping',
144
+ description: 'Delivery in 5-7 business days',
145
+ size: 'medium',
146
+ disabled: false,
147
+ error: false,
148
+ // @ts-ignore - icon is mapped from IconsList
149
+ icon: 'Delivery',
150
+ },
151
+ play: async ({ canvasElement }) => {
152
+ const canvas = within(canvasElement);
153
+ const radioButton = await canvas.findByRole('radio');
154
+ const radioButtonContainer = radioButton.closest('.dropin-radio-button');
155
+ const icon = radioButtonContainer?.querySelector(
156
+ '.dropin-radio-button__icon'
157
+ );
158
+ await expect(icon).toBeInTheDocument();
159
+ },
160
+ };
@@ -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 { FunctionComponent, VNode } from 'preact';
@@ -13,7 +13,7 @@ import { classes } from '@adobe-commerce/elsie/lib';
13
13
  import '@adobe-commerce/elsie/components/RadioButton/RadioButton.css';
14
14
 
15
15
  export interface RadioButtonProps
16
- extends Omit<HTMLAttributes<HTMLInputElement>, 'size' | 'label'> {
16
+ extends Omit<HTMLAttributes<HTMLInputElement>, 'size' | 'label' | 'icon'> {
17
17
  label: string | VNode<HTMLAttributes<HTMLElement>>;
18
18
  name: string;
19
19
  value: string;
@@ -23,6 +23,9 @@ export interface RadioButtonProps
23
23
  error?: boolean;
24
24
  description?: string;
25
25
  busy?: boolean;
26
+ icon?:
27
+ | VNode<HTMLAttributes<SVGSVGElement>>
28
+ | VNode<HTMLAttributes<HTMLImageElement>>;
26
29
  }
27
30
 
28
31
  export const RadioButton: FunctionComponent<RadioButtonProps> = ({
@@ -35,6 +38,7 @@ export const RadioButton: FunctionComponent<RadioButtonProps> = ({
35
38
  error = false,
36
39
  description = '',
37
40
  busy = false,
41
+ icon,
38
42
  className,
39
43
  children,
40
44
  ...props
@@ -70,6 +74,16 @@ export const RadioButton: FunctionComponent<RadioButtonProps> = ({
70
74
  ['dropin-radio-button__label--disabled', disabled],
71
75
  ])}
72
76
  >
77
+ {icon && (
78
+ // @ts-ignore
79
+ <icon.type
80
+ {...icon?.props}
81
+ className={classes([
82
+ 'dropin-radio-button__icon',
83
+ icon?.props?.className,
84
+ ])}
85
+ />
86
+ )}
73
87
  {label}
74
88
  </span>
75
89
  <span
@@ -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/RadioButton/RadioButton';
@@ -9,8 +9,12 @@
9
9
 
10
10
  export const debounce = (fn: Function, ms: number) => {
11
11
  let timeoutId: ReturnType<typeof setTimeout>;
12
- return function (this: any, ...args: any[]) {
12
+ const debouncedFn = function (this: any, ...args: any[]) {
13
13
  clearTimeout(timeoutId);
14
14
  timeoutId = setTimeout(() => fn.apply(this, args), ms);
15
15
  };
16
+ debouncedFn.cancel = () => {
17
+ clearTimeout(timeoutId);
18
+ };
19
+ return debouncedFn;
16
20
  };
@@ -0,0 +1,84 @@
1
+ /********************************************************************
2
+ * Copyright 2026 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 interface Extension {
11
+ id: string;
12
+ name: string;
13
+ externalScripts?: string[];
14
+ externalStyles?: string[];
15
+ hooks?: Record<
16
+ string,
17
+ (payload: { context: Record<string, unknown> }) => Promise<void> | void
18
+ >;
19
+ }
20
+
21
+ export interface ExtensionManager {
22
+ executeHook(
23
+ hookName: string,
24
+ context?: Record<string, unknown>,
25
+ ): Promise<void>;
26
+ }
27
+
28
+ export function loadScript(src: string): Promise<void> {
29
+ return new Promise((resolve, reject) => {
30
+ if (document.querySelector(`script[src="${src}"]`)) {
31
+ resolve();
32
+ return;
33
+ }
34
+
35
+ const script = document.createElement('script');
36
+ script.src = src;
37
+ script.onload = () => resolve();
38
+ script.onerror = () =>
39
+ reject(new Error(`Failed to load script: ${src}`));
40
+ document.head.appendChild(script);
41
+ });
42
+ }
43
+
44
+ export function loadStyle(href: string): Promise<void> {
45
+ return new Promise((resolve) => {
46
+ if (document.querySelector(`link[href="${href}"]`)) {
47
+ resolve();
48
+ return;
49
+ }
50
+
51
+ const link = document.createElement('link');
52
+ link.rel = 'stylesheet';
53
+ link.href = href;
54
+ link.onload = () => resolve();
55
+ document.head.appendChild(link);
56
+ });
57
+ }
58
+
59
+ export async function createExtensionManager(
60
+ extensions: Extension[],
61
+ ): Promise<ExtensionManager> {
62
+ const scripts = extensions.flatMap((ext) => ext.externalScripts || []);
63
+ const styles = extensions.flatMap((ext) => ext.externalStyles || []);
64
+
65
+ await Promise.all([...scripts.map(loadScript), ...styles.map(loadStyle)]);
66
+
67
+ return {
68
+ async executeHook(hookName: string, context: Record<string, unknown> = {}) {
69
+ for (const ext of extensions) {
70
+ if (ext.hooks?.[hookName]) {
71
+ try {
72
+ await ext.hooks[hookName]({ context });
73
+ } catch (error) {
74
+ console.error(
75
+ `[ExtensionManager] Error in hook "${hookName}" for extension "${ext.name}":`,
76
+ error,
77
+ );
78
+ throw error;
79
+ }
80
+ }
81
+ }
82
+ },
83
+ };
84
+ }
@@ -53,7 +53,7 @@ export function getPriceFormatter(
53
53
 
54
54
  const params: Intl.NumberFormatOptions = {
55
55
  style: 'currency',
56
- currency: currency || 'USD',
56
+ currency: currency && currency !== 'NONE' ? currency : 'USD',
57
57
  // These options are needed to round to whole numbers if that's what you want.
58
58
  minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
59
59
  maximumFractionDigits: 2, // (causes 2500.99 to be printed as $2,501)
package/src/lib/index.ts CHANGED
@@ -26,3 +26,4 @@ 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
28
  export * from '@adobe-commerce/elsie/lib/get-price-formatter';
29
+ export * from '@adobe-commerce/elsie/lib/extension-manager';