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 =
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();
}
};

View File

@@ -307,9 +307,7 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
}, [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<PhraseHandle, PhraseTrainerProps>(
} 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<PhraseHandle, PhraseTrainerProps>(
<RotateCcw size={18} />
Retry
</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)">
<SkipForward size={18} />
{phrase.material === 'melody' ? 'New' : 'Next'}
{(fixedMaterial || phrase.material === 'scale') ? 'Next' : 'New'}
</button>
)}
{phrase.pace === 'step' && (
@@ -787,10 +791,10 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
<RotateCcw size={18} />
Retry
</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)">
<SkipForward size={18} />
{phrase.material === 'melody' ? 'New' : 'Next'}
{(fixedMaterial || phrase.material === 'scale') ? 'Next' : 'New'}
</button>
)}
</>