@silvery/tea 0.3.0
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/package.json +54 -0
- package/src/core/index.ts +225 -0
- package/src/core/slice.ts +69 -0
- package/src/create-command-registry.ts +106 -0
- package/src/effects.ts +145 -0
- package/src/focus-events.ts +243 -0
- package/src/focus-manager.ts +491 -0
- package/src/focus-queries.ts +241 -0
- package/src/index.ts +213 -0
- package/src/keys.ts +1382 -0
- package/src/pipe.ts +110 -0
- package/src/plugins.ts +119 -0
- package/src/store/index.ts +306 -0
- package/src/streams/index.ts +405 -0
- package/src/tea/README.md +208 -0
- package/src/tea/index.ts +174 -0
- package/src/text-cursor.ts +206 -0
- package/src/text-decorations.ts +253 -0
- package/src/text-ops.ts +150 -0
- package/src/tree-utils.ts +27 -0
- package/src/types.ts +670 -0
- package/src/with-commands.ts +337 -0
- package/src/with-diagnostics.ts +955 -0
- package/src/with-dom-events.ts +168 -0
- package/src/with-focus.ts +162 -0
- package/src/with-keybindings.ts +180 -0
- package/src/with-react.ts +92 -0
- package/src/with-render.ts +92 -0
- package/src/with-terminal.ts +219 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus Manager — standalone state container for the silvery focus system.
|
|
3
|
+
*
|
|
4
|
+
* Pure TypeScript, no React dependency. The subscribe/getSnapshot pattern
|
|
5
|
+
* enables useSyncExternalStore in hooks.
|
|
6
|
+
*
|
|
7
|
+
* Replaces the flat focus list in context.ts (FocusContext with focusables Map).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TeaNode, Rect } from "./types"
|
|
11
|
+
import {
|
|
12
|
+
findByTestID,
|
|
13
|
+
findFocusableAncestor,
|
|
14
|
+
getTabOrder,
|
|
15
|
+
findSpatialTarget,
|
|
16
|
+
getExplicitFocusLink,
|
|
17
|
+
} from "./focus-queries"
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export type FocusOrigin = "keyboard" | "mouse" | "programmatic"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback fired when focus changes. Used by the runtime to dispatch
|
|
27
|
+
* DOM-level focus/blur events without coupling FocusManager to the event system.
|
|
28
|
+
*
|
|
29
|
+
* @param oldNode - The node losing focus (null if nothing was focused)
|
|
30
|
+
* @param newNode - The node gaining focus (null on blur)
|
|
31
|
+
* @param origin - How focus was acquired
|
|
32
|
+
*/
|
|
33
|
+
export type FocusChangeCallback = (oldNode: TeaNode | null, newNode: TeaNode | null, origin: FocusOrigin | null) => void
|
|
34
|
+
|
|
35
|
+
export interface FocusSnapshot {
|
|
36
|
+
activeId: string | null
|
|
37
|
+
previousId: string | null
|
|
38
|
+
focusOrigin: FocusOrigin | null
|
|
39
|
+
scopeStack: readonly string[]
|
|
40
|
+
/** The currently active peer scope (WPF FocusScope model) */
|
|
41
|
+
activeScopeId: string | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FocusManagerOptions {
|
|
45
|
+
/** Called when focus changes — wire up event dispatch here */
|
|
46
|
+
onFocusChange?: FocusChangeCallback
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FocusManager {
|
|
50
|
+
/** Currently focused node */
|
|
51
|
+
readonly activeElement: TeaNode | null
|
|
52
|
+
/** testID of the currently focused node */
|
|
53
|
+
readonly activeId: string | null
|
|
54
|
+
/** Previously focused node */
|
|
55
|
+
readonly previousElement: TeaNode | null
|
|
56
|
+
/** testID of the previously focused node */
|
|
57
|
+
readonly previousId: string | null
|
|
58
|
+
/** How focus was most recently acquired */
|
|
59
|
+
readonly focusOrigin: FocusOrigin | null
|
|
60
|
+
/** Stack of active focus scope IDs */
|
|
61
|
+
readonly scopeStack: readonly string[]
|
|
62
|
+
/** Map of scope ID -> last focused testID within that scope */
|
|
63
|
+
readonly scopeMemory: Readonly<Record<string, string>>
|
|
64
|
+
|
|
65
|
+
/** Focus a specific node */
|
|
66
|
+
focus(node: TeaNode, origin?: FocusOrigin): void
|
|
67
|
+
/** Focus a node by testID (requires root for tree search) */
|
|
68
|
+
focusById(id: string, root: TeaNode, origin?: FocusOrigin): void
|
|
69
|
+
/** Clear focus */
|
|
70
|
+
blur(): void
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle a subtree being removed from the tree.
|
|
74
|
+
* If the focused node (or previous node) is within the removed subtree,
|
|
75
|
+
* clear the reference to prevent dead node retention and broken navigation.
|
|
76
|
+
*/
|
|
77
|
+
handleSubtreeRemoved(removedRoot: TeaNode): void
|
|
78
|
+
|
|
79
|
+
/** Push a focus scope onto the stack */
|
|
80
|
+
enterScope(scopeId: string): void
|
|
81
|
+
/** Pop the current focus scope */
|
|
82
|
+
exitScope(): void
|
|
83
|
+
|
|
84
|
+
/** The currently active peer scope ID (WPF FocusScope model) */
|
|
85
|
+
readonly activeScopeId: string | null
|
|
86
|
+
/**
|
|
87
|
+
* Activate a peer focus scope. Saves current focus in the old scope's memory,
|
|
88
|
+
* switches to the new scope, and restores the remembered focus (or focuses
|
|
89
|
+
* the first focusable element in the scope subtree).
|
|
90
|
+
*/
|
|
91
|
+
activateScope(scopeId: string, root: TeaNode): void
|
|
92
|
+
|
|
93
|
+
/** Get the testID path from focused node to root */
|
|
94
|
+
getFocusPath(root: TeaNode): string[]
|
|
95
|
+
/** Check if a subtree rooted at testID contains the focused node */
|
|
96
|
+
hasFocusWithin(root: TeaNode, testID: string): boolean
|
|
97
|
+
|
|
98
|
+
/** Focus the next focusable node in tab order */
|
|
99
|
+
focusNext(root: TeaNode, scope?: TeaNode): void
|
|
100
|
+
/** Focus the previous focusable node in tab order */
|
|
101
|
+
focusPrev(root: TeaNode, scope?: TeaNode): void
|
|
102
|
+
/** Focus in a spatial direction (up/down/left/right) */
|
|
103
|
+
focusDirection(
|
|
104
|
+
root: TeaNode,
|
|
105
|
+
direction: "up" | "down" | "left" | "right",
|
|
106
|
+
layoutFn?: (node: TeaNode) => Rect | null,
|
|
107
|
+
): void
|
|
108
|
+
|
|
109
|
+
/** Subscribe for React integration (useSyncExternalStore) */
|
|
110
|
+
subscribe(listener: () => void): () => void
|
|
111
|
+
/** Get immutable snapshot for useSyncExternalStore */
|
|
112
|
+
getSnapshot(): FocusSnapshot
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Factory
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
export function createFocusManager(options?: FocusManagerOptions): FocusManager {
|
|
120
|
+
const onFocusChange = options?.onFocusChange
|
|
121
|
+
|
|
122
|
+
// Internal state
|
|
123
|
+
let activeElement: TeaNode | null = null
|
|
124
|
+
let activeId: string | null = null
|
|
125
|
+
let previousElement: TeaNode | null = null
|
|
126
|
+
let previousId: string | null = null
|
|
127
|
+
let focusOrigin: FocusOrigin | null = null
|
|
128
|
+
const scopeStack: string[] = []
|
|
129
|
+
const scopeMemory: Record<string, string> = {}
|
|
130
|
+
let activeScopeId: string | null = null
|
|
131
|
+
|
|
132
|
+
// Subscriber management
|
|
133
|
+
const listeners = new Set<() => void>()
|
|
134
|
+
let snapshot: FocusSnapshot | null = null
|
|
135
|
+
/** Counter incremented on every notify(); used by activateScope to detect inner notifications. */
|
|
136
|
+
let notifyCount = 0
|
|
137
|
+
|
|
138
|
+
function notify(): void {
|
|
139
|
+
snapshot = null // Invalidate cached snapshot
|
|
140
|
+
notifyCount++
|
|
141
|
+
for (const listener of listeners) {
|
|
142
|
+
listener()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getTestID(node: TeaNode): string | null {
|
|
147
|
+
const props = node.props as Record<string, unknown>
|
|
148
|
+
return typeof props.testID === "string" ? props.testID : null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---- Focus operations ----
|
|
152
|
+
|
|
153
|
+
function focus(node: TeaNode, origin: FocusOrigin = "programmatic"): void {
|
|
154
|
+
// Skip if already focused on this node
|
|
155
|
+
if (activeElement === node) {
|
|
156
|
+
// Still update origin if different
|
|
157
|
+
if (focusOrigin !== origin) {
|
|
158
|
+
focusOrigin = origin
|
|
159
|
+
notify()
|
|
160
|
+
}
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const oldElement = activeElement
|
|
165
|
+
previousElement = activeElement
|
|
166
|
+
previousId = activeId
|
|
167
|
+
activeElement = node
|
|
168
|
+
activeId = getTestID(node)
|
|
169
|
+
focusOrigin = origin
|
|
170
|
+
|
|
171
|
+
// Remember this focus in the current scope
|
|
172
|
+
if (activeId && scopeStack.length > 0) {
|
|
173
|
+
scopeMemory[scopeStack[scopeStack.length - 1]!] = activeId
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
notify()
|
|
177
|
+
|
|
178
|
+
// Fire focus change callback (after state is updated)
|
|
179
|
+
onFocusChange?.(oldElement, node, origin)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function focusById(id: string, root: TeaNode, origin: FocusOrigin = "programmatic"): void {
|
|
183
|
+
const node = findByTestID(root, id)
|
|
184
|
+
if (node) {
|
|
185
|
+
// Walk up to the nearest focusable ancestor if the found node isn't focusable
|
|
186
|
+
const focusable = findFocusableAncestor(node)
|
|
187
|
+
if (focusable) {
|
|
188
|
+
focus(focusable, origin)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Virtual focus: set the ID without a DOM node. This enables named focus
|
|
193
|
+
// targets (e.g. "board-area", "detail-pane") without requiring wrapper Boxes
|
|
194
|
+
// that would disrupt layout.
|
|
195
|
+
const oldElement = activeElement
|
|
196
|
+
previousElement = activeElement
|
|
197
|
+
previousId = activeId
|
|
198
|
+
activeElement = null
|
|
199
|
+
activeId = id
|
|
200
|
+
focusOrigin = origin
|
|
201
|
+
notify()
|
|
202
|
+
|
|
203
|
+
// Fire focus change callback (old element blurs, no new node for virtual focus)
|
|
204
|
+
onFocusChange?.(oldElement, null, origin)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function blur(): void {
|
|
208
|
+
if (!activeElement && !activeId) return
|
|
209
|
+
|
|
210
|
+
const oldElement = activeElement
|
|
211
|
+
previousElement = activeElement
|
|
212
|
+
previousId = activeId
|
|
213
|
+
activeElement = null
|
|
214
|
+
activeId = null
|
|
215
|
+
focusOrigin = null
|
|
216
|
+
|
|
217
|
+
notify()
|
|
218
|
+
|
|
219
|
+
// Fire focus change callback (after state is updated)
|
|
220
|
+
onFocusChange?.(oldElement, null, null)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- Subtree removal ----
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if a node is the given target or contains it as a descendant.
|
|
227
|
+
*/
|
|
228
|
+
function subtreeContains(subtreeRoot: TeaNode, target: TeaNode): boolean {
|
|
229
|
+
if (subtreeRoot === target) return true
|
|
230
|
+
for (const child of subtreeRoot.children) {
|
|
231
|
+
if (subtreeContains(child, target)) return true
|
|
232
|
+
}
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Handle a subtree being removed from the tree. If the active or previous
|
|
238
|
+
* element lives within the removed subtree, clear the dangling reference.
|
|
239
|
+
* This prevents dead node retention and broken navigation (indexOf → -1).
|
|
240
|
+
*/
|
|
241
|
+
function handleSubtreeRemoved(removedRoot: TeaNode): void {
|
|
242
|
+
let changed = false
|
|
243
|
+
|
|
244
|
+
if (activeElement && subtreeContains(removedRoot, activeElement)) {
|
|
245
|
+
const oldElement = activeElement
|
|
246
|
+
previousElement = activeElement
|
|
247
|
+
previousId = activeId
|
|
248
|
+
activeElement = null
|
|
249
|
+
activeId = null
|
|
250
|
+
focusOrigin = null
|
|
251
|
+
changed = true
|
|
252
|
+
onFocusChange?.(oldElement, null, null)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (previousElement && subtreeContains(removedRoot, previousElement)) {
|
|
256
|
+
previousElement = null
|
|
257
|
+
previousId = null
|
|
258
|
+
changed = true
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (changed) {
|
|
262
|
+
notify()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- Scope management ----
|
|
267
|
+
|
|
268
|
+
function enterScope(scopeId: string): void {
|
|
269
|
+
scopeStack.push(scopeId)
|
|
270
|
+
notify()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function exitScope(): void {
|
|
274
|
+
const exited = scopeStack.pop()
|
|
275
|
+
if (exited === undefined) return
|
|
276
|
+
|
|
277
|
+
// Restore focus to the remembered element in the parent scope
|
|
278
|
+
// (Caller is responsible for providing root to restore if needed)
|
|
279
|
+
notify()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---- Peer scope activation (WPF FocusScope model) ----
|
|
283
|
+
|
|
284
|
+
function activateScope(scopeId: string, root: TeaNode): void {
|
|
285
|
+
// Save current focus in the outgoing scope's memory
|
|
286
|
+
if (activeScopeId && activeId) {
|
|
287
|
+
scopeMemory[activeScopeId] = activeId
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Switch scope
|
|
291
|
+
activeScopeId = scopeId
|
|
292
|
+
|
|
293
|
+
// Restore focus: remembered element, or first focusable in scope.
|
|
294
|
+
// Track whether notify() fired during focus/focusById to avoid double-notify.
|
|
295
|
+
const countBefore = notifyCount
|
|
296
|
+
const remembered = scopeMemory[scopeId]
|
|
297
|
+
if (remembered) {
|
|
298
|
+
focusById(remembered, root, "programmatic")
|
|
299
|
+
} else {
|
|
300
|
+
const scopeNode = findByTestID(root, scopeId)
|
|
301
|
+
if (scopeNode) {
|
|
302
|
+
const order = getTabOrder(root, scopeNode)
|
|
303
|
+
if (order.length > 0) {
|
|
304
|
+
focus(order[0]!, "programmatic")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Only notify if focus/focusById didn't already notify.
|
|
310
|
+
if (notifyCount === countBefore) {
|
|
311
|
+
notify()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---- Tree queries ----
|
|
316
|
+
|
|
317
|
+
function getFocusPath(root: TeaNode): string[] {
|
|
318
|
+
if (!activeElement) return []
|
|
319
|
+
|
|
320
|
+
const path: string[] = []
|
|
321
|
+
let current: TeaNode | null = activeElement
|
|
322
|
+
while (current && current !== root.parent) {
|
|
323
|
+
const id = getTestID(current)
|
|
324
|
+
if (id) path.push(id)
|
|
325
|
+
current = current.parent
|
|
326
|
+
}
|
|
327
|
+
return path
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function hasFocusWithin(root: TeaNode, testID: string): boolean {
|
|
331
|
+
if (!activeElement) return false
|
|
332
|
+
|
|
333
|
+
// Find the node with the given testID
|
|
334
|
+
const target = findByTestID(root, testID)
|
|
335
|
+
if (!target) return false
|
|
336
|
+
|
|
337
|
+
// Walk up from activeElement to see if we pass through target
|
|
338
|
+
let current: TeaNode | null = activeElement
|
|
339
|
+
while (current) {
|
|
340
|
+
if (current === target) return true
|
|
341
|
+
current = current.parent
|
|
342
|
+
}
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---- Navigation ----
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Resolve the effective scope node for tab navigation.
|
|
350
|
+
* If an explicit scope is provided, use it. Otherwise, if the scopeStack
|
|
351
|
+
* is non-empty, find the topmost scope node in the tree by testID.
|
|
352
|
+
*/
|
|
353
|
+
function resolveScope(root: TeaNode, explicitScope?: TeaNode): TeaNode | undefined {
|
|
354
|
+
if (explicitScope) return explicitScope
|
|
355
|
+
|
|
356
|
+
if (scopeStack.length > 0) {
|
|
357
|
+
const scopeId = scopeStack[scopeStack.length - 1]!
|
|
358
|
+
const scopeNode = findByTestID(root, scopeId)
|
|
359
|
+
if (scopeNode) return scopeNode
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return undefined
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function focusNext(root: TeaNode, scope?: TeaNode): void {
|
|
366
|
+
const effectiveScope = resolveScope(root, scope)
|
|
367
|
+
const order = getTabOrder(root, effectiveScope)
|
|
368
|
+
if (order.length === 0) return
|
|
369
|
+
|
|
370
|
+
if (!activeElement) {
|
|
371
|
+
// Nothing focused — focus the first element
|
|
372
|
+
focus(order[0]!, "keyboard")
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const currentIndex = order.indexOf(activeElement)
|
|
377
|
+
// Wrap around to the first element
|
|
378
|
+
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % order.length
|
|
379
|
+
focus(order[nextIndex]!, "keyboard")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function focusPrev(root: TeaNode, scope?: TeaNode): void {
|
|
383
|
+
const effectiveScope = resolveScope(root, scope)
|
|
384
|
+
const order = getTabOrder(root, effectiveScope)
|
|
385
|
+
if (order.length === 0) return
|
|
386
|
+
|
|
387
|
+
if (!activeElement) {
|
|
388
|
+
// Nothing focused — focus the last element
|
|
389
|
+
focus(order[order.length - 1]!, "keyboard")
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const currentIndex = order.indexOf(activeElement)
|
|
394
|
+
// Wrap around to the last element
|
|
395
|
+
const prevIndex = currentIndex <= 0 ? order.length - 1 : currentIndex - 1
|
|
396
|
+
focus(order[prevIndex]!, "keyboard")
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function focusDirection(
|
|
400
|
+
root: TeaNode,
|
|
401
|
+
direction: "up" | "down" | "left" | "right",
|
|
402
|
+
layoutFn?: (node: TeaNode) => Rect | null,
|
|
403
|
+
): void {
|
|
404
|
+
if (!activeElement) return
|
|
405
|
+
|
|
406
|
+
// Check for explicit focus link first
|
|
407
|
+
const explicitTarget = getExplicitFocusLink(activeElement, direction)
|
|
408
|
+
if (explicitTarget) {
|
|
409
|
+
focusById(explicitTarget, root, "keyboard")
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fall back to spatial navigation
|
|
414
|
+
const candidates = getTabOrder(root)
|
|
415
|
+
const resolvedLayoutFn = layoutFn ?? ((node: TeaNode) => node.screenRect)
|
|
416
|
+
const target = findSpatialTarget(activeElement, direction, candidates, resolvedLayoutFn)
|
|
417
|
+
if (target) {
|
|
418
|
+
focus(target, "keyboard")
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ---- Subscribe/snapshot for useSyncExternalStore ----
|
|
423
|
+
|
|
424
|
+
function subscribe(listener: () => void): () => void {
|
|
425
|
+
listeners.add(listener)
|
|
426
|
+
return () => {
|
|
427
|
+
listeners.delete(listener)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function getSnapshot(): FocusSnapshot {
|
|
432
|
+
if (!snapshot) {
|
|
433
|
+
snapshot = {
|
|
434
|
+
activeId,
|
|
435
|
+
previousId,
|
|
436
|
+
focusOrigin,
|
|
437
|
+
scopeStack: [...scopeStack],
|
|
438
|
+
activeScopeId,
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return snapshot
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---- Public interface ----
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
get activeElement() {
|
|
448
|
+
return activeElement
|
|
449
|
+
},
|
|
450
|
+
get activeId() {
|
|
451
|
+
return activeId
|
|
452
|
+
},
|
|
453
|
+
get previousElement() {
|
|
454
|
+
return previousElement
|
|
455
|
+
},
|
|
456
|
+
get previousId() {
|
|
457
|
+
return previousId
|
|
458
|
+
},
|
|
459
|
+
get focusOrigin() {
|
|
460
|
+
return focusOrigin
|
|
461
|
+
},
|
|
462
|
+
get scopeStack() {
|
|
463
|
+
return [...scopeStack] as readonly string[]
|
|
464
|
+
},
|
|
465
|
+
get scopeMemory() {
|
|
466
|
+
return scopeMemory as Readonly<Record<string, string>>
|
|
467
|
+
},
|
|
468
|
+
get activeScopeId() {
|
|
469
|
+
return activeScopeId
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
focus,
|
|
473
|
+
focusById,
|
|
474
|
+
blur,
|
|
475
|
+
handleSubtreeRemoved,
|
|
476
|
+
|
|
477
|
+
enterScope,
|
|
478
|
+
exitScope,
|
|
479
|
+
activateScope,
|
|
480
|
+
|
|
481
|
+
getFocusPath,
|
|
482
|
+
hasFocusWithin,
|
|
483
|
+
|
|
484
|
+
focusNext,
|
|
485
|
+
focusPrev,
|
|
486
|
+
focusDirection,
|
|
487
|
+
|
|
488
|
+
subscribe,
|
|
489
|
+
getSnapshot,
|
|
490
|
+
}
|
|
491
|
+
}
|