@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.
- package/dist/PowerSearch/PowerSearch.js +1 -1
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +7 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +26 -0
- package/package.json +3 -3
- package/src/List/ListItem.doc.mjs +8 -5
- package/src/PowerSearch/PowerSearch.test.tsx +85 -0
- package/src/PowerSearch/PowerSearch.tsx +2 -2
- package/src/Switch/Switch.test.tsx +21 -0
- package/src/Switch/Switch.tsx +5 -0
- package/src/Tokenizer/Tokenizer.test.tsx +54 -1
- package/src/Tooltip/useTooltip.test.tsx +13 -0
- package/src/Tooltip/useTooltip.tsx +11 -0
- package/src/Typeahead/BaseTypeahead.tsx +26 -0
- package/src/Typeahead/Typeahead.test.tsx +260 -2
|
@@ -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) =>
|
|
535
|
+
const newFilters = filters.filter((_, i) => _items.some(item => item.auxiliaryData?.filterIndex === i));
|
|
536
536
|
onChange(newFilters, 'remove', auxData.filterIndex);
|
|
537
537
|
}
|
|
538
538
|
}
|
package/dist/Switch/Switch.d.ts
CHANGED
|
@@ -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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"}
|
package/dist/Switch/Switch.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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.
|
|
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.
|
|
791
|
-
"@astryxdesign/cli": "0.7.2-canary.
|
|
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: '
|
|
16
|
-
description:
|
|
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: '
|
|
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:
|
|
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
|
-
(
|
|
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', () => {
|
package/src/Switch/Switch.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
2574
|
+
render(
|
|
2317
2575
|
<Typeahead
|
|
2318
2576
|
label="Fruit"
|
|
2319
2577
|
searchSource={fruitSource}
|