diff --git a/src/App.tsx b/src/App.tsx index 4174bc7..aeb5c20 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -126,7 +126,6 @@ function App() { const onInteractive = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || - target instanceof HTMLButtonElement || target instanceof HTMLSelectElement || !!target?.isContentEditable; @@ -135,12 +134,17 @@ function App() { else if (settings.zenMode) setSettings(s => ({ ...s, zenMode: false })); return; } - // Focused buttons handle their own keys (native activation); - // global shortcuts must not double-fire or block them. - if (onInteractive) return; + // Text inputs and selects must keep their native keys — shortcuts are + // for typing-free contexts. FOCUSED BUTTONS however stay eligible: + // after a click the button keeps focus, and blocking global shortcuts + // there made N/R/P/L silently dead until the user clicked elsewhere. + // Space/Enter on a focused button activate it natively and return below, + // so no double-fire. - // Prevent default for space to stop scrolling - if (e.code === 'Space') { + // Prevent default for space to stop scrolling (Space on a focused + // button activates it natively — preventDefault only applies when the + // focus is on the body, where the shortcut below takes over). + if (e.code === 'Space' && !(target instanceof HTMLButtonElement)) { e.preventDefault(); } @@ -171,7 +175,9 @@ function App() { setListening(l => !l); break; } - if (e.code === 'Space' || e.code === 'Enter') { + // Space/Enter on a focused button natively activate that button + // (e.g. Start); only treat them as pause-toggle when NOT on a button. + if ((e.code === 'Space' || e.code === 'Enter') && !(e.target instanceof HTMLButtonElement)) { phraseRef.current?.pauseToggle(); } return; @@ -221,7 +227,7 @@ function App() { break; } - if (e.code === 'Space' || e.code === 'Enter') { + if ((e.code === 'Space' || e.code === 'Enter') && !(e.target instanceof HTMLButtonElement)) { singleNoteRef.current?.skipNote(); } }; diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index a695d9e..f1b30b4 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -307,9 +307,7 @@ export const PhraseTrainer = forwardRef( }, [scoreOk, fixedMaterial, pool, fullScore]); const nextAction = useCallback(() => { - if (phrase.material === 'melody' || (phrase.material === 'arpeggio' && phrase.arpeggioBars > 1)) { - setMelodySeed(s => (s + 1) % 999983); - } else if (fixedMaterial && scoreOk) { + if (fixedMaterial && scoreOk) { const total = fullScore!.value.measures.length; const nextStart = phrase.startBar + phrase.barCount; if (nextStart > total) { @@ -317,6 +315,12 @@ export const PhraseTrainer = forwardRef( } else { setPhrase({ startBar: nextStart }); } + return; + } + // Seeded material: roll a new variation (giuliani and single-chord + // arpeggios included — they take melodySeed too). + if (phrase.material === 'melody' || phrase.material === 'arpeggio' || phrase.material === 'giuliani') { + setMelodySeed(s => (s + 1) % 999983); } }, [phrase.material, phrase.startBar, phrase.barCount, fixedMaterial, scoreOk, fullScore, setPhrase]); @@ -766,10 +770,10 @@ export const PhraseTrainer = forwardRef( Retry - {(phrase.material === 'melody' || phrase.material === 'arpeggio' || fixedMaterial) && ( + {(phrase.material === 'melody' || phrase.material === 'arpeggio' || phrase.material === 'giuliani' || fixedMaterial) && ( )} {phrase.pace === 'step' && ( @@ -787,10 +791,10 @@ export const PhraseTrainer = forwardRef( Retry - {(phrase.material === 'melody' || phrase.material === 'arpeggio' || fixedMaterial) && ( + {(phrase.material === 'melody' || phrase.material === 'arpeggio' || phrase.material === 'giuliani' || fixedMaterial) && ( )}