@astryxdesign/core 0.7.2-canary.b69b2df → 0.7.2-canary.c5f72ea

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.
@@ -532,7 +532,7 @@ export function PowerSearch({
532
532
  } else if (change.type === 'remove' && change.item) {
533
533
  const auxData = change.item.auxiliaryData;
534
534
  if (auxData?.filterIndex != null) {
535
- const newFilters = filters.filter((_, i) => i !== auxData.filterIndex);
535
+ const newFilters = filters.filter((_, i) => _items.some(item => item.auxiliaryData?.filterIndex === i));
536
536
  onChange(newFilters, 'remove', auxData.filterIndex);
537
537
  }
538
538
  }
@@ -159,7 +159,7 @@ export interface SwitchProps extends Omit<BaseProps, 'onChange'> {
159
159
  * />
160
160
  * ```
161
161
  */
162
- export declare function Switch({ label, isLabelHidden, description, onChange, changeAction, isLoading, value, isDisabled, htmlName, disabledMessage, isOptional, isRequired, onFocus, onBlur, labelIcon, labelTooltip, labelPosition, labelSpacing, status, size, width, xstyle, className, style, ref, 'aria-describedby': ariaDescribedByProp, ...rest }: SwitchProps): import("react").JSX.Element;
162
+ export declare function Switch({ label, isLabelHidden, description, onChange, changeAction, isLoading, value, isDisabled, htmlName, disabledMessage, isOptional, isRequired, onFocus, onBlur, labelIcon, labelTooltip, labelPosition, labelSpacing, status, size, width, xstyle, className, style, ref, 'aria-describedby': ariaDescribedByProp, 'aria-labelledby': ariaLabelledByProp, ...rest }: SwitchProps): import("react").JSX.Element;
163
163
  export declare namespace Switch {
164
164
  var displayName: string;
165
165
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../src/Switch/Switch.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../src/Switch/Switch.tsx"],"names":[],"mappings":"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"}
@@ -292,6 +292,7 @@ export function Switch({
292
292
  style,
293
293
  ref,
294
294
  'aria-describedby': ariaDescribedByProp,
295
+ 'aria-labelledby': ariaLabelledByProp,
295
296
  ...rest
296
297
  }) {
297
298
  const t = useTranslator();
@@ -388,7 +389,12 @@ export function Switch({
388
389
  }
389
390
  },
390
391
  onFocus: onFocus,
391
- onBlur: onBlur,
392
+ onBlur: onBlur
393
+ // A caller's `aria-labelledby` names the control (a visible title
394
+ // elsewhere in the row, for example), so it reaches the input rather
395
+ // than the field wrapper via `...rest`.
396
+ ,
397
+ "aria-labelledby": ariaLabelledByProp,
392
398
  "aria-describedby": ariaDescribedBy,
393
399
  "aria-invalid": status?.type === 'error' ? true : undefined,
394
400
  "aria-busy": isBusy || undefined,
@@ -1 +1 @@
1
- {"version":3,"file":"useTooltip.d.ts","sourceRoot":"","sources":["../../src/Tooltip/useTooltip.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA2ClC;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9D;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;;OAOG;IACH,aAAa,EAAE,CACb,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;CAChB;AAyBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CA6WtE"}
1
+ {"version":3,"file":"useTooltip.d.ts","sourceRoot":"","sources":["../../src/Tooltip/useTooltip.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA2ClC;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9D;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;;OAOG;IACH,aAAa,EAAE,CACb,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;CAChB;AAyBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CAwXtE"}
@@ -177,6 +177,17 @@ export function useTooltip(options = {}) {
177
177
  }, delay);
178
178
  }, [isEnabled, isOpen, clearTimeouts, showLayer, delay]);
179
179
 
180
+ // A show scheduled while the triggers were enabled must not outlive them:
181
+ // disabling also detaches the leave listener that would have cancelled it,
182
+ // so the timer would open a tooltip that nothing ever closes. Only the
183
+ // pending show is dropped; a pending hide still runs.
184
+ useEffect(() => {
185
+ if (!isEnabled && showTimeoutRef.current) {
186
+ clearTimeout(showTimeoutRef.current);
187
+ showTimeoutRef.current = null;
188
+ }
189
+ }, [isEnabled]);
190
+
180
191
  // Schedule hide with delay (suppressed when isOpen is true).
181
192
  // A small hover bridge (when hideDelay is 0) lets the pointer travel from the
182
193
  // trigger onto the tooltip surface without the tooltip vanishing — required
@@ -1 +1 @@
1
- {"version":3,"file":"BaseTypeahead.d.ts","sourceRoot":"","sources":["../../src/Typeahead/BaseTypeahead.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AA2BnD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAW1D,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CACxE,SAAS,CAAC,WAAW,CAAC,EACtB,UAAU,CACX;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;OAEG;IACH,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IAEpC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;OASG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;IAEtD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAE/D;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC3B;AAoKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,EAo1BrB,CAAC,CAAC,SAAS,cAAc,EAC5B,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KACzB,KAAK,CAAC,YAAY,CAAC"}
1
+ {"version":3,"file":"BaseTypeahead.d.ts","sourceRoot":"","sources":["../../src/Typeahead/BaseTypeahead.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AA2BnD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAW1D,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CACxE,SAAS,CAAC,WAAW,CAAC,EACtB,UAAU,CACX;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;OAEG;IACH,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IAEpC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;OASG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;IAEtD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAE/D;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC3B;AAoKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,EA82BrB,CAAC,CAAC,SAAS,cAAc,EAC5B,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KACzB,KAAK,CAAC,YAAY,CAAC"}
@@ -340,6 +340,32 @@ export const BaseTypeahead = function BaseTypeahead({
340
340
  sourceGenRef.current = ++searchGenRef.current;
341
341
  }
342
342
 
343
+ // The previous source is remembered in state, not a ref, so a render React
344
+ // discards cannot consume the comparison without its state updates.
345
+ const [sourceOfState, setSourceOfState] = useState(searchSource);
346
+ if (sourceOfState !== searchSource) {
347
+ setSourceOfState(searchSource);
348
+ if (isLoading) {
349
+ setIsLoading(false);
350
+ setHasSearched(false);
351
+ }
352
+ loadingRef.current = false;
353
+ }
354
+
355
+ // When the search source is replaced, any in-flight load from the previous
356
+ // source is abandoned (generation bumped above). Notify the wrapper's busy lane
357
+ // in an effect so external store listeners run safely after commit.
358
+ const isInitialMountRef = useRef(true);
359
+ useEffect(() => {
360
+ if (isInitialMountRef.current) {
361
+ isInitialMountRef.current = false;
362
+ return;
363
+ }
364
+ if (!loadingRef.current) {
365
+ onLoadingChangeRef.current?.(false);
366
+ }
367
+ }, [searchSource]);
368
+
343
369
  // Layer for dropdown
344
370
  const handleLayerShow = useCallback(() => {
345
371
  onOpenChange?.(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.2-canary.b69b2df",
3
+ "version": "0.7.2-canary.c5f72ea",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -787,8 +787,8 @@
787
787
  "react-dom": ">=19.0.0"
788
788
  },
789
789
  "devDependencies": {
790
- "@astryxdesign/a11y-spec": "0.7.2-canary.b69b2df",
791
- "@astryxdesign/cli": "0.7.2-canary.b69b2df",
790
+ "@astryxdesign/a11y-spec": "0.7.2-canary.c5f72ea",
791
+ "@astryxdesign/cli": "0.7.2-canary.c5f72ea",
792
792
  "@babel/cli": "^7.29.7",
793
793
  "@babel/core": "^7.29.7",
794
794
  "@babel/preset-react": "^7.29.7",
@@ -12,8 +12,9 @@ export const docs = {
12
12
  props: [
13
13
  {
14
14
  name: 'label',
15
- type: 'string',
16
- description: 'Primary text.',
15
+ type: 'ReactNode',
16
+ description:
17
+ 'Primary content. A plain string gets single-line truncation automatically; a ReactNode renders as given, so child components control their own text behavior.',
17
18
  required: true,
18
19
  },
19
20
  {
@@ -123,8 +124,9 @@ export const docsZh = {
123
124
  props: [
124
125
  {
125
126
  name: 'label',
126
- type: 'string',
127
- description: '主要文本。',
127
+ type: 'ReactNode',
128
+ description:
129
+ '主要内容。纯字符串会自动应用单行截断;ReactNode 按原样渲染,由子组件自行控制文本行为。',
128
130
  required: true,
129
131
  },
130
132
  {
@@ -205,7 +207,8 @@ export const docsDense = {
205
207
  description:
206
208
  'List item w/ label, description, start/end content slots, interactive patterns.',
207
209
  propDescriptions: {
208
- label: 'Primary text.',
210
+ label:
211
+ 'Primary content. String auto-truncates; ReactNode renders as given.',
209
212
  description: 'Secondary text below label.',
210
213
  startContent: 'Content before label area (e.g. icon, avatar).',
211
214
  endContent: 'Content after label area (e.g. badge, chevron).',
@@ -165,6 +165,91 @@ describe('PowerSearch', () => {
165
165
  expect(screen.getByRole('combobox')).toHaveFocus();
166
166
  });
167
167
 
168
+ it('clears all added filters when the clear button is clicked', async () => {
169
+ const user = userEvent.setup();
170
+ render(<PowerSearchWrapper config={config} />);
171
+ const input = screen.getByRole('combobox', {name: 'Search'});
172
+ await user.type(input, 'status open');
173
+ await user.click(
174
+ await screen.findByRole('option', {name: 'Status is Open', hidden: true}),
175
+ );
176
+ await user.type(input, 'title login');
177
+ await user.click(
178
+ await screen.findByRole('option', {
179
+ name: 'Title contains "login"',
180
+ hidden: true,
181
+ }),
182
+ );
183
+
184
+ await user.type(input, 'status closed');
185
+ await user.click(
186
+ await screen.findByRole('option', {
187
+ name: 'Status is Closed',
188
+ hidden: true,
189
+ }),
190
+ );
191
+
192
+ expect(screen.getAllByRole('button', {name: /^Remove /})).toHaveLength(3);
193
+
194
+ await user.click(screen.getByRole('button', {name: 'Clear all'}));
195
+
196
+ expect(screen.queryAllByRole('button', {name: /^Remove /})).toHaveLength(0);
197
+ expect(
198
+ screen.queryByRole('button', {name: 'Clear all'}),
199
+ ).not.toBeInTheDocument();
200
+ });
201
+
202
+ it('clicking the remove button on a filter removes only that filter', async () => {
203
+ const user = userEvent.setup();
204
+ render(<PowerSearchWrapper config={config} />);
205
+ const input = screen.getByRole('combobox', {name: 'Search'});
206
+ await user.type(input, 'status open');
207
+ await user.click(
208
+ await screen.findByRole('option', {name: 'Status is Open', hidden: true}),
209
+ );
210
+ await user.type(input, 'title login');
211
+ await user.click(
212
+ await screen.findByRole('option', {
213
+ name: 'Title contains "login"',
214
+ hidden: true,
215
+ }),
216
+ );
217
+
218
+ expect(screen.getAllByRole('button', {name: /^Remove /})).toHaveLength(2);
219
+
220
+ await user.click(
221
+ screen.getAllByRole('button', {name: 'Remove Status: is'})[0],
222
+ );
223
+
224
+ expect(screen.getAllByRole('button', {name: /^Remove /})).toHaveLength(1);
225
+ expect(screen.getByText('login')).toBeInTheDocument();
226
+ });
227
+
228
+ it('Backspace on an empty input removes only the last filter', async () => {
229
+ const user = userEvent.setup();
230
+ render(<PowerSearchWrapper config={config} />);
231
+ const input = screen.getByRole('combobox', {name: 'Search'});
232
+ await user.type(input, 'status open');
233
+ await user.click(
234
+ await screen.findByRole('option', {name: 'Status is Open', hidden: true}),
235
+ );
236
+ await user.type(input, 'title login');
237
+ await user.click(
238
+ await screen.findByRole('option', {
239
+ name: 'Title contains "login"',
240
+ hidden: true,
241
+ }),
242
+ );
243
+
244
+ expect(screen.getAllByRole('button', {name: /^Remove /})).toHaveLength(2);
245
+
246
+ await user.click(input);
247
+ await user.keyboard('{Backspace}');
248
+
249
+ expect(screen.getAllByRole('button', {name: /^Remove /})).toHaveLength(1);
250
+ expect(screen.getByText('Open')).toBeInTheDocument();
251
+ });
252
+
168
253
  describe('token value truncation (#4759)', () => {
169
254
  it('truncates token values by characters, not code units', () => {
170
255
  const truncConfig: PowerSearchConfig = {
@@ -733,8 +733,8 @@ export function PowerSearch({
733
733
  } else if (change.type === 'remove' && change.item) {
734
734
  const auxData = change.item.auxiliaryData as PowerSearchAuxData;
735
735
  if (auxData?.filterIndex != null) {
736
- const newFilters = filters.filter(
737
- (_, i) => i !== auxData.filterIndex,
736
+ const newFilters = filters.filter((_, i) =>
737
+ _items.some(item => item.auxiliaryData?.filterIndex === i),
738
738
  );
739
739
  onChange(newFilters, 'remove', auxData.filterIndex);
740
740
  }
@@ -862,6 +862,27 @@ describe('Switch', () => {
862
862
  const root = container.querySelector('[data-testid="my-switch"]');
863
863
  expect(root).not.toHaveAttribute('aria-describedby');
864
864
  });
865
+
866
+ it('names the switch control from a caller aria-labelledby', () => {
867
+ const {container} = render(
868
+ <>
869
+ <span id="row-title">Release notes</span>
870
+ <Switch
871
+ label="Pin Release notes"
872
+ isLabelHidden
873
+ value={false}
874
+ onChange={() => {}}
875
+ data-testid="my-switch"
876
+ aria-labelledby="row-title"
877
+ />
878
+ </>,
879
+ );
880
+ const control = screen.getByRole('switch', {name: 'Release notes'});
881
+ expect(control).toHaveAttribute('aria-labelledby', 'row-title');
882
+ // The id reference lives on the control only, not on the field wrapper.
883
+ const root = container.querySelector('[data-testid="my-switch"]');
884
+ expect(root).not.toHaveAttribute('aria-labelledby');
885
+ });
865
886
  });
866
887
 
867
888
  describe('pressed state', () => {
@@ -567,6 +567,7 @@ export function Switch({
567
567
  style,
568
568
  ref,
569
569
  'aria-describedby': ariaDescribedByProp,
570
+ 'aria-labelledby': ariaLabelledByProp,
570
571
  ...rest
571
572
  }: SwitchProps) {
572
573
  const t = useTranslator();
@@ -664,6 +665,10 @@ export function Switch({
664
665
  }}
665
666
  onFocus={onFocus}
666
667
  onBlur={onBlur}
668
+ // A caller's `aria-labelledby` names the control (a visible title
669
+ // elsewhere in the row, for example), so it reaches the input rather
670
+ // than the field wrapper via `...rest`.
671
+ aria-labelledby={ariaLabelledByProp}
667
672
  aria-describedby={ariaDescribedBy}
668
673
  aria-invalid={status?.type === 'error' ? true : undefined}
669
674
  aria-busy={isBusy || undefined}
@@ -20,7 +20,7 @@ import {
20
20
  afterEach,
21
21
  } from 'vitest';
22
22
  import {render, screen, fireEvent, act, waitFor} from '@testing-library/react';
23
- import {createRef, Profiler} from 'react';
23
+ import {createRef, Profiler, useState} from 'react';
24
24
  import userEvent from '@testing-library/user-event';
25
25
  import {Tokenizer} from './Tokenizer';
26
26
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
@@ -1790,6 +1790,59 @@ describe('Tokenizer end-lane reserve', () => {
1790
1790
  expect(renderedTokens).toEqual([]);
1791
1791
  });
1792
1792
 
1793
+ it('clears the busy spinner when a token is added while a bootstrap load is in flight', async () => {
1794
+ let settle: (items: SearchableItem[]) => void = () => {};
1795
+ const bootstrap = vi.fn(
1796
+ async () =>
1797
+ new Promise<SearchableItem[]>(resolve => {
1798
+ settle = resolve;
1799
+ }),
1800
+ );
1801
+ const source: SearchSource = {search: () => [], bootstrap};
1802
+
1803
+ function Harness() {
1804
+ const [value, setValue] = useState<SearchableItem[]>([]);
1805
+ return (
1806
+ <>
1807
+ <Tokenizer
1808
+ label="Items"
1809
+ searchSource={source}
1810
+ value={value}
1811
+ onChange={setValue}
1812
+ hasEntriesOnFocus
1813
+ />
1814
+ <button type="button" onClick={() => setValue([users[0]])}>
1815
+ Add Token
1816
+ </button>
1817
+ </>
1818
+ );
1819
+ }
1820
+
1821
+ render(<Harness />);
1822
+ const input = screen.getByRole('combobox');
1823
+
1824
+ fireEvent.focus(input);
1825
+ await waitFor(() => {
1826
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1827
+ });
1828
+
1829
+ fireEvent.click(screen.getByRole('button', {name: 'Add Token'}));
1830
+
1831
+ await waitFor(() => {
1832
+ expect(
1833
+ screen.queryByRole('status', {name: 'Loading'}),
1834
+ ).not.toBeInTheDocument();
1835
+ });
1836
+
1837
+ await act(async () => {
1838
+ settle([]);
1839
+ await Promise.resolve();
1840
+ });
1841
+ expect(
1842
+ screen.queryByRole('status', {name: 'Loading'}),
1843
+ ).not.toBeInTheDocument();
1844
+ });
1845
+
1793
1846
  it('shares one observer across every field on the page', () => {
1794
1847
  // Browsers batch per observer instance, so one observer per lane meant N
1795
1848
  // callback dispatches a frame for N fields.
@@ -147,6 +147,19 @@ describe('useTooltip — show delay', () => {
147
147
 
148
148
  expect(onShow).not.toHaveBeenCalled();
149
149
  });
150
+
151
+ it('drops a pending show when isEnabled turns false before the delay elapses', () => {
152
+ const onShow = vi.fn();
153
+ const {rerender} = render(<Harness onShow={onShow} />);
154
+
155
+ fireEvent.mouseEnter(trigger());
156
+ tick(100);
157
+ rerender(<Harness isEnabled={false} onShow={onShow} />);
158
+ tick(1000);
159
+
160
+ expect(onShow).not.toHaveBeenCalled();
161
+ expect(tooltipElement().matches(':popover-open')).toBe(false);
162
+ });
150
163
  });
151
164
 
152
165
  describe('useTooltip — touch suppression', () => {
@@ -352,6 +352,17 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
352
352
  }, delay);
353
353
  }, [isEnabled, isOpen, clearTimeouts, showLayer, delay]);
354
354
 
355
+ // A show scheduled while the triggers were enabled must not outlive them:
356
+ // disabling also detaches the leave listener that would have cancelled it,
357
+ // so the timer would open a tooltip that nothing ever closes. Only the
358
+ // pending show is dropped; a pending hide still runs.
359
+ useEffect(() => {
360
+ if (!isEnabled && showTimeoutRef.current) {
361
+ clearTimeout(showTimeoutRef.current);
362
+ showTimeoutRef.current = null;
363
+ }
364
+ }, [isEnabled]);
365
+
355
366
  // Schedule hide with delay (suppressed when isOpen is true).
356
367
  // A small hover bridge (when hideDelay is 0) lets the pointer travel from the
357
368
  // trigger onto the tooltip surface without the tooltip vanishing — required
@@ -624,6 +624,32 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
624
624
  sourceGenRef.current = ++searchGenRef.current;
625
625
  }
626
626
 
627
+ // The previous source is remembered in state, not a ref, so a render React
628
+ // discards cannot consume the comparison without its state updates.
629
+ const [sourceOfState, setSourceOfState] = useState(searchSource);
630
+ if (sourceOfState !== searchSource) {
631
+ setSourceOfState(searchSource);
632
+ if (isLoading) {
633
+ setIsLoading(false);
634
+ setHasSearched(false);
635
+ }
636
+ loadingRef.current = false;
637
+ }
638
+
639
+ // When the search source is replaced, any in-flight load from the previous
640
+ // source is abandoned (generation bumped above). Notify the wrapper's busy lane
641
+ // in an effect so external store listeners run safely after commit.
642
+ const isInitialMountRef = useRef(true);
643
+ useEffect(() => {
644
+ if (isInitialMountRef.current) {
645
+ isInitialMountRef.current = false;
646
+ return;
647
+ }
648
+ if (!loadingRef.current) {
649
+ onLoadingChangeRef.current?.(false);
650
+ }
651
+ }, [searchSource]);
652
+
627
653
  // Layer for dropdown
628
654
  const handleLayerShow = useCallback(() => {
629
655
  onOpenChange?.(true);
@@ -17,10 +17,20 @@ import {
17
17
  beforeAll,
18
18
  afterAll,
19
19
  beforeEach,
20
+ afterEach,
20
21
  } from 'vitest';
21
22
  import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
22
23
  import userEvent from '@testing-library/user-event';
23
- import {createRef, Profiler, type ProfilerOnRenderCallback} from 'react';
24
+ import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
25
+ import {
26
+ createRef,
27
+ Profiler,
28
+ startTransition,
29
+ StrictMode,
30
+ Suspense,
31
+ useState,
32
+ type ProfilerOnRenderCallback,
33
+ } from 'react';
24
34
  import {Typeahead} from './Typeahead';
25
35
  import {readFile} from 'node:fs/promises';
26
36
  import {resolve} from 'node:path';
@@ -70,6 +80,10 @@ afterAll(() => {
70
80
  (HTMLElement.prototype as any).matches = originalMatches;
71
81
  });
72
82
 
83
+ afterEach(() => {
84
+ __resetLiveRegionsForTest();
85
+ });
86
+
73
87
  // Test data
74
88
  const fruits: SearchableItem[] = [
75
89
  {id: '1', label: 'Apple'},
@@ -964,6 +978,193 @@ describe('BaseTypeahead hasEntriesOnFocus', () => {
964
978
  ).not.toBeInTheDocument();
965
979
  });
966
980
 
981
+ it('clears the loading state when the search source is replaced during an in-flight load', async () => {
982
+ let settle: (items: SearchableItem[]) => void = () => {};
983
+ const bootstrap = vi.fn(
984
+ async () =>
985
+ new Promise<SearchableItem[]>(resolve => {
986
+ settle = resolve;
987
+ }),
988
+ );
989
+ const initialSource: SearchSource = {search: () => [], bootstrap};
990
+ const replacementSource: SearchSource = {
991
+ search: () => [],
992
+ bootstrap: () => [],
993
+ };
994
+
995
+ const {rerender} = render(
996
+ <BaseTypeahead
997
+ searchSource={initialSource}
998
+ value={null}
999
+ onChange={() => {}}
1000
+ hasEntriesOnFocus
1001
+ />,
1002
+ );
1003
+ const input = screen.getByRole('combobox');
1004
+
1005
+ fireEvent.focus(input);
1006
+ await waitFor(() => {
1007
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1008
+ });
1009
+
1010
+ rerender(
1011
+ <BaseTypeahead
1012
+ searchSource={replacementSource}
1013
+ value={null}
1014
+ onChange={() => {}}
1015
+ hasEntriesOnFocus
1016
+ />,
1017
+ );
1018
+
1019
+ await waitFor(() => {
1020
+ expect(
1021
+ screen.queryByRole('status', {name: 'Loading'}),
1022
+ ).not.toBeInTheDocument();
1023
+ });
1024
+
1025
+ await act(async () => {
1026
+ settle([]);
1027
+ await Promise.resolve();
1028
+ });
1029
+ expect(
1030
+ screen.queryByRole('status', {name: 'Loading'}),
1031
+ ).not.toBeInTheDocument();
1032
+ });
1033
+
1034
+ it('keeps the loading state under StrictMode for a pending autofocus bootstrap', async () => {
1035
+ let settle: (items: SearchableItem[]) => void = () => {};
1036
+ const bootstrap = vi.fn(
1037
+ async () =>
1038
+ new Promise<SearchableItem[]>(resolve => {
1039
+ settle = resolve;
1040
+ }),
1041
+ );
1042
+ render(
1043
+ <StrictMode>
1044
+ <Typeahead
1045
+ label="Test"
1046
+ searchSource={{search: () => [], bootstrap}}
1047
+ value={null}
1048
+ onChange={() => {}}
1049
+ hasAutoFocus
1050
+ hasEntriesOnFocus
1051
+ />
1052
+ </StrictMode>,
1053
+ );
1054
+
1055
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1056
+
1057
+ await act(async () => {
1058
+ settle([]);
1059
+ await Promise.resolve();
1060
+ });
1061
+ expect(
1062
+ screen.queryByRole('status', {name: 'Loading'}),
1063
+ ).not.toBeInTheDocument();
1064
+ });
1065
+
1066
+ let settlePendingTransitionA: (items: SearchableItem[]) => void = () => {};
1067
+ const stableTransitionSourceA: SearchSource = {
1068
+ search: vi.fn(
1069
+ async () =>
1070
+ new Promise<SearchableItem[]>(resolve => {
1071
+ settlePendingTransitionA = resolve;
1072
+ }),
1073
+ ),
1074
+ bootstrap: () => [],
1075
+ };
1076
+ const stableTransitionSourceB: SearchSource = {
1077
+ search: vi.fn(async () => []),
1078
+ bootstrap: () => [],
1079
+ };
1080
+
1081
+ it('clears loading and aria-busy when source is replaced in a transition that suspends and retries', async () => {
1082
+ let isSuspended = false;
1083
+ let resolveSuspension: () => void = () => {};
1084
+ let pendingSuspension: Promise<void> | null = null;
1085
+
1086
+ function SuspendingSibling({shouldSuspend}: {shouldSuspend: boolean}) {
1087
+ if (shouldSuspend && isSuspended && pendingSuspension) {
1088
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
1089
+ throw pendingSuspension;
1090
+ }
1091
+ return null;
1092
+ }
1093
+
1094
+ let updateState!: (source: SearchSource, shouldSuspend: boolean) => void;
1095
+
1096
+ function TransitionContainer() {
1097
+ const [source, setSource] = useState<SearchSource>(
1098
+ stableTransitionSourceA,
1099
+ );
1100
+ const [shouldSuspend, setShouldSuspend] = useState(false);
1101
+ updateState = (newSource, suspend) => {
1102
+ setSource(newSource);
1103
+ setShouldSuspend(suspend);
1104
+ };
1105
+
1106
+ return (
1107
+ <Suspense fallback={<div>Suspended...</div>}>
1108
+ <Typeahead
1109
+ label="Test"
1110
+ searchSource={source}
1111
+ value={null}
1112
+ onChange={() => {}}
1113
+ debounceMs={0}
1114
+ />
1115
+ <SuspendingSibling shouldSuspend={shouldSuspend} />
1116
+ </Suspense>
1117
+ );
1118
+ }
1119
+
1120
+ render(<TransitionContainer />);
1121
+ const input = screen.getByRole('combobox');
1122
+
1123
+ // Trigger a load so aria-busy="true" is set on the combobox
1124
+ fireEvent.change(input, {target: {value: 'query'}});
1125
+ await waitFor(() => {
1126
+ expect(input).toHaveAttribute('aria-busy', 'true');
1127
+ });
1128
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1129
+
1130
+ // Create the pending promise for suspension
1131
+ isSuspended = true;
1132
+ pendingSuspension = new Promise<void>(resolve => {
1133
+ resolveSuspension = () => {
1134
+ isSuspended = false;
1135
+ resolve();
1136
+ };
1137
+ });
1138
+
1139
+ // Inside startTransition, switch from source A to source B while sibling suspends
1140
+ act(() => {
1141
+ startTransition(() => {
1142
+ updateState(stableTransitionSourceB, true);
1143
+ });
1144
+ });
1145
+
1146
+ // The render suspended, so the previous committed state remains
1147
+ expect(input).toHaveAttribute('aria-busy', 'true');
1148
+
1149
+ // Resolve the pending promise and let the transition commit
1150
+ await act(async () => {
1151
+ resolveSuspension();
1152
+ await pendingSuspension;
1153
+ });
1154
+
1155
+ // Settle the abandoned load
1156
+ await act(async () => {
1157
+ settlePendingTransitionA([]);
1158
+ await Promise.resolve();
1159
+ });
1160
+
1161
+ // The combobox must not keep aria-busy and the spinner must be gone
1162
+ expect(input).not.toHaveAttribute('aria-busy');
1163
+ expect(
1164
+ screen.queryByRole('status', {name: 'Loading'}),
1165
+ ).not.toBeInTheDocument();
1166
+ });
1167
+
967
1168
  it('shows bootstrap results on mouse click', async () => {
968
1169
  render(
969
1170
  <BaseTypeahead
@@ -1323,6 +1524,63 @@ describe('BaseTypeahead hasSearched reset', () => {
1323
1524
  // "No results found" should disappear since hasSearched is reset
1324
1525
  expect(screen.queryByText('No results found')).not.toBeInTheDocument();
1325
1526
  });
1527
+
1528
+ it('resets hasSearched and does not announce "No results found" when search source is replaced during a pending search', async () => {
1529
+ let settle: (items: SearchableItem[]) => void = () => {};
1530
+ const initialSource: SearchSource = {
1531
+ search: vi.fn(
1532
+ async () =>
1533
+ new Promise<SearchableItem[]>(resolve => {
1534
+ settle = resolve;
1535
+ }),
1536
+ ),
1537
+ bootstrap: () => [],
1538
+ };
1539
+ const replacementSource: SearchSource = {
1540
+ search: vi.fn(async () => []),
1541
+ bootstrap: () => [],
1542
+ };
1543
+
1544
+ const {rerender} = render(
1545
+ <BaseTypeahead
1546
+ searchSource={initialSource}
1547
+ value={null}
1548
+ onChange={() => {}}
1549
+ debounceMs={0}
1550
+ />,
1551
+ );
1552
+ const input = screen.getByRole('combobox');
1553
+
1554
+ // Type a query that starts an asynchronous search
1555
+ fireEvent.change(input, {target: {value: 'abc'}});
1556
+ expect(initialSource.search).toHaveBeenCalledWith('abc');
1557
+
1558
+ // Replace the search source while the search is still pending
1559
+ rerender(
1560
+ <BaseTypeahead
1561
+ searchSource={replacementSource}
1562
+ value={null}
1563
+ onChange={() => {}}
1564
+ debounceMs={0}
1565
+ />,
1566
+ );
1567
+
1568
+ // Settle the abandoned search
1569
+ await act(async () => {
1570
+ settle([]);
1571
+ await Promise.resolve();
1572
+ });
1573
+
1574
+ await act(
1575
+ async () =>
1576
+ new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
1577
+ );
1578
+ expect(
1579
+ document.querySelector('[data-astryx-live-region="polite"]')
1580
+ ?.textContent ?? '',
1581
+ ).not.toContain('No results found');
1582
+ expect(screen.queryByText('No results found')).not.toBeInTheDocument();
1583
+ });
1326
1584
  });
1327
1585
 
1328
1586
  describe('BaseTypeahead popover after selection', () => {
@@ -2313,7 +2571,7 @@ describe('Typeahead consumer key handler order', () => {
2313
2571
  currentTarget = event.currentTarget;
2314
2572
  event.preventDefault();
2315
2573
  });
2316
- const {container} = render(
2574
+ render(
2317
2575
  <Typeahead
2318
2576
  label="Fruit"
2319
2577
  searchSource={fruitSource}