Commit 273f650
Implement Video Player Hotkey Controls (#266)
* feat: implement comprehensive video player hotkeys
Add full keyboard shortcut support for react-player video component:
- Space/K: Play/Pause toggle
- M: Toggle mute
- F: Toggle fullscreen
- Arrow keys: Seek (left/right 5s) and volume (up/down 5%)
- J/L: Seek backward/forward 10 seconds (YouTube style)
- 0-9: Jump to video percentages (0=start, 5=50%, 9=90%)
- Home/End: Jump to beginning/end of video
- </> (Shift+comma/period): Adjust playback speed by 0.25x
Fixed commented-out arrow key seek functionality by using proper
ReactPlayer API methods (seekTo() instead of direct currentTime).
All hotkeys are disabled when typing in input fields to prevent
conflicts with text entry.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: prevent native controls from interfering with arrow key seeking
Use event capture phase and stopImmediatePropagation to intercept
keyboard events before they reach the native HTML5 video controls.
This fixes the issue where arrow keys would seek 30+ seconds instead
of the intended 5 seconds due to both handlers responding.
Changes:
- Add { capture: true } to addEventListener for capture phase handling
- Add stopImmediatePropagation() to all hotkey cases
- Add explanatory comment about capture phase usage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: intercept keyboard events on wrapper element to prevent native controls interference
Changed approach to fix arrow key double-seeking issue:
- Added wrapperRef to Box container element
- Made wrapper focusable with tabindex="0" and auto-focus on mount
- Attached keyboard event listeners to both wrapper (capture phase) and document
- This intercepts events before they reach the native video element
The wrapper-level capture ensures our custom hotkeys fully override
the native HTML5 video keyboard shortcuts, preventing the 30s jump
when only 5s was intended.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix
* f
* test: fix video player unit tests for new hotkey implementation
Updated ReactPlayer mock to include methods required by the new
keyboard shortcut implementation:
- Added mockGetCurrentTime(): Returns 10 seconds (mock current time)
- Added mockGetDuration(): Returns 100 seconds (mock duration)
- Added mockSeekTo(): Mock function for seeking operations
- Added wrapper element mock for fullscreen functionality
These methods are now called in the keyboard event handler to get
the current playback position before seeking, which was causing
tests to fail with "getCurrentTime is not a function" errors.
All 12 tests now pass successfully.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: prevent duplicate handlePlay/handlePause calls on keyboard toggle
Previously, pressing Space or K to toggle play/pause would call
handlePlay()/handlePause() twice:
1. Manual call in keyboard handler
2. Automatic call via ReactPlayer's onPlay/onPause events
This caused duplicate progress tracking updates and other side effects.
Changes:
- Removed manual handlePlay()/handlePause() calls from keyboard handler
- Now just toggles `playing` state and lets ReactPlayer's event
handlers (onPlay/onPause) trigger the callbacks once
- Updated useEffect dependencies to remove unused handlePlay/handlePause
- Added explanatory comment about ReactPlayer handling side effects
All tests still pass (12/12).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: improve accessibility and scope keyboard event listeners
Accessibility improvements:
- Made wrapper focusable declaratively with tabIndex={0} in JSX
- Removed imperative auto-focus that steals focus from user on mount
- Restored visible focus indicator (2px primary color outline)
- Used :focus-visible to hide outline for mouse clicks
Event listener scoping improvements:
- Removed always-active document-level keyboard listener
- Scoped keyboard shortcuts to wrapper element (requires focus)
- Added fullscreenchange handler to dynamically add/remove document
listener when entering/exiting fullscreen
- Replaced stopImmediatePropagation() with stopPropagation()
Benefits:
- Better accessibility for screen readers and keyboard users
- More predictable behavior (shortcuts require focus outside fullscreen)
- No conflicts with other page elements
- Cleaner architecture (declarative vs imperative)
- Global shortcuts only in fullscreen when it makes UX sense
Test updates:
- Updated all keyboard tests to fire events on wrapper instead of document
- Added data-testid="video-player-wrapper" for testing
- All 12 tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a3d2233 commit 273f650
3 files changed
Lines changed: 222 additions & 51 deletions
File tree
- apps/watch/src
- packages/ui/src/watch/videos/video-player
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
1 | 3 | | |
2 | 4 | | |
3 | 5 | | |
| |||
27 | 29 | | |
28 | 30 | | |
29 | 31 | | |
30 | | - | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
36 | | - | |
| 38 | + | |
37 | 39 | | |
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
41 | 43 | | |
42 | | - | |
| 44 | + | |
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
| |||
48 | 50 | | |
49 | 51 | | |
50 | 52 | | |
51 | | - | |
| 53 | + | |
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
| |||
Lines changed: 37 additions & 20 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
9 | 13 | | |
10 | 14 | | |
11 | 15 | | |
| |||
16 | 20 | | |
17 | 21 | | |
18 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
19 | 27 | | |
20 | 28 | | |
21 | 29 | | |
| |||
191 | 199 | | |
192 | 200 | | |
193 | 201 | | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
194 | 205 | | |
195 | 206 | | |
196 | 207 | | |
197 | 208 | | |
198 | | - | |
| 209 | + | |
199 | 210 | | |
200 | | - | |
201 | | - | |
| 211 | + | |
| 212 | + | |
202 | 213 | | |
203 | 214 | | |
204 | | - | |
| 215 | + | |
205 | 216 | | |
206 | 217 | | |
207 | 218 | | |
208 | 219 | | |
209 | 220 | | |
210 | 221 | | |
211 | | - | |
| 222 | + | |
212 | 223 | | |
213 | | - | |
214 | | - | |
| 224 | + | |
| 225 | + | |
215 | 226 | | |
216 | 227 | | |
217 | | - | |
| 228 | + | |
218 | 229 | | |
219 | 230 | | |
220 | 231 | | |
221 | 232 | | |
222 | 233 | | |
223 | 234 | | |
224 | | - | |
| 235 | + | |
225 | 236 | | |
226 | | - | |
227 | | - | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
228 | 242 | | |
229 | 243 | | |
230 | | - | |
| 244 | + | |
231 | 245 | | |
232 | 246 | | |
233 | 247 | | |
234 | 248 | | |
235 | 249 | | |
236 | 250 | | |
237 | | - | |
| 251 | + | |
238 | 252 | | |
239 | | - | |
240 | | - | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
241 | 258 | | |
242 | 259 | | |
243 | | - | |
| 260 | + | |
244 | 261 | | |
245 | 262 | | |
246 | 263 | | |
247 | 264 | | |
248 | 265 | | |
249 | 266 | | |
250 | | - | |
| 267 | + | |
251 | 268 | | |
252 | | - | |
253 | | - | |
| 269 | + | |
| 270 | + | |
254 | 271 | | |
255 | 272 | | |
256 | | - | |
| 273 | + | |
257 | 274 | | |
258 | 275 | | |
259 | 276 | | |
| |||
0 commit comments