import * as rc from "../rive_advanced.mjs"; import { TextInputProxy } from "./textInputProxy"; export interface KeyboardInteractionsParams { canvas: HTMLCanvasElement; stateMachine: rc.StateMachineInstance; /** * Whether this canvas has focus nodes that should participate in tab traversal. * When true, Tab/Shift+Tab will be intercepted and routed to the Rive focus manager. * Tab is released to the page only when focusNext() returns false and Rive no longer holds * focus; a Stop edge also returns false but keeps focus, so Tab stays trapped. */ hasFocusNodes: boolean; /** * Optional accessibility overlay that should be treated as part of this Rive * instance's focus domain. This is lazy because the overlay may be created * after keyboard listeners are registered. */ getOverlayElement?: () => HTMLElement | null; } /** * Tracks the relationship between DOM focus inside this Rive focus domain (canvas or semantic overlay) * and Rive's internal focus for the current focus session. * * NotFocused — DOM focus left the domain, Rive released focus internally, or Tab walked * off the end of the tree. Keyboard input isn't ours, so Tab is ignored and * reaches the next page element. * EntryPending — DOM focus is inside the domain but Rive holds no node yet, so the next Tab * enters the focus tree. Set by pointer focus on the canvas, by assistive technology (AT) focus landing * in the overlay, and by keyboard focus whose entry attempt found no eligible node. * RiveFocused — a Rive node holds focus. Tab/Shift+Tab route to the Rive focus manager and stay * inside the domain until either Rive reports focus ended (pollFocusState) or * Tab walks off the edge of the tree (focusNext()/focusPrevious() returns false and * Rive no longer holds focus). * * Keyboard focus on the canvas enters the tree immediately: onCanvasFocus infers direction from * where focus came from and goes straight to RiveFocused when a node accepts. */ export declare enum FocusSessionState { NotFocused = "notFocused", EntryPending = "entryPending", RiveFocused = "riveFocused" } /** * Manages keyboard and DOM focus interactions for Rive's focus domain (, semantic * overlay, text input proxy). Owns the proxy and the text input session state. * Because keyboard events can apply on either part of the domain, we need to track what events we should * handle/intercept, and when to release focus back to the page outside of the domain. * * Tracks the canvas focus session state (focusSessionState) and routes * Tab/Shift+Tab to the Rive state machine's focus manager. Exposes shared * state as properties so the Rive render loop can read them directly. */ export declare class KeyboardInteractions { focusSessionState: FocusSessionState; private canvas; private mainSm; private hasFocusNodes; /** Cached callback that returns the accessibility overlay element once created. */ private getOverlayElement?; /** Whether the canvas currently has browser DOM focus. */ private canvasHasFocus; /** After Tab exits the last Rive node, ignore keydowns until focus re-enters the focus domain. */ private focusDomainReleased; /** Overlay element currently wired with focusin/keydown listeners, if any. */ private currentOverlayElement; /** Canvas parent (or document) watched for focusin to attach overlay listeners lazily. */ private focusDomainHost; /** Codes whose keydown an overlay widget took, so their keyup is skipped too. */ private claimedKeyCodes; private readonly inputProxy; private textInputSessionActive; constructor({ canvas, stateMachine, hasFocusNodes, getOverlayElement, }: KeyboardInteractionsParams); isTextInputSessionActive(): boolean; /** Session active and the proxy holds DOM focus. */ isEditingHostFocused(): boolean; /** @param focusProxy move DOM focus to the proxy; false leaves it where it is. */ beginTextInputSession(focusProxy: boolean): void; /** @param fromBlur focus is already leaving; don't park it on the canvas. */ endTextInputSession(fromBlur?: boolean): void; /** Must run synchronously in the pointer gesture or mobile keyboards won't open. */ summonForPointer(): void; /** The text-input proxy, for the semantics layer to decorate while editing. */ get editingHost(): TextInputProxy; /** * The proxy is always empty: copy Rive's selection; always preventDefault. */ private onCopy; private handleProxyBlur; /** * Set the FocusSessionState. Useful for invoking a Rive "blur" without actually blurring from the . This * helps put the DOM focus state on the canvas rather than the , so the user doesn't lose the spot in page navigation * * @param state FocusSessionState enum */ setFocusSessionState(state: FocusSessionState): void; /** * Called by pollFocusState on the Rive instance when it observes hasFocus=true. Rive acquired * focus internally (e.g. via a listener action or state transition) without a DOM focus event, * so mark the session RiveFocused. This cannot resurrect a session that a DOM blur ended, * because onCanvasBlur clears Rive's focus alongside it. */ notifyRiveFocused(): void; /** * Handles the canvas gaining browser focus. The behavior differs based on how focus was gained - * * Pointer-driven focus: the canvas now has focus but Rive holds nothing yet, so we move to EntryPending — this lets the * next Tab enter the focus tree even when the focus is pointer-driven * * Keyboard-driven focus: we enter the Rive focus tree immediately once canvas gains focus. * The direction is inferred from where focus came from: an element before the canvas in DOM order * means a forward Tab (focusNext), one after means a Shift+Tab (focusPrevious). :focus-visible * gates this so a click doesn't yank Rive focus to the first node on the focus event itself. */ onCanvasFocus: (event: FocusEvent) => void; /** * Marks internal state that the canvas has lost DOM focus. Do not actually clear * Rive focus though if: * 1. DOM focus is still within Rive domain (i.e., semantic overlay) * 2. Document just lost focus (i.e. tab switching) * * When we're not in either of those buckets, it's safe to call `clearFocus()` on the SMI. */ onCanvasBlur: (event: FocusEvent) => void; /** Blur into the focus domain, or the whole document losing focus (tab/window switch). */ private blurStaysWithRive; /** * Assistive technology (AT) focus landing inside the overlay is DOM focus inside the Rive focus domain, so open a * session even when no Rive node holds focus yet. shouldRiveHandleKeyEvent treats NotFocused * as authoritative, so without this the overlay's Tab keydowns reach onKeyDown and get dropped * at that gate — the browser would move focus out of Rive instead of to the next focus node. */ private onOverlayFocusIn; /** Overlay listeners attach lazily, so the first focusin only ever lands here. */ private onFocusDomainHostFocusIn; /** * Handles keydown events in the Rive focus domain. We route certain keys to different functions, with the following * fallback behavior: * 1. Tab/Shift+Tab - check if keyInput() takes this key, if not, call focusNext/Previous traversal if there are focus nodes * 2. Check - if a semantic overlay element's key handler claimed the event, don't intercept the key event further * 3. Send key to `keyInput()` on Rive state machine if applicable. * 4. If the keyInput returned false, and the key is directional arrow keys, call directional focus methods on state machine * 5. Printables: in a session the proxy's input event supplies text; otherwise textInput(). * A printable fires both keyInput and textInput even if a listener claims it. * @param event KeyboardEvent * @returns void */ onKeyDown: (event: KeyboardEvent) => void; onKeyUp: (event: KeyboardEvent) => void; /** Arrow keys map to directional focus; anything else is not a focus move. */ private focusInDirection; /** * Determine if Rive should handle keyboard input. If session state is `NotFocused` - no. * DOM focus stays parked on the canvas after Rive releases focus internally, and that Tab * has to reach the page rather than re-enter the tree. * * Otherwise, the event still has to belong to Rive: * 1. If the current DOM focus is in Rive domain (canvas or semantic overlay) * 2. If the target for the key input is for the semantic overlay, or the canvas */ private shouldRiveHandleKeyEvent; /** DOM that counts as inside Rive for focus: canvas, overlay subtree, text-input proxy. */ isInFocusDomain(target: EventTarget | null): boolean; /** Overlay only (excludes the canvas) — the accessibility overlay subtree. */ private isInOverlay; private syncOverlayListener; /** * Whether the canvas currently matches :focus-visible — the browser's heuristic for keyboard- * (vs pointer-) driven focus. For older browser versions that don't support this selector, return false * so that we don't incorrectly assume pointer vs keyboard focus. Next tab would enter the focus tree in those edge cases. */ private isKeyboardDrivenFocus; private cameFromBeforeCanvas; cleanup(): void; }