@astryxdesign/core 0.7.4-canary.b25f95e → 0.7.4-canary.f32572e

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.
@@ -1 +1 @@
1
- {"version":3,"file":"DateTimeInput.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/DateTimeInput.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DateTimeInput.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/DateTimeInput.tsx"],"names":[],"mappings":"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"}
@@ -32,6 +32,7 @@ import { useCalendarConstraints } from "../Calendar/hooks/index.js";
32
32
  import { usePopover } from "../Popover/index.js";
33
33
  import { useTooltip } from "../Tooltip/index.js";
34
34
  import { useInputContainer } from "../hooks/useInputContainer.js";
35
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
35
36
  import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
36
37
  import { parseDateInput, parseISOTime, parseTimeInput, formatDisplayTime12h, formatDisplayTime24h, formatISOTime, isTimeInRange, adjustTime, isImeKeyEvent, mergeProps, isFocusDetached } from "../utils/index.js";
37
38
  import { chainEventHandlers } from "../utils/chainEventHandlers.js";
@@ -317,6 +318,9 @@ function PointerDateTimeField({
317
318
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
318
319
  const isPendingChange = optimisticValue !== value;
319
320
  const isBusy = isLoading || isPendingChange;
321
+ // The busy Spinner is decorative; this control announces its busy start
322
+ // (spec:AST-086 FR8).
323
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), dateInputRef);
320
324
  // A native wheel emits several edits while it remains open. Disabling the
321
325
  // control after the first optimistic edit detaches the iOS picker, so pending
322
326
  // changeAction work stays interactive just like TouchDateTimeField. Explicit
@@ -1130,7 +1134,8 @@ function PointerDateTimeField({
1130
1134
  }),
1131
1135
  onClick: handleClear
1132
1136
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
1133
- size: "sm"
1137
+ size: "sm",
1138
+ "aria-hidden": "true"
1134
1139
  }), statusIcon]
1135
1140
  }), /*#__PURE__*/_jsx("div", {
1136
1141
  ref: timeContainerRef,
@@ -1 +1 @@
1
- {"version":3,"file":"TouchDateTimeField.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/TouchDateTimeField.tsx"],"names":[],"mappings":"AA6GA,OAAO,KAAK,EAAC,kBAAkB,EAAoB,MAAM,iBAAiB,CAAC;AAuV3E,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,eAAe,EACf,UAAkB,EAClB,UAAkB,EAElB,aAAa,EAAE,cAAc,EAC7B,kBAAkB,EAAE,mBAAmB,EACvC,QAAgB,EAChB,WAAW,EAAE,oBAAoB,EACjC,eAAe,EAAE,wBAAwB,EACzC,SAAS,EACT,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EAEZ,cAAc,EAAE,eAAe,EAC/B,YAAY,EAAE,gBAAoB,EAClC,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,kBAAkB,+BAu/BpB;yBA9hCe,kBAAkB"}
1
+ {"version":3,"file":"TouchDateTimeField.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/TouchDateTimeField.tsx"],"names":[],"mappings":"AA8GA,OAAO,KAAK,EAAC,kBAAkB,EAAoB,MAAM,iBAAiB,CAAC;AAuV3E,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,eAAe,EACf,UAAkB,EAClB,UAAkB,EAElB,aAAa,EAAE,cAAc,EAC7B,kBAAkB,EAAE,mBAAmB,EACvC,QAAgB,EAChB,WAAW,EAAE,oBAAoB,EACjC,eAAe,EAAE,wBAAwB,EACzC,SAAS,EACT,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EAEZ,cAAc,EAAE,eAAe,EAC/B,YAAY,EAAE,gBAAoB,EAClC,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,kBAAkB,+BAkgCpB;yBAziCe,kBAAkB"}
@@ -29,6 +29,7 @@ import { useCalendarConstraints } from "../Calendar/index.js";
29
29
  import { Field, InputClearButton, inputWrapperStyles, inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles } from "../Field/index.js";
30
30
  import { useInputStatusIcon, useMergedRefs } from "../hooks/index.js";
31
31
  import { useResolvedRequired } from "../hooks/useResolvedRequired.js";
32
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
32
33
  import { Icon } from "../Icon/index.js";
33
34
  import { IconButton } from "../IconButton/index.js";
34
35
  import { useTranslator, InternationalizationContext } from "../i18n/index.js";
@@ -338,6 +339,11 @@ export function TouchDateTimeField({
338
339
  dateConstraints
339
340
  });
340
341
  const [isSheetOpen, setIsSheetOpen] = useState(false);
342
+ const sheetBodyRef = useRef(null);
343
+ // The busy Spinner is decorative; this control announces its busy start
344
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
345
+ // the sheet is open, so it is announced from inside the sheet.
346
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), isSheetOpen ? sheetBodyRef : dateInputRef);
341
347
  const [activePanel, setActivePanel] = useState('date');
342
348
  const [isWheelOpen, setIsWheelOpen] = useState(false);
343
349
  const [draftTime, setDraftTime] = useState(() => normalizeISOTime(splitDateTime(value).time, hasSeconds));
@@ -990,7 +996,8 @@ export function TouchDateTimeField({
990
996
  }),
991
997
  onClick: handleClear
992
998
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
993
- size: "sm"
999
+ size: "sm",
1000
+ "aria-hidden": "true"
994
1001
  }), isRenderable(statusIcon) && statusIcon]
995
1002
  }), /*#__PURE__*/_jsxs("div", {
996
1003
  onClick: event => {
@@ -1058,6 +1065,7 @@ export function TouchDateTimeField({
1058
1065
  ,
1059
1066
  padding: 0,
1060
1067
  children: /*#__PURE__*/_jsx("div", {
1068
+ ref: sheetBodyRef,
1061
1069
  ...{
1062
1070
  className: "x9f619 xh8yej3 xeuugli x1pzlopt x1adxfkp x1awphl8"
1063
1071
  },
@@ -1 +1 @@
1
- {"version":3,"file":"TimeInput.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TimeInput.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAO3C,OAAO,EAGL,KAAK,WAAW,EAKhB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAIlB,OAAO,EACL,KAAK,aAAa,EAUnB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAgB9C,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAC;AAyDpC,QAAA,MAAM,UAAU;;;;;;;;;;;;;EAad,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,UAAU,CAAC;AAEpD,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,KAAK,CAAC;AAEhD,iFAAiF;AACjF,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjE,4EAA4E;AAC5E,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAItD,YAAY,EACV,WAAW,IAAI,eAAe,EAC9B,eAAe,IAAI,mBAAmB,GACvC,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,mBAAmB,CAAC,gBAAgB,CAAC,EACrC,UAAU,GAAG,cAAc,CAC5B;IACC,qEAAqE;IACrE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,IAAI,CAAC;IAEtD;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1E;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,GAAG,CAAC,EAAE,aAAa,CAAC;IAEpB;;OAEG;IACH,GAAG,CAAC,EAAE,aAAa,CAAC;IAEpB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,mBAAmB,CAAC;IAEjC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IAErB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;IAEnC;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA2hBD;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAyC9C;yBAzCe,SAAS"}
1
+ {"version":3,"file":"TimeInput.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TimeInput.tsx"],"names":[],"mappings":"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"}
@@ -30,6 +30,7 @@ import { joinAriaIDs } from "../utils/inputAria.js";
30
30
  import { useFieldInputStyling } from "../Field/fieldParts.js";
31
31
  import { useSize } from "../SizeContext/SizeContext.js";
32
32
  import { useAnnounce } from "../hooks/useAnnounce.js";
33
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
33
34
  import { useInputContainer } from "../hooks/useInputContainer.js";
34
35
  import { useMediaQuery } from "../hooks/useMediaQuery.js";
35
36
  import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
@@ -169,6 +170,9 @@ function TimeField({
169
170
  const [, startTransition] = useTransition();
170
171
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
171
172
  const isBusy = isLoading || optimisticValue !== value;
173
+ // The busy Spinner is decorative; this control announces its busy start
174
+ // (spec:AST-086 FR8).
175
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), containerRef);
172
176
 
173
177
  // In grouped mode the status message renders as a visually-hidden node that
174
178
  // exists only for aria-describedby. Announce it through the persistent
@@ -504,7 +508,8 @@ function TimeField({
504
508
  children: !isInputValid ? t('@astryx.timeInput.invalidTime') : ''
505
509
  })]
506
510
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
507
- size: "sm"
511
+ size: "sm",
512
+ "aria-hidden": "true"
508
513
  }), hasClear && optimisticValue && !isDisabled && /*#__PURE__*/_jsx(InputClearButton, {
509
514
  label: t('@astryx.timeInput.clearLabel', {
510
515
  label
@@ -1 +1 @@
1
- {"version":3,"file":"TouchTimeField.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TouchTimeField.tsx"],"names":[],"mappings":"AAoEA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAqHhD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,UAAkB,EAClB,QAAgB,EAChB,UAAkB,EAGlB,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,aAAa,EAC3B,WAAW,EAAE,oBAAoB,EACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,aAA0B,EAC1B,YAAY,EACZ,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,cAAc,+BAiZhB;yBApbe,cAAc"}
1
+ {"version":3,"file":"TouchTimeField.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TouchTimeField.tsx"],"names":[],"mappings":"AAqEA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAqHhD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,UAAkB,EAClB,QAAgB,EAChB,UAAkB,EAGlB,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,aAAa,EAC3B,WAAW,EAAE,oBAAoB,EACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,aAA0B,EAC1B,YAAY,EACZ,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,cAAc,+BA0ZhB;yBA7be,cAAc"}
@@ -25,6 +25,7 @@ import { useFieldInputStyling } from "../Field/fieldParts.js";
25
25
  import { joinAriaIDs } from "../utils/inputAria.js";
26
26
  import { groupStyles } from "../InputGroup/groupStyles.js";
27
27
  import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
28
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
28
29
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
29
30
  import { useResolvedRequired } from "../hooks/useResolvedRequired.js";
30
31
  import { Icon } from "../Icon/index.js";
@@ -180,6 +181,11 @@ export function TouchTimeField({
180
181
  const [, startTransition] = useTransition();
181
182
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
182
183
  const isBusy = isLoading || optimisticValue !== value;
184
+ const sheetBodyRef = useRef(null);
185
+ // The busy Spinner is decorative; this control announces its busy start
186
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
187
+ // the sheet is open, so it is announced from inside the sheet.
188
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), isSheetOpen ? sheetBodyRef : inputRef);
183
189
  const isEffectivelyDisabled = isDisabled || isLoading;
184
190
  const showsDisabledMessage = isDisabled && !!disabledMessage;
185
191
  const disabledMessageTooltip = useTooltip({
@@ -362,7 +368,8 @@ export function TouchTimeField({
362
368
  className: "x1lliihq x98rzlu xeuugli xc342km xng3xce x1717udv xjbqb8w x1a2a7pz x1ypdohk x16khyan"
363
369
  }
364
370
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
365
- size: "sm"
371
+ size: "sm",
372
+ "aria-hidden": "true"
366
373
  }), hasClear && optimisticValue && !isEffectivelyDisabled && /*#__PURE__*/_jsx(InputClearButton, {
367
374
  label: t('@astryx.timeInput.clearLabel', {
368
375
  label
@@ -377,6 +384,7 @@ export function TouchTimeField({
377
384
  ,
378
385
  padding: 0,
379
386
  children: /*#__PURE__*/_jsxs("div", {
387
+ ref: sheetBodyRef,
380
388
  ...{
381
389
  className: "x9f619 x78zum5 xdt5ytf xh8yej3 xeuugli x1txdalj x1pzlopt x1adxfkp x1awphl8"
382
390
  },
@@ -1 +1 @@
1
- {"version":3,"file":"useTypeahead.d.ts","sourceRoot":"","sources":["../../src/hooks/useTypeahead.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,aAAa,EAAE,MAAM,aAAa,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAE9D;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;IAE/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,GAAG,aAAa,KAAK,OAAO,CAAC;IAC/D,gDAAgD;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA+BD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CA8F7E"}
1
+ {"version":3,"file":"useTypeahead.d.ts","sourceRoot":"","sources":["../../src/hooks/useTypeahead.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,aAAa,EAAE,MAAM,aAAa,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAE9D;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;IAE/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,GAAG,aAAa,KAAK,OAAO,CAAC;IAC/D,gDAAgD;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA+BD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAmG7E"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useTypeahead.ts
7
- * @input Uses React useCallback/useRef
7
+ * @input Uses React useCallback/useEffect/useRef
8
8
  * @output Exports useTypeahead hook for first-character (type-to-focus) search
9
9
  * @position Core hook; adds APG typeahead to menus, listboxes, and other
10
10
  * collections. Composes with useListFocus/useGridFocus (or any collection)
@@ -13,7 +13,7 @@
13
13
  * SYNC: When modified, update:
14
14
  * - /packages/core/src/hooks/index.ts
15
15
  */
16
- import { useCallback, useRef } from 'react';
16
+ import { useCallback, useEffect, useRef } from 'react';
17
17
  /**
18
18
  * Whether a key event represents a single printable character (a type-to-focus
19
19
  * candidate) rather than a control/navigation key or a shortcut chord.
@@ -79,6 +79,11 @@ export function useTypeahead(options) {
79
79
  timeoutRef.current = undefined;
80
80
  }
81
81
  }, []);
82
+
83
+ // A collection that unmounts, or that an `<Activity>` boundary hides,
84
+ // mid-typing leaves no reset timer running and comes back with an empty
85
+ // buffer, as the reset would have left it.
86
+ useEffect(() => reset, [reset]);
82
87
  const scheduleReset = useCallback(() => {
83
88
  if (timeoutRef.current) {
84
89
  clearTimeout(timeoutRef.current);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.4-canary.b25f95e",
3
+ "version": "0.7.4-canary.f32572e",
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",
@@ -792,8 +792,8 @@
792
792
  "react-dom": ">=19.0.0"
793
793
  },
794
794
  "devDependencies": {
795
- "@astryxdesign/a11y-spec": "0.7.4-canary.b25f95e",
796
- "@astryxdesign/cli": "0.7.4-canary.b25f95e",
795
+ "@astryxdesign/a11y-spec": "0.7.4-canary.f32572e",
796
+ "@astryxdesign/cli": "0.7.4-canary.f32572e",
797
797
  "@babel/cli": "^7.29.7",
798
798
  "@babel/core": "^7.29.7",
799
799
  "@babel/preset-react": "^7.29.7",
@@ -65,6 +65,7 @@ import {useCalendarConstraints} from '../Calendar/hooks';
65
65
  import {usePopover} from '../Popover';
66
66
  import {useTooltip} from '../Tooltip';
67
67
  import {useInputContainer} from '../hooks/useInputContainer';
68
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
68
69
  import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
69
70
  import {
70
71
  type ISOTimeString,
@@ -709,6 +710,9 @@ function PointerDateTimeField({
709
710
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
710
711
  const isPendingChange = optimisticValue !== value;
711
712
  const isBusy = isLoading || isPendingChange;
713
+ // The busy Spinner is decorative; this control announces its busy start
714
+ // (spec:AST-086 FR8).
715
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), dateInputRef);
712
716
  // A native wheel emits several edits while it remains open. Disabling the
713
717
  // control after the first optimistic edit detaches the iOS picker, so pending
714
718
  // changeAction work stays interactive just like TouchDateTimeField. Explicit
@@ -1801,7 +1805,7 @@ function PointerDateTimeField({
1801
1805
  onClick={handleClear}
1802
1806
  />
1803
1807
  )}
1804
- {isBusy && <Spinner size="sm" />}
1808
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
1805
1809
  {statusIcon}
1806
1810
  </div>
1807
1811
 
@@ -47,6 +47,7 @@ import {
47
47
  } from '../Field';
48
48
  import {useInputStatusIcon, useMergedRefs} from '../hooks';
49
49
  import {useResolvedRequired} from '../hooks/useResolvedRequired';
50
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
50
51
  import {Icon} from '../Icon';
51
52
  import {IconButton} from '../IconButton';
52
53
  import {useTranslator, InternationalizationContext} from '../i18n';
@@ -576,6 +577,15 @@ export function TouchDateTimeField({
576
577
  });
577
578
 
578
579
  const [isSheetOpen, setIsSheetOpen] = useState(false);
580
+ const sheetBodyRef = useRef<HTMLDivElement | null>(null);
581
+ // The busy Spinner is decorative; this control announces its busy start
582
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
583
+ // the sheet is open, so it is announced from inside the sheet.
584
+ useBusyAnnouncement(
585
+ isBusy,
586
+ t('@astryx.spinner.loading'),
587
+ isSheetOpen ? sheetBodyRef : dateInputRef,
588
+ );
579
589
  const [activePanel, setActivePanel] = useState<TouchDateTimePanel>('date');
580
590
  const [isWheelOpen, setIsWheelOpen] = useState(false);
581
591
  const [draftTime, setDraftTime] = useState<ISOTimeString | undefined>(() =>
@@ -1412,7 +1422,7 @@ export function TouchDateTimeField({
1412
1422
  onClick={handleClear}
1413
1423
  />
1414
1424
  )}
1415
- {isBusy && <Spinner size="sm" />}
1425
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
1416
1426
  {isRenderable(statusIcon) && statusIcon}
1417
1427
  </div>
1418
1428
 
@@ -1497,7 +1507,9 @@ export function TouchDateTimeField({
1497
1507
  // touchSheetBody owns the inset, including the band under the grab
1498
1508
  // handle.
1499
1509
  padding={0}>
1500
- <div {...stylex.props(styles.touchSheetBody)}>{surface}</div>
1510
+ <div ref={sheetBodyRef} {...stylex.props(styles.touchSheetBody)}>
1511
+ {surface}
1512
+ </div>
1501
1513
  </BottomSheet>
1502
1514
  {showsDisabledMessage &&
1503
1515
  disabledMessageTooltip.renderTooltip(disabledMessage)}
@@ -70,6 +70,7 @@ import {
70
70
  import type {SizeValue} from '../utils/types';
71
71
  import {useSize} from '../SizeContext/SizeContext';
72
72
  import {useAnnounce} from '../hooks/useAnnounce';
73
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
73
74
  import {useInputContainer} from '../hooks/useInputContainer';
74
75
  import {useMediaQuery} from '../hooks/useMediaQuery';
75
76
  import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
@@ -475,6 +476,9 @@ function TimeField({
475
476
  const [, startTransition] = useTransition();
476
477
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
477
478
  const isBusy = isLoading || optimisticValue !== value;
479
+ // The busy Spinner is decorative; this control announces its busy start
480
+ // (spec:AST-086 FR8).
481
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), containerRef);
478
482
 
479
483
  // In grouped mode the status message renders as a visually-hidden node that
480
484
  // exists only for aria-describedby. Announce it through the persistent
@@ -865,7 +869,7 @@ function TimeField({
865
869
  </VisuallyHidden>
866
870
  </>
867
871
  )}
868
- {isBusy && <Spinner size="sm" />}
872
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
869
873
  {hasClear && optimisticValue && !isDisabled && (
870
874
  <InputClearButton
871
875
  label={t('@astryx.timeInput.clearLabel', {label})}
@@ -42,6 +42,7 @@ import {useFieldInputStyling} from '../Field/fieldParts';
42
42
  import {joinAriaIDs} from '../utils/inputAria';
43
43
  import {groupStyles} from '../InputGroup/groupStyles';
44
44
  import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
45
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
45
46
  import {useMergedRefs} from '../hooks/useMergedRefs';
46
47
  import {useResolvedRequired} from '../hooks/useResolvedRequired';
47
48
  import {Icon} from '../Icon';
@@ -254,6 +255,15 @@ export function TouchTimeField({
254
255
  const [, startTransition] = useTransition();
255
256
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
256
257
  const isBusy = isLoading || optimisticValue !== value;
258
+ const sheetBodyRef = useRef<HTMLDivElement | null>(null);
259
+ // The busy Spinner is decorative; this control announces its busy start
260
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
261
+ // the sheet is open, so it is announced from inside the sheet.
262
+ useBusyAnnouncement(
263
+ isBusy,
264
+ t('@astryx.spinner.loading'),
265
+ isSheetOpen ? sheetBodyRef : inputRef,
266
+ );
257
267
  const isEffectivelyDisabled = isDisabled || isLoading;
258
268
 
259
269
  const showsDisabledMessage = isDisabled && !!disabledMessage;
@@ -516,7 +526,7 @@ export function TouchTimeField({
516
526
  }
517
527
  {...stylex.props(styles.input)}
518
528
  />
519
- {isBusy && <Spinner size="sm" />}
529
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
520
530
  {hasClear && optimisticValue && !isEffectivelyDisabled && (
521
531
  <InputClearButton
522
532
  label={t('@astryx.timeInput.clearLabel', {label})}
@@ -531,7 +541,7 @@ export function TouchTimeField({
531
541
  height="hug"
532
542
  // sheetBody owns the inset, including the band under the grab handle.
533
543
  padding={0}>
534
- <div {...stylex.props(styles.sheetBody)}>
544
+ <div ref={sheetBodyRef} {...stylex.props(styles.sheetBody)}>
535
545
  <div {...stylex.props(styles.wheels)}>
536
546
  <Wheel
537
547
  label={t('@astryx.dateTimeInput.hourWheel')}
@@ -19,7 +19,7 @@ import {
19
19
  it,
20
20
  vi,
21
21
  } from 'vitest';
22
- import {act, fireEvent, render, screen} from '@testing-library/react';
22
+ import {act, fireEvent, render, screen, within} from '@testing-library/react';
23
23
  import {TextInput} from '../TextInput';
24
24
  import {TextArea} from '../TextArea';
25
25
  import {Selector} from '../Selector';
@@ -27,8 +27,11 @@ import {MultiSelector} from '../MultiSelector';
27
27
  import {ComplexSelector} from '../ComplexSelector';
28
28
  import {DateInput} from '../DateInput';
29
29
  import {DateRangeInput} from '../DateRangeInput';
30
+ import {DateTimeInput} from '../DateTimeInput';
31
+ import {TimeInput} from '../TimeInput';
30
32
  import {BaseTypeahead, Typeahead} from '../Typeahead';
31
33
  import type {SearchableItem, SearchSource} from '../Typeahead';
34
+ import type {ISOTimeString} from '../utils';
32
35
  import {Tokenizer} from '../Tokenizer';
33
36
 
34
37
  const announce = vi.fn();
@@ -104,6 +107,30 @@ const OWNERS: ReadonlyArray<
104
107
  ),
105
108
  ] as const,
106
109
  ),
110
+ ...(['popover', 'native', 'bottom-sheet'] as const).flatMap(presentation => [
111
+ [
112
+ `DateTimeInput (${presentation})`,
113
+ (isLoading: boolean) => (
114
+ <DateTimeInput
115
+ label="When"
116
+ presentation={presentation}
117
+ onChange={() => {}}
118
+ isLoading={isLoading}
119
+ />
120
+ ),
121
+ ] as const,
122
+ [
123
+ `TimeInput (${presentation})`,
124
+ (isLoading: boolean) => (
125
+ <TimeInput
126
+ label="Time"
127
+ presentation={presentation}
128
+ onChange={() => {}}
129
+ isLoading={isLoading}
130
+ />
131
+ ),
132
+ ] as const,
133
+ ]),
107
134
  [
108
135
  'DateRangeInput',
109
136
  isLoading => (
@@ -367,3 +394,77 @@ describe.each(SEARCH_OWNERS)(
367
394
  });
368
395
  },
369
396
  );
397
+
398
+ /**
399
+ * Bottom-sheet fields whose sheet stays open while a changeAction is pending:
400
+ * the sheet is a modal dialog, so the busy start must be announced inside it.
401
+ */
402
+ const SHEET_OWNERS: ReadonlyArray<
403
+ readonly [
404
+ string,
405
+ (changeAction: () => Promise<void>) => ReactElement,
406
+ () => void,
407
+ ]
408
+ > = [
409
+ [
410
+ 'TimeInput',
411
+ changeAction => (
412
+ <TimeInput
413
+ label="Time"
414
+ presentation="bottom-sheet"
415
+ value={'09:00' as ISOTimeString}
416
+ onChange={() => {}}
417
+ changeAction={changeAction}
418
+ />
419
+ ),
420
+ () => {
421
+ fireEvent.click(screen.getByRole('button', {name: /Time/}));
422
+ },
423
+ ],
424
+ ];
425
+
426
+ describe.each(SHEET_OWNERS)(
427
+ '%s busy start from inside its open sheet (spec:AST-086 Platform support)',
428
+ (_name, renderOwner, openSheet) => {
429
+ beforeEach(() => {
430
+ HTMLDialogElement.prototype.showModal = vi.fn(function (
431
+ this: HTMLDialogElement,
432
+ ) {
433
+ this.setAttribute('open', '');
434
+ });
435
+ HTMLDialogElement.prototype.close = vi.fn(function (
436
+ this: HTMLDialogElement,
437
+ ) {
438
+ this.removeAttribute('open');
439
+ });
440
+ const matches = Element.prototype.matches;
441
+ vi.spyOn(Element.prototype, 'matches').mockImplementation(function (
442
+ this: Element,
443
+ selector: string,
444
+ ) {
445
+ return selector === ':modal'
446
+ ? this instanceof HTMLDialogElement && this.open
447
+ : matches.call(this, selector);
448
+ });
449
+ });
450
+ afterEach(() => {
451
+ vi.restoreAllMocks();
452
+ });
453
+
454
+ it('announces the busy start in a region inside the open sheet, not behind it', async () => {
455
+ render(renderOwner(async () => new Promise<void>(() => {})));
456
+ openSheet();
457
+ const sheet = document.querySelector('dialog[open]');
458
+ expect(sheet).not.toBeNull();
459
+ act(() => {
460
+ fireEvent.click(within(sheet as HTMLElement).getAllByRole('option')[1]);
461
+ });
462
+ await nextFrame();
463
+ await nextFrame();
464
+ expect(
465
+ sheet!.querySelector('[data-astryx-modal-busy-region]'),
466
+ ).toHaveTextContent('Loading');
467
+ expect(busyAnnouncements()).toEqual([]);
468
+ });
469
+ },
470
+ );
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
- import {renderHook} from '@testing-library/react';
13
+ import {Activity, type ReactNode} from 'react';
14
+ import {render, renderHook} from '@testing-library/react';
14
15
  import {useTypeahead} from './useTypeahead';
15
16
 
16
17
  const LABELS = ['Apple', 'Apricot', 'Banana', 'Cherry'] as const;
@@ -86,6 +87,46 @@ describe('useTypeahead', () => {
86
87
  expect(onMatch).toHaveBeenLastCalledWith(2);
87
88
  });
88
89
 
90
+ it('clears the pending buffer reset when the component unmounts', () => {
91
+ const {result, unmount} = renderHook(() =>
92
+ useTypeahead({getItemLabels: () => LABELS, onMatch: vi.fn()}),
93
+ );
94
+ result.current.onKeyDown(key('a'));
95
+ expect(vi.getTimerCount()).toBe(1);
96
+ unmount();
97
+ expect(vi.getTimerCount()).toBe(0);
98
+ });
99
+
100
+ it('empties the buffer when an Activity boundary hides the collection', () => {
101
+ const latest: {api: ReturnType<typeof useTypeahead> | null} = {api: null};
102
+ function Probe(): ReactNode {
103
+ latest.api = useTypeahead({
104
+ getItemLabels: () => LABELS,
105
+ onMatch: vi.fn(),
106
+ });
107
+ return null;
108
+ }
109
+ const {rerender} = render(
110
+ <Activity mode="visible">
111
+ <Probe />
112
+ </Activity>,
113
+ );
114
+ latest.api?.onKeyDown(key('a'));
115
+ rerender(
116
+ <Activity mode="hidden">
117
+ <Probe />
118
+ </Activity>,
119
+ );
120
+ expect(vi.getTimerCount()).toBe(0);
121
+ rerender(
122
+ <Activity mode="visible">
123
+ <Probe />
124
+ </Activity>,
125
+ );
126
+ // The typed prefix went with the hide, so a bare Space is not typeahead.
127
+ expect(latest.api?.onKeyDown(key(' '))).toBe(false);
128
+ });
129
+
89
130
  it('skips disabled items', () => {
90
131
  const {onMatch, api} = setup({disabledIndices: [0]}); // Apple disabled
91
132
  api.onKeyDown(key('a'));
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useTypeahead.ts
7
- * @input Uses React useCallback/useRef
7
+ * @input Uses React useCallback/useEffect/useRef
8
8
  * @output Exports useTypeahead hook for first-character (type-to-focus) search
9
9
  * @position Core hook; adds APG typeahead to menus, listboxes, and other
10
10
  * collections. Composes with useListFocus/useGridFocus (or any collection)
@@ -14,7 +14,7 @@
14
14
  * - /packages/core/src/hooks/index.ts
15
15
  */
16
16
 
17
- import {useCallback, useRef} from 'react';
17
+ import {useCallback, useEffect, useRef} from 'react';
18
18
 
19
19
  export interface UseTypeaheadOptions {
20
20
  /**
@@ -135,6 +135,11 @@ export function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn {
135
135
  }
136
136
  }, []);
137
137
 
138
+ // A collection that unmounts, or that an `<Activity>` boundary hides,
139
+ // mid-typing leaves no reset timer running and comes back with an empty
140
+ // buffer, as the reset would have left it.
141
+ useEffect(() => reset, [reset]);
142
+
138
143
  const scheduleReset = useCallback(() => {
139
144
  if (timeoutRef.current) {
140
145
  clearTimeout(timeoutRef.current);