@primitiv-ui/core 0.1.62 → 0.2.1
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/combobox/policy.d.ts +62 -0
- package/dist/combobox/policy.js +50 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/listbox/policy.d.ts +93 -0
- package/dist/listbox/policy.js +120 -0
- package/dist/select/policy.d.ts +11 -0
- package/dist/select/policy.js +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A key press in a combobox's text field, with what the rules read. Each
|
|
3
|
+
* surface builds it from wherever it keeps state — React state, or a custom
|
|
4
|
+
* element's attributes — and applies the outcome back the same way.
|
|
5
|
+
*/
|
|
6
|
+
export type ComboboxKeyState = {
|
|
7
|
+
/** The key pressed. */
|
|
8
|
+
key: string;
|
|
9
|
+
/** Whether the popup is open. */
|
|
10
|
+
open: boolean;
|
|
11
|
+
/** The options the cursor can land on, in order: shown and enabled. */
|
|
12
|
+
values: readonly string[];
|
|
13
|
+
/** The option under the cursor (`aria-activedescendant`), if any. */
|
|
14
|
+
active: string | undefined;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* What a handled key asks for: open the popup, move the cursor, commit the
|
|
18
|
+
* option under it, or dismiss — close and put the committed text back.
|
|
19
|
+
*/
|
|
20
|
+
export type ComboboxKeyOutcome = {
|
|
21
|
+
open: true;
|
|
22
|
+
} | {
|
|
23
|
+
cursor: string;
|
|
24
|
+
} | {
|
|
25
|
+
commit: string;
|
|
26
|
+
} | {
|
|
27
|
+
dismiss: true;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The keyboard contract of a combobox's text field (WAI-ARIA APG combobox).
|
|
31
|
+
* Real focus never leaves the field; the cursor is virtual.
|
|
32
|
+
*
|
|
33
|
+
* - **Closed** — ArrowDown and ArrowUp open the popup without placing a
|
|
34
|
+
* cursor. Every other key, Escape and Enter included, is left to the field
|
|
35
|
+
* and the page, so an enclosing dialog or form still hears them.
|
|
36
|
+
* - **Open** — ArrowDown and ArrowUp move the cursor and wrap; with no cursor
|
|
37
|
+
* (or one on an option that has gone) they enter from the near end. Home and
|
|
38
|
+
* End jump to the ends. Enter commits the option under the cursor, and
|
|
39
|
+
* Escape dismisses.
|
|
40
|
+
*
|
|
41
|
+
* Returns `null` for a key the combobox leaves alone (no `preventDefault`):
|
|
42
|
+
* text keys and the horizontal arrows belong to the field.
|
|
43
|
+
*/
|
|
44
|
+
export declare function comboboxKeydown(state: ComboboxKeyState): ComboboxKeyOutcome | null;
|
|
45
|
+
/** The text the user has typed, and the value committed before it. */
|
|
46
|
+
export type ComboboxQueryState = {
|
|
47
|
+
/** The field's text after the keystroke. */
|
|
48
|
+
query: string;
|
|
49
|
+
/** The committed value, `""` when nothing is chosen. */
|
|
50
|
+
value: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* What a keystroke in the field does. Typing always opens the popup: the user
|
|
54
|
+
* is narrowing a list, so the list has to be on screen to be narrowed. And
|
|
55
|
+
* emptying the field is a deselect, so `clear` asks for the value to be cleared
|
|
56
|
+
* too — only when there is one, so an already-empty field never reports a
|
|
57
|
+
* change the user did not make.
|
|
58
|
+
*/
|
|
59
|
+
export declare function comboboxQuery(state: ComboboxQueryState): {
|
|
60
|
+
open: true;
|
|
61
|
+
clear: boolean;
|
|
62
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getKeyToActionMap } from "../keyboard/getKeyToActionMap.js";
|
|
2
|
+
/**
|
|
3
|
+
* The keyboard contract of a combobox's text field (WAI-ARIA APG combobox).
|
|
4
|
+
* Real focus never leaves the field; the cursor is virtual.
|
|
5
|
+
*
|
|
6
|
+
* - **Closed** — ArrowDown and ArrowUp open the popup without placing a
|
|
7
|
+
* cursor. Every other key, Escape and Enter included, is left to the field
|
|
8
|
+
* and the page, so an enclosing dialog or form still hears them.
|
|
9
|
+
* - **Open** — ArrowDown and ArrowUp move the cursor and wrap; with no cursor
|
|
10
|
+
* (or one on an option that has gone) they enter from the near end. Home and
|
|
11
|
+
* End jump to the ends. Enter commits the option under the cursor, and
|
|
12
|
+
* Escape dismisses.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` for a key the combobox leaves alone (no `preventDefault`):
|
|
15
|
+
* text keys and the horizontal arrows belong to the field.
|
|
16
|
+
*/
|
|
17
|
+
export function comboboxKeydown(state) {
|
|
18
|
+
const { key, open, values, active } = state;
|
|
19
|
+
const action = getKeyToActionMap({ orientation: "vertical", homeEnd: true })[key];
|
|
20
|
+
if (!open)
|
|
21
|
+
return action === "next" || action === "prev" ? { open: true } : null;
|
|
22
|
+
if (key === "Escape")
|
|
23
|
+
return { dismiss: true };
|
|
24
|
+
const current = values.indexOf(active);
|
|
25
|
+
if (key === "Enter")
|
|
26
|
+
return current === -1 ? null : { commit: values[current] };
|
|
27
|
+
if (!action || values.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
const last = values.length - 1;
|
|
30
|
+
let target;
|
|
31
|
+
if (action === "first")
|
|
32
|
+
target = 0;
|
|
33
|
+
else if (action === "last")
|
|
34
|
+
target = last;
|
|
35
|
+
else if (current === -1)
|
|
36
|
+
target = action === "next" ? 0 : last;
|
|
37
|
+
else
|
|
38
|
+
target = action === "next" ? (current + 1) % values.length : (current + last) % values.length;
|
|
39
|
+
return { cursor: values[target] };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What a keystroke in the field does. Typing always opens the popup: the user
|
|
43
|
+
* is narrowing a list, so the list has to be on screen to be narrowed. And
|
|
44
|
+
* emptying the field is a deselect, so `clear` asks for the value to be cleared
|
|
45
|
+
* too — only when there is one, so an already-empty field never reports a
|
|
46
|
+
* change the user did not make.
|
|
47
|
+
*/
|
|
48
|
+
export function comboboxQuery(state) {
|
|
49
|
+
return { open: true, clear: state.query === "" && state.value !== "" };
|
|
50
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,3 +16,6 @@ export { modalPointerOutside, modalTabKeydown, type ModalBox, type ModalTabOutco
|
|
|
16
16
|
export { panelPlacement, type PanelPlacement, type PlacementBox } from "./placement/placement.js";
|
|
17
17
|
export { popoverKeydown, popoverPressDismisses, type PopoverKeyOutcome, type PopoverPress, } from "./popover/policy.js";
|
|
18
18
|
export { TOOLTIP_GRACE_MS, tooltipOpenDelay, type TooltipDelayState } from "./tooltip/policy.js";
|
|
19
|
+
export { LISTBOX_TYPEAHEAD_RESET_MS, listboxKeydown, listboxSeed, listboxSelect, listboxTypeahead, type ListboxKey, type ListboxKeyOutcome, type ListboxOrientation, type ListboxReadingDirection, type ListboxState, type ListboxTypeaheadOutcome, type ListboxTypeaheadState, } from "./listbox/policy.js";
|
|
20
|
+
export { SELECT_OPTION_SELECTOR, selectKeydown } from "./select/policy.js";
|
|
21
|
+
export { comboboxKeydown, comboboxQuery, type ComboboxKeyOutcome, type ComboboxKeyState, type ComboboxQueryState, } from "./combobox/policy.js";
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,6 @@ export { modalPointerOutside, modalTabKeydown, } from "./modal/policy.js";
|
|
|
16
16
|
export { panelPlacement } from "./placement/placement.js";
|
|
17
17
|
export { popoverKeydown, popoverPressDismisses, } from "./popover/policy.js";
|
|
18
18
|
export { TOOLTIP_GRACE_MS, tooltipOpenDelay } from "./tooltip/policy.js";
|
|
19
|
+
export { LISTBOX_TYPEAHEAD_RESET_MS, listboxKeydown, listboxSeed, listboxSelect, listboxTypeahead, } from "./listbox/policy.js";
|
|
20
|
+
export { SELECT_OPTION_SELECTOR, selectKeydown } from "./select/policy.js";
|
|
21
|
+
export { comboboxKeydown, comboboxQuery, } from "./combobox/policy.js";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Layout axis of the list: which arrow pair moves the cursor. */
|
|
2
|
+
export type ListboxOrientation = "horizontal" | "vertical";
|
|
3
|
+
/** Reading direction. Under `"rtl"` the horizontal arrows are mirrored. */
|
|
4
|
+
export type ListboxReadingDirection = "ltr" | "rtl";
|
|
5
|
+
/**
|
|
6
|
+
* Everything Listbox's selection and keyboard rules depend on, as plain data.
|
|
7
|
+
* Each surface builds it from wherever it keeps state — React state, or a
|
|
8
|
+
* custom element's attributes — and applies the outcome back the same way.
|
|
9
|
+
*/
|
|
10
|
+
export type ListboxState = {
|
|
11
|
+
/** The enabled options' values, in DOM order. Disabled options are not navigable. */
|
|
12
|
+
values: readonly string[];
|
|
13
|
+
/** The selected values, in the order they were selected. */
|
|
14
|
+
selected: readonly string[];
|
|
15
|
+
/** The option under the cursor (`aria-activedescendant`), if any. */
|
|
16
|
+
active: string | undefined;
|
|
17
|
+
/** Whether more than one option may be selected (`type="multiple"`). */
|
|
18
|
+
multiple: boolean;
|
|
19
|
+
/** Whether moving the cursor also selects the option it lands on. */
|
|
20
|
+
selectionFollowsFocus: boolean;
|
|
21
|
+
orientation: ListboxOrientation;
|
|
22
|
+
dir: ListboxReadingDirection;
|
|
23
|
+
};
|
|
24
|
+
/** A key press, with the modifiers the keymap reads. */
|
|
25
|
+
export type ListboxKey = {
|
|
26
|
+
key: string;
|
|
27
|
+
shiftKey: boolean;
|
|
28
|
+
ctrlKey: boolean;
|
|
29
|
+
metaKey: boolean;
|
|
30
|
+
altKey: boolean;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* What a handled key asks for: where the cursor moves (`null` to leave it) and
|
|
34
|
+
* the selection after it (`null` to leave it). Both `null` is a key the list
|
|
35
|
+
* takes — the caller still prevents its default — that changes nothing, such
|
|
36
|
+
* as Shift+ArrowDown on the last option.
|
|
37
|
+
*/
|
|
38
|
+
export type ListboxKeyOutcome = {
|
|
39
|
+
cursor: string | null;
|
|
40
|
+
selected: string[] | null;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The selection after choosing `value`. Single mode replaces the selection —
|
|
44
|
+
* choosing the selected option again keeps it, since a listbox is not a toggle.
|
|
45
|
+
* Multiple mode toggles `value` in place and keeps the rest.
|
|
46
|
+
*/
|
|
47
|
+
export declare function listboxSelect(state: Pick<ListboxState, "selected" | "multiple">, value: string): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Where the cursor lands when the list takes focus (APG): the first selected
|
|
50
|
+
* option in list order, else the first option. `undefined` in an empty list.
|
|
51
|
+
*/
|
|
52
|
+
export declare function listboxSeed(state: Pick<ListboxState, "values" | "selected">): string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Listbox's keyboard contract. The arrows follow `orientation`, mirrored under
|
|
55
|
+
* RTL, and wrap; Home / End jump to the ends; Enter / Space choose the option
|
|
56
|
+
* under the cursor. In multiple mode Shift+Arrow moves and adds without
|
|
57
|
+
* wrapping, Ctrl/Cmd+A selects every option (or none once all are), and
|
|
58
|
+
* Ctrl/Cmd+Shift+Home/End selects from the cursor to that end. Every other
|
|
59
|
+
* chord, and Alt, belongs to the browser and the page. Returns `null` for a key
|
|
60
|
+
* the list leaves alone (no `preventDefault`), including printable characters,
|
|
61
|
+
* which go on to {@link listboxTypeahead}.
|
|
62
|
+
*/
|
|
63
|
+
export declare function listboxKeydown(state: ListboxState, press: ListboxKey): ListboxKeyOutcome | null;
|
|
64
|
+
/** How long, in milliseconds, a typeahead query lasts after the last key before it starts afresh. */
|
|
65
|
+
export declare const LISTBOX_TYPEAHEAD_RESET_MS: number;
|
|
66
|
+
/** A key press to search the options by, with the query typed so far. */
|
|
67
|
+
export type ListboxTypeaheadState = {
|
|
68
|
+
/** The query typed so far, lower-cased; empty once it has lapsed. */
|
|
69
|
+
query: string;
|
|
70
|
+
/** The key pressed. */
|
|
71
|
+
key: string;
|
|
72
|
+
/** Each enabled option's text, in order. */
|
|
73
|
+
labels: readonly string[];
|
|
74
|
+
/** The index of the option under the cursor, or `-1` when there is none. */
|
|
75
|
+
active: number;
|
|
76
|
+
};
|
|
77
|
+
/** The query after a typeahead key, and the option to move to (`null` when none matches). */
|
|
78
|
+
export type ListboxTypeaheadOutcome = {
|
|
79
|
+
query: string;
|
|
80
|
+
focus: number | null;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Typeahead (APG): a one-character key extends the query and moves to the
|
|
84
|
+
* first option whose text starts with it, ignoring case and surrounding space.
|
|
85
|
+
* A single character, or one character repeated, searches after the cursor so
|
|
86
|
+
* repeating it cycles through the matches; a longer query may match the option
|
|
87
|
+
* under the cursor. With no cursor the search starts at the top. The search
|
|
88
|
+
* wraps. Returns `null` for a key longer than one character, or when there are
|
|
89
|
+
* no options, which tells the caller to leave the event alone; the caller
|
|
90
|
+
* prevents the default only when an option matched, and lapses the query after
|
|
91
|
+
* {@link LISTBOX_TYPEAHEAD_RESET_MS}.
|
|
92
|
+
*/
|
|
93
|
+
export declare function listboxTypeahead({ query, key, labels, active }: ListboxTypeaheadState): ListboxTypeaheadOutcome | null;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getKeyToActionMap } from "../keyboard/getKeyToActionMap.js";
|
|
2
|
+
/**
|
|
3
|
+
* The selection after choosing `value`. Single mode replaces the selection —
|
|
4
|
+
* choosing the selected option again keeps it, since a listbox is not a toggle.
|
|
5
|
+
* Multiple mode toggles `value` in place and keeps the rest.
|
|
6
|
+
*/
|
|
7
|
+
export function listboxSelect(state, value) {
|
|
8
|
+
if (!state.multiple)
|
|
9
|
+
return [value];
|
|
10
|
+
return state.selected.includes(value)
|
|
11
|
+
? state.selected.filter((selected) => selected !== value)
|
|
12
|
+
: [...state.selected, value];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Where the cursor lands when the list takes focus (APG): the first selected
|
|
16
|
+
* option in list order, else the first option. `undefined` in an empty list.
|
|
17
|
+
*/
|
|
18
|
+
export function listboxSeed(state) {
|
|
19
|
+
return state.values.find((value) => state.selected.includes(value)) ?? state.values[0];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Listbox's keyboard contract. The arrows follow `orientation`, mirrored under
|
|
23
|
+
* RTL, and wrap; Home / End jump to the ends; Enter / Space choose the option
|
|
24
|
+
* under the cursor. In multiple mode Shift+Arrow moves and adds without
|
|
25
|
+
* wrapping, Ctrl/Cmd+A selects every option (or none once all are), and
|
|
26
|
+
* Ctrl/Cmd+Shift+Home/End selects from the cursor to that end. Every other
|
|
27
|
+
* chord, and Alt, belongs to the browser and the page. Returns `null` for a key
|
|
28
|
+
* the list leaves alone (no `preventDefault`), including printable characters,
|
|
29
|
+
* which go on to {@link listboxTypeahead}.
|
|
30
|
+
*/
|
|
31
|
+
export function listboxKeydown(state, press) {
|
|
32
|
+
const chord = press.ctrlKey || press.metaKey;
|
|
33
|
+
if (chord && !press.altKey && press.key.toLowerCase() === "a") {
|
|
34
|
+
if (!state.multiple)
|
|
35
|
+
return null;
|
|
36
|
+
// An empty list counts as all selected, which selects the same nothing.
|
|
37
|
+
const all = state.values.every((value) => state.selected.includes(value));
|
|
38
|
+
return { cursor: null, selected: all ? [] : [...state.values] };
|
|
39
|
+
}
|
|
40
|
+
if (chord && press.shiftKey && (press.key === "Home" || press.key === "End")) {
|
|
41
|
+
if (!state.multiple)
|
|
42
|
+
return null;
|
|
43
|
+
return extendToEdge(state, press.key === "Home");
|
|
44
|
+
}
|
|
45
|
+
if (chord || press.altKey)
|
|
46
|
+
return null;
|
|
47
|
+
const action = getKeyToActionMap({
|
|
48
|
+
orientation: state.orientation,
|
|
49
|
+
dir: state.dir,
|
|
50
|
+
homeEnd: true,
|
|
51
|
+
activate: true,
|
|
52
|
+
})[press.key];
|
|
53
|
+
if (press.shiftKey && state.multiple && (action === "next" || action === "prev")) {
|
|
54
|
+
return extendSelection(state, action === "next" ? 1 : -1);
|
|
55
|
+
}
|
|
56
|
+
if (!action || state.values.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
if (action === "activate") {
|
|
59
|
+
if (state.active === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
return { cursor: null, selected: listboxSelect(state, state.active) };
|
|
62
|
+
}
|
|
63
|
+
const last = state.values.length - 1;
|
|
64
|
+
const current = state.values.indexOf(state.active);
|
|
65
|
+
let target;
|
|
66
|
+
if (action === "first")
|
|
67
|
+
target = 0;
|
|
68
|
+
else if (action === "last")
|
|
69
|
+
target = last;
|
|
70
|
+
// No cursor yet (or it sits on an option that has gone): enter from the near end.
|
|
71
|
+
else if (current === -1)
|
|
72
|
+
target = action === "next" ? 0 : last;
|
|
73
|
+
else
|
|
74
|
+
target = action === "next" ? (current + 1) % state.values.length : (current + last) % state.values.length;
|
|
75
|
+
const cursor = state.values[target];
|
|
76
|
+
return { cursor, selected: state.selectionFollowsFocus ? listboxSelect(state, cursor) : null };
|
|
77
|
+
}
|
|
78
|
+
/** Shift+Arrow: move one option and add it, never wrapping past either end. */
|
|
79
|
+
function extendSelection(state, step) {
|
|
80
|
+
const current = state.values.indexOf(state.active);
|
|
81
|
+
const cursor = current === -1 ? undefined : state.values[current + step];
|
|
82
|
+
if (cursor === undefined)
|
|
83
|
+
return { cursor: null, selected: null };
|
|
84
|
+
return { cursor, selected: state.selected.includes(cursor) ? null : [...state.selected, cursor] };
|
|
85
|
+
}
|
|
86
|
+
/** Ctrl+Shift+Home/End: add every option from the cursor to that end, and move there. */
|
|
87
|
+
function extendToEdge(state, toStart) {
|
|
88
|
+
const current = state.values.indexOf(state.active);
|
|
89
|
+
if (current === -1)
|
|
90
|
+
return { cursor: null, selected: null };
|
|
91
|
+
const range = toStart ? state.values.slice(0, current + 1) : state.values.slice(current);
|
|
92
|
+
return {
|
|
93
|
+
cursor: toStart ? state.values[0] : state.values[state.values.length - 1],
|
|
94
|
+
selected: [...state.selected, ...range.filter((value) => !state.selected.includes(value))],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** How long, in milliseconds, a typeahead query lasts after the last key before it starts afresh. */
|
|
98
|
+
export const LISTBOX_TYPEAHEAD_RESET_MS = 500;
|
|
99
|
+
/**
|
|
100
|
+
* Typeahead (APG): a one-character key extends the query and moves to the
|
|
101
|
+
* first option whose text starts with it, ignoring case and surrounding space.
|
|
102
|
+
* A single character, or one character repeated, searches after the cursor so
|
|
103
|
+
* repeating it cycles through the matches; a longer query may match the option
|
|
104
|
+
* under the cursor. With no cursor the search starts at the top. The search
|
|
105
|
+
* wraps. Returns `null` for a key longer than one character, or when there are
|
|
106
|
+
* no options, which tells the caller to leave the event alone; the caller
|
|
107
|
+
* prevents the default only when an option matched, and lapses the query after
|
|
108
|
+
* {@link LISTBOX_TYPEAHEAD_RESET_MS}.
|
|
109
|
+
*/
|
|
110
|
+
export function listboxTypeahead({ query, key, labels, active }) {
|
|
111
|
+
if (key.length !== 1 || labels.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const next = (query + key).toLowerCase();
|
|
114
|
+
const repeated = [...next].every((char) => char === next[0]);
|
|
115
|
+
const search = repeated ? next[0] : next;
|
|
116
|
+
const start = active === -1 ? 0 : active + (search.length === 1 ? 1 : 0);
|
|
117
|
+
const order = [...labels.keys()].map((i) => (start + i) % labels.length);
|
|
118
|
+
const focus = order.find((index) => labels[index].trim().toLowerCase().startsWith(search));
|
|
119
|
+
return { query: next, focus: focus ?? null };
|
|
120
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type MenuKeyOutcome, type MenuKeyState } from "../menu/policy.js";
|
|
2
|
+
/** A select's enabled options, as the selector a host finds them with. */
|
|
3
|
+
export declare const SELECT_OPTION_SELECTOR: string;
|
|
4
|
+
/**
|
|
5
|
+
* The keyboard contract of an open select's listbox: a menu's
|
|
6
|
+
* ({@link menuKeydown}) — the arrows move through the options and wrap, Home
|
|
7
|
+
* and End jump to the ends, Enter and Space choose the focused option — except
|
|
8
|
+
* that Escape closes even when there are no options to move through. Typeahead
|
|
9
|
+
* is `menuTypeahead`'s. Returns `null` for a key the select leaves alone.
|
|
10
|
+
*/
|
|
11
|
+
export declare function selectKeydown(state: MenuKeyState): MenuKeyOutcome | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { menuKeydown } from "../menu/policy.js";
|
|
2
|
+
/** A select's enabled options, as the selector a host finds them with. */
|
|
3
|
+
export const SELECT_OPTION_SELECTOR = '[role="option"]:not([aria-disabled="true"])';
|
|
4
|
+
/**
|
|
5
|
+
* The keyboard contract of an open select's listbox: a menu's
|
|
6
|
+
* ({@link menuKeydown}) — the arrows move through the options and wrap, Home
|
|
7
|
+
* and End jump to the ends, Enter and Space choose the focused option — except
|
|
8
|
+
* that Escape closes even when there are no options to move through. Typeahead
|
|
9
|
+
* is `menuTypeahead`'s. Returns `null` for a key the select leaves alone.
|
|
10
|
+
*/
|
|
11
|
+
export function selectKeydown(state) {
|
|
12
|
+
if (state.key === "Escape")
|
|
13
|
+
return { open: false };
|
|
14
|
+
return menuKeydown(state);
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitiv-ui/core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The framework-free behaviour core shared by every Primitiv surface — keyboard maps, roving-focus targets, id derivation and per-component policy, as pure functions with no DOM and no dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|