mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 13:53:55 +01:00
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:
22
src/App.tsx
22
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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user