diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuBack.scss b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuBack.scss new file mode 100644 index 000000000..a52608e5c --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuBack.scss @@ -0,0 +1,35 @@ +@import './styles'; + +.bp-MediaSettingsMenuBack { + @include bp-MediaSettingsRow; + + &:hover { + background-color: $hover-blue-background; + + .bp-MediaSettingsMenuBack-label { + color: lighten($blue-steel, 50%); + } + } + + .bp-is-focused &:focus { + background-color: $box-blue; + + .bp-MediaSettingsMenuBack-arrow { + fill: $white; + } + + .bp-MediaSettingsMenuBack-label { + color: $white; + } + } +} + +.bp-MediaSettingsMenuBack-arrow { + @include bp-MediaSettingsRow-cell; +} + +.bp-MediaSettingsMenuBack-label { + @include bp-MediaSettingsRow-label; + + text-align: center; +} diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuBack.tsx b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuBack.tsx new file mode 100644 index 000000000..d2b0a204e --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuBack.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import IconArrowLeft24 from '../../icons/IconArrowLeft24'; +import MediaSettingsContext, { Menu } from './MediaSettingsContext'; +import { decodeKeydown } from '../../../../util'; +import './MediaSettingsMenuBack.scss'; + +export type Props = { + label: string; +}; +export type Ref = HTMLDivElement; + +function MediaSettingsMenuBack({ label }: Props, ref: React.Ref): JSX.Element { + const { setActiveMenu } = React.useContext(MediaSettingsContext); + + const handleClick = (): void => { + setActiveMenu(Menu.MAIN); + }; + + const handleKeydown = (event: React.KeyboardEvent): void => { + const key = decodeKeydown(event); + + if (key !== 'ArrowLeft' && key !== 'Enter' && key !== 'Space') { + return; + } + + setActiveMenu(Menu.MAIN); + }; + + return ( +
+
+ +
+
+ {label} +
+
+ ); +} + +export default React.forwardRef(MediaSettingsMenuBack); diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuItem.scss b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuItem.scss new file mode 100644 index 000000000..278497c82 --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuItem.scss @@ -0,0 +1,42 @@ +@import './styles'; + +.bp-MediaSettingsMenuItem { + @include bp-MediaSettingsRow; + + &:hover { + background-color: $hover-blue-background; + + .bp-MediaSettingsMenuItem-label { + color: lighten($blue-steel, 50%); + } + + .bp-MediaSettingsMenuItem-value { + color: $blue-steel; + } + } + + .bp-is-focused &:focus { + background-color: $box-blue; + + .bp-MediaSettingsMenuItem-arrow { + fill: $white; + } + + .bp-MediaSettingsMenuItem-label, + .bp-MediaSettingsMenuItem-value { + color: $white; + } + } +} + +.bp-MediaSettingsMenuItem-arrow { + @include bp-MediaSettingsRow-cell; +} + +.bp-MediaSettingsMenuItem-label { + @include bp-MediaSettingsRow-label; +} + +.bp-MediaSettingsMenuItem-value { + @include bp-MediaSettingsRow-value; +} diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuItem.tsx b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuItem.tsx new file mode 100644 index 000000000..4004bbebd --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsMenuItem.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import classNames from 'classnames'; +import IconArrowRight24 from '../../icons/IconArrowRight24'; +import MediaSettingsContext, { Menu } from './MediaSettingsContext'; +import { decodeKeydown } from '../../../../util'; +import './MediaSettingsMenuItem.scss'; + +export type Props = { + className?: string; + label: string; + target: Menu; + value: string; +}; +export type Ref = HTMLDivElement; + +function MediaSettingsMenuItem(props: Props, ref: React.Ref): JSX.Element { + const { className, label, target, value } = props; + const { setActiveMenu } = React.useContext(MediaSettingsContext); + + const handleClick = (): void => { + setActiveMenu(target); + }; + + const handleKeydown = (event: React.KeyboardEvent): void => { + const key = decodeKeydown(event); + + if (key !== 'ArrowRight' && key !== 'Enter' && key !== 'Space') { + return; + } + + setActiveMenu(target); + }; + + return ( +
+
+ {label} +
+
{value}
+
+ +
+
+ ); +} + +export default React.forwardRef(MediaSettingsMenuItem); diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsRadioItem.scss b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsRadioItem.scss new file mode 100644 index 000000000..595da9c5e --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsRadioItem.scss @@ -0,0 +1,51 @@ +@import './styles'; + +.bp-MediaSettingsRadioItem { + @include bp-MediaSettingsRow; + + &.bp-is-selected { + .bp-MediaSettingsRadioItem-check-icon { + visibility: visible; + } + + .bp-MediaSettingsRadioItem-value { + color: $box-blue; + } + } + + &:hover { + background-color: $hover-blue-background; + + .bp-MediaSettingsRadioItem-check-icon { + fill: $blue-steel; + } + + .bp-MediaSettingsRadioItem-value { + color: $blue-steel; + } + } + + .bp-is-focused &:focus { + background-color: $box-blue; + + .bp-MediaSettingsRadioItem-check-icon { + fill: $white; + } + + .bp-MediaSettingsRadioItem-value { + color: $white; + } + } +} + +.bp-MediaSettingsRadioItem-check { + @include bp-MediaSettingsRow-cell; +} + +.bp-MediaSettingsRadioItem-check-icon { + visibility: hidden; +} + +.bp-MediaSettingsRadioItem-value { + @include bp-MediaSettingsRow-value; +} diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsRadioItem.tsx b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsRadioItem.tsx new file mode 100644 index 000000000..0c4394575 --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/MediaSettingsRadioItem.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import classNames from 'classnames'; +import IconCheckMark24 from '../../icons/IconCheckMark24'; +import { decodeKeydown } from '../../../../util'; +import './MediaSettingsRadioItem.scss'; + +export type Props = { + className?: string; + isSelected?: boolean; + label?: string; + onChange: (value: V) => void; + value: V; +}; +export type Ref = HTMLDivElement; +export type Value = boolean | number | string; + +function MediaSettingsRadioItem(props: Props, ref: React.Ref): JSX.Element { + const { className, isSelected, label, onChange, value } = props; + + const handleClick = (): void => { + onChange(value); + }; + + const handleKeydown = (event: React.KeyboardEvent): void => { + const key = decodeKeydown(event); + + if (key !== 'ArrowLeft' && key !== 'Enter' && key !== 'Space') { + return; + } + + onChange(value); + }; + + return ( +
+
+ +
+
{label || value}
+
+ ); +} + +export default React.forwardRef(MediaSettingsRadioItem) as typeof MediaSettingsRadioItem; diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsContext-test.tsx b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsContext-test.tsx index 4449751cf..6ef990d12 100644 --- a/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsContext-test.tsx +++ b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsContext-test.tsx @@ -5,7 +5,6 @@ import MediaSettingsContext, { Context, Menu } from '../MediaSettingsContext'; describe('MediaSettingsContext', () => { const getContext = (): Context => ({ activeMenu: Menu.MAIN, - activeRect: undefined, setActiveMenu: jest.fn(), setActiveRect: jest.fn(), }); diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsMenuBack-test.tsx b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsMenuBack-test.tsx new file mode 100644 index 000000000..5dd00c1af --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsMenuBack-test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import IconArrowLeft24 from '../../../icons/IconArrowLeft24'; +import MediaSettingsContext, { Context } from '../MediaSettingsContext'; +import MediaSettingsMenuBack from '../MediaSettingsMenuBack'; + +describe('MediaSettingsMenuBack', () => { + const getContext = (): Partial => ({ setActiveMenu: jest.fn() }); + const getWrapper = (props = {}, context = {}): ReactWrapper => + mount(, { + wrappingComponent: MediaSettingsContext.Provider, + wrappingComponentProps: { value: context }, + }); + + describe('event handlers', () => { + test('should set the active menu when clicked', () => { + const context = getContext(); + const wrapper = getWrapper({}, context); + + wrapper.simulate('click'); + + expect(context.setActiveMenu).toBeCalled(); + }); + + test.each` + key | calledTimes + ${'ArrowLeft'} | ${1} + ${'Enter'} | ${1} + ${'Escape'} | ${0} + ${'Space'} | ${1} + `('should set the active menu $calledTimes times when $key is pressed', ({ key, calledTimes }) => { + const context = getContext(); + const wrapper = getWrapper({}, context); + + wrapper.simulate('keydown', { key }); + + expect(context.setActiveMenu).toBeCalledTimes(calledTimes); + }); + }); + + describe('render', () => { + test('should return a valid wrapper', () => { + const wrapper = getWrapper(); + + expect(wrapper.getDOMNode()).toHaveClass('bp-MediaSettingsMenuBack'); + expect(wrapper.find('.bp-MediaSettingsMenuBack-label').contains('Autoplay')).toBe(true); + expect(wrapper.exists(IconArrowLeft24)).toBe(true); + }); + }); +}); diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsMenuItem-test.tsx b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsMenuItem-test.tsx new file mode 100644 index 000000000..a12da1967 --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsMenuItem-test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import IconArrowRight24 from '../../../icons/IconArrowRight24'; +import MediaSettingsContext, { Menu, Context } from '../MediaSettingsContext'; +import MediaSettingsMenuItem from '../MediaSettingsMenuItem'; + +describe('MediaSettingsMenuItem', () => { + const getContext = (): Partial => ({ setActiveMenu: jest.fn() }); + const getWrapper = (props = {}, context = {}): ReactWrapper => + mount(, { + wrappingComponent: MediaSettingsContext.Provider, + wrappingComponentProps: { value: context }, + }); + + describe('event handlers', () => { + test('should set the active menu when clicked', () => { + const context = getContext(); + const wrapper = getWrapper({}, context); + + wrapper.simulate('click'); + + expect(context.setActiveMenu).toBeCalled(); + }); + + test.each` + key | calledTimes + ${'ArrowRight'} | ${1} + ${'Enter'} | ${1} + ${'Escape'} | ${0} + ${'Space'} | ${1} + `('should set the active menu $calledTimes times when $key is pressed', ({ key, calledTimes }) => { + const context = getContext(); + const wrapper = getWrapper({}, context); + + wrapper.simulate('keydown', { key }); + + expect(context.setActiveMenu).toBeCalledTimes(calledTimes); + }); + }); + + describe('render', () => { + test('should return a valid wrapper', () => { + const wrapper = getWrapper(); + + expect(wrapper.getDOMNode()).toHaveClass('bp-MediaSettingsMenuItem'); + expect(wrapper.find('.bp-MediaSettingsMenuItem-label').contains('Speed')).toBe(true); + expect(wrapper.find('.bp-MediaSettingsMenuItem-value').contains('Normal')).toBe(true); + expect(wrapper.exists(IconArrowRight24)).toBe(true); + }); + }); +}); diff --git a/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsRadioItem-test.tsx b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsRadioItem-test.tsx new file mode 100644 index 000000000..a7bf59840 --- /dev/null +++ b/src/lib/viewers/controls/media/MediaSettingsControls/__tests__/MediaSettingsRadioItem-test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import IconCheckMark24 from '../../../icons/IconCheckMark24'; +import MediaSettingsRadioItem from '../MediaSettingsRadioItem'; + +describe('MediaSettingsRadioItem', () => { + const getWrapper = (props = {}): ShallowWrapper => + shallow(); + + describe('event handlers', () => { + test('should set the active menu when clicked', () => { + const onChange = jest.fn(); + const wrapper = getWrapper({ onChange }); + + wrapper.simulate('click'); + + expect(onChange).toBeCalledWith(1); + }); + + test.each` + key | calledTimes + ${'ArrowLeft'} | ${1} + ${'Enter'} | ${1} + ${'Escape'} | ${0} + ${'Space'} | ${1} + `('should set the active menu $calledTimes times when $key is pressed', ({ key, calledTimes }) => { + const onChange = jest.fn(); + const wrapper = getWrapper({ onChange }); + + wrapper.simulate('keydown', { key }); + + expect(onChange).toBeCalledTimes(calledTimes); + }); + }); + + describe('render', () => { + test.each([true, false])('should set classes based on isSelected prop %s', isSelected => { + const wrapper = getWrapper({ isSelected }); + + expect(wrapper.hasClass('bp-is-selected')).toBe(isSelected); + }); + + test('should return a valid wrapper', () => { + const wrapper = getWrapper(); + + expect(wrapper.hasClass('bp-MediaSettingsRadioItem')).toBe(true); + expect(wrapper.find('.bp-MediaSettingsRadioItem-value').contains('1.0')).toBe(true); + expect(wrapper.exists(IconCheckMark24)).toBe(true); // Rendered, but visually hidden by default + }); + }); +});