mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-18 23:01:07 +02:00
69 lines
2.6 KiB
JavaScript
69 lines
2.6 KiB
JavaScript
// Copyright (C) 2026 half144 and PS5 React contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution term: see LICENSE-ATTRIBUTION.
|
|
import {createElement, useContext, useLayoutEffect, useRef, useState} from 'react';
|
|
import {manager, navigationListeners, ScopeContext} from './runtime.js';
|
|
|
|
/**
|
|
* Groups focusable elements: navigation searches the innermost scope first, then its parents.
|
|
* @param {import('./manager.js').ScopeProps & {children?: import('react').ReactNode}} props
|
|
* `restoreFocus` defaults to true.
|
|
*/
|
|
export function FocusScope(props) {
|
|
const parent = useContext(ScopeContext);
|
|
const [scope] = useState(() => manager.createScope(parent, props));
|
|
useLayoutEffect(() => { manager.setProps(scope, props); });
|
|
useLayoutEffect(() => {
|
|
manager.mountScope(scope);
|
|
return () => manager.unmountScope(scope);
|
|
}, []);
|
|
return createElement(ScopeContext.Provider, {value: scope}, props.children);
|
|
}
|
|
|
|
const focusedKey = () => manager.focused?.props.focusKey ?? null;
|
|
|
|
/**
|
|
* The focused element's key, and imperative focus by `focusKey` (an element or a FocusScope).
|
|
* Re-renders the caller when the focused key changes.
|
|
* @returns {{focusedKey: string | null, focus: (key: string) => boolean, blur: () => void}}
|
|
*/
|
|
export function useFocus() {
|
|
const [key, setKey] = useState(focusedKey);
|
|
useLayoutEffect(() => {
|
|
setKey(focusedKey());
|
|
return manager.subscribe(() => setKey(focusedKey()));
|
|
}, []);
|
|
return {focusedKey: key, ...actions};
|
|
}
|
|
|
|
const actions = {
|
|
focus: target => manager.focus(target),
|
|
blur: () => manager.blur(),
|
|
focusedRect: () => manager.focusedRect(),
|
|
};
|
|
|
|
/**
|
|
* `useFocus()` without `focusedKey`: imperative focus that never re-renders the caller, for components
|
|
* that move focus but do not draw it (an app root, a page).
|
|
* @returns {{focus: (key: string) => boolean, blur: () => void, focusedRect: () => object | null}}
|
|
*/
|
|
export function useFocusActions() {
|
|
return actions;
|
|
}
|
|
|
|
/**
|
|
* Calls `listener` after the focus manager handles each input: a move or a blocked direction, Cross,
|
|
* Circle and the other buttons, with whether something handled them. For feedback such as sounds;
|
|
* it does not re-render the caller.
|
|
* @param {(event: import('./runtime.js').NavigationEvent) => void} listener
|
|
*/
|
|
export function useNavigationEvents(listener) {
|
|
const current = useRef(listener);
|
|
useLayoutEffect(() => { current.current = listener; });
|
|
useLayoutEffect(() => {
|
|
const forward = event => current.current(event);
|
|
navigationListeners.add(forward);
|
|
return () => { navigationListeners.delete(forward); };
|
|
}, []);
|
|
}
|