@astryxdesign/core 0.7.3 → 0.7.4-canary.1a552f2
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/BottomSheet/BottomSheet.d.ts +18 -6
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +268 -58
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +41 -12
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +294 -165
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +51 -1
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +7 -11
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +8 -1
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +46 -7
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -0
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +6 -11
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +7 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +6 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +12 -0
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +6 -1
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +6 -1
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +9 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +6 -1
- 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/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +7 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +16 -2
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +6 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +6 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +6 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +6 -1
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +9 -2
- 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/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +9 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +7 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +9 -1
- package/dist/astryx.css +2 -0
- package/dist/hooks/useBusyAnnouncement.d.ts +22 -0
- package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -0
- package/dist/hooks/useBusyAnnouncement.js +87 -0
- package/dist/hooks/useMenuPress.d.ts +2 -2
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +5 -1
- package/dist/hooks/useTypeahead.d.ts.map +1 -1
- package/dist/hooks/useTypeahead.js +7 -2
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +1 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
- package/locales/en.json +6 -2
- package/locales/pseudo.json +3 -0
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +2 -2
- package/src/BottomSheet/BottomSheet.spec.md +3 -5
- package/src/BottomSheet/BottomSheet.tsx +260 -47
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +29 -4
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +11 -10
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +35 -41
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +1051 -49
- package/src/BottomSheet/BottomSheetSwitcher.tsx +396 -215
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +57 -2
- package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +461 -0
- package/src/Carousel/Carousel.test.tsx +8 -12
- package/src/Carousel/Carousel.tsx +6 -13
- package/src/Chat/ChatComposerInput.test.tsx +167 -1
- package/src/Chat/ChatMessageList.test.tsx +6 -2
- package/src/Chat/ChatMessageList.tsx +7 -1
- package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +20 -12
- package/src/Chat/useTriggerMenu.tsx +55 -6
- package/src/CheckboxInput/CheckboxInput.tsx +6 -0
- package/src/CommandPalette/CommandPalette.test.tsx +4 -2
- package/src/CommandPalette/CommandPalette.tsx +6 -11
- package/src/CommandPalette/CommandPaletteInput.test.tsx +3 -1
- package/src/CommandPalette/CommandPaletteInput.tsx +10 -1
- package/src/ComplexSelector/ComplexSelector.tsx +5 -1
- package/src/ContextMenu/ContextMenu.test.tsx +34 -0
- package/src/ContextMenu/ContextMenu.tsx +13 -1
- package/src/DateInput/DateInput.tsx +5 -1
- package/src/DateInput/DateInputTouch.test.tsx +45 -0
- package/src/DateInput/NativeDateField.test.tsx +10 -2
- package/src/DateInput/NativeDateField.tsx +5 -1
- package/src/DateInput/TouchDateField.tsx +14 -2
- package/src/DateRangeInput/DateRangeInput.tsx +5 -1
- package/src/DateTimeInput/DateTimeInput.tsx +5 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +14 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +10 -1
- package/src/FileInput/FileInput.tsx +18 -2
- package/src/MultiSelector/MultiSelector.test.tsx +18 -4
- package/src/MultiSelector/MultiSelector.tsx +5 -1
- package/src/ProgressBar/ProgressBar.test.tsx +4 -21
- package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +114 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +12 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +38 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +123 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +107 -0
- package/src/Selector/Selector.test.tsx +22 -5
- package/src/Selector/Selector.tsx +5 -1
- package/src/TextArea/TextArea.test.tsx +9 -1
- package/src/TextArea/TextArea.tsx +5 -1
- package/src/TextInput/TextInput.tsx +5 -1
- package/src/Thumbnail/Thumbnail.test.tsx +6 -1
- package/src/Thumbnail/Thumbnail.tsx +12 -3
- package/src/TimeInput/TimeInput.tsx +5 -1
- package/src/TimeInput/TouchTimeField.tsx +12 -2
- package/src/Tokenizer/Tokenizer.test.tsx +17 -12
- package/src/Tokenizer/Tokenizer.tsx +10 -1
- package/src/Typeahead/BaseTypeahead.tsx +14 -1
- package/src/Typeahead/Typeahead.test.tsx +32 -41
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +792 -0
- package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +50 -0
- package/src/hooks/useBusyAnnouncement.ts +100 -0
- package/src/hooks/useMenuPress.test.tsx +27 -1
- package/src/hooks/useMenuPress.ts +12 -3
- package/src/hooks/useTypeahead.test.tsx +42 -1
- package/src/hooks/useTypeahead.ts +7 -2
- package/src/i18n/generated-locales/en.generated.ts +1 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MenuPressShadowRoot.a11y.chromium.spec.ts
|
|
5
|
+
* @input The `a11y/Menu press in a shadow root` story in a built Storybook and
|
|
6
|
+
* real mouse input.
|
|
7
|
+
* @output Real-engine proof that a menu rendered inside a shadow root acts on
|
|
8
|
+
* the row under a released pointer (module:DropdownMenu/useMenuPress FR1).
|
|
9
|
+
* @position Run with `pnpm test:a11y-contract`. Only a real engine resolves a
|
|
10
|
+
* point inside a shadow root to the shadow host at the document level.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {expect, test} from '@playwright/test';
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_STORYBOOK_DIR,
|
|
16
|
+
serveStorybook,
|
|
17
|
+
type StaticServer,
|
|
18
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
19
|
+
|
|
20
|
+
let storybook: StaticServer;
|
|
21
|
+
|
|
22
|
+
test.beforeAll(async () => {
|
|
23
|
+
storybook = await serveStorybook(
|
|
24
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test.afterAll(async () => {
|
|
29
|
+
await storybook?.close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const STORY = 'a11y-menu-press-in-a-shadow-root--selector-in-shadow-root';
|
|
33
|
+
|
|
34
|
+
test('a mouse click on an option inside a shadow root selects it', async ({
|
|
35
|
+
page,
|
|
36
|
+
}) => {
|
|
37
|
+
await page.goto(`${storybook.origin}/iframe.html?id=${STORY}&viewMode=story`);
|
|
38
|
+
// Playwright's role locators pierce open shadow roots.
|
|
39
|
+
await page.getByRole('combobox', {name: 'Fruit'}).click();
|
|
40
|
+
const option = page.getByRole('option', {name: 'Banana'});
|
|
41
|
+
await expect(option).toBeVisible();
|
|
42
|
+
const box = await option.boundingBox();
|
|
43
|
+
if (box == null) {
|
|
44
|
+
throw new Error('the Banana option has no layout box');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
48
|
+
|
|
49
|
+
await expect(page.getByTestId('selected-value')).toHaveText('Banana');
|
|
50
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useBusyAnnouncement.ts
|
|
7
|
+
* @input Whether the owning control is busy, the words to announce, and a ref
|
|
8
|
+
* to an element inside the control
|
|
9
|
+
* @output Announces each busy start once, through the shared polite channel
|
|
10
|
+
* or a persistent region inside the open modal dialog holding the control
|
|
11
|
+
* @position Core a11y hook for controls that show a decorative busy Spinner
|
|
12
|
+
*
|
|
13
|
+
* The busy Spinner inside a control is decorative (hidden, no live role); the
|
|
14
|
+
* control announces its busy state instead (`spec:AST-086` FR8), once per busy
|
|
15
|
+
* start (FR6): rerenders and StrictMode replay do not repeat it, and settling
|
|
16
|
+
* re-arms it. While a native modal is open, the body-level channel is not
|
|
17
|
+
* exposed, so a control inside it announces through one persistent polite
|
|
18
|
+
* region in that dialog, inserted empty and filled a frame later (Platform
|
|
19
|
+
* support, FR3).
|
|
20
|
+
*
|
|
21
|
+
* SYNC: deliberately NOT exported from /packages/core/src/hooks/index.ts —
|
|
22
|
+
* this is a shared implementation detail of Astryx's busy controls.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {useEffect, useRef, type RefObject} from 'react';
|
|
26
|
+
import {useAnnounce} from './useAnnounce';
|
|
27
|
+
|
|
28
|
+
const REGION_ATTR = 'data-astryx-modal-busy-region';
|
|
29
|
+
const VISUALLY_HIDDEN_CSS =
|
|
30
|
+
'position:absolute;width:1px;height:1px;margin:-1px;padding:0;' +
|
|
31
|
+
'overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
|
|
32
|
+
const CLEAR_DELAY_MS = 2000; // Matches useAnnounce.
|
|
33
|
+
|
|
34
|
+
// The pending write and clear of each in-dialog region; a new write cancels
|
|
35
|
+
// them so an earlier message can neither land late nor clear a later one.
|
|
36
|
+
const pendingByRegion = new WeakMap<
|
|
37
|
+
HTMLElement,
|
|
38
|
+
{frame: number; timer?: ReturnType<typeof setTimeout>}
|
|
39
|
+
>();
|
|
40
|
+
|
|
41
|
+
function announceInModal(dialog: HTMLDialogElement, message: string): void {
|
|
42
|
+
let region = dialog.querySelector<HTMLElement>(`:scope > [${REGION_ATTR}]`);
|
|
43
|
+
if (region == null) {
|
|
44
|
+
region = document.createElement('div');
|
|
45
|
+
region.setAttribute(REGION_ATTR, '');
|
|
46
|
+
region.setAttribute('role', 'status');
|
|
47
|
+
region.setAttribute('aria-live', 'polite');
|
|
48
|
+
region.setAttribute('aria-atomic', 'true');
|
|
49
|
+
region.style.cssText = VISUALLY_HIDDEN_CSS;
|
|
50
|
+
dialog.appendChild(region);
|
|
51
|
+
}
|
|
52
|
+
const target = region;
|
|
53
|
+
const previous = pendingByRegion.get(target);
|
|
54
|
+
if (previous != null) {
|
|
55
|
+
cancelAnimationFrame(previous.frame);
|
|
56
|
+
clearTimeout(previous.timer);
|
|
57
|
+
}
|
|
58
|
+
target.textContent = '';
|
|
59
|
+
const pending: {frame: number; timer?: ReturnType<typeof setTimeout>} = {
|
|
60
|
+
frame: 0,
|
|
61
|
+
};
|
|
62
|
+
pending.frame = requestAnimationFrame(() => {
|
|
63
|
+
target.textContent = message;
|
|
64
|
+
pending.timer = setTimeout(() => {
|
|
65
|
+
target.textContent = '';
|
|
66
|
+
pendingByRegion.delete(target);
|
|
67
|
+
}, CLEAR_DELAY_MS);
|
|
68
|
+
});
|
|
69
|
+
pendingByRegion.set(target, pending);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useBusyAnnouncement(
|
|
73
|
+
isBusy: boolean,
|
|
74
|
+
message: string,
|
|
75
|
+
elementRef: RefObject<Element | null>,
|
|
76
|
+
): void {
|
|
77
|
+
const announce = useAnnounce();
|
|
78
|
+
const hasAnnouncedRef = useRef(false);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!isBusy) {
|
|
81
|
+
hasAnnouncedRef.current = false;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (hasAnnouncedRef.current) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Choose the route a frame later: a Dialog opening in the same commit
|
|
88
|
+
// calls showModal() in its own Effect, which runs after this one.
|
|
89
|
+
const frame = requestAnimationFrame(() => {
|
|
90
|
+
hasAnnouncedRef.current = true;
|
|
91
|
+
const dialog = elementRef.current?.closest('dialog');
|
|
92
|
+
if (dialog?.open && dialog.matches(':modal')) {
|
|
93
|
+
announceInModal(dialog, message);
|
|
94
|
+
} else {
|
|
95
|
+
announce(message, 'polite');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return () => cancelAnimationFrame(frame);
|
|
99
|
+
}, [announce, elementRef, isBusy, message]);
|
|
100
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
14
|
-
import {act, fireEvent, render, screen} from '@testing-library/react';
|
|
14
|
+
import {act, fireEvent, render, screen, within} from '@testing-library/react';
|
|
15
15
|
import {useRef, useState} from 'react';
|
|
16
16
|
import {
|
|
17
17
|
MENU_PRESS_AUTOSCROLL_ZONE_PX,
|
|
@@ -145,6 +145,32 @@ describe('useMenuPress — tracking inside the menu', () => {
|
|
|
145
145
|
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
it('hit-tests in the shadow root that holds the menu, not the document', () => {
|
|
149
|
+
const host = document.createElement('div');
|
|
150
|
+
document.body.append(host);
|
|
151
|
+
const shadow = host.attachShadow({mode: 'open'});
|
|
152
|
+
const container = document.createElement('div');
|
|
153
|
+
shadow.append(container);
|
|
154
|
+
const onSelect = vi.fn();
|
|
155
|
+
render(<Harness onSelect={onSelect} />, {container});
|
|
156
|
+
const inShadow = (label: string) =>
|
|
157
|
+
within(container).getByRole('menuitem', {name: label});
|
|
158
|
+
// As in a browser: the document answers with the shadow host; only the
|
|
159
|
+
// shadow root knows the row under the point.
|
|
160
|
+
Object.assign(document, {elementFromPoint: () => host});
|
|
161
|
+
Object.assign(shadow, {elementFromPoint: () => inShadow('C')});
|
|
162
|
+
try {
|
|
163
|
+
const events = {...mouse(), composed: true, bubbles: true};
|
|
164
|
+
fireEvent.pointerDown(inShadow('A'), events);
|
|
165
|
+
fireEvent.pointerUp(inShadow('A'), events);
|
|
166
|
+
expect(onSelect).toHaveBeenCalledWith('C');
|
|
167
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
168
|
+
} finally {
|
|
169
|
+
delete (document as {elementFromPoint?: unknown}).elementFromPoint;
|
|
170
|
+
host.remove();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
148
174
|
it('moves the highlight as focus with preventScroll', () => {
|
|
149
175
|
render(<Harness />);
|
|
150
176
|
const b = row('B');
|
|
@@ -209,8 +209,8 @@ export interface UseMenuPressOptions {
|
|
|
209
209
|
getScroller?: () => HTMLElement | null;
|
|
210
210
|
/**
|
|
211
211
|
* Resolve the element under a viewport point. Defaults to
|
|
212
|
-
* `
|
|
213
|
-
* document lacks it (jsdom). A test seam.
|
|
212
|
+
* `elementFromPoint` on the menu's own document or shadow root, falling back
|
|
213
|
+
* to the event target where the document lacks it (jsdom). A test seam.
|
|
214
214
|
*/
|
|
215
215
|
hitTest?: (x: number, y: number) => Element | null;
|
|
216
216
|
/** @default 500 */
|
|
@@ -364,7 +364,16 @@ export function useMenuPress(options: UseMenuPressOptions): UseMenuPressReturn {
|
|
|
364
364
|
if (hitTest != null) {
|
|
365
365
|
element = hitTest(event.clientX, event.clientY);
|
|
366
366
|
} else if (typeof doc.elementFromPoint === 'function') {
|
|
367
|
-
|
|
367
|
+
// Hit-test in the menu's own tree: inside a shadow root the document
|
|
368
|
+
// answers with the shadow host, never the row under the pointer.
|
|
369
|
+
const tree = menu?.getRootNode();
|
|
370
|
+
const scope: Pick<Document, 'elementFromPoint'> =
|
|
371
|
+
tree != null &&
|
|
372
|
+
tree !== doc &&
|
|
373
|
+
typeof (tree as ShadowRoot).elementFromPoint === 'function'
|
|
374
|
+
? (tree as ShadowRoot)
|
|
375
|
+
: doc;
|
|
376
|
+
element = scope.elementFromPoint(event.clientX, event.clientY);
|
|
368
377
|
} else {
|
|
369
378
|
element = event.target instanceof Element ? event.target : null;
|
|
370
379
|
}
|
|
@@ -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);
|
|
@@ -340,6 +340,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
|
|
|
340
340
|
'@astryx.chatToolCalls.status.complete': 'Complete',
|
|
341
341
|
'@astryx.chatToolCalls.status.error': 'Failed',
|
|
342
342
|
'@astryx.chatToolCalls.groupLabel': '{count} tool calls',
|
|
343
|
+
'@astryx.chatTriggerMenu.searching': 'Searching…',
|
|
343
344
|
'@astryx.chatTriggerMenu.suggestions': 'Suggestions',
|
|
344
345
|
'@astryx.citation.label': 'Citation {number}: {title}',
|
|
345
346
|
'@astryx.codeBlock.copied': 'Copied',
|
|
@@ -347,6 +347,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
|
|
|
347
347
|
'@astryx.chatToolCalls.status.complete': '⟦Çóɱþłéţé⟧',
|
|
348
348
|
'@astryx.chatToolCalls.status.error': '⟦Ƒàíłéð⟧',
|
|
349
349
|
'@astryx.chatToolCalls.groupLabel': '⟦{count} ţóół çàłłš⟧',
|
|
350
|
+
'@astryx.chatTriggerMenu.searching': '⟦Šéàřçĥíñĝ…⟧',
|
|
350
351
|
'@astryx.chatTriggerMenu.suggestions': '⟦Šúĝĝéšţíóñš⟧',
|
|
351
352
|
'@astryx.citation.label': '⟦Çíţàţíóñ {number}: {title}⟧',
|
|
352
353
|
'@astryx.codeBlock.copied': '⟦Çóþíéð⟧',
|