diff --git a/src/lib/Preview.js b/src/lib/Preview.js index f3d1867b4c..03240e8768 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -1875,7 +1875,7 @@ class Preview extends EventEmitter { // Ignore key events when we are inside certain fields if ( !target || - KEYDOWN_EXCEPTIONS.indexOf(target.nodeName) > -1 || + (KEYDOWN_EXCEPTIONS.indexOf(target.nodeName) > -1 && !target.getAttribute('data-allow-keydown')) || (target.nodeName === 'DIV' && !!target.getAttribute('contenteditable')) ) { return; diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index 7ff986348b..f21bd751d3 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -2761,6 +2761,7 @@ describe('lib/Preview', () => { preventDefault: jest.fn(), stopPropagation: jest.fn(), target: { + getAttribute: jest.fn(), nodeName: KEYDOWN_EXCEPTIONS[0], }, }; @@ -2781,7 +2782,7 @@ describe('lib/Preview', () => { expect(stubs.decodeKeydown).not.toBeCalled(); }); - test('should do nothing if there is no target the target is a keydown exception', () => { + test('should do nothing if there is no target or the target is a keydown exception', () => { preview.keydownHandler(stubs.event); expect(stubs.decodeKeydown).not.toBeCalled(); }); diff --git a/src/lib/viewers/controls/media/PlayPauseToggle.tsx b/src/lib/viewers/controls/media/PlayPauseToggle.tsx index d658cfe60a..0259f00d1f 100644 --- a/src/lib/viewers/controls/media/PlayPauseToggle.tsx +++ b/src/lib/viewers/controls/media/PlayPauseToggle.tsx @@ -3,37 +3,17 @@ import noop from 'lodash/noop'; import IconPlay24 from '../icons/IconPlay24'; import IconPause24 from '../icons/IconPause24'; import MediaToggle from './MediaToggle'; -import { decodeKeydown } from '../../../util'; import './PlayPauseToggle.scss'; export type Props = { isPlaying?: boolean; onPlayPause: (isPlaying: boolean) => void; - useHotkeys?: boolean; }; -export default function PlayPauseToggle({ isPlaying, onPlayPause = noop, useHotkeys }: Props): JSX.Element { +export default function PlayPauseToggle({ isPlaying, onPlayPause = noop }: Props): JSX.Element { const Icon = isPlaying ? IconPause24 : IconPlay24; const title = isPlaying ? __('media_pause') : __('media_play'); - React.useEffect(() => { - const handleKeydown = (event: KeyboardEvent): void => { - const key = decodeKeydown(event); - - if (key === 'k' || key === 'Shift+K' || key === 'Space') { - onPlayPause(!isPlaying); - } - }; - - if (useHotkeys) { - document.addEventListener('keydown', handleKeydown); - } - - return (): void => { - document.removeEventListener('keydown', handleKeydown); - }; - }, [isPlaying, onPlayPause, useHotkeys]); - return ( onPlayPause(!isPlaying)} title={title}> diff --git a/src/lib/viewers/controls/media/VolumeControls.tsx b/src/lib/viewers/controls/media/VolumeControls.tsx index 9fe12da13c..6bb995bf3f 100644 --- a/src/lib/viewers/controls/media/VolumeControls.tsx +++ b/src/lib/viewers/controls/media/VolumeControls.tsx @@ -7,99 +7,48 @@ import IconVolumeMute24 from '../icons/IconVolumeMute24'; import MediaToggle from './MediaToggle'; import SliderControl from '../slider/SliderControl'; import useAttention from '../hooks/useAttention'; -import { decodeKeydown } from '../../../util'; import './VolumeControls.scss'; export type Props = { - initialVolume?: number; + onMuteChange: (isMuted: boolean) => void; onVolumeChange: (volume: number) => void; - useHotkeys?: boolean; + volume?: number; }; export const MAX_VOLUME = 1.0; -export const MID_VOLUME = 0.5; export const MIN_VOLUME = 0.0; -export const STEP_VOLUME = 0.05; export function getIcon(volume: number): (props: React.SVGProps) => JSX.Element { - let icon = IconVolumeMute24; + let Icon = IconVolumeMute24; if (volume >= 0.66) { - icon = IconVolumeHigh24; + Icon = IconVolumeHigh24; } else if (volume >= 0.33) { - icon = IconVolumeMedium24; + Icon = IconVolumeMedium24; } else if (volume >= 0.01) { - icon = IconVolumeLow24; + Icon = IconVolumeLow24; } - return icon; + return Icon; } -export default function VolumeControls({ initialVolume = MAX_VOLUME, onVolumeChange, useHotkeys }: Props): JSX.Element { +export default function VolumeControls({ onMuteChange, onVolumeChange, volume = MAX_VOLUME }: Props): JSX.Element { const [isActive, handlers] = useAttention(); - const [isMuted, setMuted] = React.useState(!initialVolume); - const [volume, setVolume] = React.useState(initialVolume); + const isMuted = !volume; const Icon = isMuted ? IconVolumeMute24 : getIcon(volume); const title = isMuted ? __('media_unmute') : __('media_mute'); - const value = isMuted ? 0 : volume; - const updateMuted = React.useCallback((): void => { - if (isMuted && !volume) { - setVolume(MID_VOLUME); // Reset internal state to audible level if volume was zero *and* muted - } + const handleMute = (): void => { + onMuteChange(!isMuted); + }; - const newMuted = !isMuted; - const newVolume = newMuted ? MIN_VOLUME : volume || MID_VOLUME; // Mute or revert to the last known volume - - setMuted(newMuted); - onVolumeChange(newVolume); - }, [isMuted, onVolumeChange, volume]); - - const updateVolume = React.useCallback( - (newValue: number): void => { - const newVolume = Math.min(Math.max(newValue, MIN_VOLUME), MAX_VOLUME); - - setMuted(!newVolume); - setVolume(newVolume); - onVolumeChange(newVolume); - }, - [onVolumeChange], - ); - - React.useEffect(() => { - const handleKeydown = (event: KeyboardEvent): void => { - const key = decodeKeydown(event); - - if (key === 'ArrowDown') { - updateVolume(volume - STEP_VOLUME); - } - - if (key === 'ArrowUp') { - updateVolume(volume + STEP_VOLUME); - } - - if (key === 'm' || key === 'Shift+M') { - updateMuted(); - } - }; - - if (useHotkeys) { - document.addEventListener('keydown', handleKeydown); - } - - return (): void => { - document.removeEventListener('keydown', handleKeydown); - }; - }, [updateMuted, updateVolume, useHotkeys, volume]); + const handleVolume = (newVolume: number): void => { + onVolumeChange(Math.min(Math.max(newVolume, MIN_VOLUME), MAX_VOLUME)); + }; return (
- updateMuted()} - title={title} - {...handlers} - > + @@ -107,10 +56,10 @@ export default function VolumeControls({ initialVolume = MAX_VOLUME, onVolumeCha
diff --git a/src/lib/viewers/controls/media/__tests__/VolumeControls-test.tsx b/src/lib/viewers/controls/media/__tests__/VolumeControls-test.tsx index d84c74e528..274f0fa9c4 100644 --- a/src/lib/viewers/controls/media/__tests__/VolumeControls-test.tsx +++ b/src/lib/viewers/controls/media/__tests__/VolumeControls-test.tsx @@ -4,7 +4,7 @@ import VolumeControls from '../VolumeControls'; describe('VolumeControls', () => { const getWrapper = (props = {}): ShallowWrapper => - shallow(); + shallow(); describe('render', () => { test('should return a valid wrapper', () => { diff --git a/src/lib/viewers/controls/slider/SliderControl.tsx b/src/lib/viewers/controls/slider/SliderControl.tsx index 74fbd46068..f76e412ea7 100644 --- a/src/lib/viewers/controls/slider/SliderControl.tsx +++ b/src/lib/viewers/controls/slider/SliderControl.tsx @@ -24,6 +24,7 @@ export default function SliderControl({ className, onChange, value, ...rest }: P
- - + +
diff --git a/src/lib/viewers/media/MP3Viewer.js b/src/lib/viewers/media/MP3Viewer.js index 50b5a5597d..d119a279b8 100644 --- a/src/lib/viewers/media/MP3Viewer.js +++ b/src/lib/viewers/media/MP3Viewer.js @@ -48,16 +48,16 @@ class MP3Viewer extends MediaBaseViewer { renderUI() { super.renderUI(); - if (this.getViewerOption('useReactControls')) { + if (this.controls && this.getViewerOption('useReactControls')) { this.controls.render( , ); } diff --git a/src/lib/viewers/media/MediaBaseViewer.js b/src/lib/viewers/media/MediaBaseViewer.js index 131637c1b6..67eda3ee11 100644 --- a/src/lib/viewers/media/MediaBaseViewer.js +++ b/src/lib/viewers/media/MediaBaseViewer.js @@ -513,6 +513,7 @@ class MediaBaseViewer extends BaseViewer { renderUI() { if (this.mediaControls) { this.mediaControls.setTimeCode(this.mediaEl.currentTime); + this.mediaControls.updateVolumeIcon(this.mediaEl.volume); } } @@ -586,9 +587,7 @@ class MediaBaseViewer extends BaseViewer { * @return {void} */ updateVolumeIcon() { - if (this.mediaControls) { - this.mediaControls.updateVolumeIcon(this.mediaEl.volume); - } + this.renderUI(); } /** @@ -807,7 +806,7 @@ class MediaBaseViewer extends BaseViewer { /** * Toggle playback * - * @private + * @protected * @return {void} */ togglePlay() { @@ -821,7 +820,7 @@ class MediaBaseViewer extends BaseViewer { /** * Toggle mute * - * @private + * @protected * @return {void} */ toggleMute() { @@ -837,7 +836,7 @@ class MediaBaseViewer extends BaseViewer { /** * Hides the loading indicator * - * @private + * @protected * @return {void} */ hideLoadingIcon() { @@ -920,67 +919,8 @@ class MediaBaseViewer extends BaseViewer { this.processBufferFillMetric(); } - /** - * Abstract. Processes the buffer fill metric which represents the initial buffer time before playback begins - * @emits MEDIA_METRIC_EVENTS.bufferFill - * @return {void} - */ - processBufferFillMetric() {} - - /** - * Seeks forwards/backwards from current point - * - * @private - * @param {number} increment - Increment in seconds. Negative to seek backwards, positive to seek forwards - * @return {void} - */ - quickSeek(increment) { - let newTime = this.mediaEl.currentTime + increment; - this.removePauseEventListener(); - // Make sure it's within bounds - newTime = Math.max(0, Math.min(newTime, this.mediaEl.duration)); - this.setMediaTime(newTime); - } - - /** - * Increases volume by a small increment - * - * @private - * @return {void} - */ - increaseVolume() { - let newVol = Math.round((this.mediaEl.volume + MEDIA_VOLUME_INCREMENT) * 100) / 100; - newVol = Math.min(1, newVol); - this.setVolume(newVol); - } - - /** - * Decreases volume by a small increment - * - * @private - * @return {void} - */ - decreaseVolume() { - let newVol = Math.round((this.mediaEl.volume - MEDIA_VOLUME_INCREMENT) * 100) / 100; - newVol = Math.max(0, newVol); - this.setVolume(newVol); - } - - /** - * Handles keyboard events for media - * - * @protected - * @param {string} key - keydown key - * @return {boolean} consumed or not - */ - onKeydown(key) { - // Return false when media controls are not ready - if (!this.mediaControls) { - return false; - } - - const k = key.toLowerCase(); - switch (k) { + handleKeydown(key) { + switch (key.toLowerCase()) { case 'tab': case 'shift+tab': this.mediaContainerEl.classList.add(CLASS_ELEM_KEYBOARD_FOCUS); @@ -1049,10 +989,116 @@ class MediaBaseViewer extends BaseViewer { default: return false; } + this.mediaControls.show(); + return true; } + handleKeydownReact(key) { + switch (key.toLowerCase()) { + case 'space': + case 'k': + this.togglePlay(); + break; + case 'arrowleft': + this.quickSeek(-5); + break; + case 'j': + this.quickSeek(-10); + break; + case 'arrowright': + this.quickSeek(5); + break; + case 'l': + this.quickSeek(10); + break; + case '0': + case 'home': + this.setMediaTime(0); + break; + case 'arrowup': + this.increaseVolume(); + break; + case 'arrowdown': + this.decreaseVolume(); + break; + case 'm': + case 'shift+m': + this.toggleMute(); + break; + default: + return false; + } + + return true; + } + + /** + * Abstract. Processes the buffer fill metric which represents the initial buffer time before playback begins + * @emits MEDIA_METRIC_EVENTS.bufferFill + * @return {void} + */ + processBufferFillMetric() {} + + /** + * Seeks forwards/backwards from current point + * + * @private + * @param {number} increment - Increment in seconds. Negative to seek backwards, positive to seek forwards + * @return {void} + */ + quickSeek(increment) { + let newTime = this.mediaEl.currentTime + increment; + this.removePauseEventListener(); + // Make sure it's within bounds + newTime = Math.max(0, Math.min(newTime, this.mediaEl.duration)); + this.setMediaTime(newTime); + } + + /** + * Increases volume by a small increment + * + * @private + * @return {void} + */ + increaseVolume() { + let newVol = Math.round((this.mediaEl.volume + MEDIA_VOLUME_INCREMENT) * 100) / 100; + newVol = Math.min(1, newVol); + this.setVolume(newVol); + } + + /** + * Decreases volume by a small increment + * + * @private + * @return {void} + */ + decreaseVolume() { + let newVol = Math.round((this.mediaEl.volume - MEDIA_VOLUME_INCREMENT) * 100) / 100; + newVol = Math.max(0, newVol); + this.setVolume(newVol); + } + + /** + * Handles keyboard events for media + * + * @protected + * @param {string} key - keydown key + * @return {boolean} consumed or not + */ + onKeydown(key) { + if (this.mediaControls) { + return this.handleKeydown(key); + } + + if (this.controls && this.getViewerOption('useReactControls')) { + return this.handleKeydownReact(key); + } + + return false; // Return false if controls are not ready + } + /** * Converts from a youtube style timestamp to seconds *