@astryxdesign/core 0.7.1 → 0.7.2-canary.374b53a
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/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +38 -14
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +10 -2
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +3 -1
- package/dist/Pagination/Pagination.d.ts +8 -5
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +21 -8
- package/dist/PowerSearch/PowerSearch.js +1 -1
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +6 -2
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -1
- package/dist/Text/useTruncation.d.ts +7 -5
- package/dist/Text/useTruncation.d.ts.map +1 -1
- package/dist/Text/useTruncation.js +16 -5
- 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/dist/astryx.css +1 -0
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +19 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +19 -0
- package/dist/utils/sharedMutationObserver.d.ts +21 -0
- package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
- package/dist/utils/sharedMutationObserver.js +69 -0
- package/locales/en.json +76 -0
- package/locales/pseudo.json +57 -0
- package/package.json +4 -4
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.test.tsx +155 -9
- package/src/Button/Button.tsx +39 -16
- package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
- package/src/Button/__tests__/Button.a11y.states.ts +0 -10
- package/src/HoverCard/HoverCard.test.tsx +58 -0
- package/src/HoverCard/useHoverCard.tsx +10 -2
- package/src/IconButton/IconButton.doc.mjs +2 -2
- package/src/IconButton/IconButton.test.tsx +3 -1
- package/src/List/ListItem.doc.mjs +8 -5
- package/src/MultiSelector/hooks.ts +3 -1
- package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
- package/src/Pagination/Pagination.doc.mjs +7 -7
- package/src/Pagination/Pagination.spec.md +9 -9
- package/src/Pagination/Pagination.test.tsx +242 -0
- package/src/Pagination/Pagination.tsx +27 -11
- package/src/PowerSearch/PowerSearch.test.tsx +85 -0
- package/src/PowerSearch/PowerSearch.tsx +2 -2
- package/src/Selector/hooks.ts +6 -2
- package/src/Selector/useCombobox.doc.mjs +237 -0
- package/src/Switch/Switch.test.tsx +49 -0
- package/src/Switch/Switch.tsx +11 -0
- package/src/Text/useTruncation.test.tsx +69 -0
- package/src/Text/useTruncation.ts +16 -5
- 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
- package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
- package/src/i18n/InternationalizationProvider.spec.md +252 -0
- package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
- package/src/i18n/generated-locales/en.generated.ts +21 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +21 -0
- package/src/i18n/useTranslator.test.tsx +37 -0
- package/src/utils/sharedMutationObserver.test.ts +53 -0
- package/src/utils/sharedMutationObserver.ts +78 -0
|
@@ -22,65 +22,4 @@
|
|
|
22
22
|
|
|
23
23
|
import type {KnownFailure} from '@astryxdesign/a11y-spec';
|
|
24
24
|
|
|
25
|
-
export const BUTTON_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [
|
|
26
|
-
// ---- Button and IconButton go natively disabled while busy --------------
|
|
27
|
-
// One defect, one record per component.
|
|
28
|
-
// They are written out rather than generated, because a record has to name
|
|
29
|
-
// exactly one thing that is broken — a loop would make it easy to widen this
|
|
30
|
-
// later without anyone noticing.
|
|
31
|
-
{
|
|
32
|
-
expectation: 'button.focus.reachable-and-escapable',
|
|
33
|
-
binding: 'Button',
|
|
34
|
-
state: 'button-loading',
|
|
35
|
-
evidenceLayer: 'real-browser',
|
|
36
|
-
failureEquals:
|
|
37
|
-
'10 presses of Tab from the start of the document never reached the button, so a keyboard user cannot get to this action',
|
|
38
|
-
standardsReference:
|
|
39
|
-
'WCAG 2.2 2.1.1 Keyboard and 2.1.2 No Keyboard Trap (Level A)',
|
|
40
|
-
userImpact:
|
|
41
|
-
'A keyboard user who activates Save and waits is dropped to the top of the document the moment the action starts, and the button leaves the tab sequence entirely — so they cannot get back to it, to see that it is busy or to interrupt it. The next Tab restarts from the beginning of the page.',
|
|
42
|
-
reason:
|
|
43
|
-
'Button sets the native disabled attribute while a clickAction is pending. A natively disabled element is neither focusable nor a tab stop. Separate implementation work will turn this record into an unexpected pass.',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
expectation: 'button.focus.reachable-and-escapable',
|
|
47
|
-
binding: 'IconButton',
|
|
48
|
-
state: 'icon-button-loading',
|
|
49
|
-
evidenceLayer: 'real-browser',
|
|
50
|
-
failureEquals:
|
|
51
|
-
'10 presses of Tab from the start of the document never reached the button, so a keyboard user cannot get to this action',
|
|
52
|
-
standardsReference:
|
|
53
|
-
'WCAG 2.2 2.1.1 Keyboard and 2.1.2 No Keyboard Trap (Level A)',
|
|
54
|
-
userImpact:
|
|
55
|
-
'The same drop from an icon button, where it is worse: an icon button is usually one of several in a row, so the user loses their place among them.',
|
|
56
|
-
reason:
|
|
57
|
-
'IconButton is a thin wrapper over Button and inherits the behaviour exactly.',
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
expectation: 'button.unavailable.inert',
|
|
62
|
-
binding: 'Button',
|
|
63
|
-
state: 'button-loading',
|
|
64
|
-
evidenceLayer: 'real-browser',
|
|
65
|
-
failureEquals:
|
|
66
|
-
'this state is declared focusable so its reason stays reachable, but it cannot take focus — so a keyboard user can neither read why it is unavailable nor reach it to confirm it refuses to act',
|
|
67
|
-
standardsReference:
|
|
68
|
-
'Current Astryx family:buttons FR3 and WAI-ARIA APG Button unavailable-state requirement; supports WCAG 2.2 4.1.2 Name, Role, Value (Level A)',
|
|
69
|
-
userImpact:
|
|
70
|
-
'The third face of the same defect: because the busy button cannot take focus, there is no way to confirm from the keyboard that it declines a second press — the user can neither reach it nor see why it is unavailable.',
|
|
71
|
-
reason:
|
|
72
|
-
'Same native disabled attribute. Recorded separately because a record names exactly one outcome, and a fix that restored focus without keeping the button inert would satisfy one of these and not the other.',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
expectation: 'button.unavailable.inert',
|
|
76
|
-
binding: 'IconButton',
|
|
77
|
-
state: 'icon-button-loading',
|
|
78
|
-
evidenceLayer: 'real-browser',
|
|
79
|
-
failureEquals:
|
|
80
|
-
'this state is declared focusable so its reason stays reachable, but it cannot take focus — so a keyboard user can neither read why it is unavailable nor reach it to confirm it refuses to act',
|
|
81
|
-
standardsReference:
|
|
82
|
-
'Current Astryx family:buttons FR3 and WAI-ARIA APG Button unavailable-state requirement; supports WCAG 2.2 4.1.2 Name, Role, Value (Level A)',
|
|
83
|
-
userImpact: 'The same, from an icon button.',
|
|
84
|
-
reason: 'Inherited from Button, as above.',
|
|
85
|
-
},
|
|
86
|
-
];
|
|
25
|
+
export const BUTTON_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [];
|
|
@@ -177,16 +177,9 @@ export const BUTTON_BINDING_STATES = [
|
|
|
177
177
|
binding: 'Button',
|
|
178
178
|
summary:
|
|
179
179
|
'a button waiting on the action it started: the action is unavailable, and it is meant to stay focusable so the user can see that and interrupt it',
|
|
180
|
-
// `focusable: true` is the declaration, and the component does not deliver
|
|
181
|
-
// it — see the known-failure records. Declaring what the component does
|
|
182
|
-
// instead would make the expectations that catch it not-applicable, which
|
|
183
|
-
// is how a contract quietly stops noticing a defect.
|
|
184
180
|
facts: facts({operable: false, unavailable: true}),
|
|
185
181
|
visibleLabel: null,
|
|
186
182
|
storyId: 'a11y-button-pattern--button-loading',
|
|
187
|
-
declaredNotDelivered: [
|
|
188
|
-
{fact: 'focusable', owned: 'button.focus.reachable-and-escapable'},
|
|
189
|
-
],
|
|
190
183
|
},
|
|
191
184
|
|
|
192
185
|
// ---- IconButton ---------------------------------------------------------
|
|
@@ -214,9 +207,6 @@ export const BUTTON_BINDING_STATES = [
|
|
|
214
207
|
facts: facts({operable: false, unavailable: true}),
|
|
215
208
|
visibleLabel: null,
|
|
216
209
|
storyId: 'a11y-button-pattern--icon-button-loading',
|
|
217
|
-
declaredNotDelivered: [
|
|
218
|
-
{fact: 'focusable', owned: 'button.focus.reachable-and-escapable'},
|
|
219
|
-
],
|
|
220
210
|
},
|
|
221
211
|
|
|
222
212
|
// ---- ClickableCard ------------------------------------------------------
|
|
@@ -109,6 +109,64 @@ describe('useHoverCard identity', () => {
|
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
// A card closed while the pointer rests on it unmounts without a mouseleave,
|
|
113
|
+
// so the hook must not keep believing the pointer is over the card: if it
|
|
114
|
+
// does, the next open of the same trigger never closes on leave.
|
|
115
|
+
describe('useHoverCard closing while the pointer is on the card', () => {
|
|
116
|
+
function Harness() {
|
|
117
|
+
const card = useHoverCard({delay: 0, hideDelay: 0});
|
|
118
|
+
return (
|
|
119
|
+
<div>
|
|
120
|
+
<span ref={card.ref}>Trigger</span>
|
|
121
|
+
<output data-testid="state">{card.isOpen ? 'open' : 'closed'}</output>
|
|
122
|
+
{card.renderHoverCard(
|
|
123
|
+
<button type="button" onClick={card.hide}>
|
|
124
|
+
Pick
|
|
125
|
+
</button>,
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function openThenRestOnCard() {
|
|
132
|
+
const state = screen.getByTestId('state');
|
|
133
|
+
fireEvent.mouseEnter(screen.getByText('Trigger'));
|
|
134
|
+
await waitFor(() => expect(state).toHaveTextContent('open'));
|
|
135
|
+
const pick = screen.getByRole('button', {name: 'Pick', hidden: true});
|
|
136
|
+
fireEvent.mouseLeave(screen.getByText('Trigger'));
|
|
137
|
+
fireEvent.mouseEnter(pick.parentElement!);
|
|
138
|
+
return {state, pick};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function expectNextOpenClosesOnLeave(state: HTMLElement) {
|
|
142
|
+
const trigger = screen.getByText('Trigger');
|
|
143
|
+
fireEvent.mouseEnter(trigger);
|
|
144
|
+
await waitFor(() => expect(state).toHaveTextContent('open'));
|
|
145
|
+
fireEvent.mouseLeave(trigger);
|
|
146
|
+
await waitFor(() => expect(state).toHaveTextContent('closed'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
it('closes the next open on leave after hide() from inside the card', async () => {
|
|
150
|
+
render(<Harness />);
|
|
151
|
+
const {state, pick} = await openThenRestOnCard();
|
|
152
|
+
|
|
153
|
+
fireEvent.click(pick);
|
|
154
|
+
await waitFor(() => expect(state).toHaveTextContent('closed'));
|
|
155
|
+
|
|
156
|
+
await expectNextOpenClosesOnLeave(state);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('closes the next open on leave after Escape dismisses the card', async () => {
|
|
160
|
+
render(<Harness />);
|
|
161
|
+
const {state} = await openThenRestOnCard();
|
|
162
|
+
|
|
163
|
+
fireEvent.keyDown(document.body, {key: 'Escape'});
|
|
164
|
+
await waitFor(() => expect(state).toHaveTextContent('closed'));
|
|
165
|
+
|
|
166
|
+
await expectNextOpenClosesOnLeave(state);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
112
170
|
describe('HoverCard', () => {
|
|
113
171
|
it('renders trigger element', () => {
|
|
114
172
|
render(
|
|
@@ -345,6 +345,14 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
345
345
|
hideLayer();
|
|
346
346
|
}, [clearTimeouts, hideLayer]);
|
|
347
347
|
|
|
348
|
+
// The imperative hide. A card closed while the pointer rests on it unmounts
|
|
349
|
+
// without a mouseleave, so the content-hover latch is cleared here; left set,
|
|
350
|
+
// it would keep the trigger's next hover-open from closing on leave.
|
|
351
|
+
const hide = useCallback(() => {
|
|
352
|
+
isHoveringContentRef.current = false;
|
|
353
|
+
hideLayer();
|
|
354
|
+
}, [hideLayer]);
|
|
355
|
+
|
|
348
356
|
const {
|
|
349
357
|
isTouchPointerRef,
|
|
350
358
|
isTouchInteraction,
|
|
@@ -508,7 +516,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
508
516
|
? null
|
|
509
517
|
: document.getElementById(layerId);
|
|
510
518
|
const hadFocus = card?.contains(document.activeElement) ?? false;
|
|
511
|
-
|
|
519
|
+
hide();
|
|
512
520
|
if (hadFocus) {
|
|
513
521
|
isEscapeDismissingRef.current = true;
|
|
514
522
|
triggerRef.current?.focus();
|
|
@@ -720,6 +728,6 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
720
728
|
isOpen: isLayerOpen,
|
|
721
729
|
renderHoverCard,
|
|
722
730
|
show: showLayer,
|
|
723
|
-
hide
|
|
731
|
+
hide,
|
|
724
732
|
};
|
|
725
733
|
}
|
|
@@ -46,7 +46,7 @@ export const docs = {
|
|
|
46
46
|
{
|
|
47
47
|
name: 'isLoading',
|
|
48
48
|
type: 'boolean',
|
|
49
|
-
description: 'Shows a loading spinner and
|
|
49
|
+
description: 'Shows a loading spinner and blocks re-activation while staying focusable (aria-busy + aria-disabled, not native disabled).',
|
|
50
50
|
default: 'false',
|
|
51
51
|
},
|
|
52
52
|
{
|
|
@@ -141,7 +141,7 @@ export const docsDense = {
|
|
|
141
141
|
variant: 'visual style variant',
|
|
142
142
|
size: 'size variant',
|
|
143
143
|
elevation: 'resting shadow depth: none|low|med|high; raise for a floating action button (FAB)',
|
|
144
|
-
isLoading: 'shows loading spinner
|
|
144
|
+
isLoading: 'shows loading spinner, blocks re-activation but stays focusable (aria-disabled, not native disabled)',
|
|
145
145
|
isDisabled: 'disables button',
|
|
146
146
|
tooltip: 'tooltip text shown on hover',
|
|
147
147
|
onClick: 'standard click handler',
|
|
@@ -124,7 +124,9 @@ describe('IconButton', () => {
|
|
|
124
124
|
it('shows loading state', () => {
|
|
125
125
|
render(<IconButton label="Save" icon={<span>💾</span>} isLoading />);
|
|
126
126
|
const button = screen.getByRole('button');
|
|
127
|
-
|
|
127
|
+
// Busy is aria-disabled (focusable), not native disabled — see #4871.
|
|
128
|
+
expect(button).not.toBeDisabled();
|
|
129
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
it('forwards ref correctly', () => {
|
|
@@ -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).',
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* @file hooks.ts
|
|
7
7
|
* @input Uses React hooks, MultiSelectorOptionData type
|
|
8
8
|
* @output Exports useMultiCombobox hook
|
|
9
|
-
* @position
|
|
9
|
+
* @position Public hook, exported from the MultiSelector entry point; used by
|
|
10
|
+
* MultiSelector.tsx
|
|
10
11
|
*
|
|
11
12
|
* SYNC: When modified, update:
|
|
12
13
|
* - /packages/core/src/MultiSelector/index.ts
|
|
14
|
+
* - /packages/core/src/MultiSelector/useMultiCombobox.doc.mjs
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import {useCallback, useRef, useState} from 'react';
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').HookDoc} */
|
|
4
|
+
export const docs = {
|
|
5
|
+
name: 'useMultiCombobox',
|
|
6
|
+
displayName: 'useMultiCombobox',
|
|
7
|
+
group: 'MultiSelector',
|
|
8
|
+
keywords: [
|
|
9
|
+
'combobox',
|
|
10
|
+
'multi-select',
|
|
11
|
+
'multiselect',
|
|
12
|
+
'listbox',
|
|
13
|
+
'grid',
|
|
14
|
+
'toggle',
|
|
15
|
+
'highlight',
|
|
16
|
+
'active option',
|
|
17
|
+
'aria-activedescendant',
|
|
18
|
+
'keyboard',
|
|
19
|
+
'headless',
|
|
20
|
+
'apg',
|
|
21
|
+
'accessibility',
|
|
22
|
+
'a11y',
|
|
23
|
+
],
|
|
24
|
+
params: [
|
|
25
|
+
{
|
|
26
|
+
name: 'options',
|
|
27
|
+
type: 'UseMultiComboboxOptions',
|
|
28
|
+
description: 'Configuration object.',
|
|
29
|
+
required: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'options.selectableItems',
|
|
33
|
+
type: 'MultiSelectorOptionData[]',
|
|
34
|
+
description:
|
|
35
|
+
'The options in the same order as they render. Options with `disabled` cannot be toggled and are skipped while moving through the open list; ArrowDown or ArrowUp that opens the list still lands on the first or last option.',
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'options.isOpen',
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: 'Whether the list is shown. The caller owns this state.',
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'options.onOpen',
|
|
46
|
+
type: '() => unknown',
|
|
47
|
+
description:
|
|
48
|
+
'Called to show the list. Return `false` to refuse; the hook then leaves the highlight alone.',
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'options.onClose',
|
|
53
|
+
type: '() => void',
|
|
54
|
+
description: 'Called to hide the list. The highlight resets first.',
|
|
55
|
+
required: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'options.onToggle',
|
|
59
|
+
type: '(itemValue: string) => void',
|
|
60
|
+
description:
|
|
61
|
+
'Called with the value of the option to add or remove. Toggling keeps the list open.',
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'options.listboxId',
|
|
66
|
+
type: 'string',
|
|
67
|
+
description:
|
|
68
|
+
"The listbox (or grid) element's id. Option and action-cell ids are derived from it.",
|
|
69
|
+
required: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'options.hasSearch',
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description:
|
|
75
|
+
'Whether the list has its own search field. Space then types instead of toggling, opening by a press or Enter does not pre-highlight (ArrowDown and ArrowUp still open on the first and last option), Delete and Backspace edit the query, and type-to-select is off. Home and End still jump to the first and last option, so a search input that keeps them for the caret should stop them before this handler and rely on PageUp and PageDown.',
|
|
76
|
+
default: 'false',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'options.onClear',
|
|
80
|
+
type: '() => void',
|
|
81
|
+
description:
|
|
82
|
+
'Clears every selected value. When provided, Delete or Backspace on the closed control clears them, so clearing is not pointer-only. Ignored while the list is open or with `hasSearch`.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'options.hasValue',
|
|
86
|
+
type: 'boolean',
|
|
87
|
+
description:
|
|
88
|
+
'Whether anything is selected. The Delete and Backspace clear is skipped when false.',
|
|
89
|
+
default: 'false',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'options.isGrid',
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
description:
|
|
95
|
+
"Whether each row pairs an option cell with an action cell. The inline-end arrow then moves the highlight to the row's action and the inline-start arrow back, following visual direction in right-to-left layouts, and Enter on the action runs `onActivateAction` instead of toggling.",
|
|
96
|
+
default: 'false',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'options.rowHasAction',
|
|
100
|
+
type: '(index: number) => boolean',
|
|
101
|
+
description:
|
|
102
|
+
'In a grid, whether the row at `index` has an action cell to move to.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'options.onActivateAction',
|
|
106
|
+
type: '(index: number) => void',
|
|
107
|
+
description:
|
|
108
|
+
"In a grid, runs the control in the highlighted row's action cell. The selection does not change.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'options.isDisabled',
|
|
112
|
+
type: 'boolean',
|
|
113
|
+
description: 'Whether the control ignores clicks and keys.',
|
|
114
|
+
default: 'false',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
returns: [
|
|
118
|
+
{
|
|
119
|
+
name: 'highlightedIndex',
|
|
120
|
+
type: 'number',
|
|
121
|
+
description: 'Index of the active row, or -1 when none is active.',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'setHighlightedIndex',
|
|
125
|
+
type: '(index: number) => void',
|
|
126
|
+
description:
|
|
127
|
+
"Sets the active row, for example after filtering. The highlight lands on the row's option cell.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'highlightedCell',
|
|
131
|
+
type: "'option' | 'action'",
|
|
132
|
+
description:
|
|
133
|
+
"Which cell of the active row is highlighted; `'action'` only in a grid.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'activeDescendantId',
|
|
137
|
+
type: 'string | undefined',
|
|
138
|
+
description:
|
|
139
|
+
"The id to pass as the control's `aria-activedescendant`: the active option, or its action cell in a grid. Undefined while the list is closed or nothing is active.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'getItemId',
|
|
143
|
+
type: '(index: number) => string',
|
|
144
|
+
description: 'The id to render on the option at `index`.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'getActionCellId',
|
|
148
|
+
type: '(index: number) => string',
|
|
149
|
+
description:
|
|
150
|
+
'In a grid, the id to render on the action cell of row `index`.',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'onTriggerClick',
|
|
154
|
+
type: '() => void',
|
|
155
|
+
description: 'Toggles the list from a press on the control.',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'onKeyDown',
|
|
159
|
+
type: '(e: React.KeyboardEvent) => void',
|
|
160
|
+
description:
|
|
161
|
+
'Key handler for the control that keeps focus. ArrowDown and ArrowUp open the list or move the active row; Enter (and Space without search) toggles the active option and keeps the list open; Escape and Tab close it; Home, End, PageUp and PageDown jump to the first or last option; without search, typed characters move to the first option whose label starts with them.',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'onItemMouseEnter',
|
|
165
|
+
type: "(item: MultiSelectorOptionData, index: number, cell?: 'option' | 'action') => void",
|
|
166
|
+
description:
|
|
167
|
+
'Makes the hovered option, or its action cell, active without scrolling the list or moving focus.',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
usage: {
|
|
171
|
+
description:
|
|
172
|
+
'Tracks the active option of a multi-select combobox and handles its keys, while DOM focus stays on the control. It powers MultiSelector; unlike useCombobox, toggling an option keeps the list open, and rows can carry an action cell. It renders nothing and sets no attributes: the caller renders the control and the list, and wires the ARIA attributes from what it returns.',
|
|
173
|
+
bestPractices: [
|
|
174
|
+
{
|
|
175
|
+
guidance: true,
|
|
176
|
+
description:
|
|
177
|
+
"Render each option with `id={getItemId(index)}` and pass `activeDescendantId` as the control's `aria-activedescendant`.",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
guidance: true,
|
|
181
|
+
description:
|
|
182
|
+
'Use `isGrid` only when rows carry a control next to the option; give each action cell `id={getActionCellId(index)}`.',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
guidance: true,
|
|
186
|
+
description:
|
|
187
|
+
'Announce the number of results through useAnnounce when the options change for a new query.',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
guidance: false,
|
|
191
|
+
description:
|
|
192
|
+
'Close the list after each toggle; a multi-select stays open so several options can be picked in a row.',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
relatedComponents: ['MultiSelector'],
|
|
197
|
+
relatedHooks: ['useCombobox', 'useAnnounce'],
|
|
198
|
+
importPath: '@astryxdesign/core/MultiSelector',
|
|
199
|
+
category: 'focus',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */
|
|
203
|
+
export const docsDense = {
|
|
204
|
+
description:
|
|
205
|
+
'Multi-select combobox state + keys; focus stays on control. Powers MultiSelector. Toggle keeps list open; rows may carry an action cell (grid). Renders nothing, sets no attrs: caller renders + wires ARIA.',
|
|
206
|
+
paramDescriptions: {
|
|
207
|
+
options: 'config.',
|
|
208
|
+
'options.selectableItems':
|
|
209
|
+
'options in render order; disabled ones not toggleable, skipped while moving in open list (ArrowDown/Up opening still lands first / last).',
|
|
210
|
+
'options.isOpen': 'list shown; caller owns state.',
|
|
211
|
+
'options.onOpen':
|
|
212
|
+
'show list; return false to refuse (highlight untouched).',
|
|
213
|
+
'options.onClose': 'hide list; highlight resets first.',
|
|
214
|
+
'options.onToggle': 'add / remove value; list stays open.',
|
|
215
|
+
'options.listboxId':
|
|
216
|
+
'listbox / grid id; option + action-cell ids derive from it.',
|
|
217
|
+
'options.hasSearch':
|
|
218
|
+
'own search field: Space types, press / Enter opening does not pre-highlight (ArrowDown/Up still first / last), Delete / Backspace edit query, no type-to-select. Home / End still jump; stop them first to keep them for the caret, use PageUp / PageDown.',
|
|
219
|
+
'options.onClear':
|
|
220
|
+
'clear all; Delete / Backspace on closed control. ignored when open or hasSearch.',
|
|
221
|
+
'options.hasValue': 'anything selected; clear skipped when false.',
|
|
222
|
+
'options.isGrid':
|
|
223
|
+
'rows = option + action cell; inline-end / inline-start arrows move between them (RTL-aware); Enter on action -> onActivateAction.',
|
|
224
|
+
'options.rowHasAction': 'grid: row at index has an action cell.',
|
|
225
|
+
'options.onActivateAction':
|
|
226
|
+
"grid: run highlighted row's action; selection unchanged.",
|
|
227
|
+
'options.isDisabled': 'ignore clicks + keys.',
|
|
228
|
+
},
|
|
229
|
+
returnDescriptions: {
|
|
230
|
+
highlightedIndex: 'active row index, -1 = none.',
|
|
231
|
+
setHighlightedIndex: 'set active row (lands on option cell).',
|
|
232
|
+
highlightedCell: "'option' | 'action' (action only in grid).",
|
|
233
|
+
activeDescendantId:
|
|
234
|
+
'control aria-activedescendant: active option or its action cell; undefined when closed / none.',
|
|
235
|
+
getItemId: 'id for option at index.',
|
|
236
|
+
getActionCellId: 'grid: id for action cell of row index.',
|
|
237
|
+
onTriggerClick: 'toggle list from a press on control.',
|
|
238
|
+
onKeyDown:
|
|
239
|
+
'control keydown: ArrowDown/Up open or move; Enter (Space w/o search) toggles, stays open; Escape / Tab close; Home / End / PageUp / PageDown jump; typed chars jump by label prefix (w/o search).',
|
|
240
|
+
onItemMouseEnter:
|
|
241
|
+
'hover makes option / action cell active; no scroll, no focus move.',
|
|
242
|
+
},
|
|
243
|
+
usage: {
|
|
244
|
+
description:
|
|
245
|
+
'Headless multi-select combobox: active option + keys, focus stays on control; toggle keeps list open. Caller renders + wires ARIA.',
|
|
246
|
+
bestPractices: [
|
|
247
|
+
{
|
|
248
|
+
guidance: true,
|
|
249
|
+
description:
|
|
250
|
+
'Option id = getItemId(index); control aria-activedescendant = activeDescendantId.',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
guidance: true,
|
|
254
|
+
description:
|
|
255
|
+
'isGrid only when rows carry a control; action cell id = getActionCellId(index).',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
guidance: true,
|
|
259
|
+
description: 'Announce result count via useAnnounce on new query.',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
guidance: false,
|
|
263
|
+
description: 'Close after each toggle; multi-select stays open.',
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
};
|
|
@@ -137,7 +137,7 @@ export const docs = {
|
|
|
137
137
|
name: 'variant',
|
|
138
138
|
type: "'pages' | 'count' | 'compact' | 'dots' | 'input' | 'none'",
|
|
139
139
|
description:
|
|
140
|
-
"Visual variant controlling what appears between prev/next buttons. 'pages' shows page number buttons with ellipsis, 'count' shows 'X-Y of Z' text, 'compact' shows 'Page X of Y', 'dots' shows dot indicators, 'input' shows an editable page-number box with a leading label ('Page [ n ] / N') flanked by first/last buttons by default (the box needs a known total to clamp against, so it is disabled in cursor/hasMore mode), 'none' shows just prev/next buttons.",
|
|
140
|
+
"Visual variant controlling what appears between prev/next buttons. 'pages' shows page number buttons with ellipsis, 'count' shows 'X-Y of Z' text, 'compact' shows 'Page X of Y' (add first/last buttons with hasFirstLast), 'dots' shows dot indicators, 'input' shows an editable page-number box with a leading label ('Page [ n ] / N') flanked by first/last buttons by default (the box needs a known total to clamp against, so it is disabled in cursor/hasMore mode), 'none' shows just prev/next buttons.",
|
|
141
141
|
default: "'pages'",
|
|
142
142
|
},
|
|
143
143
|
{
|
|
@@ -150,8 +150,8 @@ export const docs = {
|
|
|
150
150
|
name: 'hasFirstLast',
|
|
151
151
|
type: 'boolean',
|
|
152
152
|
description:
|
|
153
|
-
"Whether to show first/last («/») double-chevron buttons flanking prev/next.
|
|
154
|
-
default:
|
|
153
|
+
"Whether to show first/last («/») double-chevron buttons flanking prev/next. Applies to the 'input' variant (shown unless false) and the 'compact' variant (shown only when true); other variants ignore it. Omitted when the page count is unknown (cursor/hasMore pagination).",
|
|
154
|
+
default: "true for 'input'; false for 'compact'",
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
name: 'step',
|
|
@@ -288,7 +288,7 @@ export const docsZh = {
|
|
|
288
288
|
name: 'variant',
|
|
289
289
|
type: "'pages' | 'count' | 'compact' | 'dots' | 'input' | 'none'",
|
|
290
290
|
description:
|
|
291
|
-
"控制上一页/下一页按钮之间显示内容的视觉变体。'pages' 显示带省略号的页码按钮,'count' 显示 'X-Y of Z' 文本,'compact' 显示 'Page X of Y'
|
|
291
|
+
"控制上一页/下一页按钮之间显示内容的视觉变体。'pages' 显示带省略号的页码按钮,'count' 显示 'X-Y of Z' 文本,'compact' 显示 'Page X of Y'(可通过 hasFirstLast 添加首页/末页按钮),'dots' 显示点指示器,'input' 显示带前置标签的可编辑页码框——'Page [ n ] / N'——默认两侧带首页/末页按钮,'none' 仅显示上一页/下一页按钮。",
|
|
292
292
|
default: "'pages'",
|
|
293
293
|
},
|
|
294
294
|
{
|
|
@@ -301,8 +301,8 @@ export const docsZh = {
|
|
|
301
301
|
name: 'hasFirstLast',
|
|
302
302
|
type: 'boolean',
|
|
303
303
|
description:
|
|
304
|
-
"
|
|
305
|
-
default: 'true',
|
|
304
|
+
"是否显示首页/末页(«/»)双箭头按钮,位于上一页/下一页两侧。适用于 'input' 变体(除非设为 false 否则显示)和 'compact' 变体(仅在设为 true 时显示);其他变体忽略此项。当页数未知(游标/hasMore 分页)时省略。",
|
|
305
|
+
default: "'input' 为 true;'compact' 为 false",
|
|
306
306
|
},
|
|
307
307
|
{
|
|
308
308
|
name: 'step',
|
|
@@ -400,7 +400,7 @@ export const docsDense = {
|
|
|
400
400
|
onPageSizeChange: 'Called on page size change. Auto resets to page 1.',
|
|
401
401
|
variant: "Display between prev/next. 'input' = 'Page [n] / N' + first/last.",
|
|
402
402
|
pageLabel: "Noun before the 'input' box (e.g. 'Page' or 'Row'). Navigation stays page-based. Default localized 'Page'.",
|
|
403
|
-
hasFirstLast: "Show first/last («/») buttons
|
|
403
|
+
hasFirstLast: "Show first/last («/») buttons. 'input' default true; 'compact' default false (opt in). Other variants ignore. Omitted when total unknown.",
|
|
404
404
|
step: 'Pages the prev/next buttons advance per click. Clamped to range; < 1 or non-integer falls back to 1.',
|
|
405
405
|
siblingCount: "Page buttons each side of current; only variant='pages'.",
|
|
406
406
|
size: 'Control size.',
|
|
@@ -92,15 +92,15 @@ and usage remain documented in `Pagination.doc.mjs`.
|
|
|
92
92
|
|
|
93
93
|
### Representative states
|
|
94
94
|
|
|
95
|
-
| State | Required invariant
|
|
96
|
-
| ----------------------------- |
|
|
97
|
-
| Numbered, known page count | Page number buttons render; Ellipsis appears only where pages are omitted.
|
|
98
|
-
| Count with known item total | Count readout renders between Previous/next buttons.
|
|
99
|
-
| Compact with known page count | Compact readout renders between Previous/next buttons.
|
|
100
|
-
| Dots with known page count | One Dot renders for each page and active state stays on the Dot target.
|
|
101
|
-
| Input with known page count | Page input label, Page input, and Page input total render; First/last may render.
|
|
102
|
-
| Input with unknown page count | Page input label and disabled Page input remain; total and First/last are absent.
|
|
103
|
-
| None | Previous/next buttons remain without a presentation-specific part between them.
|
|
95
|
+
| State | Required invariant | Allowed variation |
|
|
96
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
97
|
+
| Numbered, known page count | Page number buttons render; Ellipsis appears only where pages are omitted. | Page count, sibling range, and active page. |
|
|
98
|
+
| Count with known item total | Count readout renders between Previous/next buttons. | Range and total text. |
|
|
99
|
+
| Compact with known page count | Compact readout renders between Previous/next buttons; First/last render only when opted in. | Current and total page text and first/last opt-in. |
|
|
100
|
+
| Dots with known page count | One Dot renders for each page and active state stays on the Dot target. | Dot count and active Dot. |
|
|
101
|
+
| Input with known page count | Page input label, Page input, and Page input total render; First/last may render. | Label text and first/last opt-out. |
|
|
102
|
+
| Input with unknown page count | Page input label and disabled Page input remain; total and First/last are absent. | Cursor-backed current page. |
|
|
103
|
+
| None | Previous/next buttons remain without a presentation-specific part between them. | Enabled state follows current paging availability. |
|
|
104
104
|
|
|
105
105
|
The optional Page-size selector may precede the paging controls in any current
|
|
106
106
|
presentation when page-size options are provided.
|