Skip to content

Commit

Permalink
feat(controls): Add react controls for mp4 viewer (#1374)
Browse files Browse the repository at this point in the history
  • Loading branch information
jstoffan authored May 12, 2021
1 parent b103cd5 commit 29a6487
Show file tree
Hide file tree
Showing 37 changed files with 459 additions and 90 deletions.
6 changes: 5 additions & 1 deletion src/lib/viewers/controls/controls-layer/ControlsLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ export type Helpers = {

export type Props = {
children: React.ReactNode;
onHide?: () => void;
onMount?: (helpers: Helpers) => void;
onShow?: () => void;
};

export const HIDE_DELAY_MS = 2000;
export const SHOW_CLASSNAME = 'bp-is-visible';

export default function ControlsLayer({ children, onMount = noop }: Props): JSX.Element {
export default function ControlsLayer({ children, onHide = noop, onMount = noop, onShow = noop }: Props): JSX.Element {
const [isForced, setIsForced] = React.useState(false);
const [isShown, setIsShown] = React.useState(false);
const hasFocusRef = React.useRef(false);
Expand All @@ -38,6 +40,7 @@ export default function ControlsLayer({ children, onMount = noop }: Props): JSX.
}

setIsShown(false);
onHide();
}, HIDE_DELAY_MS);
},
reset() {
Expand All @@ -47,6 +50,7 @@ export default function ControlsLayer({ children, onMount = noop }: Props): JSX.
show() {
helpersRef.current.clean();
setIsShown(true);
onShow();
},
});

Expand Down
21 changes: 18 additions & 3 deletions src/lib/viewers/controls/controls-root/ControlsRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import ControlsLayer, { Helpers } from '../controls-layer';
import './ControlsRoot.scss';

export type Options = {
className?: string;
containerEl: HTMLElement;
fileId: string;
onHide?: () => void;
onShow?: () => void;
};

export default class ControlsRoot {
Expand All @@ -21,9 +24,13 @@ export default class ControlsRoot {
show: noop,
};

constructor({ containerEl, fileId }: Options) {
handleHide: () => void;

handleShow: () => void;

constructor({ className = 'bp-ControlsRoot', containerEl, fileId, onHide = noop, onShow = noop }: Options) {
this.controlsEl = document.createElement('div');
this.controlsEl.setAttribute('class', 'bp-ControlsRoot');
this.controlsEl.setAttribute('class', className);
this.controlsEl.setAttribute('data-testid', 'bp-controls');
this.controlsEl.setAttribute('data-resin-component', 'toolbar');
this.controlsEl.setAttribute('data-resin-fileid', fileId);
Expand All @@ -32,6 +39,9 @@ export default class ControlsRoot {
this.containerEl.addEventListener('mousemove', this.handleMouseMove);
this.containerEl.addEventListener('touchstart', this.handleTouchStart);
this.containerEl.appendChild(this.controlsEl);

this.handleHide = onHide;
this.handleShow = onShow;
}

handleMount = (helpers: Helpers): void => {
Expand Down Expand Up @@ -68,6 +78,11 @@ export default class ControlsRoot {
}

render(controls: JSX.Element): void {
ReactDOM.render(<ControlsLayer onMount={this.handleMount}>{controls}</ControlsLayer>, this.controlsEl);
ReactDOM.render(
<ControlsLayer onHide={this.handleHide} onMount={this.handleMount} onShow={this.handleShow}>
{controls}
</ControlsLayer>,
this.controlsEl,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import ControlsLayer from '../../controls-layer';
import ControlsRoot from '../ControlsRoot';

describe('ControlsRoot', () => {
Expand Down Expand Up @@ -106,5 +108,25 @@ describe('ControlsRoot', () => {
expect(instance.controlsEl.firstChild).toHaveClass('bp-ControlsLayer');
expect(instance.controlsEl.firstChild).toContainHTML(ReactDOMServer.renderToStaticMarkup(controls));
});

test('should attach onHide and onShow handlers to the underlying controls layer', () => {
jest.spyOn(ReactDOM, 'render');

const controls = <div className="TestControls">Controls</div>;
const onHide = jest.fn();
const onShow = jest.fn();
const instance = getInstance({ onHide, onShow });

instance.render(controls);

expect(instance.handleHide).toEqual(onHide);
expect(instance.handleShow).toEqual(onShow);
expect(ReactDOM.render).toBeCalledWith(
<ControlsLayer onHide={instance.handleHide} onMount={instance.handleMount} onShow={instance.handleShow}>
{controls}
</ControlsLayer>,
expect.anything(),
);
});
});
});
1 change: 1 addition & 0 deletions src/lib/viewers/controls/controls-root/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './ControlsRoot';
export { default } from './ControlsRoot';
5 changes: 3 additions & 2 deletions src/lib/viewers/controls/fullscreen/FullscreenToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import './FullscreenToggle.scss';

export type Props = {
onFullscreenToggle: (isFullscreen: boolean) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
};

export default function FullscreenToggle({ onFullscreenToggle }: Props): JSX.Element {
export default function FullscreenToggle({ onFullscreenToggle, ...rest }: Props): JSX.Element {
const isFullscreen = useFullscreen();
const Icon = isFullscreen ? IconFullscreenOut24 : IconFullscreenIn24;
const title = isFullscreen ? __('exit_fullscreen') : __('enter_fullscreen');
Expand All @@ -18,7 +19,7 @@ export default function FullscreenToggle({ onFullscreenToggle }: Props): JSX.Ele
};

return (
<button className="bp-FullscreenToggle" onClick={handleClick} title={title} type="button">
<button className="bp-FullscreenToggle" onClick={handleClick} title={title} type="button" {...rest}>
<Icon />
</button>
);
Expand Down
3 changes: 1 addition & 2 deletions src/lib/viewers/controls/icons/IconArrowLeft24.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import * as React from 'react';

function IconArrowLeft24(props: React.SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg fill="#999EA4" focusable="false" height={24} viewBox="0 0 24 24" width={24} {...props}>
<svg fill="#999EA4" focusable="false" height={24} viewBox="0 0 24 23" width={24} {...props}>
<path d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z" />
<path d="M0-.5h24v24H0z" fill="none" />
</svg>
);
}
Expand Down
3 changes: 1 addition & 2 deletions src/lib/viewers/controls/icons/IconArrowRight24.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import * as React from 'react';

function IconArrowRight24(props: React.SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg fill="#999EA4" focusable="false" height={24} viewBox="0 0 24 24" width={24} {...props}>
<svg fill="#999EA4" focusable="false" height={24} viewBox="0 0 24 23" width={24} {...props}>
<path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z" />
<path d="M0-.25h24v24H0z" fill="none" />
</svg>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/viewers/controls/icons/IconGear24.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';

function IconGear24(props: React.SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg focusable={false} height={24} width={24} {...props}>
<svg focusable={false} height={24} viewBox="0 0 24 24" width={24} {...props}>
<path
d="M19.4 13c0-.3.1-.6.1-1s0-.7-.1-1l2.1-1.6c.2-.1.2-.4.1-.6l-2-3.5c-.1-.2-.4-.3-.6-.2l-2.5 1c-.5-.4-1.1-.7-1.7-1l-.4-2.7c.1-.2-.2-.4-.4-.4h-4c-.2 0-.5.2-.5.4l-.4 2.7c-.6.2-1.1.6-1.7 1l-2.5-1c-.2-.1-.4 0-.6.2l-2 3.5c-.1.1 0 .4.2.6L4.6 11c0 .3-.1.6-.1 1s0 .7.1 1l-2.1 1.6c-.2.1-.2.4-.1.6l2 3.5c.1.2.3.3.6.2l2.5-1c.5.4 1.1.7 1.7 1l.4 2.6c0 .2.2.4.5.4h4c.2 0 .5-.2.5-.4l.4-2.6c.6-.2 1.2-.6 1.7-1l2.5 1c.2.1.5 0 .6-.2l2-3.5c.1-.2.1-.5-.1-.6L19.4 13zM12 15.5c-1.9 0-3.5-1.6-3.5-3.5s1.6-3.5 3.5-3.5 3.5 1.6 3.5 3.5-1.6 3.5-3.5 3.5z"
fill="#fff"
Expand Down
3 changes: 1 addition & 2 deletions src/lib/viewers/controls/icons/IconPlay24.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import * as React from 'react';

function IconPlay24(props: React.SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg fill="#fff" focusable={false} height={24} width={24} {...props}>
<svg fill="#fff" focusable={false} height={24} viewBox="0 0 24 24" width={24} {...props}>
<path d="M8 5v14l11-7z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
Expand Down
8 changes: 5 additions & 3 deletions src/lib/viewers/controls/media/DurationLabels.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import isFinite from 'lodash/isFinite';
import './DurationLabels.scss';

export type Props = {
Expand All @@ -7,9 +8,10 @@ export type Props = {
};

export function formatTime(time: number): string {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
const seconds = Math.floor((time % 3600) % 60);
const val = isFinite(time) ? time : 0;
const hours = Math.floor(val / 3600);
const minutes = Math.floor((val % 3600) / 60);
const seconds = Math.floor((val % 3600) % 60);
const hour = hours > 0 ? `${hours.toString()}:` : '';
const min = hours > 0 && minutes < 10 ? `0${minutes.toString()}` : minutes.toString();
const sec = seconds < 10 ? `0${seconds.toString()}` : seconds.toString();
Expand Down
17 changes: 17 additions & 0 deletions src/lib/viewers/controls/media/MediaFullscreenToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import FullscreenToggle, { Props as FullscreenToggleProps } from '../fullscreen';
import { decodeKeydown } from '../../../util';

export type Props = FullscreenToggleProps;

export default function MediaFullscreenToggle(props: Props): JSX.Element {
const handleKeydown = (event: React.KeyboardEvent): void => {
const key = decodeKeydown(event);

if (key === 'Enter' || key === 'Space') {
event.stopPropagation();
}
};

return <FullscreenToggle onKeyDown={handleKeydown} {...props} />;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React from 'react';
import Settings, { Menu } from '../controls/settings';
import MediaSettingsMenuAutoplay, { Props as AutoplayProps } from '../controls/media/MediaSettingsMenuAutoplay';
import MediaSettingsMenuRate, { Props as RateProps } from '../controls/media/MediaSettingsMenuRate';
import './MP3Settings.scss';
import MediaSettingsMenuAutoplay, { Props as AutoplayProps } from './MediaSettingsMenuAutoplay';
import MediaSettingsMenuRate, { Props as RateProps } from './MediaSettingsMenuRate';
import Settings, { Menu } from '../settings';

export type Props = AutoplayProps & RateProps;
export type Props = AutoplayProps & RateProps & { className?: string };

export default function MP3Settings({ autoplay, onAutoplayChange, onRateChange, rate }: Props): JSX.Element {
export default function MediaSettings({
autoplay,
className,
onAutoplayChange,
onRateChange,
rate,
}: Props): JSX.Element {
const autoValue = autoplay ? __('media_autoplay_enabled') : __('media_autoplay_disabled');
const rateValue = rate === '1.0' || !rate ? __('media_speed_normal') : rate;

return (
<Settings className="bp-MP3Settings">
<Settings className={className}>
<Settings.Menu name={Menu.MAIN}>
<Settings.MenuItem label={__('media_autoplay')} target={Menu.AUTOPLAY} value={autoValue} />
<Settings.MenuItem label={__('media_speed')} target={Menu.RATE} value={rateValue} />
Expand Down
3 changes: 2 additions & 1 deletion src/lib/viewers/controls/media/TimeControls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import isFinite from 'lodash/isFinite';
import { bdlBoxBlue, bdlGray62, white } from 'box-ui-elements/es/styles/variables';
import SliderControl from '../slider';
import './TimeControls.scss';
Expand All @@ -24,7 +25,7 @@ export default function TimeControls({
durationTime = 0,
onTimeChange,
}: Props): JSX.Element {
const currentValue = percent(currentTime, durationTime);
const currentValue = isFinite(currentTime) && isFinite(durationTime) ? percent(currentTime, durationTime) : 0;
const bufferedAmount = bufferedRange && bufferedRange.length ? bufferedRange.end(bufferedRange.length - 1) : 0;
const bufferedValue = percent(bufferedAmount, durationTime);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('DurationLabels', () => {

test.each`
input | result
${NaN} | ${'0:00'}
${0} | ${'0:00'}
${9} | ${'0:09'}
${105} | ${'1:45'}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import MP3Settings from '../MP3Settings';
import Settings from '../../controls/settings/Settings';
import SettingsMenu from '../../controls/settings/SettingsMenu';
import SettingsMenuItem from '../../controls/settings/SettingsMenuItem';
import MediaSettings from '../MediaSettings';
import Settings from '../../settings/Settings';
import SettingsMenu from '../../settings/SettingsMenu';
import SettingsMenuItem from '../../settings/SettingsMenuItem';

describe('MP3SettingsControls', () => {
describe('MediaSettings', () => {
const getWrapper = (props = {}): ShallowWrapper =>
shallow(
<MP3Settings
<MediaSettings
autoplay={false}
onAutoplayChange={jest.fn()}
onRateChange={jest.fn()}
Expand Down
10 changes: 10 additions & 0 deletions src/lib/viewers/controls/media/__tests__/TimeControls-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ describe('TimeControls', () => {
expect(wrapper.prop('step')).toEqual(0.1);
});

test('should not calculate percentage for invalid currentTime value', () => {
const wrapper = getWrapper({ currentTime: NaN, durationTime: 100 });
expect(wrapper.find(SliderControl).prop('value')).toBe(0);
});

test('should not calculate percentage for invalid durationTime value', () => {
const wrapper = getWrapper({ currentTime: 100, durationTime: NaN });
expect(wrapper.find(SliderControl).prop('value')).toBe(0);
});

test.each`
currentTime | track | value
${0} | ${'linear-gradient(to right, #0061d5 0%, #fff 0%, #fff 10%, #767676 10%, #767676 100%)'} | ${0}
Expand Down
2 changes: 0 additions & 2 deletions src/lib/viewers/controls/settings/SettingsMenuBack.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,4 @@

.bp-SettingsMenuBack-label {
@include bp-SettingsRow-label;

text-align: center;
}
7 changes: 6 additions & 1 deletion src/lib/viewers/controls/settings/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@

@mixin bp-SettingsRow-cell {
display: table-cell;
line-height: 1;
vertical-align: middle;

svg {
display: block;
}
}

@mixin bp-SettingsRow-label {
Expand All @@ -27,7 +32,7 @@
@mixin bp-SettingsRow-value {
@include bp-SettingsRow-cell;

padding: 6px 6px 6px 10px;
padding: 6px;
color: $sunset-grey;
font-size: 12px;
text-align: left;
Expand Down
9 changes: 8 additions & 1 deletion src/lib/viewers/media/MP3Controls.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import '~box-ui-elements/es/styles/variables';
@import '../controls/media/styles';

.bp-MP3Controls {
display: flex;
Expand All @@ -17,3 +17,10 @@
display: flex;
align-items: center;
}

.bp-MP3Controls-settings {
.bp-SettingsToggle {
width: $bp-MediaControl-width;
height: $bp-MediaControl-height;
}
}
7 changes: 4 additions & 3 deletions src/lib/viewers/media/MP3Controls.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import DurationLabels, { Props as DurationLabelsProps } from '../controls/media/DurationLabels';
import MP3Settings, { Props as MP3SettingsProps } from './MP3Settings';
import MediaSettings, { Props as MediaSettingsProps } from '../controls/media/MediaSettings';
import PlayPauseToggle, { Props as PlayControlsProps } from '../controls/media/PlayPauseToggle';
import TimeControls, { Props as TimeControlsProps } from '../controls/media/TimeControls';
import VolumeControls, { Props as VolumeControlsProps } from '../controls/media/VolumeControls';
import './MP3Controls.scss';

export type Props = DurationLabelsProps &
MP3SettingsProps &
MediaSettingsProps &
PlayControlsProps &
TimeControlsProps &
VolumeControlsProps;
Expand Down Expand Up @@ -44,8 +44,9 @@ export default function MP3Controls({
</div>

<div className="bp-MP3Controls-group">
<MP3Settings
<MediaSettings
autoplay={autoplay}
className="bp-MP3Controls-settings"
onAutoplayChange={onAutoplayChange}
onRateChange={onRateChange}
rate={rate}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.bp-MediaControlsRoot {
.bp-MP3ControlsRoot {
position: absolute;
top: 50%;
left: 50%;
Expand Down
Loading

0 comments on commit 29a6487

Please sign in to comment.