@adobe-commerce/elsie 2.0.0-alpha-20260717142751 → 2.0.0-beta.1

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/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @adobe-commerce/elsie
2
2
 
3
- ## 2.0.0-alpha-20260717142751
3
+ ## 2.0.0-beta.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 256007e: Fix ToggleButton generating invalid HTML ids when value prop contains spaces, breaking aria-labelledby label association
8
+
9
+ ## 2.0.0-beta.0
4
10
 
5
11
  ### Major Changes
6
12
 
@@ -118,7 +124,6 @@
118
124
  - 58da630: Fix `Picker` accessible name and label association. The `<select>` now falls back to `floatingLabel` or `placeholder` for its `aria-label` when no `name` is provided, and the floating `<label>` is now correctly associated with the rendered `<select>` via its generated id instead of the raw `id` prop.
119
125
  - 450c408: fix(Picker): auto-select and emit the sole option when the control auto-disables, so single-option narrowing on configurable PDPs no longer leaves the value unselected and Add to Cart permanently disabled
120
126
  - 33ebe8a: Add accessible labels to password validation and input status icons (WCAG 1.1.1)
121
- - a920177: fix(Incrementer): prevent quantity input flicker during in-progress typing, and prevent double onValue call when debounce fires before blur
122
127
  - 2ad7316: Fix `ToggleButton`'s underlying radio input announcing the shared radio-group `name` (e.g. "payment-method") as its accessible name for every option instead of the option's own visible label, which violates WCAG 2.4.6 (Headings and Labels) and 2.5.3 (Label in Name). The radio input's accessible name now defaults to `aria-labelledby` pointing at the option's own visible label content (e.g. "Check / Money order"), which works correctly whether `label` is a string or a `VNode`. An optional `ariaLabel` prop is still available for consumers who need to set an explicit accessible name via `aria-label` instead.
123
128
  - 016a558: Fix low-contrast field label text in `Input` when a field is in an error state. The floating label color now meets WCAG AA contrast requirements for normal-size text against light backgrounds, matching the color already used for error text elsewhere (helper text, alerts).
124
129
  - 51fcb35: fix(a11y): darken low-contrast focus indicators to meet WCAG 1.4.11 (3:1 non-text contrast)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "2.0.0-alpha-20260717142751",
3
+ "version": "2.0.0-beta.1",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -30,12 +30,12 @@
30
30
  "postpublish": "node ./scripts/publish-tools.mjs"
31
31
  },
32
32
  "devDependencies": {
33
- "@adobe-commerce/event-bus": "1.1.1-alpha-20260717142751",
34
- "@adobe-commerce/fetch-graphql": "1.3.1-alpha-20260717142751",
35
- "@adobe-commerce/recaptcha": "1.2.1-alpha-20260717142751",
36
- "@adobe-commerce/storefront-design": "1.1.1-alpha-20260717142751",
33
+ "@adobe-commerce/event-bus": "~1.1.1-beta.0",
34
+ "@adobe-commerce/fetch-graphql": "~1.3.1-beta.0",
35
+ "@adobe-commerce/recaptcha": "1.2.1-beta.0",
36
+ "@adobe-commerce/storefront-design": "~1.1.1-beta.0",
37
37
  "@chromatic-com/storybook": "^5",
38
- "@dropins/build-tools": "1.2.1-alpha-20260717142751"
38
+ "@dropins/build-tools": "~1.2.1-beta.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "eslint-plugin-cypress": ">=3.0.0"
@@ -8,7 +8,7 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { FunctionComponent } from 'preact';
11
- import { useState, useEffect, useRef } 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';
@@ -48,7 +48,6 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
48
48
  }) => {
49
49
  const [currentValue, setCurrentValue] = useState<number>(Number(value));
50
50
  const [isEmpty, setIsEmpty] = useState<boolean>(false);
51
- const isFocusedRef = useRef<boolean>(false);
52
51
  const minValue = Number(min);
53
52
  const maxValue = Number(max);
54
53
  const isInvalid = error || isEmpty || currentValue < minValue || currentValue > maxValue;
@@ -61,45 +60,38 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
61
60
  ? 'Dropin.Incrementer.maxQuantityMessage'
62
61
  : 'Dropin.Incrementer.errorMessage';
63
62
 
64
- // Keep callback refs current so debounced handlers never capture stale closures
65
- const onValueRef = useRef(onValue);
66
- useEffect(() => { onValueRef.current = onValue; }, [onValue]);
67
-
68
- const onUpdateErrorRef = useRef(onUpdateError);
69
- useEffect(() => { onUpdateErrorRef.current = onUpdateError; }, [onUpdateError]);
70
-
71
- const invoke = (newValue: number) => {
72
- try {
73
- onValueRef.current?.(newValue);
74
- } catch (e) {
75
- onUpdateErrorRef.current?.(e as Error);
76
- }
77
- };
78
-
79
- // Created once — stable across re-renders caused by cart/data events
80
- // Buttons: 200ms debounce, created once, never recreated
81
- const debouncedButtonHandler = useRef(debounce(invoke, 200)).current;
82
-
83
- // True while the input debounce timer is still pending.
84
- // Blur only calls invoke if it's cancelling a live timer — not if the timer already fired.
85
- const hasPendingInputRef = useRef(false);
86
- const debouncedInputHandler = useRef(
87
- debounce((newValue: number) => {
88
- hasPendingInputRef.current = false;
89
- invoke(newValue);
90
- }, 1000)
91
- ).current;
92
-
93
- // Skip server sync while user is actively typing to prevent flicker
63
+ // Add this effect to synchronize internal state with external value prop
94
64
  useEffect(() => {
95
65
  const propValue = Number(value);
96
- if (!isFocusedRef.current && propValue !== currentValue) {
66
+ if (propValue !== currentValue) {
97
67
  setCurrentValue(propValue);
98
68
  setIsEmpty(false);
99
69
  }
100
70
  // eslint-disable-next-line react-hooks/exhaustive-deps
101
71
  }, [value]);
102
72
 
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ const debouncedOnValueHandler = useCallback(
75
+ debounce(async (newValue: any) => {
76
+ if (onValue) {
77
+ try {
78
+ onValue(newValue);
79
+ } catch (e) {
80
+ if (onUpdateError) {
81
+ onUpdateError(e as Error);
82
+ }
83
+ }
84
+ }
85
+ }, 200),
86
+ [onValue, onUpdateError]
87
+ );
88
+
89
+ const handleIncrementer = (_value: number) => {
90
+ setIsEmpty(false);
91
+ debouncedOnValueHandler(_value);
92
+ setCurrentValue(_value);
93
+ };
94
+
103
95
  return (
104
96
  <div
105
97
  className={classes([
@@ -133,8 +125,7 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
133
125
  'dropin-incrementer__decrease-button',
134
126
  [`dropin-incrementer__decrease-button--disabled`, disabled],
135
127
  ])}
136
- onMouseDown={(e) => e.preventDefault()}
137
- onClick={() => { setIsEmpty(false); debouncedInputHandler.cancel(); hasPendingInputRef.current = false; setCurrentValue(currentValue - 1); debouncedButtonHandler(currentValue - 1); }}
128
+ onClick={() => handleIncrementer(currentValue - 1)}
138
129
  disabled={disabled || currentValue < minValue + 1}
139
130
  aria-label={
140
131
  (<Text id="Dropin.Incrementer.decreaseLabel" />) as any
@@ -162,24 +153,17 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
162
153
  name={name}
163
154
  value={isEmpty ? '' : currentValue}
164
155
  disabled={disabled}
165
- onFocus={() => { isFocusedRef.current = true; }}
166
156
  onBlur={() => {
167
- isFocusedRef.current = false;
168
- debouncedInputHandler.cancel();
169
- const wasPending = hasPendingInputRef.current;
170
- hasPendingInputRef.current = false;
171
- if (!isEmpty && wasPending) invoke(Number(currentValue));
157
+ if (!isEmpty) {
158
+ handleIncrementer(Number(currentValue));
159
+ }
172
160
  }}
173
161
  onChange={(e) => {
174
162
  const newValue = e.currentTarget.value;
175
163
  if (newValue === '') {
176
164
  setIsEmpty(true);
177
165
  } else {
178
- setIsEmpty(false);
179
- setCurrentValue(Number(newValue));
180
- debouncedButtonHandler.cancel();
181
- hasPendingInputRef.current = true;
182
- debouncedInputHandler(Number(newValue));
166
+ handleIncrementer(Number(newValue));
183
167
  }
184
168
  }}
185
169
  {...props}
@@ -200,8 +184,7 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
200
184
  'dropin-incrementer__increase-button',
201
185
  [`dropin-incrementer__increase-button--disabled`, disabled],
202
186
  ])}
203
- onMouseDown={(e) => e.preventDefault()}
204
- onClick={() => { setIsEmpty(false); debouncedInputHandler.cancel(); hasPendingInputRef.current = false; setCurrentValue(currentValue + 1); debouncedButtonHandler(currentValue + 1); }}
187
+ onClick={() => handleIncrementer(currentValue + 1)}
205
188
  disabled={disabled || currentValue > maxValue - 1}
206
189
  aria-label={
207
190
  (<Text id="Dropin.Incrementer.increaseLabel" />) as any
@@ -42,7 +42,7 @@ export const ToggleButton: FunctionComponent<ToggleButtonProps> = ({
42
42
  selected = true,
43
43
  ...props
44
44
  }) => {
45
- const contentId = `dropin-toggle-button__content-${name}-${value}`;
45
+ const contentId = `dropin-toggle-button__content-${name}-${value}`.replace(/\s+/g, '-');
46
46
 
47
47
  return (
48
48
  <div