@primitiv-ui/core 0.0.0-stage → 0.1.43
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/LICENSE +21 -0
- package/README.md +33 -2
- package/dist/ids/deriveId.d.ts +25 -0
- package/dist/ids/deriveId.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/keyboard/getKeyToActionMap.d.ts +64 -0
- package/dist/keyboard/getKeyToActionMap.js +51 -0
- package/dist/keyboard/resolveRovingTarget.d.ts +42 -0
- package/dist/keyboard/resolveRovingTarget.js +44 -0
- package/dist/tabs/policy.d.ts +54 -0
- package/dist/tabs/policy.js +44 -0
- package/package.json +38 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simon Revill
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @primitiv-ui/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The framework-free behaviour core shared by every Primitiv surface. Pure
|
|
4
|
+
TypeScript functions with **no DOM, no framework and no dependencies**: keyboard
|
|
5
|
+
maps, roving-focus targets, id derivation and, as components move over, each
|
|
6
|
+
component's keyboard and state policy.
|
|
7
|
+
|
|
8
|
+
You do not normally install this package yourself. It arrives as a dependency
|
|
9
|
+
of the surface you chose: [`@primitiv-ui/react`](../react/README.md) today, and
|
|
10
|
+
`@primitiv-ui/web` (standard custom elements) as it lands. Both surfaces import
|
|
11
|
+
the same functions, so they cannot disagree about how a component behaves. See
|
|
12
|
+
[RFC 0033](../../docs/rfcs/0033-web-components.md) for the design.
|
|
13
|
+
|
|
14
|
+
## Exports
|
|
15
|
+
|
|
16
|
+
| Export | What it does |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `getKeyToActionMap(options)` | Builds the key → roving action map (`"next"`, `"prev"`, `"first"`, `"last"`, `"activate"`) for an orientation, reading direction, Home/End and Enter/Space. |
|
|
19
|
+
| `RovingKeyAction` | The action vocabulary those maps return. |
|
|
20
|
+
| `resolveRovingTarget({ action, navigable, currentKey })` | Resolves which item a roving action lands on (wrapping, Home/End, Enter/Space), as `{ target }` or `null`. |
|
|
21
|
+
| `deriveId(rootId, suffix, value)` | Builds the paired ids (`aria-controls`, `aria-labelledby`) a compound component wires between its parts. |
|
|
22
|
+
| `tabsKeydown(state, focused, key)` | Tabs' keyboard contract: which tab to focus and which, if any, to activate. |
|
|
23
|
+
| `tabsClick(state, value)` | The tab a click activates, or `null` when it is disabled or already active. |
|
|
24
|
+
| `tabsTabStop(state)` | The tab that takes part in the page's Tab order. |
|
|
25
|
+
| `TabsState`, `TabsKeyOutcome`, `TabsOrientation`, `TabsReadingDirection`, `TabsActivationMode` | The plain data those functions read and return. |
|
|
26
|
+
|
|
27
|
+
## Rules for this package
|
|
28
|
+
|
|
29
|
+
- **No DOM.** `tsconfig.json` leaves `DOM` out of `lib`, so a DOM type in a
|
|
30
|
+
signature is a compile error.
|
|
31
|
+
- **100% coverage and 100% mutation score**, both gated: `pnpm qa:units` and
|
|
32
|
+
`pnpm mutate`.
|
|
33
|
+
- **Versioned in lockstep** with every other Primitiv package
|
|
34
|
+
(`scripts/bump-version.mjs`).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a deterministic sub-id from a stable root id (typically the
|
|
3
|
+
* value returned by React's `useId()`), a fixed role suffix, and a
|
|
4
|
+
* per-item value. Used to wire `aria-controls`, `aria-labelledby`, and
|
|
5
|
+
* matching DOM `id` attributes between paired sub-components such as
|
|
6
|
+
* Tabs.Trigger ↔ Tabs.Content and Accordion.Trigger ↔ Accordion.Content
|
|
7
|
+
* without each compound component re-deriving the same template literal.
|
|
8
|
+
*
|
|
9
|
+
* @example Inside a Tabs.Trigger
|
|
10
|
+
* ```ts
|
|
11
|
+
* const triggerId = deriveId(tabsId, "trigger", value);
|
|
12
|
+
* const panelId = deriveId(tabsId, "panel", value);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @param rootId - The shared id for the compound component, normally
|
|
16
|
+
* from `useId()`. Any string is accepted, including React 18's
|
|
17
|
+
* colon-bracketed ids (`":r1:"`).
|
|
18
|
+
* @param suffix - The role of the derived id within the compound,
|
|
19
|
+
* e.g. `"trigger"`, `"panel"`, `"heading"`.
|
|
20
|
+
* @param value - The per-item discriminator (the value prop on the
|
|
21
|
+
* trigger / panel pair).
|
|
22
|
+
*
|
|
23
|
+
* @returns The concatenation `${rootId}-${suffix}-${value}`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function deriveId(rootId: string, suffix: string, value: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a deterministic sub-id from a stable root id (typically the
|
|
3
|
+
* value returned by React's `useId()`), a fixed role suffix, and a
|
|
4
|
+
* per-item value. Used to wire `aria-controls`, `aria-labelledby`, and
|
|
5
|
+
* matching DOM `id` attributes between paired sub-components such as
|
|
6
|
+
* Tabs.Trigger ↔ Tabs.Content and Accordion.Trigger ↔ Accordion.Content
|
|
7
|
+
* without each compound component re-deriving the same template literal.
|
|
8
|
+
*
|
|
9
|
+
* @example Inside a Tabs.Trigger
|
|
10
|
+
* ```ts
|
|
11
|
+
* const triggerId = deriveId(tabsId, "trigger", value);
|
|
12
|
+
* const panelId = deriveId(tabsId, "panel", value);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @param rootId - The shared id for the compound component, normally
|
|
16
|
+
* from `useId()`. Any string is accepted, including React 18's
|
|
17
|
+
* colon-bracketed ids (`":r1:"`).
|
|
18
|
+
* @param suffix - The role of the derived id within the compound,
|
|
19
|
+
* e.g. `"trigger"`, `"panel"`, `"heading"`.
|
|
20
|
+
* @param value - The per-item discriminator (the value prop on the
|
|
21
|
+
* trigger / panel pair).
|
|
22
|
+
*
|
|
23
|
+
* @returns The concatenation `${rootId}-${suffix}-${value}`.
|
|
24
|
+
*/
|
|
25
|
+
export function deriveId(rootId, suffix, value) {
|
|
26
|
+
return `${rootId}-${suffix}-${value}`;
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { deriveId } from "./ids/deriveId.js";
|
|
2
|
+
export { getKeyToActionMap, type RovingKeyAction } from "./keyboard/getKeyToActionMap.js";
|
|
3
|
+
export { resolveRovingTarget, type ResolveRovingTargetOptions, } from "./keyboard/resolveRovingTarget.js";
|
|
4
|
+
export { tabsClick, tabsKeydown, tabsTabStop, type TabsActivationMode, type TabsKeyOutcome, type TabsOrientation, type TabsReadingDirection, type TabsState, } from "./tabs/policy.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The abstract roving-tabindex action that a keyboard event maps to.
|
|
3
|
+
*
|
|
4
|
+
* Consumers translate the action into whatever "go to" semantics suit
|
|
5
|
+
* their pattern (focus only, focus + activate, focus + select, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export type RovingKeyAction = "next" | "prev" | "first" | "last" | "activate";
|
|
8
|
+
type GetKeyToActionMapOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Which arrow-key axis is active. `"horizontal"` enables Arrow Left/Right,
|
|
11
|
+
* `"vertical"` enables Arrow Up/Down, `"both"` enables all four. RTL
|
|
12
|
+
* inversion (see {@link GetKeyToActionMapOptions.dir | `dir`}) swaps the
|
|
13
|
+
* horizontal pair for both `"horizontal"` and `"both"`; the vertical pair
|
|
14
|
+
* is never swapped, since vertical reading order does not depend on
|
|
15
|
+
* direction.
|
|
16
|
+
*/
|
|
17
|
+
orientation: "horizontal" | "vertical" | "both";
|
|
18
|
+
/**
|
|
19
|
+
* Reading direction. When `"rtl"`, swaps the meaning of Arrow Left and
|
|
20
|
+
* Arrow Right — applied for both `"horizontal"` and `"both"` orientation
|
|
21
|
+
* (never the vertical pair). Defaults to `"ltr"`.
|
|
22
|
+
*/
|
|
23
|
+
dir?: "ltr" | "rtl";
|
|
24
|
+
/**
|
|
25
|
+
* When `true`, Home maps to `"first"` and End to `"last"`. Use for
|
|
26
|
+
* patterns where Home/End jumping is part of the keyboard contract
|
|
27
|
+
* (Tabs, Accordion). Defaults to `false`.
|
|
28
|
+
*/
|
|
29
|
+
homeEnd?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* When `true`, Enter and Space map to `"activate"`. Use for patterns
|
|
32
|
+
* where the focused item can be confirmed via the keyboard (Tabs in
|
|
33
|
+
* `"manual"` activation mode). Defaults to `false`.
|
|
34
|
+
*/
|
|
35
|
+
activate?: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Builds the keyboard-key → roving-tabindex-action map for a single
|
|
39
|
+
* focusable item in a compound widget. Each consumer shares the same
|
|
40
|
+
* RTL / orientation / Home-End / Enter-Space conventions and just needs
|
|
41
|
+
* to translate the abstract action ("next", "prev", "first", "last",
|
|
42
|
+
* "activate") into its own movement / activation logic.
|
|
43
|
+
*
|
|
44
|
+
* Returns a plain `Record<string, RovingKeyAction>` so call sites can do
|
|
45
|
+
* `const action = map[event.key]; if (action === "next") ...`.
|
|
46
|
+
*
|
|
47
|
+
* @example Tabs trigger (horizontal, RTL, all features)
|
|
48
|
+
* ```ts
|
|
49
|
+
* const map = getKeyToActionMap({
|
|
50
|
+
* orientation: "horizontal",
|
|
51
|
+
* dir,
|
|
52
|
+
* homeEnd: true,
|
|
53
|
+
* activate: true,
|
|
54
|
+
* });
|
|
55
|
+
* const action = map[event.key];
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example Accordion trigger (vertical, no Enter/Space — handled separately)
|
|
59
|
+
* ```ts
|
|
60
|
+
* const map = getKeyToActionMap({ orientation: "vertical", homeEnd: true });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function getKeyToActionMap({ orientation, dir, homeEnd, activate, }: GetKeyToActionMapOptions): Record<string, RovingKeyAction>;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the keyboard-key → roving-tabindex-action map for a single
|
|
3
|
+
* focusable item in a compound widget. Each consumer shares the same
|
|
4
|
+
* RTL / orientation / Home-End / Enter-Space conventions and just needs
|
|
5
|
+
* to translate the abstract action ("next", "prev", "first", "last",
|
|
6
|
+
* "activate") into its own movement / activation logic.
|
|
7
|
+
*
|
|
8
|
+
* Returns a plain `Record<string, RovingKeyAction>` so call sites can do
|
|
9
|
+
* `const action = map[event.key]; if (action === "next") ...`.
|
|
10
|
+
*
|
|
11
|
+
* @example Tabs trigger (horizontal, RTL, all features)
|
|
12
|
+
* ```ts
|
|
13
|
+
* const map = getKeyToActionMap({
|
|
14
|
+
* orientation: "horizontal",
|
|
15
|
+
* dir,
|
|
16
|
+
* homeEnd: true,
|
|
17
|
+
* activate: true,
|
|
18
|
+
* });
|
|
19
|
+
* const action = map[event.key];
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Accordion trigger (vertical, no Enter/Space — handled separately)
|
|
23
|
+
* ```ts
|
|
24
|
+
* const map = getKeyToActionMap({ orientation: "vertical", homeEnd: true });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function getKeyToActionMap({ orientation,
|
|
28
|
+
// No `= "ltr"` default: only `"rtl"` is ever tested, so an omitted `dir`
|
|
29
|
+
// already reads as LTR, and a default here is an equivalent mutant.
|
|
30
|
+
dir, homeEnd = false, activate = false, }) {
|
|
31
|
+
const map = {};
|
|
32
|
+
if (orientation === "horizontal" || orientation === "both") {
|
|
33
|
+
const forward = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
34
|
+
const backward = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
35
|
+
map[forward] = "next";
|
|
36
|
+
map[backward] = "prev";
|
|
37
|
+
}
|
|
38
|
+
if (orientation === "vertical" || orientation === "both") {
|
|
39
|
+
map.ArrowDown = "next";
|
|
40
|
+
map.ArrowUp = "prev";
|
|
41
|
+
}
|
|
42
|
+
if (homeEnd) {
|
|
43
|
+
map.Home = "first";
|
|
44
|
+
map.End = "last";
|
|
45
|
+
}
|
|
46
|
+
if (activate) {
|
|
47
|
+
map.Enter = "activate";
|
|
48
|
+
map[" "] = "activate";
|
|
49
|
+
}
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RovingKeyAction } from "./getKeyToActionMap.js";
|
|
2
|
+
/** Input to {@link resolveRovingTarget}. */
|
|
3
|
+
export type ResolveRovingTargetOptions<K> = {
|
|
4
|
+
/**
|
|
5
|
+
* The abstract action a key press mapped to (see `getKeyToActionMap`), or
|
|
6
|
+
* `undefined` for a key the map does not cover, which resolves nothing. So a
|
|
7
|
+
* caller can pass `map[event.key]` straight through.
|
|
8
|
+
*/
|
|
9
|
+
action: RovingKeyAction | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The ordered keys navigation walks through. The caller filters it: RadioGroup
|
|
12
|
+
* and Accordion leave disabled items out so arrows skip them, while Tabs keeps
|
|
13
|
+
* them in so arrows land on a disabled tab without activating it.
|
|
14
|
+
*/
|
|
15
|
+
navigable: readonly K[];
|
|
16
|
+
/** The key of the item that owns the key press: the pivot for next / prev. */
|
|
17
|
+
currentKey: K;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolves which item a roving-tabindex action lands on: the index arithmetic
|
|
21
|
+
* of the WAI-ARIA roving-tabindex pattern, with no DOM and no framework.
|
|
22
|
+
*
|
|
23
|
+
* - `"next"` / `"prev"` move one step and wrap at either end. They resolve
|
|
24
|
+
* nothing when `currentKey` is not in `navigable`, which is RadioGroup's
|
|
25
|
+
* contract: arrows do nothing while focus sits on a disabled radio.
|
|
26
|
+
* - `"first"` / `"last"` jump to either end, whatever the current key.
|
|
27
|
+
* - `"activate"` targets the current key, so the caller confirms it.
|
|
28
|
+
* - An empty `navigable`, or no action at all, resolves nothing.
|
|
29
|
+
*
|
|
30
|
+
* The result is wrapped as `{ target }`, with `null` for "nothing", because a
|
|
31
|
+
* key can itself be `null` or `undefined` (Combobox navigates `string | null`).
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const action = getKeyToActionMap({ orientation: "horizontal" })[event.key];
|
|
36
|
+
* const result = resolveRovingTarget({ action, navigable, currentKey });
|
|
37
|
+
* if (result) focusItem(result.target);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveRovingTarget<K>({ action, navigable, currentKey, }: ResolveRovingTargetOptions<K>): {
|
|
41
|
+
target: K;
|
|
42
|
+
} | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves which item a roving-tabindex action lands on: the index arithmetic
|
|
3
|
+
* of the WAI-ARIA roving-tabindex pattern, with no DOM and no framework.
|
|
4
|
+
*
|
|
5
|
+
* - `"next"` / `"prev"` move one step and wrap at either end. They resolve
|
|
6
|
+
* nothing when `currentKey` is not in `navigable`, which is RadioGroup's
|
|
7
|
+
* contract: arrows do nothing while focus sits on a disabled radio.
|
|
8
|
+
* - `"first"` / `"last"` jump to either end, whatever the current key.
|
|
9
|
+
* - `"activate"` targets the current key, so the caller confirms it.
|
|
10
|
+
* - An empty `navigable`, or no action at all, resolves nothing.
|
|
11
|
+
*
|
|
12
|
+
* The result is wrapped as `{ target }`, with `null` for "nothing", because a
|
|
13
|
+
* key can itself be `null` or `undefined` (Combobox navigates `string | null`).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const action = getKeyToActionMap({ orientation: "horizontal" })[event.key];
|
|
18
|
+
* const result = resolveRovingTarget({ action, navigable, currentKey });
|
|
19
|
+
* if (result) focusItem(result.target);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function resolveRovingTarget({ action, navigable, currentKey, }) {
|
|
23
|
+
const total = navigable.length;
|
|
24
|
+
if (total === 0)
|
|
25
|
+
return null;
|
|
26
|
+
switch (action) {
|
|
27
|
+
case "activate":
|
|
28
|
+
return { target: currentKey };
|
|
29
|
+
case "first":
|
|
30
|
+
return { target: navigable[0] };
|
|
31
|
+
case "last":
|
|
32
|
+
return { target: navigable[total - 1] };
|
|
33
|
+
case undefined:
|
|
34
|
+
return null;
|
|
35
|
+
case "next":
|
|
36
|
+
case "prev": {
|
|
37
|
+
const currentIndex = navigable.indexOf(currentKey);
|
|
38
|
+
if (currentIndex === -1)
|
|
39
|
+
return null;
|
|
40
|
+
const step = action === "next" ? 1 : total - 1;
|
|
41
|
+
return { target: navigable[(currentIndex + step) % total] };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Layout axis of the tab list: which arrow pair moves between tabs. */
|
|
2
|
+
export type TabsOrientation = "horizontal" | "vertical";
|
|
3
|
+
/** Reading direction. Under `"rtl"` the horizontal arrows are mirrored. */
|
|
4
|
+
export type TabsReadingDirection = "ltr" | "rtl";
|
|
5
|
+
/** When a focused tab becomes active: on arrival (`"automatic"`), or only on
|
|
6
|
+
* Enter / Space (`"manual"`). */
|
|
7
|
+
export type TabsActivationMode = "automatic" | "manual";
|
|
8
|
+
/**
|
|
9
|
+
* Everything Tabs' keyboard and activation rules depend on, as plain data. Each
|
|
10
|
+
* surface builds it from wherever it keeps state — React state, or a custom
|
|
11
|
+
* element's attributes — and applies the outcome back the same way.
|
|
12
|
+
*/
|
|
13
|
+
export type TabsState = {
|
|
14
|
+
/** Every tab's value, in DOM order, disabled ones included. */
|
|
15
|
+
values: readonly string[];
|
|
16
|
+
/** The values of the disabled tabs. */
|
|
17
|
+
disabled: ReadonlySet<string>;
|
|
18
|
+
/** The active tab's value, or `undefined` when no tab is active. */
|
|
19
|
+
active: string | undefined;
|
|
20
|
+
orientation: TabsOrientation;
|
|
21
|
+
dir: TabsReadingDirection;
|
|
22
|
+
activationMode: TabsActivationMode;
|
|
23
|
+
};
|
|
24
|
+
/** What a key press on a tab asks for: always a tab to focus, and a tab to
|
|
25
|
+
* activate when the press should change the selection. */
|
|
26
|
+
export type TabsKeyOutcome = {
|
|
27
|
+
focus: string;
|
|
28
|
+
activate: string | null;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The tab that takes part in the page's Tab order (the roving tab stop): the
|
|
32
|
+
* active tab, or the first tab while none is active so the tablist is still
|
|
33
|
+
* reachable by keyboard. An active value that matches no tab is returned as it
|
|
34
|
+
* is, which leaves no tab as the stop — the behaviour Tabs has always had.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tabsTabStop(state: Pick<TabsState, "values" | "active">): string | undefined;
|
|
37
|
+
/** The tab a click on `value` activates: `null` when it is disabled or already
|
|
38
|
+
* active. */
|
|
39
|
+
export declare function tabsClick(state: Pick<TabsState, "disabled" | "active">, value: string): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Tabs' keyboard contract for a key press on the tab `focused`:
|
|
42
|
+
*
|
|
43
|
+
* - arrows follow `orientation`, mirrored under RTL; Home / End jump to the ends;
|
|
44
|
+
* Enter / Space confirm;
|
|
45
|
+
* - arrows **land** on a disabled tab but never activate it — unlike RadioGroup
|
|
46
|
+
* and Accordion, which skip disabled items;
|
|
47
|
+
* - in `"automatic"` mode arriving at a tab activates it; in `"manual"` mode only
|
|
48
|
+
* Enter / Space do;
|
|
49
|
+
* - the already-active tab is never re-activated, so no change is reported.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` for a key Tabs does not handle, which tells the caller to leave
|
|
52
|
+
* the event alone (no `preventDefault`).
|
|
53
|
+
*/
|
|
54
|
+
export declare function tabsKeydown(state: TabsState, focused: string, key: string): TabsKeyOutcome | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getKeyToActionMap } from "../keyboard/getKeyToActionMap.js";
|
|
2
|
+
import { resolveRovingTarget } from "../keyboard/resolveRovingTarget.js";
|
|
3
|
+
/**
|
|
4
|
+
* The tab that takes part in the page's Tab order (the roving tab stop): the
|
|
5
|
+
* active tab, or the first tab while none is active so the tablist is still
|
|
6
|
+
* reachable by keyboard. An active value that matches no tab is returned as it
|
|
7
|
+
* is, which leaves no tab as the stop — the behaviour Tabs has always had.
|
|
8
|
+
*/
|
|
9
|
+
export function tabsTabStop(state) {
|
|
10
|
+
return state.active ?? state.values[0];
|
|
11
|
+
}
|
|
12
|
+
/** The tab a click on `value` activates: `null` when it is disabled or already
|
|
13
|
+
* active. */
|
|
14
|
+
export function tabsClick(state, value) {
|
|
15
|
+
return state.disabled.has(value) || state.active === value ? null : value;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Tabs' keyboard contract for a key press on the tab `focused`:
|
|
19
|
+
*
|
|
20
|
+
* - arrows follow `orientation`, mirrored under RTL; Home / End jump to the ends;
|
|
21
|
+
* Enter / Space confirm;
|
|
22
|
+
* - arrows **land** on a disabled tab but never activate it — unlike RadioGroup
|
|
23
|
+
* and Accordion, which skip disabled items;
|
|
24
|
+
* - in `"automatic"` mode arriving at a tab activates it; in `"manual"` mode only
|
|
25
|
+
* Enter / Space do;
|
|
26
|
+
* - the already-active tab is never re-activated, so no change is reported.
|
|
27
|
+
*
|
|
28
|
+
* Returns `null` for a key Tabs does not handle, which tells the caller to leave
|
|
29
|
+
* the event alone (no `preventDefault`).
|
|
30
|
+
*/
|
|
31
|
+
export function tabsKeydown(state, focused, key) {
|
|
32
|
+
const action = getKeyToActionMap({
|
|
33
|
+
orientation: state.orientation,
|
|
34
|
+
dir: state.dir,
|
|
35
|
+
homeEnd: true,
|
|
36
|
+
activate: true,
|
|
37
|
+
})[key];
|
|
38
|
+
const result = resolveRovingTarget({ action, navigable: state.values, currentKey: focused });
|
|
39
|
+
if (!result)
|
|
40
|
+
return null;
|
|
41
|
+
const target = result.target;
|
|
42
|
+
const requested = state.activationMode === "automatic" || action === "activate";
|
|
43
|
+
return { focus: target, activate: requested ? tabsClick(state, target) : null };
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitiv-ui/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.43",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/primitiv-ui/primitiv.git",
|
|
21
|
+
"directory": "packages/core"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@stryker-mutator/core": "9.6.1",
|
|
25
|
+
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
26
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
27
|
+
"typescript": "^6.0.2",
|
|
28
|
+
"vitest": "^4.1.4"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/finalize-dist.mjs dist",
|
|
35
|
+
"qa": "pnpm qa:units",
|
|
36
|
+
"qa:units": "vitest run --coverage",
|
|
37
|
+
"qa:units:watch": "vitest --coverage",
|
|
38
|
+
"mutate": "stryker run",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
40
|
+
}
|
|
6
41
|
}
|