refactor(navigation): suppress the Space hotkey by prop

Replaces the view hotkey store with a suppressSpaceHotkey prop on
ViewNavigationMenu, alongside the suppressSettings prop which already exists
for exactly this purpose: a view opting out of a navigation chrome behaviour.

The store had one advantage, that it covered the preset route without the
router naming the view. That is a single expression in PresetView, and paying
for it with a global claim was the wrong trade: the claim was lifecycle state
that had to be released on unmount to avoid stranding Space for the whole
client, where a prop simply describes the route.

Verified both routes: /teleprompter and a locked preset targeting the
teleprompter leave Space to the view, while the timer and a preset targeting
the timer still open the navigation menu with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
This commit is contained in:
Claude
2026-08-13 19:13:02 +00:00
parent 323b30ea68
commit e9289685fd
4 changed files with 14 additions and 42 deletions
+6 -2
View File
@@ -100,7 +100,7 @@ export default function AppRouter() {
path='teleprompter' path='teleprompter'
element={ element={
<ViewLoader> <ViewLoader>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} /> <ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSpaceHotkey />
<Teleprompter /> <Teleprompter />
</ViewLoader> </ViewLoader>
} }
@@ -225,7 +225,11 @@ function PresetView() {
const Component = PresetViewMap[preset.target as OntimeViewPresettable]; const Component = PresetViewMap[preset.target as OntimeViewPresettable];
return ( return (
<PresetContext value={preset}> <PresetContext value={preset}>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings /> <ViewNavigationMenu
isNavigationLocked={getIsNavigationLocked()}
suppressSettings
suppressSpaceHotkey={preset.target === OntimeView.Teleprompter}
/>
{Component ? <Component /> : <NotFound />} {Component ? <Component /> : <NotFound />}
</PresetContext> </PresetContext>
); );
@@ -3,7 +3,6 @@ import { memo } from 'react';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
import { hasCustomParams } from '../../stores/savedViewParams'; import { hasCustomParams } from '../../stores/savedViewParams';
import { useViewHotkeysStore } from '../../stores/viewHotkeys';
import { useViewParamsEditorStore } from '../view-params-editor/viewParamsEditor.store'; import { useViewParamsEditorStore } from '../view-params-editor/viewParamsEditor.store';
import FloatingNavigation from './floating-navigation/FloatingNavigation'; import FloatingNavigation from './floating-navigation/FloatingNavigation';
import NavigationMenu from './NavigationMenu'; import NavigationMenu from './NavigationMenu';
@@ -14,10 +13,12 @@ interface ViewNavigationMenuProps {
isNavigationLocked?: boolean; isNavigationLocked?: boolean;
/** prevent showing settings */ /** prevent showing settings */
suppressSettings?: boolean; suppressSettings?: boolean;
/** leave Space to the view, for views which need the key themselves */
suppressSpaceHotkey?: boolean;
} }
export default memo(ViewNavigationMenu); export default memo(ViewNavigationMenu);
function ViewNavigationMenu({ isNavigationLocked, suppressSettings }: ViewNavigationMenuProps) { function ViewNavigationMenu({ isNavigationLocked, suppressSettings, suppressSpaceHotkey }: ViewNavigationMenuProps) {
const [isMenuOpen, menuHandler] = useDisclosure(); const [isMenuOpen, menuHandler] = useDisclosure();
const { open: showEditFormDrawer } = useViewParamsEditorStore(); const { open: showEditFormDrawer } = useViewParamsEditorStore();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -27,9 +28,7 @@ function ViewNavigationMenu({ isNavigationLocked, suppressSettings }: ViewNaviga
[ [
'Space', 'Space',
() => { () => {
if (isNavigationLocked) return; if (isNavigationLocked || suppressSpaceHotkey) return;
// a view can take over Space for itself, see viewHotkeys store
if (useViewHotkeysStore.getState().spaceClaimed) return;
menuHandler.toggle(); menuHandler.toggle();
}, },
{ preventDefault: true }, { preventDefault: true },
@@ -1,23 +0,0 @@
import { create } from 'zustand';
interface ViewHotkeysStore {
/** a view has taken over the Space key and the navigation menu must not react to it */
spaceClaimed: boolean;
claimSpace: () => void;
releaseSpace: () => void;
}
/**
* Lets a view take ownership of a hotkey which the view chrome also binds.
*
* The navigation menu binds Space at document level, but the teleprompter needs
* it for playback, which is the convention every prompter and every foot pedal
* follows. Prop threading does not work here because AppRouter renders
* ViewNavigationMenu generically for presets, so the claim lives in a store
* which the view sets on mount and clears on unmount.
*/
export const useViewHotkeysStore = create<ViewHotkeysStore>((set) => ({
spaceClaimed: false,
claimSpace: () => set({ spaceClaimed: true }),
releaseSpace: () => set({ spaceClaimed: false }),
}));
@@ -1,7 +1,6 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useViewParamsEditorStore } from '../../common/components/view-params-editor/viewParamsEditor.store'; import { useViewParamsEditorStore } from '../../common/components/view-params-editor/viewParamsEditor.store';
import { useViewHotkeysStore } from '../../common/stores/viewHotkeys';
import { resolveTeleprompterAction } from './teleprompter.keymap'; import { resolveTeleprompterAction } from './teleprompter.keymap';
import type { TeleprompterAction, TeleprompterController } from './teleprompter.types'; import type { TeleprompterAction, TeleprompterController } from './teleprompter.types';
@@ -18,22 +17,15 @@ const ignoredTags = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
/** /**
* Binds the prompter keymap. * Binds the prompter keymap.
* *
* Space is claimed from the navigation menu for the lifetime of the view: it is * Space is the universal run/stop key on prompter software and the centre pedal
* the universal run/stop key on prompter software and on the centre pedal of * of every three pedal controller, so this view cannot leave it to the
* every three pedal controller, so the view cannot leave it to the menu. * navigation menu. The routes which render the teleprompter pass
* suppressSpaceHotkey to ViewNavigationMenu so the menu stands down.
*/ */
export function useTeleprompterControls(args: UseTeleprompterControlsArgs) { export function useTeleprompterControls(args: UseTeleprompterControlsArgs) {
const argsRef = useRef(args); const argsRef = useRef(args);
argsRef.current = args; argsRef.current = args;
const claimSpace = useViewHotkeysStore((state) => state.claimSpace);
const releaseSpace = useViewHotkeysStore((state) => state.releaseSpace);
useEffect(() => {
claimSpace();
return releaseSpace;
}, [claimSpace, releaseSpace]);
useEffect(() => { useEffect(() => {
function applyAction(action: TeleprompterAction) { function applyAction(action: TeleprompterAction) {
const { controller, onFlip, onFontSize, onResetFontSize, onToggleHelp } = argsRef.current; const { controller, onFlip, onFontSize, onResetFontSize, onToggleHelp } = argsRef.current;