Skip to content

Commit

Permalink
feat(controls): Centralize media state and keydown handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jstoffan committed Dec 9, 2020
1 parent 271b641 commit 21e0161
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 168 deletions.
2 changes: 1 addition & 1 deletion src/lib/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/__tests__/Preview-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,7 @@ describe('lib/Preview', () => {
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
target: {
getAttribute: jest.fn(),
nodeName: KEYDOWN_EXCEPTIONS[0],
},
};
Expand All @@ -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();
});
Expand Down
22 changes: 1 addition & 21 deletions src/lib/viewers/controls/media/PlayPauseToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<MediaToggle className="bp-PlayPauseToggle" onClick={(): void => onPlayPause(!isPlaying)} title={title}>
<Icon />
Expand Down
89 changes: 19 additions & 70 deletions src/lib/viewers/controls/media/VolumeControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,110 +7,59 @@ 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<SVGSVGElement>) => 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 (
<div className="bp-VolumeControls">
<MediaToggle
className="bp-VolumeControls-toggle"
onClick={(): void => updateMuted()}
title={title}
{...handlers}
>
<MediaToggle className="bp-VolumeControls-toggle" onClick={handleMute} title={title} {...handlers}>
<Icon />
</MediaToggle>

<div className={classNames('bp-VolumeControls-flyout', { 'bp-is-open': isActive })}>
<SliderControl
max={MAX_VOLUME}
min={MIN_VOLUME}
onChange={updateVolume}
step={STEP_VOLUME}
onChange={handleVolume}
step={0.05}
title={__('media_volume_slider')}
value={value}
value={volume}
{...handlers}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import VolumeControls from '../VolumeControls';

describe('VolumeControls', () => {
const getWrapper = (props = {}): ShallowWrapper =>
shallow(<VolumeControls onVolumeChange={jest.fn()} {...props} />);
shallow(<VolumeControls onMuteChange={jest.fn()} onVolumeChange={jest.fn()} {...props} />);

describe('render', () => {
test('should return a valid wrapper', () => {
Expand Down
1 change: 1 addition & 0 deletions src/lib/viewers/controls/slider/SliderControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function SliderControl({ className, onChange, value, ...rest }: P
<input
ref={inputElRef}
className={classNames('bp-SliderControl', className)}
data-allow-keydown="true" // Enable global event handling within input field
onChange={handleChange}
style={{
backgroundImage: `linear-gradient(to right, ${bdlBoxBlue} ${percent}%, ${white} ${percent}%)`,
Expand Down
8 changes: 4 additions & 4 deletions src/lib/viewers/media/MP3Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ export type Props = PlayControlsProps & DurationLabelsProps & VolumeControlsProp
export default function MP3Controls({
currentTime,
durationTime,
initialVolume,
isPlaying,
onMuteChange,
onPlayPause,
onVolumeChange,
useHotkeys,
volume,
}: Props): JSX.Element {
return (
<div className="bp-MP3Controls">
<div className="bp-MP3Controls-group">
<PlayPauseToggle isPlaying={isPlaying} onPlayPause={onPlayPause} useHotkeys={useHotkeys} />
<VolumeControls initialVolume={initialVolume} onVolumeChange={onVolumeChange} useHotkeys={useHotkeys} />
<PlayPauseToggle isPlaying={isPlaying} onPlayPause={onPlayPause} />
<VolumeControls onMuteChange={onMuteChange} onVolumeChange={onVolumeChange} volume={volume} />
<DurationLabels currentTime={currentTime} durationTime={durationTime} />
</div>

Expand Down
6 changes: 3 additions & 3 deletions src/lib/viewers/media/MP3Viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ class MP3Viewer extends MediaBaseViewer {
renderUI() {
super.renderUI();

if (this.getViewerOption('useReactControls')) {
if (this.controls && this.getViewerOption('useReactControls')) {
this.controls.render(
<MP3Controls
currentTime={this.mediaEl.currentTime}
durationTime={this.mediaEl.duration}
initialVolume={this.cache.get('media-volume')}
isPlaying={!this.mediaEl.paused}
onMuteChange={this.toggleMute}
onPlayPause={this.togglePlay}
onVolumeChange={this.setVolume}
useHotkeys={this.options.useHotkeys}
volume={this.mediaEl.volume}
/>,
);
}
Expand Down
Loading

0 comments on commit 21e0161

Please sign in to comment.