Skip to content

packages/core/src/lib/focus/policy-components.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

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 built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.