Fix Next/New no-op for seeded material + keyboard shortcuts after button clicks

- nextAction: giuliani and single-chord arpeggios now roll a new seed
  too (they consume melodySeed); previously the handler silently did
  nothing for them. Scale drills keep no Next (deterministic).
- New/Next button visibility matches the same condition, and the label
  is 'Next' for fixed material only.
- Keyboard shortcuts: HTMLButtonElement removed from the interactive
  gate — after any mouse click the button keeps focus and previously
  swallowed N/R/P/H/L/V/M. Space/Enter on a focused button still
  activate it natively and no longer double-fire the global
  pauseToggle/skip handlers.
This commit is contained in:
OpenClaw
2026-09-15 21:32:27 +00:00
parent b4ba788d8e
commit 97a9a83f68
2 changed files with 25 additions and 15 deletions

View File

@@ -126,7 +126,6 @@ function App() {
const onInteractive = const onInteractive =
target instanceof HTMLInputElement || target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement || target instanceof HTMLTextAreaElement ||
target instanceof HTMLButtonElement ||
target instanceof HTMLSelectElement || target instanceof HTMLSelectElement ||
!!target?.isContentEditable; !!target?.isContentEditable;
@@ -135,12 +134,17 @@ function App() {
else if (settings.zenMode) setSettings(s => ({ ...s, zenMode: false })); else if (settings.zenMode) setSettings(s => ({ ...s, zenMode: false }));
return; return;
} }
// Focused buttons handle their own keys (native activation); // Text inputs and selects must keep their native keys — shortcuts are
// global shortcuts must not double-fire or block them. // for typing-free contexts. FOCUSED BUTTONS however stay eligible:
if (onInteractive) return; // 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 // Prevent default for space to stop scrolling (Space on a focused
if (e.code === 'Space') { // 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(); e.preventDefault();
} }
@@ -171,7 +175,9 @@ function App() {
setListening(l => !l); setListening(l => !l);
break; 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(); phraseRef.current?.pauseToggle();
} }
return; return;
@@ -221,7 +227,7 @@ function App() {
break; break;
} }
if (e.code === 'Space' || e.code === 'Enter') { if ((e.code === 'Space' || e.code === 'Enter') && !(e.target instanceof HTMLButtonElement)) {
singleNoteRef.current?.skipNote(); singleNoteRef.current?.skipNote();
} }
}; };

View File

@@ -307,9 +307,7 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
}, [scoreOk, fixedMaterial, pool, fullScore]); }, [scoreOk, fixedMaterial, pool, fullScore]);
const nextAction = useCallback(() => { const nextAction = useCallback(() => {
if (phrase.material === 'melody' || (phrase.material === 'arpeggio' && phrase.arpeggioBars > 1)) { if (fixedMaterial && scoreOk) {
setMelodySeed(s => (s + 1) % 999983);
} else if (fixedMaterial && scoreOk) {
const total = fullScore!.value.measures.length; const total = fullScore!.value.measures.length;
const nextStart = phrase.startBar + phrase.barCount; const nextStart = phrase.startBar + phrase.barCount;
if (nextStart > total) { if (nextStart > total) {
@@ -317,6 +315,12 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
} else { } else {
setPhrase({ startBar: nextStart }); 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]); }, [phrase.material, phrase.startBar, phrase.barCount, fixedMaterial, scoreOk, fullScore, setPhrase]);
@@ -766,10 +770,10 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
<RotateCcw size={18} /> <RotateCcw size={18} />
Retry Retry
</button> </button>
{(phrase.material === 'melody' || phrase.material === 'arpeggio' || fixedMaterial) && ( {(phrase.material === 'melody' || phrase.material === 'arpeggio' || phrase.material === 'giuliani' || fixedMaterial) && (
<button className="hint-button" onClick={nextAction} title="New melody / next bars (N)"> <button className="hint-button" onClick={nextAction} title="New melody / next bars (N)">
<SkipForward size={18} /> <SkipForward size={18} />
{phrase.material === 'melody' ? 'New' : 'Next'} {(fixedMaterial || phrase.material === 'scale') ? 'Next' : 'New'}
</button> </button>
)} )}
{phrase.pace === 'step' && ( {phrase.pace === 'step' && (
@@ -787,10 +791,10 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
<RotateCcw size={18} /> <RotateCcw size={18} />
Retry Retry
</button> </button>
{(phrase.material === 'melody' || phrase.material === 'arpeggio' || fixedMaterial) && ( {(phrase.material === 'melody' || phrase.material === 'arpeggio' || phrase.material === 'giuliani' || fixedMaterial) && (
<button className="hint-button" onClick={nextAction} title="New / Next (N)"> <button className="hint-button" onClick={nextAction} title="New / Next (N)">
<SkipForward size={18} /> <SkipForward size={18} />
{phrase.material === 'melody' ? 'New' : 'Next'} {(fixedMaterial || phrase.material === 'scale') ? 'Next' : 'New'}
</button> </button>
)} )}
</> </>