packages/core/src/lib/focus/policy-components.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import {
2 appendElementEnhancer,
3 hasElementEnhancer,
4 type PibblElementEnhancerDescriptor,
5 } from '../element-enhancers.js';
6 import {
7 setElementPolicyDefinition,
8 type ElementPolicyDefinition,
9 } from '../element/metadata.js';
10 import type {
11 PibblComponent,
12 PibblElement,
13 PibblEmptyNode,
14 } from '../element/types.js';
15 import type { PibblKeyboardNavigationOptions } from './types.js';
16 import {
17 createDirectionalBoundary,
18 createFocusManager,
19 requireFocusManager,
20 type PibblFocusManager,
21 } from './focus-manager.js';
22
23 const FOCUS_MANAGEMENT_ENHANCER_KEY = Symbol('Pibbl focus management');
24 const FOCUS_IMPLEMENTATION_MARKER = 'PIBBL_FOCUS_MANAGEMENT_V1';
25 const KEYBOARD_NAVIGATION_ENHANCER_KEY = Symbol('Pibbl keyboard navigation');
26
27 const focusManagementDescriptor: PibblElementEnhancerDescriptor = {
28 key: FOCUS_MANAGEMENT_ENHANCER_KEY,
29 mount: ({ instance }) => {
30 const manager = createFocusManager(instance.eventRoot, instance);
31 return {
32 update: () => undefined,
33 enter: () => manager.beginCollection(),
34 exit: success => manager.finishCollection(success),
35 dispose: () => manager.dispose(),
36 };
37 },
38 };
39
40 const keyboardNavigationDescriptor: PibblElementEnhancerDescriptor = {
41 key: KEYBOARD_NAVIGATION_ENHANCER_KEY,
42 mount: ({ instance }) => {
43 const boundary = createDirectionalBoundary(instance);
44 let manager: PibblFocusManager | undefined;
45 return {
46 update: () => undefined,
47 enter: () => {
48 manager = requireFocusManager(instance.eventRoot);
49 manager.enterDirectionalBoundary(boundary);
50 },
51 exit: () => {
52 try {
53 manager?.exitDirectionalBoundary(boundary);
54 } finally {
55 manager = undefined;
56 }
57 },
58 dispose: () => undefined,
59 };
60 },
61 };
62
63 type PibblPolicyChild =
64 | PibblElement<any>
65 | PibblEmptyNode
66 | readonly PibblPolicyChild[]
67 | (Iterable<PibblPolicyChild> & { readonly charAt?: never });
68
69 /**
70 * Authored inputs for FocusManagement, including the declared data and presentation options.
71 *
72 * @see {@link FocusManagement}
73 */
74 export interface FocusManagementProps {
75 /** The single child receiving a logical focus manager. See {@link PibblPolicyChild}. */
76 readonly children: PibblPolicyChild;
77 }
78
79 /**
80 * Authored inputs for KeyboardNavigation, including the declared data and presentation options.
81 *
82 * @see {@link PibblKeyboardNavigationOptions}
83 * @see {@link KeyboardNavigation}
84 */
85 export interface KeyboardNavigationProps extends PibblKeyboardNavigationOptions {
86 /** The single child receiving a directional keyboard-navigation boundary. See {@link PibblPolicyChild}. */
87 readonly children: PibblPolicyChild;
88 }
89
90 /**
91 * Adds one logical focus manager to exactly one child without adding identity.
92 *
93 * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
94 * props and style types.
95 * @throws When called directly; Pibbl mounts this component through JSX or createElement.
96 *
97 * @see {@link FocusManagementProps}
98 */
99 export const FocusManagement = /* @__PURE__ */ definePolicyComponent<FocusManagementProps>(
100 'FocusManagement',
101 (_policy, child) => {
102 if (hasElementEnhancer(child, FOCUS_MANAGEMENT_ENHANCER_KEY)) {
103 throw new Error(
104 `${FOCUS_IMPLEMENTATION_MARKER}: FocusManagement cannot be applied ` +
105 'more than once to the same Pibbl element.',
106 );
107 }
108 return appendElementEnhancer(child, focusManagementDescriptor);
109 },
110 );
111
112 /**
113 * Adds one directional-navigation boundary without adding identity.
114 *
115 * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
116 * props and style types.
117 * @throws When called directly; Pibbl mounts this component through JSX or createElement.
118 *
119 * @see {@link KeyboardNavigationProps}
120 */
121 export const KeyboardNavigation = /* @__PURE__ */ definePolicyComponent<KeyboardNavigationProps>(
122 'KeyboardNavigation',
123 (policy, child) => {
124 const options = policy.props as KeyboardNavigationProps;
125 if (options.mode !== 'directional') {
126 throw new Error('KeyboardNavigation mode must be "directional".');
127 }
128 if (hasElementEnhancer(child, KEYBOARD_NAVIGATION_ENHANCER_KEY)) {
129 throw new Error(
130 'KeyboardNavigation cannot be applied more than once to the same Pibbl element.',
131 );
132 }
133 return appendElementEnhancer(child, keyboardNavigationDescriptor);
134 },
135 );
136
137 function definePolicyComponent<P>(
138 name: string,
139 prepare: ElementPolicyDefinition['prepare'],
140 ): PibblComponent<P> {
141 const component = function PibblPolicyComponent(): never {
142 throw new Error(
143 `${name} is a Pibbl component and was invoked outside the Pibbl renderer.\n` +
144 `Use <${name} ... /> in a Pibbl JSX file or ` +
145 `createElement(${name}, props).`,
146 );
147 } as PibblComponent<P>;
148 Object.defineProperty(component, 'name', {
149 configurable: true,
150 value: name,
151 });
152 setElementPolicyDefinition(component, { prepare });
153 return component;
154 }
155
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.