@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.
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +6 -1
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +9 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +9 -1
- package/dist/hooks/useTypeahead.d.ts.map +1 -1
- package/dist/hooks/useTypeahead.js +7 -2
- package/package.json +3 -3
- package/src/DateTimeInput/DateTimeInput.tsx +5 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +14 -2
- package/src/TimeInput/TimeInput.tsx +5 -1
- package/src/TimeInput/TouchTimeField.tsx +12 -2
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +102 -1
- package/src/hooks/useTypeahead.test.tsx +42 -1
- package/src/hooks/useTypeahead.ts +7 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DateTimeInput.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/DateTimeInput.tsx"],"names":[],"mappings":"AA4CA,OAAO,EAGL,KAAK,WAAW,EAKjB,MAAM,UAAU,CAAC;AAIlB,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;
|
|
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":"
|
|
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;
|
|
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":"
|
|
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,
|
|
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.
|
|
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.
|
|
796
|
-
"@astryxdesign/cli": "0.7.4-canary.
|
|
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)}>
|
|
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 {
|
|
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);
|