Skip to content

Commit

Permalink
feat(controls): Add Subtitles menu for Dash (#1400)
Browse files Browse the repository at this point in the history
* feat(controls): Add Subtitles menu for Dash

* chore: address pr comments

* chore: add unit tests

* chore: fix linting issues

* chore: pr comments

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
Conrad Chan and mergify[bot] authored Jun 23, 2021
1 parent 2ffb42e commit 6d04ebc
Show file tree
Hide file tree
Showing 18 changed files with 628 additions and 55 deletions.
2 changes: 2 additions & 0 deletions src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,5 @@ export const ERROR_CODE_403_FORBIDDEN_BY_POLICY = 'forbidden_by_policy';
// LocalStorage Keys
export const DOCUMENT_FTUX_CURSOR_SEEN_KEY = 'ftux-cursor-seen-document';
export const IMAGE_FTUX_CURSOR_SEEN_KEY = 'ftux-cursor-seen-image';

export const SUBTITLES_OFF = -1;
32 changes: 22 additions & 10 deletions src/lib/viewers/controls/media/MediaSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import MediaSettingsMenuQuality, {
Props as QualityProps,
} from './MediaSettingsMenuQuality';
import MediaSettingsMenuRate, { Props as RateProps } from './MediaSettingsMenuRate';
import MediaSettingsMenuSubtitles, { getDisplayLanguage, Props as SubtitlesProps } from './MediaSettingsMenuSubtitles';
import Settings, { Menu, Props as SettingsProps } from '../settings';

export type Props = Partial<AudioTracksProps> &
Partial<QualityProps> &
Partial<SettingsProps> &
Partial<SubtitlesProps> &
AutoplayProps &
RateProps & { className?: string };

Expand All @@ -25,16 +27,21 @@ export default function MediaSettings({
onAutoplayChange,
onQualityChange,
onRateChange,
onSubtitleChange,
quality,
rate,
subtitle,
subtitles = [],
toggle,
}: Props): JSX.Element {
const subtitleDisplayLanguage = getDisplayLanguage(subtitle, subtitles);
const autoValue = autoplay ? __('media_autoplay_enabled') : __('media_autoplay_disabled');
const rateValue = rate === '1.0' || !rate ? __('media_speed_normal') : rate;
const labelledAudioTracks = React.useMemo(() => addLabels(audioTracks), [audioTracks]);
const hydratedSelectedAudioTrack = labelledAudioTracks.find(({ id }) => audioTrack === id);
const audioTrackLabel = hydratedSelectedAudioTrack ? hydratedSelectedAudioTrack.label : '';
const showAudioTrackItems = audioTracks.length > 1;
const showSubtitles = subtitles.length > 0 && onSubtitleChange;

return (
<Settings badge={badge} className={className} toggle={toggle}>
Expand All @@ -59,6 +66,14 @@ export default function MediaSettings({
value={getQualityLabel(quality)}
/>
)}
{showSubtitles && (
<Settings.MenuItem
data-testid="bp-media-settings-subtitles"
label={`${__('subtitles')}/CC`}
target={Menu.SUBTITLES}
value={subtitleDisplayLanguage}
/>
)}
{showAudioTrackItems && (
<Settings.MenuItem
data-testid="bp-media-settings-audiotracks"
Expand All @@ -71,16 +86,13 @@ export default function MediaSettings({

<MediaSettingsMenuAutoplay autoplay={autoplay} onAutoplayChange={onAutoplayChange} />
<MediaSettingsMenuRate onRateChange={onRateChange} rate={rate} />
{quality && onQualityChange && (
<MediaSettingsMenuQuality onQualityChange={onQualityChange} quality={quality} />
)}
{showAudioTrackItems && (
<MediaSettingsMenuAudioTracks
audioTrack={audioTrack}
audioTracks={labelledAudioTracks}
onAudioTrackChange={onAudioTrackChange}
/>
)}
<MediaSettingsMenuQuality onQualityChange={onQualityChange} quality={quality} />
<MediaSettingsMenuSubtitles onSubtitleChange={onSubtitleChange} subtitle={subtitle} subtitles={subtitles} />
<MediaSettingsMenuAudioTracks
audioTrack={audioTrack}
audioTracks={labelledAudioTracks}
onAudioTrackChange={onAudioTrackChange}
/>
</Settings>
);
}
6 changes: 5 additions & 1 deletion src/lib/viewers/controls/media/MediaSettingsAudioTracks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ export default function MediaSettingsMenuAudioTracks({
audioTrack,
audioTracks,
onAudioTrackChange,
}: Props): JSX.Element {
}: Props): JSX.Element | null {
const { setActiveMenu } = React.useContext(Settings.Context);

if (audioTracks.length <= 1) {
return null;
}

const handleChange = (value: number): void => {
setActiveMenu(Menu.MAIN);
onAudioTrackChange(value);
Expand Down
11 changes: 7 additions & 4 deletions src/lib/viewers/controls/media/MediaSettingsMenuQuality.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import noop from 'lodash/noop';
import Settings, { Menu } from '../settings';

export type Props = {
onQualityChange: (quality: Quality) => void;
quality: Quality;
onQualityChange?: (quality: Quality) => void;
quality?: Quality;
};

export enum Quality {
Expand All @@ -21,9 +20,13 @@ const QUALITY_LABEL_MAP: Record<Quality, string> = {

export const getLabel = (quality: Quality): string => QUALITY_LABEL_MAP[quality];

export default function MediaSettingsMenuQuality({ onQualityChange = noop, quality }: Props): JSX.Element {
export default function MediaSettingsMenuQuality({ onQualityChange, quality }: Props): JSX.Element | null {
const { setActiveMenu } = React.useContext(Settings.Context);

if (!quality || !onQualityChange) {
return null;
}

const handleChange = (value: Quality): void => {
setActiveMenu(Menu.MAIN);
onQualityChange(value);
Expand Down
60 changes: 60 additions & 0 deletions src/lib/viewers/controls/media/MediaSettingsMenuSubtitles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import Settings, { Menu } from '../settings';
import { SUBTITLES_OFF } from '../../../constants';

export type Subtitle = {
displayLanguage: string;
id: number;
};

export type Props = {
onSubtitleChange?: (id: number) => void;
subtitle?: number;
subtitles?: Array<Subtitle>;
};

export const getDisplayLanguage = (subtitle?: number, subtitles: Array<Subtitle> = []): string => {
const { displayLanguage } = subtitles.find(({ id }) => subtitle === id) || {
displayLanguage: __('off'),
};

return displayLanguage;
};

export default function MediaSettingsMenuSubtitles({
onSubtitleChange,
subtitle,
subtitles = [],
}: Props): JSX.Element | null {
const { setActiveMenu } = React.useContext(Settings.Context);

if (!subtitles.length || !onSubtitleChange) {
return null;
}

const handleChange = (value: number): void => {
setActiveMenu(Menu.MAIN);
onSubtitleChange(value);
};

return (
<Settings.Menu name={Menu.SUBTITLES}>
<Settings.MenuBack label={`${__('subtitles')}/CC`} />
<Settings.RadioItem
isSelected={subtitle === SUBTITLES_OFF}
label={__('off')}
onChange={handleChange}
value={SUBTITLES_OFF}
/>
{subtitles.map(({ displayLanguage, id }) => (
<Settings.RadioItem
key={id}
isSelected={subtitle === id}
label={displayLanguage}
onChange={handleChange}
value={id}
/>
))}
</Settings.Menu>
);
}
26 changes: 26 additions & 0 deletions src/lib/viewers/controls/media/SubtitlesToggle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import './styles';

.bp-SubtitlesToggle {
@include bp-MediaButton;

&[aria-pressed='true'] {
.bp-SubtitlesToggle-text {
color: $white;
background-color: $box-blue;
}
}
}

.bp-SubtitlesToggle-text {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $white;
font-weight: bold;
font-size: 12px;
letter-spacing: .1em;
background-color: rgba($black, .3); // Anything less than .3 looks too transparent on IE/Edge
border-radius: 4px;
}
33 changes: 33 additions & 0 deletions src/lib/viewers/controls/media/SubtitlesToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import noop from 'lodash/noop';
import MediaToggle from './MediaToggle';
import { Subtitle } from './MediaSettingsMenuSubtitles';
import './SubtitlesToggle.scss';

export type Props = {
isShowingSubtitles?: boolean;
onSubtitlesToggle?: (isShowingSubtitles: boolean) => void;
subtitles?: Array<Subtitle>;
};

export default function SubtitlesToggle({
isShowingSubtitles,
onSubtitlesToggle = noop,
subtitles = [],
}: Props): JSX.Element | null {
if (!subtitles.length) {
return null;
}

return (
<MediaToggle
aria-pressed={isShowingSubtitles}
className="bp-SubtitlesToggle"
data-testid="bp-media-cc-button"
onClick={(): void => onSubtitlesToggle(!isShowingSubtitles)}
title={__('media_subtitles_cc')}
>
<div className="bp-SubtitlesToggle-text">CC</div>
</MediaToggle>
);
}
6 changes: 6 additions & 0 deletions src/lib/viewers/controls/media/__mocks__/subtitles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const subtitles = [
{ id: 0, displayLanguage: 'English' },
{ id: 1, displayLanguage: 'Spanish' },
];

export default subtitles;
63 changes: 37 additions & 26 deletions src/lib/viewers/controls/media/__tests__/MediaSettings-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import MediaSettings from '../MediaSettings';
import Settings from '../../settings/Settings';
import SettingsMenu from '../../settings/SettingsMenu';
import SettingsMenuItem from '../../settings/SettingsMenuItem';
import subtitles from '../__mocks__/subtitles';
import MediaSettingsMenuAudioTracks from '../MediaSettingsAudioTracks';
import MediaSettingsMenuQuality from '../MediaSettingsMenuQuality';
import MediaSettingsMenuSubtitles from '../MediaSettingsMenuSubtitles';

describe('MediaSettings', () => {
const getWrapper = (props = {}): ShallowWrapper =>
Expand Down Expand Up @@ -55,39 +57,48 @@ describe('MediaSettings', () => {
expect(wrapper.find({ target: menuItem }).prop('value')).toBe(displayValue);
});

test('should not render the audio menu item if no audio tracks are provided', () => {
const wrapper = getWrapper();
expect(wrapper.exists({ target: 'audio' })).toBe(false);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(false);
});
describe('audiotracks menu', () => {
test('should render the audio menu if > 1 audio tracks are present', () => {
const wrapper = getWrapper({ audioTracks });
expect(wrapper.exists({ target: 'audio' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(true);
});

test('should not render the audio menu item if only 1 audio track is present', () => {
const singleAudioTrack = [{ id: 0, language: 'und' }];
const wrapper = getWrapper({ audioTracks: singleAudioTrack });
expect(wrapper.exists({ target: 'audio' })).toBe(false);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(false);
test('should display the generated track label for the selected audio track', () => {
const wrapper = getWrapper({ audioTrack: 1, audioTracks });
const expectedLabel = `${__('track')} 2 (English)`;
expect(wrapper.find({ target: 'audio' }).prop('value')).toBe(expectedLabel);
});
});

test('should render the audio menu if > 1 audio tracks are present', () => {
const wrapper = getWrapper({ audioTracks });
expect(wrapper.exists({ target: 'audio' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(true);
describe('quality menu', () => {
test('should render the quality menu if the quality is provided', () => {
const wrapper = getWrapper({ quality: 'auto', onQualityChange: jest.fn() });
expect(wrapper.exists(MediaSettingsMenuQuality)).toBe(true);
});
});

test('should display the generated track label for the selected audio track', () => {
const wrapper = getWrapper({ audioTrack: 1, audioTracks });
const expectedLabel = `${__('track')} 2 (English)`;
expect(wrapper.find({ target: 'audio' }).prop('value')).toBe(expectedLabel);
});
describe('subtitles menu', () => {
test('should render the subtitles menu item if only 1 subtitles track is present', () => {
const onSubtitleChange = jest.fn();
const singleSubtitle = [{ id: 0, displayLanguage: 'English' }];
const wrapper = getWrapper({ onSubtitleChange, subtitles: singleSubtitle });
expect(wrapper.exists({ target: 'subtitles' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuSubtitles)).toBe(true);
});

test('should not render the quality menu item if no quality is provided', () => {
const wrapper = getWrapper();
expect(wrapper.exists(MediaSettingsMenuQuality)).toBe(false);
});
test('should render the subtitle menu if > 1 subtitles are present', () => {
const onSubtitleChange = jest.fn();
const wrapper = getWrapper({ onSubtitleChange, subtitles });
expect(wrapper.exists({ target: 'subtitles' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuSubtitles)).toBe(true);
});

test('should render the quality menu if the quality is provided', () => {
const wrapper = getWrapper({ quality: 'auto', onQualityChange: jest.fn() });
expect(wrapper.exists(MediaSettingsMenuQuality)).toBe(true);
test('should display the subtitle language for the selected audio track', () => {
const onSubtitleChange = jest.fn();
const wrapper = getWrapper({ onSubtitleChange, subtitle: 1, subtitles });
expect(wrapper.find({ target: 'subtitles' }).prop('value')).toBe('Spanish');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ describe('MediaSettingsAudioTracks', () => {
});

describe('render', () => {
test('should not render if audiotracks is <= 1', () => {
const wrapper = getWrapper({ audioTracks: [] });

expect(wrapper.isEmptyRender()).toBe(true);
});

test('should return a valid wrapper', () => {
const wrapper = getWrapper();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ describe('MediaSettingsMenuQuality', () => {
});

describe('render', () => {
test('should not render if no quality is provided', () => {
const wrapper = getWrapper({ quality: undefined });

expect(wrapper.isEmptyRender()).toBe(true);
});

test('should not render if no callback is provided', () => {
const wrapper = getWrapper({ onQualityChange: undefined });

expect(wrapper.isEmptyRender()).toBe(true);
});

test('should return a valid wrapper', () => {
const wrapper = getWrapper();
const radioItems = wrapper.find(Settings.RadioItem);
Expand Down
Loading

0 comments on commit 6d04ebc

Please sign in to comment.