From 5e2b98aa0cad86e18a06e0b9fa03ac155c2227a6 Mon Sep 17 00:00:00 2001 From: Jared Stoffan Date: Wed, 9 Dec 2020 17:53:46 -0800 Subject: [PATCH] feat(controls): Add react versions of core media controls --- src/lib/viewers/controls/_styles.scss | 33 +++++++++------ .../annotations/AnnotationsButton.tsx | 4 +- .../hooks/__tests__/usePreventKey-test.tsx | 32 +++++++++++++++ src/lib/viewers/controls/hooks/index.ts | 2 +- .../viewers/controls/hooks/usePreventKey.ts | 26 ++++++++++++ .../controls/media/DurationLabels.scss | 17 ++++++++ .../viewers/controls/media/DurationLabels.tsx | 28 +++++++++++++ .../viewers/controls/media/MediaToggle.tsx | 12 ++++++ .../controls/media/PlayPauseToggle.scss | 5 +++ .../controls/media/PlayPauseToggle.tsx | 22 ++++++++++ .../controls/media/SettingsControls.scss | 16 ++++++++ .../controls/media/SettingsControls.tsx | 27 +++++++++++++ .../media/__tests__/DurationLabels-test.tsx | 33 +++++++++++++++ .../media/__tests__/MediaToggle-test.tsx | 27 +++++++++++++ .../media/__tests__/PlayPauseToggle-test.tsx | 40 +++++++++++++++++++ .../media/__tests__/SettingsControls-test.tsx | 32 +++++++++++++++ src/lib/viewers/controls/media/_styles.scss | 8 ++++ 17 files changed, 349 insertions(+), 15 deletions(-) create mode 100644 src/lib/viewers/controls/hooks/__tests__/usePreventKey-test.tsx create mode 100644 src/lib/viewers/controls/hooks/usePreventKey.ts create mode 100644 src/lib/viewers/controls/media/DurationLabels.scss create mode 100644 src/lib/viewers/controls/media/DurationLabels.tsx create mode 100644 src/lib/viewers/controls/media/MediaToggle.tsx create mode 100644 src/lib/viewers/controls/media/PlayPauseToggle.scss create mode 100644 src/lib/viewers/controls/media/PlayPauseToggle.tsx create mode 100644 src/lib/viewers/controls/media/SettingsControls.scss create mode 100644 src/lib/viewers/controls/media/SettingsControls.tsx create mode 100644 src/lib/viewers/controls/media/__tests__/DurationLabels-test.tsx create mode 100644 src/lib/viewers/controls/media/__tests__/MediaToggle-test.tsx create mode 100644 src/lib/viewers/controls/media/__tests__/PlayPauseToggle-test.tsx create mode 100644 src/lib/viewers/controls/media/__tests__/SettingsControls-test.tsx create mode 100644 src/lib/viewers/controls/media/_styles.scss diff --git a/src/lib/viewers/controls/_styles.scss b/src/lib/viewers/controls/_styles.scss index 5cf52ecd6..e3b528e8c 100644 --- a/src/lib/viewers/controls/_styles.scss +++ b/src/lib/viewers/controls/_styles.scss @@ -2,7 +2,28 @@ $bp-controls-background: fade-out($black, .2); +@mixin bp-Control--hover { + opacity: .7; + transition: opacity 150ms; + + &:focus, + &:hover { + opacity: 1; + } +} + +@mixin bp-Control--focus { + outline: 0; + + &:focus { + box-shadow: inset 0 0 0 1px fade-out($white, .5), 0 1px 2px fade-out($black, .9); + } +} + @mixin bp-ControlButton($height: 48px, $width: 48px) { + @include bp-Control--hover; + @include bp-Control--focus; + display: flex; align-items: center; justify-content: center; @@ -13,23 +34,11 @@ $bp-controls-background: fade-out($black, .2); color: $white; background: transparent; border: 1px solid transparent; - outline: 0; cursor: pointer; - opacity: .7; - transition: opacity 150ms; user-select: none; touch-action: manipulation; zoom: 1; - &:focus, - &:hover { - opacity: 1; - } - - &:focus { - box-shadow: inset 0 0 0 1px fade-out($white, .5), 0 1px 2px fade-out($black, .9); - } - &:disabled { cursor: default; opacity: .2; diff --git a/src/lib/viewers/controls/annotations/AnnotationsButton.tsx b/src/lib/viewers/controls/annotations/AnnotationsButton.tsx index c2a3f20d2..67262924f 100644 --- a/src/lib/viewers/controls/annotations/AnnotationsButton.tsx +++ b/src/lib/viewers/controls/annotations/AnnotationsButton.tsx @@ -1,10 +1,10 @@ -import React, { ButtonHTMLAttributes } from 'react'; +import React from 'react'; import classNames from 'classnames'; import noop from 'lodash/noop'; import { AnnotationMode } from './types'; import './AnnotationsButton.scss'; -export type Props = Omit, 'onClick'> & { +export type Props = Omit, 'onClick'> & { children?: React.ReactNode; className?: string; isActive?: boolean; diff --git a/src/lib/viewers/controls/hooks/__tests__/usePreventKey-test.tsx b/src/lib/viewers/controls/hooks/__tests__/usePreventKey-test.tsx new file mode 100644 index 000000000..f7bdd51f2 --- /dev/null +++ b/src/lib/viewers/controls/hooks/__tests__/usePreventKey-test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import usePreventKey from '../usePreventKey'; + +describe('usePreventKey', () => { + function TestComponent({ keys }: { keys?: string[] }): JSX.Element { + const ref = React.useRef(null); + usePreventKey(ref, keys); + return
; + } + + const getElement = (wrapper: ReactWrapper): HTMLDivElement => wrapper.childAt(0).getDOMNode(); + const getEvent = (options = {}): KeyboardEvent => { + const event = new KeyboardEvent('keydown', options); + event.stopPropagation = jest.fn(); + return event; + }; + const getWrapper = (props = {}): ReactWrapper => mount(); + + test('should stop propagation of a matching event triggered on the provided element', () => { + const wrapper = getWrapper({ keys: ['Enter'] }); + const element = getElement(wrapper); + const enterEvent = getEvent({ key: 'Enter' }); + const escapeEvent = getEvent({ key: 'Escape' }); + + element.dispatchEvent(enterEvent); + expect(enterEvent.stopPropagation).toBeCalled(); + + element.dispatchEvent(escapeEvent); + expect(escapeEvent.stopPropagation).not.toBeCalled(); + }); +}); diff --git a/src/lib/viewers/controls/hooks/index.ts b/src/lib/viewers/controls/hooks/index.ts index c9f0eddfe..2a7594ea7 100644 --- a/src/lib/viewers/controls/hooks/index.ts +++ b/src/lib/viewers/controls/hooks/index.ts @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export { default as useFullscreen } from './useFullscreen'; +export { default as usePreventKey } from './usePreventKey'; diff --git a/src/lib/viewers/controls/hooks/usePreventKey.ts b/src/lib/viewers/controls/hooks/usePreventKey.ts new file mode 100644 index 000000000..61f63806e --- /dev/null +++ b/src/lib/viewers/controls/hooks/usePreventKey.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { decodeKeydown } from '../../../util'; + +export default function usePreventKey(ref: React.RefObject, keys: string[] = []): void { + React.useEffect(() => { + const { current: element } = ref; + + const handleKeydown = (event: KeyboardEvent): void => { + const key = decodeKeydown(event); + + if (keys.includes(key)) { + event.stopPropagation(); // Prevents global key handling. Can be simplified with React v17. + } + }; + + if (element) { + element.addEventListener('keydown', handleKeydown); + } + + return (): void => { + if (element) { + element.removeEventListener('keydown', handleKeydown); + } + }; + }, [keys, ref]); +} diff --git a/src/lib/viewers/controls/media/DurationLabels.scss b/src/lib/viewers/controls/media/DurationLabels.scss new file mode 100644 index 000000000..b5f55285d --- /dev/null +++ b/src/lib/viewers/controls/media/DurationLabels.scss @@ -0,0 +1,17 @@ +@import './styles'; + +.bp-DurationLabels { + display: flex; + align-items: center; + margin-right: 10px; + margin-left: 10px; +} + +.bp-DurationLabels-label { + @include bp-Control--hover; + + padding-right: 4px; + padding-left: 4px; + color: $white; + cursor: default; +} diff --git a/src/lib/viewers/controls/media/DurationLabels.tsx b/src/lib/viewers/controls/media/DurationLabels.tsx new file mode 100644 index 000000000..63ae44177 --- /dev/null +++ b/src/lib/viewers/controls/media/DurationLabels.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import './DurationLabels.scss'; + +export type Props = { + currentTime?: number; + durationTime?: number; +}; + +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 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(); + + return `${hour}${min}:${sec}`; +} + +export default function DurationLabels({ currentTime = 0, durationTime = 0 }: Props): JSX.Element { + return ( +
+ {formatTime(currentTime)} + / + {formatTime(durationTime)} +
+ ); +} diff --git a/src/lib/viewers/controls/media/MediaToggle.tsx b/src/lib/viewers/controls/media/MediaToggle.tsx new file mode 100644 index 000000000..c5325e557 --- /dev/null +++ b/src/lib/viewers/controls/media/MediaToggle.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import usePreventKey from '../hooks/usePreventKey'; + +export type Props = React.ButtonHTMLAttributes; + +export default function MediaToggle(props: Props): JSX.Element { + const buttonElRef = React.useRef(null); + + usePreventKey(buttonElRef, ['Enter', 'Space']); + + return