Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions src/hooks/use-gear-control.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { CONTROL_FLASH_MS } from '../constants';
import { eventTargetsEditableControl, keyboardEventHasModifiers } from '../lib/dom';
import {
eventTargetsEditableControl,
keyboardEventHasModifiers,
keyboardEventUsesNativeEnterAction,
} from '../lib/dom';
import {
GEAR_STORAGE_KEY,
SHIFTING_CONNECTION_MESSAGE,
shiftedGear,
storedGear,
} from '../lib/gears';
import { resistanceDirectionForKey } from '../lib/resistance';
import { resistanceDirectionForKeyboardEvent } from '../lib/resistance';
import type { ResistanceAdjustmentDirection } from '../types';

export function useGearControl({
Expand Down Expand Up @@ -83,12 +87,13 @@ export function useGearControl({
if (
event.defaultPrevented ||
keyboardEventHasModifiers(event) ||
keyboardEventUsesNativeEnterAction(event) ||
!keyboardControlsEnabled.current ||
(!isGearControl && eventTargetsEditableControl(event))
) {
return;
}
const direction = resistanceDirectionForKey(event.key);
const direction = resistanceDirectionForKeyboardEvent(event);
if (!direction) {
return;
}
Expand Down
13 changes: 9 additions & 4 deletions src/hooks/use-trainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { useSelector } from '@tanstack/react-store';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { CONTROL_FLASH_MS } from '../constants';
import { deviceConnectionView } from '../lib/device-connection';
import { eventTargetsEditableControl, keyboardEventHasModifiers } from '../lib/dom';
import {
eventTargetsEditableControl,
keyboardEventHasModifiers,
keyboardEventUsesNativeEnterAction,
} from '../lib/dom';
import { errorMessage } from '../lib/errors';
import { resistanceAfterGearShift } from '../lib/gears';
import { scheduleNoticeDismissal } from '../lib/notification';
Expand All @@ -12,7 +16,7 @@ import type { RememberedBluetoothDeviceCatalog } from '../lib/remembered-bluetoo
import {
clampResistance,
DEFAULT_RESISTANCE,
resistanceDirectionForKey,
resistanceDirectionForKeyboardEvent,
resistanceRampDuration,
smoothedResistance,
} from '../lib/resistance';
Expand Down Expand Up @@ -203,13 +207,14 @@ export function useTrainer(
if (
event.defaultPrevented ||
keyboardEventHasModifiers(event) ||
keyboardEventUsesNativeEnterAction(event) ||
(!isResistanceControl && eventTargetsEditableControl(event)) ||
!keyboardControlsEnabled.current ||
gearControlsEnabled.current
) {
return;
}
const direction = resistanceDirectionForKey(event.key);
const direction = resistanceDirectionForKeyboardEvent(event);
if (!direction) {
return;
}
Expand All @@ -219,7 +224,7 @@ export function useTrainer(
updateResistance(resistanceTarget.current + (direction === 'increase' ? 1 : -1));
};
const handleKeyUp = (event: KeyboardEvent) => {
if (!resistanceDirectionForKey(event.key)) {
if (!resistanceDirectionForKeyboardEvent(event)) {
return;
}
window.clearTimeout(resistanceKeyFlashTimer.current);
Expand Down
4 changes: 4 additions & 0 deletions src/lib/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ export function eventTargetsInteractiveControl(event: Event): boolean {
export function keyboardEventHasModifiers(event: KeyboardEvent): boolean {
return event.altKey || event.ctrlKey || event.metaKey;
}

export function keyboardEventUsesNativeEnterAction(event: KeyboardEvent): boolean {
return event.key === 'Enter' && eventTargetsInteractiveControl(event);
}
17 changes: 11 additions & 6 deletions src/lib/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,23 @@ export const dashboardKeyboardShortcuts: KeyboardShortcutDescription[] = [
{ group: 'Session', keys: ['q'], label: 'End the current session' },
{ group: 'Session', keys: ['n'], label: 'Start a new session after ending' },
{ group: 'Session', keys: ['h'], label: 'Open session history' },
{ group: 'Ride controls', keys: ['↑', '↓'], label: 'Increase or decrease resistance' },
{ group: 'Ride controls', keys: ['↑', 'Return'], label: 'Increase resistance' },
{ group: 'Ride controls', keys: ['↓', 'Right Shift'], label: 'Decrease resistance' },
{ group: 'Ride controls', keys: ['←', '→'], label: 'Change the chart view' },
{ group: 'General', keys: ['?'], label: 'Show keyboard shortcuts' },
{ group: 'General', keys: ['Esc'], label: 'Close an open dialog' },
];

export const gearingKeyboardShortcuts: KeyboardShortcutDescription[] =
dashboardKeyboardShortcuts.map((shortcut) =>
shortcut.label === 'Increase or decrease resistance'
? { ...shortcut, label: 'Shift to a harder or easier gear' }
: shortcut
);
dashboardKeyboardShortcuts.map((shortcut) => {
if (shortcut.label === 'Increase resistance') {
return { ...shortcut, label: 'Shift to a harder gear' };
}
if (shortcut.label === 'Decrease resistance') {
return { ...shortcut, label: 'Shift to an easier gear' };
}
return shortcut;
});

export const historyKeyboardShortcuts: KeyboardShortcutDescription[] = [
{ group: 'Navigation', keys: ['↑', '↓'], label: 'Select the previous or next session' },
Expand Down
9 changes: 6 additions & 3 deletions src/lib/resistance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ export function resistanceAdjustmentDirection(
}
}

export function resistanceDirectionForKey(key: string): ResistanceAdjustmentDirection | undefined {
if (key === 'ArrowUp') {
export function resistanceDirectionForKeyboardEvent({
code,
key,
}: Pick<KeyboardEvent, 'code' | 'key'>): ResistanceAdjustmentDirection | undefined {
if (key === 'ArrowUp' || key === 'Enter') {
return 'increase';
}
if (key === 'ArrowDown') {
if (key === 'ArrowDown' || code === 'ShiftRight') {
return 'decrease';
}
}
Expand Down
5 changes: 4 additions & 1 deletion tests/components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,10 @@ describe('view components', () => {
expect(html).toContain('Open session history');
expect(html).toContain('End the current session');
expect(html).toContain('Start a new session after ending');
expect(html).toContain('Increase or decrease resistance');
expect(html).toContain('Increase resistance');
expect(html).toContain('Decrease resistance');
expect(html).toContain('Return');
expect(html).toContain('Right Shift');
expect(html).toContain('Change the chart view');
expect(html).toContain('SESSION');
expect(html).toContain('RIDE CONTROLS');
Expand Down
9 changes: 8 additions & 1 deletion tests/keyboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ describe('keyboard shortcuts', () => {
});

test('describes gear keys when Click is paired', () => {
expect(gearingKeyboardShortcuts[4]?.label).toBe('Shift to a harder or easier gear');
expect(gearingKeyboardShortcuts.slice(4, 6)).toEqual([
{ group: 'Ride controls', keys: ['↑', 'Return'], label: 'Shift to a harder gear' },
{
group: 'Ride controls',
keys: ['↓', 'Right Shift'],
label: 'Shift to an easier gear',
},
]);
});

test('maps history navigation keys', () => {
Expand Down
25 changes: 20 additions & 5 deletions tests/resistance.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'bun:test';
import {
resistanceAdjustmentDirection,
resistanceDirectionForKey,
resistanceDirectionForKeyboardEvent,
resistanceRampDuration,
smoothedResistance,
} from '../src/lib/resistance';
Expand All @@ -13,10 +13,25 @@ describe('resistance smoothing', () => {
expect(resistanceAdjustmentDirection(20, 20)).toBeUndefined();
});

test('maps arrow keys to resistance adjustment directions', () => {
expect(resistanceDirectionForKey('ArrowUp')).toBe('increase');
expect(resistanceDirectionForKey('ArrowDown')).toBe('decrease');
expect(resistanceDirectionForKey('Enter')).toBeUndefined();
test('maps arrow and right-side keyboard keys to adjustment directions', () => {
expect(resistanceDirectionForKeyboardEvent({ code: 'ArrowUp', key: 'ArrowUp' })).toBe(
'increase'
);
expect(resistanceDirectionForKeyboardEvent({ code: 'Enter', key: 'Enter' })).toBe(
'increase'
);
expect(resistanceDirectionForKeyboardEvent({ code: 'NumpadEnter', key: 'Enter' })).toBe(
'increase'
);
expect(resistanceDirectionForKeyboardEvent({ code: 'ArrowDown', key: 'ArrowDown' })).toBe(
'decrease'
);
expect(resistanceDirectionForKeyboardEvent({ code: 'ShiftRight', key: 'Shift' })).toBe(
'decrease'
);
expect(
resistanceDirectionForKeyboardEvent({ code: 'ShiftLeft', key: 'Shift' })
).toBeUndefined();
});

test('scales and clamps the ramp duration', () => {
Expand Down