@therealtinhtute/baroiplayer 1.2.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -73
- package/dist/components/icons/index.d.ts +36 -0
- package/dist/components/icons/index.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/ArrowLeftIcon.d.ts +4 -0
- package/dist/components/icons/svg-icons/ArrowLeftIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/ArrowRightIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/ArrowRightIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/AudioIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/AudioIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/BackwardIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/BackwardIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/CameraIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/CameraIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/CheckIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/CheckIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/EnablePiPIcon.d.ts +4 -0
- package/dist/components/icons/svg-icons/EnablePiPIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/ForwardIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/ForwardIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/FullscreenEnterIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/FullscreenEnterIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/FullscreenExitIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/FullscreenExitIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/LoadingIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/LoadingIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/PauseIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/PauseIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/PlayIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/PlayIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/PlaybackSpeedIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/PlaybackSpeedIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/QualityIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/QualityIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SettingsIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SettingsIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SliderIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SliderIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SubtitleIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SubtitleIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SubtitleOffIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SubtitleOffIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/UnPiPIcon.d.ts +4 -0
- package/dist/components/icons/svg-icons/UnPiPIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeMutedIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeMutedIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeOneIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeOneIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeThreeIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeThreeIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeTwoIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeTwoIcon.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-audio.d.ts +13 -0
- package/dist/components/variants/gradient/gradient-audio.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-controls.d.ts +4 -0
- package/dist/components/variants/gradient/gradient-controls.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-glyphs.d.ts +30 -0
- package/dist/components/variants/gradient/gradient-glyphs.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-video.d.ts +16 -0
- package/dist/components/variants/gradient/gradient-video.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -112
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +786 -6271
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +3 -0
- package/package.json +110 -108
- package/src/components/variants/gradient/gradient-audio.tsx +47 -0
- package/src/components/variants/gradient/gradient-controls.tsx +254 -0
- package/src/components/variants/gradient/gradient-glyphs.test.tsx +69 -0
- package/src/components/variants/gradient/gradient-glyphs.tsx +129 -0
- package/src/components/variants/gradient/gradient-skin.css +211 -0
- package/src/components/variants/gradient/gradient-video.tsx +70 -0
- package/src/index.ts +5 -198
- package/src/test/accessibility-utils.ts +0 -59
- package/dist/context/media.js +0 -2
- package/dist/context/media.js.map +0 -1
- package/dist/context/media.mjs +0 -15
- package/dist/context/media.mjs.map +0 -1
- package/dist/context/store.js +0 -2
- package/dist/context/store.js.map +0 -1
- package/dist/context/store.mjs +0 -5
- package/dist/context/store.mjs.map +0 -1
- package/dist/context-BYwDoSRX.js +0 -1172
- package/dist/context-BYwDoSRX.js.map +0 -1
- package/dist/context-eNS62_ac.cjs +0 -23
- package/dist/context-eNS62_ac.cjs.map +0 -1
- package/dist/core-events-B1iM2F46.cjs +0 -2
- package/dist/core-events-B1iM2F46.cjs.map +0 -1
- package/dist/core-events-DunPHKRE.js +0 -484
- package/dist/core-events-DunPHKRE.js.map +0 -1
- package/dist/core-plugins-By-4-kGs.cjs +0 -2
- package/dist/core-plugins-By-4-kGs.cjs.map +0 -1
- package/dist/core-plugins-DSBWcTuX.js +0 -821
- package/dist/core-plugins-DSBWcTuX.js.map +0 -1
- package/dist/core.js +0 -37
- package/dist/core.js.map +0 -1
- package/dist/core.mjs +0 -1233
- package/dist/core.mjs.map +0 -1
- package/dist/enhanced-components-B_l3cist.js +0 -1142
- package/dist/enhanced-components-B_l3cist.js.map +0 -1
- package/dist/enhanced-components-BspFs9OO.cjs +0 -2
- package/dist/enhanced-components-BspFs9OO.cjs.map +0 -1
- package/dist/hooks-BXwI8QG6.cjs +0 -4
- package/dist/hooks-BXwI8QG6.cjs.map +0 -1
- package/dist/hooks-Bpi-jDiO.js +0 -1816
- package/dist/hooks-Bpi-jDiO.js.map +0 -1
- package/dist/minimal-components-C4z900Ro.js +0 -1926
- package/dist/minimal-components-C4z900Ro.js.map +0 -1
- package/dist/minimal-components-v4qBp6jv.cjs +0 -2
- package/dist/minimal-components-v4qBp6jv.cjs.map +0 -1
- package/dist/minimal-components.css +0 -1
- package/dist/mona-loading.gif +0 -0
- package/dist/performance-monitor-C5k6FDXw.js +0 -301
- package/dist/performance-monitor-C5k6FDXw.js.map +0 -1
- package/dist/performance-monitor-lJmuUjQQ.cjs +0 -2
- package/dist/performance-monitor-lJmuUjQQ.cjs.map +0 -1
- package/dist/utils-CFOue_9K.cjs +0 -9
- package/dist/utils-CFOue_9K.cjs.map +0 -1
- package/dist/utils-Dzpl2ArK.js +0 -2844
- package/dist/utils-Dzpl2ArK.js.map +0 -1
- package/dist/utils.js +0 -2
- package/dist/utils.js.map +0 -1
- package/dist/utils.mjs +0 -11
- package/dist/utils.mjs.map +0 -1
- package/src/adapters/videojs-adapter.tsx +0 -470
- package/src/assets/mona-loading.gif +0 -0
- package/src/components/AnimateLoading.tsx +0 -107
- package/src/components/LoadingState.tsx +0 -239
- package/src/components/__tests__/media-player-accessibility.test.tsx +0 -510
- package/src/components/__tests__/media-player-react19.test.tsx +0 -457
- package/src/components/accessibility/keyboard-help-overlay.tsx +0 -207
- package/src/components/audio-player.tsx +0 -85
- package/src/components/basic/index.ts +0 -37
- package/src/components/enhanced/advanced-features.tsx +0 -311
- package/src/components/enhanced/debug-panel.tsx +0 -448
- package/src/components/enhanced/enhanced-controls.tsx +0 -597
- package/src/components/enhanced/enhanced-media-player.tsx +0 -566
- package/src/components/enhanced/error-handling.tsx +0 -338
- package/src/components/enhanced/index.ts +0 -39
- package/src/components/enhanced/media-player-enhanced.tsx +0 -383
- package/src/components/examples/basic-video-player.tsx +0 -50
- package/src/components/examples/custom-controls-example.tsx +0 -63
- package/src/components/examples/enhanced-controls-example.tsx +0 -100
- package/src/components/examples/enhanced-media-player-example.tsx +0 -302
- package/src/components/examples/loading-states-demo.tsx +0 -142
- package/src/components/examples/react-19-demo.tsx +0 -353
- package/src/components/examples/volume-orientation-demo.tsx +0 -106
- package/src/components/icon-demo.tsx +0 -196
- package/src/components/legacy.tsx +0 -87
- package/src/components/media-metadata-display.tsx +0 -99
- package/src/components/media-player-audio.tsx +0 -39
- package/src/components/media-player-controls.tsx +0 -223
- package/src/components/media-player-error.tsx +0 -163
- package/src/components/media-player-fullscreen.tsx +0 -109
- package/src/components/media-player-hls.tsx +0 -111
- package/src/components/media-player-loading.tsx +0 -76
- package/src/components/media-player-play.tsx +0 -75
- package/src/components/media-player-root.tsx +0 -338
- package/src/components/media-player-seek.tsx +0 -168
- package/src/components/media-player-time.tsx +0 -99
- package/src/components/media-player-tooltip.tsx +0 -58
- package/src/components/media-player-video.tsx +0 -54
- package/src/components/media-player-volume.tsx +0 -155
- package/src/components/media-player.tsx +0 -292
- package/src/components/media-suspense.tsx +0 -344
- package/src/components/player-settings-form.tsx +0 -158
- package/src/components/react-player-wrapper.tsx +0 -651
- package/src/components/simple-video-player.tsx +0 -62
- package/src/components/ui/blur-poster.tsx +0 -93
- package/src/components/ui/empty-state.tsx +0 -91
- package/src/components/ui/enhanced-seek-bar.tsx +0 -254
- package/src/components/ui/error-state.tsx +0 -124
- package/src/components/ui/fullscreen-button.tsx +0 -81
- package/src/components/ui/loading-skeleton.tsx +0 -107
- package/src/components/ui/media-resource-preloader.tsx +0 -458
- package/src/components/ui/offline-message.tsx +0 -98
- package/src/components/ui/play-button.tsx +0 -99
- package/src/components/ui/seek-bar.tsx +0 -191
- package/src/components/ui/time-display.tsx +0 -61
- package/src/components/ui/volume-control.tsx +0 -190
- package/src/components/unified/base-media-player.tsx +0 -252
- package/src/components/unified/media-player.tsx +0 -75
- package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +0 -587
- package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +0 -573
- package/src/components/variants/minimal/MinimalControls.tsx +0 -124
- package/src/components/variants/minimal/MinimalLoading.tsx +0 -28
- package/src/components/variants/minimal/MinimalMediaPlayer.tsx +0 -318
- package/src/components/variants/minimal/MinimalOverlay.tsx +0 -58
- package/src/components/variants/minimal/MinimalPlayButton.tsx +0 -56
- package/src/components/variants/minimal/MinimalSeekBar.tsx +0 -140
- package/src/components/variants/minimal/MinimalVolumeControl.tsx +0 -75
- package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +0 -206
- package/src/components/variants/minimal/components/CenterPlayButton.tsx +0 -22
- package/src/components/variants/minimal/components/ControlBar.tsx +0 -142
- package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +0 -412
- package/src/components/variants/minimal/components/PlaybackControls.tsx +0 -46
- package/src/components/variants/minimal/components/RightControls.tsx +0 -164
- package/src/components/variants/minimal/components/SeekBar.tsx +0 -127
- package/src/components/variants/minimal/components/SettingsDropdown.tsx +0 -68
- package/src/components/variants/minimal/components/SubtitleDisplay.tsx +0 -21
- package/src/components/variants/minimal/components/TimeDisplay.tsx +0 -15
- package/src/components/variants/minimal/components/VideoElement.tsx +0 -33
- package/src/components/variants/minimal/components/VolumeControl.tsx +0 -220
- package/src/components/variants/minimal/index.ts +0 -23
- package/src/components/video-player.tsx +0 -100
- package/src/context/__tests__/media-store.test.ts +0 -509
- package/src/context/error-handling.ts +0 -398
- package/src/context/media-context.tsx +0 -254
- package/src/context/media-provider.tsx +0 -264
- package/src/context/media-store.ts +0 -387
- package/src/context/persistence.ts +0 -462
- package/src/core/events/event-system.ts +0 -448
- package/src/core/events/hooks.ts +0 -325
- package/src/core/events/index.ts +0 -22
- package/src/core/events/middleware.ts +0 -286
- package/src/core/index.ts +0 -22
- package/src/core/optimization/bundle-optimizer.tsx +0 -312
- package/src/core/performance/cicd.ts +0 -98
- package/src/core/performance/index.ts +0 -38
- package/src/core/performance/media-loading-optimizer.ts +0 -544
- package/src/core/performance/performance-budgets.ts +0 -437
- package/src/core/performance/performance-monitor.tsx +0 -488
- package/src/core/performance/performance-testing.ts +0 -461
- package/src/core/plugins/__tests__/registry.test.ts +0 -595
- package/src/core/plugins/base-plugin.ts +0 -225
- package/src/core/plugins/enhanced-plugin-system.ts +0 -435
- package/src/core/plugins/examples/analytics-plugin.ts +0 -241
- package/src/core/plugins/index.ts +0 -39
- package/src/core/plugins/registry.ts +0 -546
- package/src/core/plugins/types.ts +0 -169
- package/src/core/plugins/utils.ts +0 -317
- package/src/core/progressive-loading.tsx +0 -331
- package/src/core/providers/enhanced-hls-provider.ts +0 -307
- package/src/core/providers/lazy-provider.tsx +0 -14
- package/src/core/providers/service-provider.tsx +0 -164
- package/src/core/utils.tsx +0 -34
- package/src/features/media-session/media-session-manager.ts +0 -411
- package/src/features/picture-in-picture/pip-manager.ts +0 -428
- package/src/features/playlist/playlist-manager.ts +0 -626
- package/src/features/subtitles/subtitle-manager.ts +0 -435
- package/src/features/subtitles/subtitle-parser.ts +0 -284
- package/src/features/thumbnails/thumbnail-generator.ts +0 -528
- package/src/hooks/__tests__/currentTime-fix.test.ts +0 -149
- package/src/hooks/__tests__/use-media-player.test.ts +0 -712
- package/src/hooks/__tests__/use-media-state.test.tsx +0 -316
- package/src/hooks/accessibility/index.ts +0 -27
- package/src/hooks/accessibility/use-accessibility-preferences.ts +0 -190
- package/src/hooks/accessibility/use-focus-management.ts +0 -183
- package/src/hooks/accessibility/use-keyboard-shortcuts.ts +0 -152
- package/src/hooks/accessibility/use-screen-reader.ts +0 -212
- package/src/hooks/core/index.ts +0 -47
- package/src/hooks/legacy.ts +0 -21
- package/src/hooks/use-buffer-health.ts +0 -218
- package/src/hooks/use-enhanced-media-player.ts +0 -575
- package/src/hooks/use-focus-trap.tsx +0 -162
- package/src/hooks/use-intelligent-preloading.ts +0 -387
- package/src/hooks/use-keyboard-controls.ts +0 -115
- package/src/hooks/use-keyboard-shortcuts.ts +0 -255
- package/src/hooks/use-media-actions.ts +0 -184
- package/src/hooks/use-media-metadata.ts +0 -91
- package/src/hooks/use-media-optimistic.ts +0 -264
- package/src/hooks/use-media-player.ts +0 -343
- package/src/hooks/use-media-state.ts +0 -194
- package/src/hooks/use-memory-optimization.ts +0 -360
- package/src/hooks/use-network-status.ts +0 -124
- package/src/hooks/use-optimistic-controls.ts +0 -95
- package/src/hooks/use-optimized-media.ts +0 -283
- package/src/hooks/use-smart-preload.ts +0 -229
- package/src/hooks/use-touch-gestures.ts +0 -461
- package/src/hooks/useEnhancedSubtitles.ts +0 -258
- package/src/hooks/useKeyboardShortcuts.ts +0 -106
- package/src/hooks/useMediaControls.ts +0 -52
- package/src/hooks/useScreenshot.ts +0 -61
- package/src/hooks/useSubtitles.ts +0 -91
- package/src/hooks/useVideoPlayer.ts +0 -278
- package/src/icons/components/FullscreenIcon.tsx +0 -36
- package/src/icons/components/SettingsIcon.tsx +0 -37
- package/src/icons/components/animated/LoadingIcon.tsx +0 -27
- package/src/icons/components/animated/PlayPauseIcon.tsx +0 -42
- package/src/icons/components/media/PauseIcon.tsx +0 -38
- package/src/icons/components/media/PlayIcon.tsx +0 -37
- package/src/icons/components/volume/VolumeIcon.tsx +0 -38
- package/src/icons/components/volume/VolumeOffIcon.tsx +0 -38
- package/src/icons/index.ts +0 -56
- package/src/icons/provider.tsx +0 -58
- package/src/icons/types.ts +0 -60
- package/src/index-enhanced.ts +0 -35
- package/src/index-legacy.ts +0 -324
- package/src/index.css +0 -102
- package/src/lib/__tests__/async-utils.test.ts +0 -291
- package/src/lib/async-utils.ts +0 -194
- package/src/lib/deprecation.ts +0 -51
- package/src/lib/format-time.ts +0 -37
- package/src/lib/index.ts +0 -4
- package/src/lib/utils.ts +0 -6
- package/src/styles/globals.css +0 -462
- package/src/styles/minimal-player.css +0 -621
- package/src/styles/pixel-art.css +0 -41
- package/src/styles/videojs.css +0 -100
- package/src/types/events.ts +0 -47
- package/src/types/formats.ts +0 -79
- package/src/types/index.ts +0 -203
- package/src/types/videojs-plugins.d.ts +0 -14
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
import { act, render, screen, waitFor } from "@testing-library/react"
|
|
2
|
-
import { userEvent } from "@testing-library/user-event"
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
4
|
-
import { MediaProvider } from "../../context/media-provider"
|
|
5
|
-
import { setupMediaMocks } from "../../test/media-mocks"
|
|
6
|
-
import { MediaPlayer } from "../media-player"
|
|
7
|
-
|
|
8
|
-
// Import React 19 hooks
|
|
9
|
-
import { useActionState, useOptimistic, useTransition } from "react"
|
|
10
|
-
|
|
11
|
-
describe("Media Player React 19 Features", () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
setupMediaMocks()
|
|
14
|
-
vi.clearAllMocks()
|
|
15
|
-
vi.useFakeTimers()
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
vi.useRealTimers()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
describe("useTransition Implementation", () => {
|
|
23
|
-
it("should handle media state transitions with loading indicators", async () => {
|
|
24
|
-
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
25
|
-
|
|
26
|
-
render(
|
|
27
|
-
<MediaProvider>
|
|
28
|
-
<MediaPlayer src="test-video.mp4" enableTransitions={true} />
|
|
29
|
-
</MediaProvider>
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const playButton = screen.getByRole("button", { name: /play/i })
|
|
33
|
-
expect(playButton).toBeInTheDocument()
|
|
34
|
-
|
|
35
|
-
// Start playback - should trigger transition
|
|
36
|
-
await user.click(playButton)
|
|
37
|
-
|
|
38
|
-
// Should show loading state during transition
|
|
39
|
-
await waitFor(() => {
|
|
40
|
-
expect(screen.getByTestId("transition-loading")).toBeInTheDocument()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
// Complete transition
|
|
44
|
-
act(() => {
|
|
45
|
-
vi.advanceTimersByTime(100)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
await waitFor(() => {
|
|
49
|
-
expect(screen.queryByTestId("transition-loading")).not.toBeInTheDocument()
|
|
50
|
-
expect(screen.getByRole("button", { name: /pause/i })).toBeInTheDocument()
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it("should handle seeking transitions separately from playback transitions", async () => {
|
|
55
|
-
const user = userEvent.setup()
|
|
56
|
-
|
|
57
|
-
render(
|
|
58
|
-
<MediaProvider>
|
|
59
|
-
<MediaPlayer src="test-video.mp4" enableTransitions={true} />
|
|
60
|
-
</MediaProvider>
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
// Start playback first
|
|
64
|
-
const playButton = screen.getByRole("button", { name: /play/i })
|
|
65
|
-
await user.click(playButton)
|
|
66
|
-
|
|
67
|
-
// Wait for playback to start
|
|
68
|
-
await waitFor(() => {
|
|
69
|
-
expect(screen.getByRole("button", { name: /pause/i })).toBeInTheDocument()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
// Then seek - should trigger separate transition
|
|
73
|
-
const seekBar = screen.getByRole("slider", { name: /seek/i })
|
|
74
|
-
await user.click(seekBar)
|
|
75
|
-
await user.keyboard("{ArrowRight}")
|
|
76
|
-
|
|
77
|
-
// Should show seeking state
|
|
78
|
-
await waitFor(() => {
|
|
79
|
-
expect(screen.getByTestId("seeking-transition")).toBeInTheDocument()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// Complete seeking transition
|
|
83
|
-
act(() => {
|
|
84
|
-
vi.advanceTimersByTime(50)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
await waitFor(() => {
|
|
88
|
-
expect(screen.queryByTestId("seeking-transition")).not.toBeInTheDocument()
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
describe("useOptimistic Implementation", () => {
|
|
94
|
-
it("should provide immediate UI feedback for volume changes", async () => {
|
|
95
|
-
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
96
|
-
|
|
97
|
-
render(
|
|
98
|
-
<MediaProvider>
|
|
99
|
-
<MediaPlayer src="test-video.mp4" enableOptimistic={true} />
|
|
100
|
-
</MediaProvider>
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
const volumeSlider = screen.getByRole("slider", { name: /volume/i })
|
|
104
|
-
|
|
105
|
-
// Initial volume should be 1.0
|
|
106
|
-
expect(volumeSlider).toHaveValue("1")
|
|
107
|
-
|
|
108
|
-
// Change volume - should update optimistically
|
|
109
|
-
await user.click(volumeSlider)
|
|
110
|
-
await user.keyboard("{ArrowDown}")
|
|
111
|
-
|
|
112
|
-
// Optimistic update should be immediate
|
|
113
|
-
expect(volumeSlider).toHaveValue("0.9")
|
|
114
|
-
expect(screen.getByTestId("optimistic-volume")).toHaveTextContent("90%")
|
|
115
|
-
|
|
116
|
-
// Simulate async operation completion
|
|
117
|
-
act(() => {
|
|
118
|
-
vi.advanceTimersByTime(100)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
// State should be consistent after async operation
|
|
122
|
-
expect(volumeSlider).toHaveValue("0.9")
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it("should handle optimistic seek operations", async () => {
|
|
126
|
-
const user = userEvent.setup()
|
|
127
|
-
|
|
128
|
-
render(
|
|
129
|
-
<MediaProvider>
|
|
130
|
-
<MediaPlayer src="test-video.mp4" enableOptimistic={true} duration={100} />
|
|
131
|
-
</MediaProvider>
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
const seekBar = screen.getByRole("slider", { name: /seek/i })
|
|
135
|
-
const timeDisplay = screen.getByTestId("time-display")
|
|
136
|
-
|
|
137
|
-
// Initial time should be 0:00
|
|
138
|
-
expect(timeDisplay).toHaveTextContent("0:00")
|
|
139
|
-
|
|
140
|
-
// Seek to 30 seconds - should update optimistically
|
|
141
|
-
await user.click(seekBar)
|
|
142
|
-
await user.keyboard("3") // Should seek to 30%
|
|
143
|
-
|
|
144
|
-
// Optimistic update should be immediate
|
|
145
|
-
expect(timeDisplay).toHaveTextContent("0:30")
|
|
146
|
-
expect(screen.getByTestId("optimistic-time")).toHaveTextContent("30%")
|
|
147
|
-
|
|
148
|
-
// Simulate media element updating
|
|
149
|
-
const video = screen.getByRole("application") as HTMLVideoElement
|
|
150
|
-
act(() => {
|
|
151
|
-
video.currentTime = 30
|
|
152
|
-
const seekingEvent = new Event("seeking")
|
|
153
|
-
video.dispatchEvent(seekingEvent)
|
|
154
|
-
|
|
155
|
-
vi.advanceTimersByTime(100)
|
|
156
|
-
|
|
157
|
-
const seekedEvent = new Event("seeked")
|
|
158
|
-
video.dispatchEvent(seekedEvent)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
await waitFor(() => {
|
|
162
|
-
expect(timeDisplay).toHaveTextContent("0:30")
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it("should revert optimistic updates on error", async () => {
|
|
167
|
-
const user = userEvent.setup()
|
|
168
|
-
|
|
169
|
-
render(
|
|
170
|
-
<MediaProvider>
|
|
171
|
-
<MediaPlayer src="test-video.mp4" enableOptimistic={true} />
|
|
172
|
-
</MediaProvider>
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
const volumeSlider = screen.getByRole("slider", { name: /volume/i })
|
|
176
|
-
|
|
177
|
-
// Change volume
|
|
178
|
-
await user.click(volumeSlider)
|
|
179
|
-
await user.keyboard("{ArrowUp}")
|
|
180
|
-
|
|
181
|
-
// Optimistic update should show
|
|
182
|
-
expect(volumeSlider).toHaveValue("1")
|
|
183
|
-
|
|
184
|
-
// Simulate error
|
|
185
|
-
const video = screen.getByRole("application") as HTMLVideoElement
|
|
186
|
-
act(() => {
|
|
187
|
-
video.error = new Error("Volume change failed")
|
|
188
|
-
const errorEvent = new Event("error")
|
|
189
|
-
video.dispatchEvent(errorEvent)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
await waitFor(() => {
|
|
193
|
-
expect(volumeSlider).toHaveValue("1") // Should revert to previous state
|
|
194
|
-
expect(screen.getByTestId("error-message")).toBeInTheDocument()
|
|
195
|
-
})
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
describe("useActionState Implementation", () => {
|
|
200
|
-
it("should handle media settings form with async submission", async () => {
|
|
201
|
-
const mockSubmit = vi.fn().mockResolvedValue({ success: true })
|
|
202
|
-
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
203
|
-
|
|
204
|
-
render(
|
|
205
|
-
<MediaProvider>
|
|
206
|
-
<MediaPlayer src="test-video.mp4" showSettings={true} onSettingsChange={mockSubmit} />
|
|
207
|
-
</MediaProvider>
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
// Open settings panel
|
|
211
|
-
const settingsButton = screen.getByRole("button", { name: /settings/i })
|
|
212
|
-
await user.click(settingsButton)
|
|
213
|
-
|
|
214
|
-
// Change playback speed
|
|
215
|
-
const speedSelect = screen.getByRole("combobox", { name: /playback speed/i })
|
|
216
|
-
await user.selectOptions(speedSelect, "1.5")
|
|
217
|
-
|
|
218
|
-
// Enable subtitles
|
|
219
|
-
const subtitleToggle = screen.getByRole("switch", { name: /subtitles/i })
|
|
220
|
-
await user.click(subtitleToggle)
|
|
221
|
-
|
|
222
|
-
// Submit form
|
|
223
|
-
const saveButton = screen.getByRole("button", { name: /save settings/i })
|
|
224
|
-
await user.click(saveButton)
|
|
225
|
-
|
|
226
|
-
// Should show loading state
|
|
227
|
-
expect(saveButton).toBeDisabled()
|
|
228
|
-
expect(screen.getByText(/saving/i)).toBeInTheDocument()
|
|
229
|
-
|
|
230
|
-
// Complete async operation
|
|
231
|
-
act(() => {
|
|
232
|
-
vi.advanceTimersByTime(200)
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
await waitFor(() => {
|
|
236
|
-
expect(saveButton).not.toBeDisabled()
|
|
237
|
-
expect(screen.getByText(/settings saved/i)).toBeInTheDocument()
|
|
238
|
-
expect(mockSubmit).toHaveBeenCalledWith({
|
|
239
|
-
playbackRate: 1.5,
|
|
240
|
-
subtitles: true
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it("should handle form submission errors gracefully", async () => {
|
|
246
|
-
const mockSubmit = vi.fn().mockRejectedValue(new Error("Network error"))
|
|
247
|
-
const user = userEvent.setup()
|
|
248
|
-
|
|
249
|
-
render(
|
|
250
|
-
<MediaProvider>
|
|
251
|
-
<MediaPlayer src="test-video.mp4" showSettings={true} onSettingsChange={mockSubmit} />
|
|
252
|
-
</MediaProvider>
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
// Open settings and submit
|
|
256
|
-
const settingsButton = screen.getByRole("button", { name: /settings/i })
|
|
257
|
-
await user.click(settingsButton)
|
|
258
|
-
|
|
259
|
-
const saveButton = screen.getByRole("button", { name: /save settings/i })
|
|
260
|
-
await user.click(saveButton)
|
|
261
|
-
|
|
262
|
-
await waitFor(() => {
|
|
263
|
-
expect(screen.getByText(/network error/i)).toBeInTheDocument()
|
|
264
|
-
expect(saveButton).not.toBeDisabled()
|
|
265
|
-
})
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it("should validate form inputs before submission", async () => {
|
|
269
|
-
const mockSubmit = vi.fn()
|
|
270
|
-
const user = userEvent.setup()
|
|
271
|
-
|
|
272
|
-
render(
|
|
273
|
-
<MediaProvider>
|
|
274
|
-
<MediaPlayer src="test-video.mp4" showSettings={true} onSettingsChange={mockSubmit} />
|
|
275
|
-
</MediaProvider>
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
const settingsButton = screen.getByRole("button", { name: /settings/i })
|
|
279
|
-
await user.click(settingsButton)
|
|
280
|
-
|
|
281
|
-
// Try to set invalid volume
|
|
282
|
-
const volumeInput = screen.getByRole("spinbutton", { name: /volume/i })
|
|
283
|
-
await user.clear(volumeInput)
|
|
284
|
-
await user.type(volumeInput, "150") // Invalid: > 100
|
|
285
|
-
|
|
286
|
-
const saveButton = screen.getByRole("button", { name: /save settings/i })
|
|
287
|
-
await user.click(saveButton)
|
|
288
|
-
|
|
289
|
-
// Should show validation error
|
|
290
|
-
await waitFor(() => {
|
|
291
|
-
expect(screen.getByText(/volume must be between 0 and 100/i)).toBeInTheDocument()
|
|
292
|
-
expect(mockSubmit).not.toHaveBeenCalled()
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
describe("React 19 Concurrent Features", () => {
|
|
298
|
-
it("should handle Suspense boundaries for media loading", async () => {
|
|
299
|
-
const LazyMediaPlayer = vi.fn().mockImplementation(() => {
|
|
300
|
-
throw new Promise(() => {}) // Never resolves to test loading state
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
render(
|
|
304
|
-
<MediaProvider>
|
|
305
|
-
<MediaPlayer src="test-video.mp4" component={LazyMediaPlayer} />
|
|
306
|
-
</MediaProvider>
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
// Should show loading fallback
|
|
310
|
-
expect(screen.getByTestId("media-loading-fallback")).toBeInTheDocument()
|
|
311
|
-
expect(screen.getByText(/loading media/i)).toBeInTheDocument()
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it("should handle startTransition for non-urgent updates", async () => {
|
|
315
|
-
const user = userEvent.setup()
|
|
316
|
-
|
|
317
|
-
render(
|
|
318
|
-
<MediaProvider>
|
|
319
|
-
<MediaPlayer src="test-video.mp4" enableConcurrentFeatures={true} />
|
|
320
|
-
</MediaProvider>
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
const qualitySelector = screen.getByRole("combobox", { name: /quality/i })
|
|
324
|
-
|
|
325
|
-
// Change quality - should use startTransition
|
|
326
|
-
await user.selectOptions(qualitySelector, "1080p")
|
|
327
|
-
|
|
328
|
-
// UI should remain responsive during transition
|
|
329
|
-
const playButton = screen.getByRole("button", { name: /play/i })
|
|
330
|
-
expect(playButton).not.toBeDisabled()
|
|
331
|
-
|
|
332
|
-
// Should show quality change indicator
|
|
333
|
-
expect(screen.getByTestId("quality-changing")).toBeInTheDocument()
|
|
334
|
-
|
|
335
|
-
// Complete transition
|
|
336
|
-
act(() => {
|
|
337
|
-
vi.advanceTimersByTime(300)
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
await waitFor(() => {
|
|
341
|
-
expect(screen.queryByTestId("quality-changing")).not.toBeInTheDocument()
|
|
342
|
-
expect(screen.getByDisplayValue("1080p")).toBeInTheDocument()
|
|
343
|
-
})
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
it("should handle concurrent media operations", async () => {
|
|
347
|
-
const user = userEvent.setup()
|
|
348
|
-
|
|
349
|
-
render(
|
|
350
|
-
<MediaProvider>
|
|
351
|
-
<MediaPlayer src="test-video.mp4" enableConcurrentFeatures={true} />
|
|
352
|
-
</MediaProvider>
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
const playButton = screen.getByRole("button", { name: /play/i })
|
|
356
|
-
const volumeSlider = screen.getByRole("slider", { name: /volume/i })
|
|
357
|
-
const seekBar = screen.getByRole("slider", { name: /seek/i })
|
|
358
|
-
|
|
359
|
-
// Trigger multiple operations concurrently
|
|
360
|
-
await user.click(playButton)
|
|
361
|
-
await user.click(volumeSlider)
|
|
362
|
-
await user.keyboard("{ArrowDown}")
|
|
363
|
-
await user.click(seekBar)
|
|
364
|
-
await user.keyboard("{ArrowRight}")
|
|
365
|
-
|
|
366
|
-
// All operations should complete without blocking UI
|
|
367
|
-
await waitFor(() => {
|
|
368
|
-
expect(screen.getByRole("button", { name: /pause/i })).toBeInTheDocument()
|
|
369
|
-
expect(volumeSlider).toHaveValue("0.9")
|
|
370
|
-
expect(screen.getByTestId("current-time")).not.toHaveTextContent("0:00")
|
|
371
|
-
})
|
|
372
|
-
})
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
describe("React 19 Server Components Integration", () => {
|
|
376
|
-
it("should handle streaming server-side rendering", async () => {
|
|
377
|
-
// Mock streaming SSR
|
|
378
|
-
const mockStream = {
|
|
379
|
-
write: vi.fn(),
|
|
380
|
-
end: vi.fn()
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
render(
|
|
384
|
-
<MediaProvider>
|
|
385
|
-
<MediaPlayer src="test-video.mp4" streaming={true} stream={mockStream} />
|
|
386
|
-
</MediaProvider>
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
// Should render shell immediately
|
|
390
|
-
expect(screen.getByRole("application")).toBeInTheDocument()
|
|
391
|
-
|
|
392
|
-
// Should stream content when ready
|
|
393
|
-
await waitFor(() => {
|
|
394
|
-
expect(mockStream.write).toHaveBeenCalledWith('<div data-testid="media-content">')
|
|
395
|
-
})
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
it("should handle progressive hydration", async () => {
|
|
399
|
-
render(
|
|
400
|
-
<MediaProvider>
|
|
401
|
-
<MediaPlayer src="test-video.mp4" progressiveHydration={true} />
|
|
402
|
-
</MediaProvider>
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
// Should render static content immediately
|
|
406
|
-
expect(screen.getByTestId("media-player-shell")).toBeInTheDocument()
|
|
407
|
-
|
|
408
|
-
// Should hydrate interactive elements progressively
|
|
409
|
-
await waitFor(() => {
|
|
410
|
-
expect(screen.getByRole("button", { name: /play/i })).toBeInTheDocument()
|
|
411
|
-
expect(screen.getByRole("slider", { name: /volume/i })).toBeInTheDocument()
|
|
412
|
-
})
|
|
413
|
-
})
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
describe("Performance Optimizations with React 19", () => {
|
|
417
|
-
it("should use React Compiler optimizations", async () => {
|
|
418
|
-
const renderSpy = vi.spyOn(console, "log").mockImplementation(() => {})
|
|
419
|
-
|
|
420
|
-
render(
|
|
421
|
-
<MediaProvider>
|
|
422
|
-
<MediaPlayer src="test-video.mp4" enableCompilerOptimizations={true} />
|
|
423
|
-
</MediaProvider>
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
// Should log compiler optimizations
|
|
427
|
-
expect(renderSpy).toHaveBeenCalledWith(
|
|
428
|
-
expect.stringContaining("React Compiler optimizations applied")
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
renderSpy.mockRestore()
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
it("should optimize re-renders with useMemo and useCallback", async () => {
|
|
435
|
-
const user = userEvent.setup()
|
|
436
|
-
|
|
437
|
-
const { rerender } = render(
|
|
438
|
-
<MediaProvider>
|
|
439
|
-
<MediaPlayer src="test-video.mp4" enableOptimizations={true} />
|
|
440
|
-
</MediaProvider>
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
const initialRenderCount = vi.fn().mockImplementation(() => {})
|
|
444
|
-
initialRenderCount()
|
|
445
|
-
|
|
446
|
-
// Re-render with same props
|
|
447
|
-
rerender(
|
|
448
|
-
<MediaProvider>
|
|
449
|
-
<MediaPlayer src="test-video.mp4" enableOptimizations={true} />
|
|
450
|
-
</MediaProvider>
|
|
451
|
-
)
|
|
452
|
-
|
|
453
|
-
// Component should not re-render unnecessarily
|
|
454
|
-
expect(initialRenderCount).toHaveBeenCalledTimes(1)
|
|
455
|
-
})
|
|
456
|
-
})
|
|
457
|
-
})
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import React from "react"
|
|
2
|
-
import type { KeyboardShortcut } from "../../hooks/accessibility/use-keyboard-shortcuts"
|
|
3
|
-
import { cn } from "../../lib/utils"
|
|
4
|
-
|
|
5
|
-
interface KeyboardHelpOverlayProps {
|
|
6
|
-
isOpen: boolean
|
|
7
|
-
onClose: () => void
|
|
8
|
-
shortcuts: Array<{
|
|
9
|
-
key: string
|
|
10
|
-
description: string
|
|
11
|
-
modifiers?: {
|
|
12
|
-
ctrl?: boolean
|
|
13
|
-
alt?: boolean
|
|
14
|
-
shift?: boolean
|
|
15
|
-
}
|
|
16
|
-
}>
|
|
17
|
-
className?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function KeyboardHelpOverlay({
|
|
21
|
-
isOpen,
|
|
22
|
-
onClose,
|
|
23
|
-
shortcuts,
|
|
24
|
-
className
|
|
25
|
-
}: KeyboardHelpOverlayProps) {
|
|
26
|
-
// Handle escape key to close
|
|
27
|
-
React.useEffect(() => {
|
|
28
|
-
if (!isOpen) return
|
|
29
|
-
|
|
30
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
31
|
-
if (e.key === "Escape") {
|
|
32
|
-
onClose()
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
document.addEventListener("keydown", handleKeyDown)
|
|
37
|
-
return () => document.removeEventListener("keydown", handleKeyDown)
|
|
38
|
-
}, [isOpen, onClose])
|
|
39
|
-
|
|
40
|
-
// Focus trap within the overlay
|
|
41
|
-
React.useEffect(() => {
|
|
42
|
-
if (!isOpen) return
|
|
43
|
-
|
|
44
|
-
const overlay = document.querySelector("[data-keyboard-help-overlay]")
|
|
45
|
-
if (!overlay) return
|
|
46
|
-
|
|
47
|
-
const focusableElements = Array.from(
|
|
48
|
-
overlay.querySelectorAll(
|
|
49
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
50
|
-
)
|
|
51
|
-
) as HTMLElement[]
|
|
52
|
-
|
|
53
|
-
if (focusableElements.length === 0) return
|
|
54
|
-
|
|
55
|
-
const firstElement = focusableElements[0]
|
|
56
|
-
const lastElement = focusableElements[focusableElements.length - 1]
|
|
57
|
-
|
|
58
|
-
if (!firstElement || !lastElement) return
|
|
59
|
-
|
|
60
|
-
// Focus the close button initially
|
|
61
|
-
const closeButton = overlay.querySelector("[data-close-button]") as HTMLElement
|
|
62
|
-
closeButton?.focus()
|
|
63
|
-
|
|
64
|
-
const handleTabKey = (e: KeyboardEvent) => {
|
|
65
|
-
if (e.key !== "Tab") return
|
|
66
|
-
|
|
67
|
-
if (e.shiftKey) {
|
|
68
|
-
if (document.activeElement === firstElement) {
|
|
69
|
-
e.preventDefault()
|
|
70
|
-
lastElement.focus()
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
if (document.activeElement === lastElement) {
|
|
74
|
-
e.preventDefault()
|
|
75
|
-
firstElement.focus()
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
document.addEventListener("keydown", handleTabKey)
|
|
81
|
-
return () => document.removeEventListener("keydown", handleTabKey)
|
|
82
|
-
}, [isOpen])
|
|
83
|
-
|
|
84
|
-
if (!isOpen) return null
|
|
85
|
-
|
|
86
|
-
const formatKey = (
|
|
87
|
-
key: string,
|
|
88
|
-
modifiers?: { ctrl?: boolean; alt?: boolean; shift?: boolean }
|
|
89
|
-
) => {
|
|
90
|
-
const parts = []
|
|
91
|
-
|
|
92
|
-
if (modifiers?.ctrl) parts.push("Ctrl")
|
|
93
|
-
if (modifiers?.alt) parts.push("Alt")
|
|
94
|
-
if (modifiers?.shift) parts.push("Shift")
|
|
95
|
-
|
|
96
|
-
// Format special keys
|
|
97
|
-
let keyDisplay = key
|
|
98
|
-
switch (key) {
|
|
99
|
-
case " ":
|
|
100
|
-
keyDisplay = "Space"
|
|
101
|
-
break
|
|
102
|
-
case "ArrowUp":
|
|
103
|
-
keyDisplay = "↑"
|
|
104
|
-
break
|
|
105
|
-
case "ArrowDown":
|
|
106
|
-
keyDisplay = "↓"
|
|
107
|
-
break
|
|
108
|
-
case "ArrowLeft":
|
|
109
|
-
keyDisplay = "←"
|
|
110
|
-
break
|
|
111
|
-
case "ArrowRight":
|
|
112
|
-
keyDisplay = "→"
|
|
113
|
-
break
|
|
114
|
-
default:
|
|
115
|
-
keyDisplay = key.toUpperCase()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
parts.push(keyDisplay)
|
|
119
|
-
return parts.join(" + ")
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div
|
|
124
|
-
className={cn(
|
|
125
|
-
"fixed inset-0 z-50 flex items-center justify-center bg-black/50",
|
|
126
|
-
"backdrop-blur-sm",
|
|
127
|
-
className
|
|
128
|
-
)}
|
|
129
|
-
data-keyboard-help-overlay
|
|
130
|
-
onClick={(e) => {
|
|
131
|
-
if (e.target === e.currentTarget) {
|
|
132
|
-
onClose()
|
|
133
|
-
}
|
|
134
|
-
}}
|
|
135
|
-
role="dialog"
|
|
136
|
-
aria-modal="true"
|
|
137
|
-
aria-labelledby="keyboard-help-title"
|
|
138
|
-
aria-describedby="keyboard-help-description">
|
|
139
|
-
<div
|
|
140
|
-
className={cn(
|
|
141
|
-
"w-full max-w-md rounded-lg bg-white p-6 shadow-xl",
|
|
142
|
-
"dark:bg-gray-900 dark:text-white",
|
|
143
|
-
"focus-within:outline-none",
|
|
144
|
-
"fade-in-0 zoom-in-95 animate-in duration-200"
|
|
145
|
-
)}>
|
|
146
|
-
<div className="mb-4 flex items-center justify-between">
|
|
147
|
-
<h2 id="keyboard-help-title" className="font-semibold text-lg">
|
|
148
|
-
Keyboard Shortcuts
|
|
149
|
-
</h2>
|
|
150
|
-
<button
|
|
151
|
-
data-close-button
|
|
152
|
-
onClick={onClose}
|
|
153
|
-
className={cn(
|
|
154
|
-
"rounded-md p-2 text-gray-500 transition-colors",
|
|
155
|
-
"hover:bg-gray-100 hover:text-gray-700",
|
|
156
|
-
"focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
157
|
-
"dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
|
158
|
-
)}
|
|
159
|
-
aria-label="Close keyboard shortcuts help">
|
|
160
|
-
<svg
|
|
161
|
-
className="h-5 w-5"
|
|
162
|
-
fill="none"
|
|
163
|
-
stroke="currentColor"
|
|
164
|
-
viewBox="0 0 24 24"
|
|
165
|
-
aria-hidden="true">
|
|
166
|
-
<path
|
|
167
|
-
strokeLinecap="round"
|
|
168
|
-
strokeLinejoin="round"
|
|
169
|
-
strokeWidth={2}
|
|
170
|
-
d="M6 18L18 6M6 6l12 12"
|
|
171
|
-
/>
|
|
172
|
-
</svg>
|
|
173
|
-
</button>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
<p id="keyboard-help-description" className="mb-4 text-gray-600 text-sm dark:text-gray-400">
|
|
177
|
-
Use these keyboard shortcuts to control the media player:
|
|
178
|
-
</p>
|
|
179
|
-
|
|
180
|
-
<div className="max-h-64 overflow-y-auto">
|
|
181
|
-
<dl className="space-y-3">
|
|
182
|
-
{shortcuts.map(({ key, description, modifiers }, index) => (
|
|
183
|
-
<div key={`${key}-${index}`} className="flex items-center justify-between">
|
|
184
|
-
<dt className="text-gray-600 text-sm dark:text-gray-400">{description}</dt>
|
|
185
|
-
<dd>
|
|
186
|
-
<kbd
|
|
187
|
-
className={cn(
|
|
188
|
-
"inline-flex items-center rounded px-2 py-1 font-mono text-xs",
|
|
189
|
-
"border border-gray-300 bg-gray-100 text-gray-800",
|
|
190
|
-
"dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
|
191
|
-
)}>
|
|
192
|
-
{formatKey(key, modifiers)}
|
|
193
|
-
</kbd>
|
|
194
|
-
</dd>
|
|
195
|
-
</div>
|
|
196
|
-
))}
|
|
197
|
-
</dl>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div className="mt-4 text-gray-500 text-xs dark:text-gray-400">
|
|
201
|
-
Press <kbd className="rounded bg-gray-100 px-1 dark:bg-gray-800">Esc</kbd> to close this
|
|
202
|
-
help
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
)
|
|
207
|
-
}
|