@primitiv-ui/core 0.1.53 → 0.1.55

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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Elements that can receive keyboard focus and take part in the natural tab
3
+ * order. Two consumers rely on it: moving initial focus into a surface when it
4
+ * opens (e.g. `Popover`), and locating the boundary elements of a focus trap
5
+ * (e.g. `Modal`). `[tabindex="-1"]` is excluded because such elements are only
6
+ * programmatically focusable, not tabbable.
7
+ */
8
+ export declare const FOCUSABLE_SELECTOR: string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Elements that can receive keyboard focus and take part in the natural tab
3
+ * order. Two consumers rely on it: moving initial focus into a surface when it
4
+ * opens (e.g. `Popover`), and locating the boundary elements of a focus trap
5
+ * (e.g. `Modal`). `[tabindex="-1"]` is excluded because such elements are only
6
+ * programmatically focusable, not tabbable.
7
+ */
8
+ export const FOCUSABLE_SELECTOR = [
9
+ "a[href]",
10
+ "button:not([disabled])",
11
+ "input:not([disabled])",
12
+ "select:not([disabled])",
13
+ "textarea:not([disabled])",
14
+ '[tabindex]:not([tabindex="-1"])',
15
+ ].join(",");
package/dist/index.d.ts CHANGED
@@ -4,3 +4,6 @@ export { resolveRovingTarget, type ResolveRovingTargetOptions, } from "./keyboar
4
4
  export { tabsClick, tabsKeydown, tabsTabStop, type TabsActivationMode, type TabsKeyOutcome, type TabsOrientation, type TabsReadingDirection, type TabsState, } from "./tabs/policy.js";
5
5
  export { accordionKeydown, accordionToggle, type AccordionKeyOutcome, type AccordionOrientation, type AccordionReadingDirection, type AccordionState, } from "./accordion/policy.js";
6
6
  export { collapsibleKeydown, collapsibleToggle, type CollapsibleKeyOutcome, type CollapsibleState, } from "./collapsible/policy.js";
7
+ export { FOCUSABLE_SELECTOR } from "./focus/focusable.js";
8
+ export { modalPointerOutside, modalTabKeydown, type ModalBox, type ModalTabOutcome, type ModalTabState, } from "./modal/policy.js";
9
+ export { popoverKeydown, popoverPressDismisses, type PopoverKeyOutcome, type PopoverPress, } from "./popover/policy.js";
package/dist/index.js CHANGED
@@ -4,3 +4,6 @@ export { resolveRovingTarget, } from "./keyboard/resolveRovingTarget.js";
4
4
  export { tabsClick, tabsKeydown, tabsTabStop, } from "./tabs/policy.js";
5
5
  export { accordionKeydown, accordionToggle, } from "./accordion/policy.js";
6
6
  export { collapsibleKeydown, collapsibleToggle, } from "./collapsible/policy.js";
7
+ export { FOCUSABLE_SELECTOR } from "./focus/focusable.js";
8
+ export { modalPointerOutside, modalTabKeydown, } from "./modal/policy.js";
9
+ export { popoverKeydown, popoverPressDismisses, } from "./popover/policy.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What a key press inside an open modal dialog needs to know: the key, whether
3
+ * Shift was held, how many focusable elements the dialog holds, and which of
4
+ * them (by index, in tab order) has focus — `-1` when focus is on none of them.
5
+ */
6
+ export type ModalTabState = {
7
+ key: string;
8
+ shiftKey: boolean;
9
+ /** The index of the focused element among the focusables, or `-1`. */
10
+ active: number;
11
+ /** How many focusable elements the dialog holds. */
12
+ count: number;
13
+ };
14
+ /**
15
+ * What a Tab press inside the dialog asks for: prevent the browser's default
16
+ * and focus the focusable at `focus`, or, when `focus` is `null`, focus
17
+ * nothing (there is nothing inside to focus).
18
+ */
19
+ export type ModalTabOutcome = {
20
+ focus: number | null;
21
+ };
22
+ /**
23
+ * The modal dialog's focus trap (WAI-ARIA APG Modal Dialog). A native modal
24
+ * `<dialog>` makes the page behind it inert but does not wrap: Tab past the last
25
+ * element escapes to the browser chrome. So only the two boundary presses are
26
+ * taken over — Tab on the last focusable moves to the first, Shift+Tab on the
27
+ * first moves to the last — and a dialog with nothing focusable blocks Tab so
28
+ * focus cannot leak out. Every interior press is left to the browser, whose
29
+ * inert-aware order keeps radio groups and the like working.
30
+ *
31
+ * Returns `null` for anything to leave alone (no `preventDefault`).
32
+ */
33
+ export declare function modalTabKeydown({ key, shiftKey, active, count }: ModalTabState): ModalTabOutcome | null;
34
+ /** A box in viewport coordinates, as `getBoundingClientRect()` reports one. */
35
+ export type ModalBox = {
36
+ left: number;
37
+ top: number;
38
+ right: number;
39
+ bottom: number;
40
+ };
41
+ /**
42
+ * Whether a pointer at `point` landed outside the dialog's `box`. A modal
43
+ * `<dialog>` paints its own `::backdrop` in the top layer, so a press on the
44
+ * dimmed page targets the dialog itself; its coordinates are the only way to
45
+ * tell the backdrop from the dialog. The box's edges count as inside.
46
+ */
47
+ export declare function modalPointerOutside(box: ModalBox, point: {
48
+ x: number;
49
+ y: number;
50
+ }): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The modal dialog's focus trap (WAI-ARIA APG Modal Dialog). A native modal
3
+ * `<dialog>` makes the page behind it inert but does not wrap: Tab past the last
4
+ * element escapes to the browser chrome. So only the two boundary presses are
5
+ * taken over — Tab on the last focusable moves to the first, Shift+Tab on the
6
+ * first moves to the last — and a dialog with nothing focusable blocks Tab so
7
+ * focus cannot leak out. Every interior press is left to the browser, whose
8
+ * inert-aware order keeps radio groups and the like working.
9
+ *
10
+ * Returns `null` for anything to leave alone (no `preventDefault`).
11
+ */
12
+ export function modalTabKeydown({ key, shiftKey, active, count }) {
13
+ if (key !== "Tab")
14
+ return null;
15
+ if (count === 0)
16
+ return { focus: null };
17
+ const last = count - 1;
18
+ if (!shiftKey && active === last)
19
+ return { focus: 0 };
20
+ if (shiftKey && active === 0)
21
+ return { focus: last };
22
+ return null;
23
+ }
24
+ /**
25
+ * Whether a pointer at `point` landed outside the dialog's `box`. A modal
26
+ * `<dialog>` paints its own `::backdrop` in the top layer, so a press on the
27
+ * dimmed page targets the dialog itself; its coordinates are the only way to
28
+ * tell the backdrop from the dialog. The box's edges count as inside.
29
+ */
30
+ export function modalPointerOutside(box, point) {
31
+ return point.x < box.left || point.x > box.right || point.y < box.top || point.y > box.bottom;
32
+ }
@@ -0,0 +1,31 @@
1
+ /** What a key press inside an open popover asks for: the open state to move to. */
2
+ export type PopoverKeyOutcome = {
3
+ open: false;
4
+ };
5
+ /**
6
+ * The popover's keyboard contract for a key press inside its panel: Escape
7
+ * closes it (and the caller hands focus back to the trigger, since focus moved
8
+ * into the panel on open). Returns `null` for any other key, which tells the
9
+ * caller to leave the event alone.
10
+ */
11
+ export declare function popoverKeydown(key: string): PopoverKeyOutcome | null;
12
+ /** Where a press landed, relative to an open popover, and whether the browser handles it. */
13
+ export type PopoverPress = {
14
+ /** Whether the popover is open. */
15
+ open: boolean;
16
+ /** Whether the browser light-dismisses `popover="auto"` itself (the Popover API exists). */
17
+ nativeLightDismiss: boolean;
18
+ /** Whether the press landed on (or in) the trigger. */
19
+ inTrigger: boolean;
20
+ /** Whether the press landed inside a popover — this one, or one nested in it. */
21
+ inPopover: boolean;
22
+ };
23
+ /**
24
+ * Whether a press dismisses the popover. A browser with the Popover API
25
+ * light-dismisses an `auto` popover on an outside press and reports it with a
26
+ * `toggle` event, so this only answers where it does not; answering there too
27
+ * would catch the very click that opened the popover from an outside control
28
+ * and close it again. A press on the trigger toggles it instead, and one inside
29
+ * any popover is inside.
30
+ */
31
+ export declare function popoverPressDismisses({ open, nativeLightDismiss, inTrigger, inPopover }: PopoverPress): boolean;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The popover's keyboard contract for a key press inside its panel: Escape
3
+ * closes it (and the caller hands focus back to the trigger, since focus moved
4
+ * into the panel on open). Returns `null` for any other key, which tells the
5
+ * caller to leave the event alone.
6
+ */
7
+ export function popoverKeydown(key) {
8
+ return key === "Escape" ? { open: false } : null;
9
+ }
10
+ /**
11
+ * Whether a press dismisses the popover. A browser with the Popover API
12
+ * light-dismisses an `auto` popover on an outside press and reports it with a
13
+ * `toggle` event, so this only answers where it does not; answering there too
14
+ * would catch the very click that opened the popover from an outside control
15
+ * and close it again. A press on the trigger toggles it instead, and one inside
16
+ * any popover is inside.
17
+ */
18
+ export function popoverPressDismisses({ open, nativeLightDismiss, inTrigger, inPopover }) {
19
+ return open && !nativeLightDismiss && !inTrigger && !inPopover;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitiv-ui/core",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
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",