@adobe-commerce/elsie 1.5.3 → 1.6.0-alpha10

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.5.3",
3
+ "version": "1.6.0-alpha10",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -8,7 +8,7 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import '@adobe-commerce/elsie/components/Field/Field.css';
11
- import { classes } from '@adobe-commerce/elsie/lib';
11
+ import { classes, VComponent } from '@adobe-commerce/elsie/lib';
12
12
  import { FunctionComponent, VNode } from 'preact';
13
13
  import { HTMLAttributes } from 'preact/compat';
14
14
 
@@ -36,8 +36,23 @@ export const Field: FunctionComponent<FieldProps> = ({
36
36
  }) => {
37
37
  const id =
38
38
  children?.props?.id ?? `dropin-field-${Math.random().toString(36)}`;
39
- const ChildComponent =
40
- children && typeof children.type !== 'string' ? children.type : null;
39
+
40
+ let fieldContent: VNode | string | null = null;
41
+
42
+ if (children && typeof children !== 'string') {
43
+ fieldContent = (
44
+ <VComponent
45
+ node={children}
46
+ id={id}
47
+ key={children.key}
48
+ disabled={disabled}
49
+ size={size}
50
+ error={!!error}
51
+ success={!!success && !error}
52
+ />
53
+ );
54
+ }
55
+
41
56
 
42
57
  return (
43
58
  <div {...props} className={classes(['dropin-field', className])}>
@@ -55,17 +70,7 @@ export const Field: FunctionComponent<FieldProps> = ({
55
70
  )}
56
71
 
57
72
  <div className={classes(['dropin-field__content'])}>
58
- {ChildComponent && children && (
59
- <ChildComponent
60
- {...children.props}
61
- id={id}
62
- key={children.key}
63
- disabled={disabled}
64
- size={size}
65
- error={!!error}
66
- success={!!success && !error}
67
- />
68
- )}
73
+ {fieldContent}
69
74
  </div>
70
75
 
71
76
  <div
@@ -18,52 +18,57 @@ export type IconType = keyof typeof import('@adobe-commerce/elsie/icons');
18
18
 
19
19
  const lazyIcons = {
20
20
  Add: lazy(() => import('@adobe-commerce/elsie/icons/Add.svg')),
21
+ AddressBook: lazy(() => import('@adobe-commerce/elsie/icons/AddressBook.svg')),
21
22
  Bulk: lazy(() => import('@adobe-commerce/elsie/icons/Bulk.svg')),
22
23
  Burger: lazy(() => import('@adobe-commerce/elsie/icons/Burger.svg')),
24
+ Business: lazy(() => import('@adobe-commerce/elsie/icons/Business.svg')),
25
+ Card: lazy(() => import('@adobe-commerce/elsie/icons/Card.svg')),
23
26
  Cart: lazy(() => import('@adobe-commerce/elsie/icons/Cart.svg')),
24
27
  Check: lazy(() => import('@adobe-commerce/elsie/icons/Check.svg')),
28
+ CheckWithCircle: lazy(() => import('@adobe-commerce/elsie/icons/CheckWithCircle.svg')),
25
29
  ChevronDown: lazy(() => import('@adobe-commerce/elsie/icons/ChevronDown.svg')),
26
- ChevronUp: lazy(() => import('@adobe-commerce/elsie/icons/ChevronUp.svg')),
27
30
  ChevronRight: lazy(() => import('@adobe-commerce/elsie/icons/ChevronRight.svg')),
31
+ ChevronUp: lazy(() => import('@adobe-commerce/elsie/icons/ChevronUp.svg')),
28
32
  Close: lazy(() => import('@adobe-commerce/elsie/icons/Close.svg')),
33
+ Coupon: lazy(() => import('@adobe-commerce/elsie/icons/Coupon.svg')),
34
+ Date: lazy(() => import('@adobe-commerce/elsie/icons/Date.svg')),
35
+ Delivery: lazy(() => import('@adobe-commerce/elsie/icons/Delivery.svg')),
36
+ Edit: lazy(() => import('@adobe-commerce/elsie/icons/Edit.svg')),
37
+ EmptyBox: lazy(() => import('@adobe-commerce/elsie/icons/EmptyBox.svg')),
38
+ Eye: lazy(() => import('@adobe-commerce/elsie/icons/Eye.svg')),
39
+ EyeClose: lazy(() => import('@adobe-commerce/elsie/icons/EyeClose.svg')),
40
+ Gift: lazy(() => import('@adobe-commerce/elsie/icons/Gift.svg')),
41
+ GiftCard: lazy(() => import('@adobe-commerce/elsie/icons/GiftCard.svg')),
29
42
  Heart: lazy(() => import('@adobe-commerce/elsie/icons/Heart.svg')),
43
+ HeartFilled: lazy(() => import('@adobe-commerce/elsie/icons/HeartFilled.svg')),
44
+ InfoFilled: lazy(() => import('@adobe-commerce/elsie/icons/InfoFilled.svg')),
45
+ List: lazy(() => import('@adobe-commerce/elsie/icons/List.svg')),
46
+ Locker: lazy(() => import('@adobe-commerce/elsie/icons/Locker.svg')),
30
47
  Minus: lazy(() => import('@adobe-commerce/elsie/icons/Minus.svg')),
48
+ Order: lazy(() => import('@adobe-commerce/elsie/icons/Order.svg')),
49
+ OrderError: lazy(() => import('@adobe-commerce/elsie/icons/OrderError.svg')),
50
+ OrderSuccess: lazy(() => import('@adobe-commerce/elsie/icons/OrderSuccess.svg')),
51
+ PaymentError: lazy(() => import('@adobe-commerce/elsie/icons/PaymentError.svg')),
31
52
  Placeholder: lazy(() => import('@adobe-commerce/elsie/icons/Placeholder.svg')),
32
53
  PlaceholderFilled: lazy(
33
54
  () => import('@adobe-commerce/elsie/icons/PlaceholderFilled.svg')
34
55
  ),
56
+ Quote: lazy(() => import('@adobe-commerce/elsie/icons/Quote.svg')),
35
57
  Search: lazy(() => import('@adobe-commerce/elsie/icons/Search.svg')),
36
58
  SearchFilled: lazy(() => import('@adobe-commerce/elsie/icons/SearchFilled.svg')),
37
59
  Sort: lazy(() => import('@adobe-commerce/elsie/icons/Sort.svg')),
38
60
  Star: lazy(() => import('@adobe-commerce/elsie/icons/Star.svg')),
39
- View: lazy(() => import('@adobe-commerce/elsie/icons/View.svg')),
61
+ Structure: lazy(() => import('@adobe-commerce/elsie/icons/Structure.svg')),
62
+ Team: lazy(() => import('@adobe-commerce/elsie/icons/Team.svg')),
63
+ Trash: lazy(() => import('@adobe-commerce/elsie/icons/Trash.svg')),
40
64
  User: lazy(() => import('@adobe-commerce/elsie/icons/User.svg')),
41
- Warning: lazy(() => import('@adobe-commerce/elsie/icons/Warning.svg')),
42
- Locker: lazy(() => import('@adobe-commerce/elsie/icons/Locker.svg')),
65
+ View: lazy(() => import('@adobe-commerce/elsie/icons/View.svg')),
43
66
  Wallet: lazy(() => import('@adobe-commerce/elsie/icons/Wallet.svg')),
44
- Card: lazy(() => import('@adobe-commerce/elsie/icons/Card.svg')),
45
- Order: lazy(() => import('@adobe-commerce/elsie/icons/Order.svg')),
46
- Delivery: lazy(() => import('@adobe-commerce/elsie/icons/Delivery.svg')),
47
- OrderError: lazy(() => import('@adobe-commerce/elsie/icons/OrderError.svg')),
48
- OrderSuccess: lazy(() => import('@adobe-commerce/elsie/icons/OrderSuccess.svg')),
49
- PaymentError: lazy(() => import('@adobe-commerce/elsie/icons/PaymentError.svg')),
50
- CheckWithCircle: lazy(() => import('@adobe-commerce/elsie/icons/CheckWithCircle.svg')),
67
+ Warning: lazy(() => import('@adobe-commerce/elsie/icons/Warning.svg')),
68
+ WarningFilled: lazy(() => import('@adobe-commerce/elsie/icons/WarningFilled.svg')),
51
69
  WarningWithCircle: lazy(
52
70
  () => import('@adobe-commerce/elsie/icons/WarningWithCircle.svg')
53
71
  ),
54
- WarningFilled: lazy(() => import('@adobe-commerce/elsie/icons/WarningFilled.svg')),
55
- InfoFilled: lazy(() => import('@adobe-commerce/elsie/icons/InfoFilled.svg')),
56
- HeartFilled: lazy(() => import('@adobe-commerce/elsie/icons/HeartFilled.svg')),
57
- Trash: lazy(() => import('@adobe-commerce/elsie/icons/Trash.svg')),
58
- Eye: lazy(() => import('@adobe-commerce/elsie/icons/Eye.svg')),
59
- EyeClose: lazy(() => import('@adobe-commerce/elsie/icons/EyeClose.svg')),
60
- Date: lazy(() => import('@adobe-commerce/elsie/icons/Date.svg')),
61
- AddressBook: lazy(() => import('@adobe-commerce/elsie/icons/AddressBook.svg')),
62
- EmptyBox: lazy(() => import('@adobe-commerce/elsie/icons/EmptyBox.svg')),
63
- Coupon: lazy(() => import('@adobe-commerce/elsie/icons/Coupon.svg')),
64
- Gift: lazy(() => import('@adobe-commerce/elsie/icons/Gift.svg')),
65
- GiftCard: lazy(() => import('@adobe-commerce/elsie/icons/GiftCard.svg')),
66
- Edit: lazy(() => import('@adobe-commerce/elsie/icons/Edit.svg')),
67
72
  };
68
73
 
69
74
  export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'size'> {
@@ -8,7 +8,7 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import type { Meta, StoryObj } from '@storybook/preact';
11
- import { expect, userEvent, within } from '@storybook/test';
11
+ import { expect, userEvent, waitFor, within } from '@storybook/test';
12
12
  import {
13
13
  MultiSelect,
14
14
  type MultiSelectProps,
@@ -430,11 +430,13 @@ export const BulkSelectionActions = {
430
430
  const selectAllButton = canvas.getByTestId('multi-select-select-all');
431
431
  await userEvent.click(selectAllButton);
432
432
 
433
- // All options should now be selected - check tags appear in the tags area
433
+ // Wait for all options to be selected - check tags appear in the tags area
434
434
  const tagsArea = canvas.getByTestId('multi-select-tags-area');
435
- await expect(tagsArea).toHaveTextContent('Option 1');
436
- await expect(tagsArea).toHaveTextContent('Option 2');
437
- await expect(tagsArea).toHaveTextContent('Option 10');
435
+ await waitFor(async () => {
436
+ await expect(tagsArea).toHaveTextContent('Option 1');
437
+ await expect(tagsArea).toHaveTextContent('Option 2');
438
+ await expect(tagsArea).toHaveTextContent('Option 10');
439
+ });
438
440
 
439
441
  // Deselect All should now be enabled
440
442
  await expect(deselectAllButton).not.toBeDisabled();
@@ -64,7 +64,8 @@
64
64
  align-items: center;
65
65
  }
66
66
 
67
- .dropin-pagination_list-item button {
67
+ .dropin-pagination_list-item button,
68
+ .dropin-pagination_list-item a {
68
69
  cursor: pointer;
69
70
  margin: 0;
70
71
  padding: 0;
@@ -72,24 +73,32 @@
72
73
  border: none;
73
74
  font: var(--type-details-caption-1-font);
74
75
  letter-spacing: var(--type-details-caption-1-letter-spacing);
76
+ text-decoration: none;
75
77
  }
76
78
 
77
- .dropin-pagination_list-item--active button {
79
+ .dropin-pagination_list-item--active button,
80
+ .dropin-pagination_list-item--active a {
78
81
  cursor: default;
79
82
  }
80
83
 
81
84
  .dropin-pagination_list-item--active button:disabled,
82
85
  .dropin-pagination_list-item--ellipsis button,
86
+ .dropin-pagination_list-item--ellipsis a,
83
87
  .dropin-pagination-arrow--backward:disabled,
84
- .dropin-pagination-arrow--forward:disabled {
88
+ .dropin-pagination-arrow--forward:disabled,
89
+ .dropin-pagination-arrow--disabled {
85
90
  cursor: default;
91
+ opacity: 0.5;
92
+ pointer-events: none;
86
93
  }
87
94
 
88
- .dropin-pagination button:not(:disabled) {
95
+ .dropin-pagination button:not(:disabled),
96
+ .dropin-pagination a:not([aria-disabled="true"]) {
89
97
  color: var(--color-neutral-800);
90
98
  }
91
99
 
92
- .dropin-pagination button {
100
+ .dropin-pagination button,
101
+ .dropin-pagination a {
93
102
  appearance: none;
94
103
  -webkit-appearance: none;
95
104
  }
@@ -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
@@ -16,6 +16,8 @@ import {
16
16
  } from '@adobe-commerce/elsie/components/Pagination';
17
17
  import { expect, userEvent, within } from '@storybook/test';
18
18
  import { useState } from 'preact/hooks';
19
+ import { action } from '@storybook/addon-actions';
20
+
19
21
 
20
22
  /**
21
23
  * A component that divides large content sets into multiple pages, allowing users to navigate through the data with controls like "Next," "Previous," or page numbers, enhancing performance and usability by limiting the number of items displayed per page.
@@ -40,6 +42,10 @@ const meta: Meta<PaginationProps> = {
40
42
  description:
41
43
  'Called when the page number is changed, and it takes the resulting page number.',
42
44
  },
45
+ routePage: {
46
+ description:
47
+ 'Optional function that generates hrefs for each page. When provided, pagination will render anchor tags instead of buttons, enabling proper SEO and link behavior.',
48
+ },
43
49
  },
44
50
  };
45
51
 
@@ -115,3 +121,26 @@ export const LongPagination: Story = {
115
121
  );
116
122
  },
117
123
  };
124
+
125
+ export const WithAnchors: Story = {
126
+ name: 'As anchor tags',
127
+ render: () => {
128
+ const [currentPage, setCurrentPage] = useState(1);
129
+ const totalPages = 10;
130
+
131
+ const handlePageChange = (newPage: number, e?: Event) => {
132
+ e?.preventDefault();
133
+ setCurrentPage(newPage);
134
+ action('pageChanged')(newPage, e);
135
+ };
136
+
137
+ return (
138
+ <Pagination
139
+ totalPages={totalPages}
140
+ currentPage={currentPage}
141
+ onChange={handlePageChange}
142
+ routePage={(page) => `?page=${page}`}
143
+ />
144
+ );
145
+ },
146
+ };
@@ -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 } from 'preact';
@@ -14,12 +14,14 @@ import { ChevronDown } from '@adobe-commerce/elsie/icons';
14
14
  import { Icon } from '@adobe-commerce/elsie/components/Icon';
15
15
  import { useText } from '@adobe-commerce/elsie/i18n';
16
16
  import '@adobe-commerce/elsie/components/Pagination/Pagination.css';
17
+ import { PaginationButton } from './PaginationButton';
17
18
 
18
19
  export interface PaginationProps {
19
20
  className?: string;
20
21
  currentPage?: number;
21
22
  totalPages?: number;
22
- onChange?: (currentPage: number) => void;
23
+ onChange?: (currentPage: number, e?: Event) => void;
24
+ routePage?: (page: number) => string;
23
25
  }
24
26
 
25
27
  export type PaginationList = {
@@ -32,6 +34,7 @@ export const Pagination: FunctionComponent<PaginationProps> = ({
32
34
  totalPages = 10,
33
35
  currentPage = 1,
34
36
  onChange,
37
+ routePage,
35
38
  className,
36
39
  ...props
37
40
  }) => {
@@ -40,22 +43,22 @@ export const Pagination: FunctionComponent<PaginationProps> = ({
40
43
  forwardButton: 'Dropin.Pagination.forwardButton.ariaLabel',
41
44
  });
42
45
 
43
- const handleForward = useCallback(() => {
46
+ const handleForward = useCallback((e?: Event) => {
44
47
  const nextPage = Math.min(currentPage + 1, totalPages);
45
48
 
46
- onChange?.(nextPage);
49
+ onChange?.(nextPage, e);
47
50
  }, [currentPage, onChange, totalPages]);
48
51
 
49
- const handleBackward = useCallback(() => {
52
+ const handleBackward = useCallback((e?: Event) => {
50
53
  const prevPage = Math.max(currentPage - 1, 1);
51
54
 
52
- onChange?.(prevPage);
55
+ onChange?.(prevPage, e);
53
56
  }, [currentPage, onChange]);
54
57
 
55
58
  const handleSetPage = useCallback(
56
- (currentPage: number | string) => {
59
+ (currentPage: number | string, e?: Event) => {
57
60
  if (isNumber(currentPage)) {
58
- onChange?.(currentPage as number);
61
+ onChange?.(currentPage as number, e);
59
62
  }
60
63
  },
61
64
  [onChange]
@@ -97,19 +100,20 @@ export const Pagination: FunctionComponent<PaginationProps> = ({
97
100
 
98
101
  return (
99
102
  <div {...props} className={classes(['dropin-pagination', className])}>
100
- <button
101
- type="button"
103
+ <PaginationButton
102
104
  data-testid="prev-button"
103
105
  aria-label={translations.backwardButton}
104
106
  disabled={currentPage === 1}
105
- onClick={handleBackward}
107
+ onClick={(e: Event) => handleBackward(e)}
108
+ href={routePage?.(currentPage) ?? undefined}
106
109
  className={classes([
107
110
  'dropin-pagination-arrow',
108
111
  'dropin-pagination-arrow--backward',
112
+ ['dropin-pagination-arrow--disabled', currentPage === 1],
109
113
  ])}
110
114
  >
111
115
  <Icon size="24" source={ChevronDown} />
112
- </button>
116
+ </PaginationButton>
113
117
  <ul className="dropin-pagination_list">
114
118
  {(paginationList as PaginationList[]).map((item, index) => (
115
119
  <li
@@ -121,29 +125,31 @@ export const Pagination: FunctionComponent<PaginationProps> = ({
121
125
  [`dropin-pagination_list-item--active`, item.isActive],
122
126
  ])}
123
127
  >
124
- <button
125
- type="button"
128
+ <PaginationButton
126
129
  data-testid={`set-page-button-${item.page}`}
127
- onClick={() => handleSetPage(item.page)}
130
+ onClick={(e: Event) => handleSetPage(item.page, e)}
131
+ href={routePage?.(item.page as number) ?? undefined}
132
+
128
133
  >
129
134
  {item.label}
130
- </button>
135
+ </PaginationButton>
131
136
  </li>
132
137
  ))}
133
138
  </ul>
134
- <button
135
- type="button"
139
+ <PaginationButton
136
140
  data-testid="next-button"
137
141
  aria-label={translations.forwardButton}
138
142
  disabled={currentPage === totalPages}
139
- onClick={handleForward}
143
+ onClick={(e: Event) => handleForward(e)}
144
+ href={routePage?.(currentPage) ?? undefined}
140
145
  className={classes([
141
146
  'dropin-pagination-arrow',
142
147
  'dropin-pagination-arrow--forward',
148
+ ['dropin-pagination-arrow--disabled', currentPage === totalPages],
143
149
  ])}
144
150
  >
145
151
  <Icon size="24" source={ChevronDown} />
146
- </button>
152
+ </PaginationButton>
147
153
  </div>
148
154
  );
149
155
  };
@@ -0,0 +1,46 @@
1
+ /********************************************************************
2
+ * Copyright 2024 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, JSX } from 'preact';
11
+
12
+ type BaseProps = {
13
+ href?: string;
14
+ type?: 'button';
15
+ disabled?: boolean;
16
+ };
17
+
18
+ export type PaginationButtonProps = BaseProps &
19
+ (
20
+ | Omit<JSX.HTMLAttributes<HTMLAnchorElement>, 'type'>
21
+ | Omit<JSX.HTMLAttributes<HTMLButtonElement>, 'href'>
22
+ );
23
+
24
+ export const PaginationButton: FunctionComponent<PaginationButtonProps> = (
25
+ props
26
+ ) => {
27
+ if (props.href) {
28
+
29
+ const { href, disabled, ...anchorProps } = props;
30
+ return (
31
+ <a
32
+ href={href}
33
+ aria-disabled={disabled}
34
+ {...(anchorProps as JSX.HTMLAttributes<HTMLAnchorElement>)}
35
+ />
36
+ );
37
+ }
38
+
39
+ const { type = 'button', ...buttonProps } = props;
40
+ return (
41
+ <button
42
+ type={type}
43
+ {...(buttonProps as JSX.HTMLAttributes<HTMLButtonElement>)}
44
+ />
45
+ );
46
+ };
@@ -2,14 +2,14 @@
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 } from 'preact';
11
11
  import { HTMLAttributes, useMemo } from 'preact/compat';
12
- import { classes, getGlobalLocale } from '@adobe-commerce/elsie/lib';
12
+ import { classes, getPriceFormatter } from '@adobe-commerce/elsie/lib';
13
13
  import '@adobe-commerce/elsie/components/Price/Price.css';
14
14
 
15
15
  export interface PriceProps
@@ -39,43 +39,10 @@ export const Price: FunctionComponent<PriceProps> = ({
39
39
  size = 'small',
40
40
  ...props
41
41
  }) => {
42
- // Determine the locale to use: prop locale > global locale > browser locale
43
- const effectiveLocale = useMemo(() => {
44
- if (locale) {
45
- return locale;
46
- }
47
- const globalLocale = getGlobalLocale();
48
- if (globalLocale) {
49
- return globalLocale;
50
- }
51
- // Fallback to browser locale or default
52
- return process.env.LOCALE && process.env.LOCALE !== 'undefined' ? process.env.LOCALE : 'en-US';
53
- }, [locale]);
54
-
55
- const formatter = useMemo(
56
- () => {
57
- const params: Intl.NumberFormatOptions = {
58
- style: 'currency',
59
- currency: currency || 'USD',
60
- // These options are needed to round to whole numbers if that's what you want.
61
- minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
62
- maximumFractionDigits: 2, // (causes 2500.99 to be printed as $2,501)
63
- ...formatOptions,
64
- }
65
- try {
66
- return new Intl.NumberFormat(effectiveLocale, params);
67
- } catch (error) {
68
- console.error(`Error creating Intl.NumberFormat instance for locale ${effectiveLocale}. Falling back to en-US.`, error);
69
- return new Intl.NumberFormat('en-US', params);
70
- }
71
- },
72
- [effectiveLocale, currency, formatOptions]
73
- );
74
-
75
- const formattedAmount = useMemo(
76
- () => formatter.format(amount),
77
- [amount, formatter]
78
- );
42
+ const formattedAmount = useMemo(() => {
43
+ const formatter = getPriceFormatter({ currency, locale, formatOptions });
44
+ return formatter.format(amount);
45
+ }, [amount, currency, locale, formatOptions]);
79
46
 
80
47
  return (
81
48
  <span
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path vector-effect="non-scaling-stroke" d="M14 3.25C14.9665 3.25 15.75 4.0335 15.75 5V6C15.75 6.08497 15.7422 6.16827 15.7305 6.25H21C21.9665 6.25 22.75 7.0335 22.75 8V10.6807C22.7498 11.3681 22.3487 11.9754 21.75 12.2598V19C21.75 19.9665 20.9665 20.75 20 20.75H4C3.0335 20.75 2.25 19.9665 2.25 19V12.2598C1.65126 11.9754 1.25018 11.3681 1.25 10.6807V8C1.25 7.0335 2.0335 6.25 3 6.25H8.26953C8.25783 6.16827 8.25 6.08497 8.25 6V5C8.25 4.0335 9.0335 3.25 10 3.25H14ZM14.75 13.7148V14C14.75 14.9665 13.9665 15.75 13 15.75H11C10.0335 15.75 9.25 14.9665 9.25 14V13.7148L3.75 12.6143V19C3.75 19.1381 3.86193 19.25 4 19.25H20C20.1381 19.25 20.25 19.1381 20.25 19V12.6143L14.75 13.7148ZM11 12.75C10.8619 12.75 10.75 12.8619 10.75 13V14C10.75 14.1381 10.8619 14.25 11 14.25H13C13.1381 14.25 13.25 14.1381 13.25 14V13C13.25 12.8619 13.1381 12.75 13 12.75H11ZM3 7.75C2.86193 7.75 2.75 7.86193 2.75 8V10.6807C2.75022 10.7996 2.83447 10.9024 2.95117 10.9258L9.43262 12.2207C9.71926 11.6453 10.3135 11.25 11 11.25H13C13.6862 11.25 14.2786 11.6457 14.5654 12.2207L21.0488 10.9258C21.1655 10.9024 21.2498 10.7996 21.25 10.6807V8C21.25 7.86193 21.1381 7.75 21 7.75H3ZM10 4.75C9.86193 4.75 9.75 4.86193 9.75 5V6C9.75 6.13807 9.86193 6.25 10 6.25H14C14.1381 6.25 14.25 6.13807 14.25 6V5C14.25 4.86193 14.1381 4.75 14 4.75H10Z" fill="currentColor" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path vector-effect="non-scaling-stroke" d="M16.8569 2C18.5138 2 19.8569 3.34315 19.8569 5V19C19.8569 20.6569 18.5138 22 16.8569 22H7.14307L6.98877 21.9961C5.45484 21.9183 4.22469 20.6883 4.14697 19.1543L4.14307 19V5C4.14307 3.39498 5.40374 2.08434 6.98877 2.00391L7.14307 2H16.8569ZM7.14307 3.5C6.31475 3.50013 5.64307 4.17165 5.64307 5V19C5.64307 19.8283 6.31475 20.4999 7.14307 20.5H16.8569C17.6854 20.5 18.3569 19.8284 18.3569 19V5C18.3569 4.17157 17.6854 3.5 16.8569 3.5H7.14307ZM7.71436 15.5713C8.10871 15.5713 8.42897 15.8909 8.4292 16.2852C8.4292 16.6796 8.10884 17 7.71436 17C7.32006 16.9998 7.00049 16.6795 7.00049 16.2852C7.00071 15.891 7.3202 15.5715 7.71436 15.5713ZM16.2866 17H9.85791V15.5H16.2866V17ZM16.2866 12.7148H9.85791V11.2148H16.2866V12.7148ZM7.71436 11.2861C8.10884 11.2861 8.4292 11.6055 8.4292 12C8.42916 12.3945 8.10882 12.7139 7.71436 12.7139C7.32008 12.7136 7.00053 12.3943 7.00049 12C7.00049 11.6057 7.32006 11.2864 7.71436 11.2861ZM7.71436 7C8.10871 7 8.42897 7.31957 8.4292 7.71387C8.4292 8.10836 8.10884 8.42871 7.71436 8.42871C7.32006 8.42848 7.00049 8.10822 7.00049 7.71387C7.00071 7.31971 7.3202 7.00023 7.71436 7ZM16.2866 8.42871H9.85791V6.92871H16.2866V8.42871Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path vector-effect="non-scaling-stroke" d="M16.8569 2C18.5138 2 19.8569 3.34315 19.8569 5V19C19.8569 20.6569 18.5138 22 16.8569 22H7.14307L6.98877 21.9961C5.45473 21.9184 4.22469 20.6883 4.14697 19.1543L4.14307 19V5C4.14307 3.39489 5.40362 2.08421 6.98877 2.00391L7.14307 2H16.8569ZM7.14307 3.5C6.31464 3.5 5.64307 4.17157 5.64307 5V19C5.64307 19.8284 6.31464 20.5 7.14307 20.5H16.8569C17.6854 20.5 18.3569 19.8284 18.3569 19V5C18.3569 4.17157 17.6854 3.5 16.8569 3.5H7.14307ZM7.71436 17C8.10871 17 8.42897 17.3196 8.4292 17.7139C8.4292 18.1084 8.10884 18.4287 7.71436 18.4287C7.31993 18.4286 7.00049 18.1083 7.00049 17.7139C7.00071 17.3196 7.32007 17.0001 7.71436 17ZM16.2856 18.4287H9.85693V16.9287H16.2856V18.4287ZM7.71436 14.1426C8.10884 14.1426 8.4292 14.4629 8.4292 14.8574C8.42905 15.2518 8.10875 15.5713 7.71436 15.5713C7.32002 15.5712 7.00064 15.2517 7.00049 14.8574C7.00049 14.463 7.31993 14.1427 7.71436 14.1426ZM16.2856 15.5713H9.85693V14.0713H16.2856V15.5713ZM12.7856 5.6748C13.5717 5.96006 14.1792 6.66301 14.1792 7.57227H12.6792C12.6792 7.34883 12.4515 7.0362 12.0005 7.03613C11.5756 7.03613 11.3506 7.31331 11.3257 7.53223C11.333 7.54311 11.3434 7.56003 11.3608 7.58008C11.4311 7.66062 11.5467 7.75541 11.6958 7.85254C11.84 7.94645 11.9892 8.02578 12.105 8.08203C12.1617 8.10958 12.2084 8.13079 12.2397 8.14453C12.2553 8.15136 12.2679 8.15625 12.2749 8.15918C12.2781 8.16054 12.2807 8.16169 12.2817 8.16211L12.2837 8.16309C12.2851 8.16366 12.2875 8.16419 12.2896 8.16504C12.2938 8.16681 12.2993 8.16999 12.3062 8.17285C12.32 8.17866 12.3396 8.1861 12.3628 8.19629C12.4095 8.21678 12.4744 8.24686 12.5513 8.28418C12.7031 8.35794 12.911 8.46704 13.1235 8.60547C13.3312 8.74079 13.5734 8.92349 13.771 9.15039C13.963 9.37099 14.1792 9.71019 14.1792 10.1436C14.1789 11.0523 13.5713 11.753 12.7856 12.0381V12.7148H11.2856V12.0605C10.4664 11.7925 9.8221 11.0791 9.82178 10.1436H11.3218C11.3223 10.367 11.5499 10.6787 12.0005 10.6787C12.4257 10.6786 12.6498 10.4006 12.6743 10.1816C12.667 10.1709 12.657 10.1542 12.6401 10.1348C12.5699 10.0542 12.4543 9.95949 12.3052 9.8623C12.1609 9.76835 12.0118 9.68909 11.896 9.63281C11.839 9.60514 11.7916 9.58407 11.7603 9.57031L11.7192 9.55273H11.7183L11.7163 9.55176C11.715 9.55122 11.7133 9.55056 11.7114 9.5498C11.7071 9.54803 11.7008 9.5449 11.6938 9.54199C11.6801 9.53621 11.6611 9.5286 11.6382 9.51855C11.5915 9.49808 11.5265 9.46797 11.4497 9.43066C11.2978 9.35687 11.0892 9.24796 10.8765 9.10938C10.6688 8.97407 10.4266 8.7913 10.229 8.56445C10.0371 8.34398 9.82204 8.00515 9.82178 7.57227C9.82178 6.63623 10.466 5.92047 11.2856 5.65234L11.2866 5H12.7866L12.7856 5.6748Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,8 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect vector-effect="non-scaling-stroke" x="5" y="5.28076" width="14" height="1.5" rx="0.75" fill="currentColor"/>
3
+ <rect vector-effect="non-scaling-stroke" x="8.27539" y="11.2192" width="14" height="1.5" rx="0.75" fill="currentColor"/>
4
+ <rect vector-effect="non-scaling-stroke" x="8.44922" y="17.2192" width="14" height="1.5" rx="0.75" fill="currentColor"/>
5
+ <circle vector-effect="non-scaling-stroke" cx="2.30078" cy="6.03076" r="0.75" fill="currentColor"/>
6
+ <circle vector-effect="non-scaling-stroke" cx="5.67578" cy="11.9692" r="0.75" fill="currentColor"/>
7
+ <circle vector-effect="non-scaling-stroke" cx="5.67578" cy="17.9692" r="0.75" fill="currentColor"/>
8
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path vector-effect="non-scaling-stroke" d="M10.6047 10.011L10.4765 10.2749C10.4093 10.4774 10.2566 10.6307 10.0551 10.7044C9.43878 10.926 8.91201 11.1763 8.49707 11.4375M13.4262 10.2442L13.2674 10.0908C13.3957 10.3608 13.6094 10.5817 13.872 10.7228C14.3816 10.8972 14.9429 11.145 15.4124 11.4375M11.8262 10.2994C11.246 10.2994 10.7085 10.011 10.3849 9.52631L10.3665 9.49563L10.3482 9.45882C9.5848 8.29306 9.21837 6.90642 9.30998 5.50751C9.35273 4.12087 10.4826 3.01033 11.8628 3.00419H11.9483C13.3347 2.92443 14.5195 3.99202 14.5989 5.37866C14.5989 5.42775 14.5989 5.4707 14.5989 5.51978C14.6844 6.89415 14.3302 8.26238 13.5851 9.41587L13.5362 9.4895C13.2003 9.98648 12.6384 10.281 12.046 10.2871H11.9483C11.9056 10.2871 11.8628 10.2933 11.8201 10.2933L11.8262 10.2994Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
3
+ <path vector-effect="non-scaling-stroke" d="M1.125 20.7002C1.14332 20.0069 1.21661 19.3136 1.33875 18.6325C1.47312 18.0128 2.43807 17.3134 3.92214 16.7796C4.12368 16.7059 4.27636 16.5525 4.34354 16.3501L4.47179 16.0862M7.29326 16.3194L7.13447 16.166C7.26272 16.436 7.47648 16.6569 7.73909 16.798C8.83229 17.1722 10.1637 17.884 10.3164 18.6509C10.4446 19.3258 10.5057 20.013 10.4996 20.6941M5.69325 16.3746C5.11306 16.3746 4.57562 16.0862 4.25193 15.6015L4.23361 15.5708L4.21529 15.534C3.45188 14.3683 3.08544 12.9816 3.17705 11.5827C3.2198 10.1961 4.34965 9.08553 5.72989 9.07939H5.8154C7.20175 8.99963 8.38656 10.0672 8.46596 11.4539C8.46596 11.5029 8.46596 11.5459 8.46596 11.595C8.55146 12.9693 8.19724 14.3376 7.45215 15.4911L7.40329 15.5647C7.06739 16.0617 6.50552 16.3562 5.91311 16.3623H5.8154C5.77264 16.3623 5.72989 16.3685 5.68714 16.3685L5.69325 16.3746Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
4
+ <path vector-effect="non-scaling-stroke" d="M13.5 20.7002C13.5183 20.0069 13.5916 19.3136 13.7138 18.6325C13.8481 18.0128 14.8131 17.3134 16.2971 16.7796C16.4987 16.7059 16.6514 16.5525 16.7185 16.3501L16.8468 16.0862M19.6683 16.3194L19.5095 16.166C19.6377 16.436 19.8515 16.6569 20.1141 16.798C21.2073 17.1722 22.5387 17.884 22.6914 18.6509C22.8196 19.3258 22.8807 20.013 22.8746 20.6941M18.0682 16.3746C17.4881 16.3746 16.9506 16.0862 16.6269 15.6015L16.6086 15.5708L16.5903 15.534C15.8269 14.3683 15.4604 12.9816 15.552 11.5827C15.5948 10.1961 16.7246 9.08553 18.1049 9.07939H18.1904C19.5767 8.99963 20.7616 10.0672 20.841 11.4539C20.841 11.5029 20.841 11.5459 20.841 11.595C20.9265 12.9693 20.5722 14.3376 19.8271 15.4911L19.7783 15.5647C19.4424 16.0617 18.8805 16.3562 18.2881 16.3623H18.1904C18.1476 16.3623 18.1049 16.3685 18.0621 16.3685L18.0682 16.3746Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
5
+ </svg>
@@ -1,43 +1,48 @@
1
1
  export { default as Add } from './Add.svg';
2
+ export { default as AddressBook } from './AddressBook.svg';
2
3
  export { default as Bulk } from './Bulk.svg';
3
4
  export { default as Burger } from './Burger.svg';
5
+ export { default as Business } from './Business.svg';
6
+ export { default as Card } from './Card.svg';
4
7
  export { default as Cart } from './Cart.svg';
5
8
  export { default as Check } from './Check.svg';
9
+ export { default as CheckWithCircle } from './CheckWithCircle.svg';
6
10
  export { default as ChevronDown } from './ChevronDown.svg';
7
- export { default as ChevronUp } from './ChevronUp.svg';
8
11
  export { default as ChevronRight } from './ChevronRight.svg';
12
+ export { default as ChevronUp } from './ChevronUp.svg';
9
13
  export { default as Close } from './Close.svg';
14
+ export { default as Coupon } from './Coupon.svg';
15
+ export { default as Date } from './Date.svg';
16
+ export { default as Delivery } from './Delivery.svg';
17
+ export { default as Edit } from './Edit.svg';
18
+ export { default as EmptyBox } from './EmptyBox.svg';
19
+ export { default as Eye } from './Eye.svg';
20
+ export { default as EyeClose } from './EyeClose.svg';
21
+ export { default as Gift } from './Gift.svg';
22
+ export { default as GiftCard } from './GiftCard.svg';
10
23
  export { default as Heart } from './Heart.svg';
24
+ export { default as HeartFilled } from './HeartFilled.svg';
25
+ export { default as InfoFilled } from './InfoFilled.svg';
26
+ export { default as List } from './List.svg';
27
+ export { default as Locker } from './Locker.svg';
11
28
  export { default as Minus } from './Minus.svg';
29
+ export { default as Order } from './Order.svg';
30
+ export { default as OrderError } from './OrderError.svg';
31
+ export { default as OrderSuccess } from './OrderSuccess.svg';
32
+ export { default as PaymentError } from './PaymentError.svg';
12
33
  export { default as Placeholder } from './Placeholder.svg';
13
34
  export { default as PlaceholderFilled } from './PlaceholderFilled.svg';
35
+ export { default as Quote } from './Quote.svg';
14
36
  export { default as Search } from './Search.svg';
15
37
  export { default as SearchFilled } from './SearchFilled.svg';
16
38
  export { default as Sort } from './Sort.svg';
17
39
  export { default as Star } from './Star.svg';
18
- export { default as View } from './View.svg';
40
+ export { default as Structure } from './Structure.svg';
41
+ export { default as Team } from './Team.svg';
42
+ export { default as Trash } from './Trash.svg';
19
43
  export { default as User } from './User.svg';
20
- export { default as Warning } from './Warning.svg';
21
- export { default as Locker } from './Locker.svg';
44
+ export { default as View } from './View.svg';
22
45
  export { default as Wallet } from './Wallet.svg';
23
- export { default as Card } from './Card.svg';
24
- export { default as Order } from './Order.svg';
25
- export { default as Delivery } from './Delivery.svg';
26
- export { default as OrderError } from './OrderError.svg';
27
- export { default as OrderSuccess } from './OrderSuccess.svg';
28
- export { default as PaymentError } from './PaymentError.svg';
29
- export { default as CheckWithCircle } from './CheckWithCircle.svg';
30
- export { default as WarningWithCircle } from './WarningWithCircle.svg';
46
+ export { default as Warning } from './Warning.svg';
31
47
  export { default as WarningFilled } from './WarningFilled.svg';
32
- export { default as InfoFilled } from './InfoFilled.svg';
33
- export { default as HeartFilled } from './HeartFilled.svg';
34
- export { default as Trash } from './Trash.svg';
35
- export { default as Eye } from './Eye.svg';
36
- export { default as EyeClose } from './EyeClose.svg';
37
- export { default as Date } from './Date.svg';
38
- export { default as AddressBook } from './AddressBook.svg';
39
- export { default as EmptyBox } from './EmptyBox.svg';
40
- export { default as Coupon } from './Coupon.svg';
41
- export { default as Gift } from './Gift.svg';
42
- export { default as GiftCard } from './GiftCard.svg';
43
- export { default as Edit } from './Edit.svg';
48
+ export { default as WarningWithCircle } from './WarningWithCircle.svg';
@@ -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';