Skip to content

Commit

Permalink
feat(a11y): video meta data items separated (#116)
Browse files Browse the repository at this point in the history
Co-authored-by: Mike van Veenhuijzen <mike@videodock.com>
  • Loading branch information
2 people authored and ChristiaanScheermeijer committed Feb 29, 2024
1 parent 52a620c commit d72aa2b
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 62 deletions.
50 changes: 7 additions & 43 deletions packages/common/src/utils/formatting.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { Playlist, PlaylistItem } from '../../types/playlist';

export const formatDurationTag = (seconds: number): string | null => {
if (!seconds) return null;
export const formatDurationTag = (seconds: number) => {
if (!seconds) return '';

const minutes = Math.ceil(seconds / 60);

Expand All @@ -15,17 +13,17 @@ export const formatDurationTag = (seconds: number): string | null => {
* Hours are only shown if at least 1
* Minutes get rounded
*
* @returns string, such as '2h 24m' or '31m'
* @returns string, such as '2hrs 24min' or '31min'
*/

export const formatDuration = (duration: number): string | null => {
if (!duration) return null;
export const formatDuration = (duration: number) => {
if (!duration) return '';

const hours = Math.floor(duration / 3600);
const minutes = Math.round((duration - hours * 3600) / 60);

const hoursString = hours ? `${hours}h ` : '';
const minutesString = minutes ? `${minutes}m ` : '';
const hoursString = hours ? `${hours}hrs ` : '';
const minutesString = minutes ? `${minutes}min ` : '';

return `${hoursString}${minutesString}`;
};
Expand All @@ -37,28 +35,6 @@ export const formatPrice = (price: number, currency: string, country?: string) =
}).format(price);
};

export const formatVideoMetaString = (item: PlaylistItem, episodesLabel?: string) => {
const metaData = [];

if (item.pubdate) metaData.push(new Date(item.pubdate * 1000).getFullYear());
if (!episodesLabel && item.duration) metaData.push(formatDuration(item.duration));
if (episodesLabel) metaData.push(episodesLabel);
if (item.genre) metaData.push(item.genre);
if (item.rating) metaData.push(item.rating);

return metaData.join(' • ');
};

export const formatPlaylistMetaString = (item: Playlist, episodesLabel?: string) => {
const metaData = [];

if (episodesLabel) metaData.push(episodesLabel);
if (item.genre) metaData.push(item.genre);
if (item.rating) metaData.push(item.rating);

return metaData.join(' • ');
};

export const formatSeriesMetaString = (seasonNumber?: string, episodeNumber?: string) => {
if (!seasonNumber && !episodeNumber) {
return '';
Expand All @@ -67,18 +43,6 @@ export const formatSeriesMetaString = (seasonNumber?: string, episodeNumber?: st
return seasonNumber && seasonNumber !== '0' ? `S${seasonNumber}:E${episodeNumber}` : `E${episodeNumber}`;
};

export const formatLiveEventMetaString = (media: PlaylistItem, locale: string) => {
const metaData = [];
const scheduled = formatVideoSchedule(locale, media.scheduledStart, media.scheduledEnd);

if (scheduled) metaData.push(scheduled);
if (media.duration) metaData.push(formatDuration(media.duration));
if (media.genre) metaData.push(media.genre);
if (media.rating) metaData.push(media.rating);

return metaData.join(' • ');
};

export const formatVideoSchedule = (locale: string, scheduledStart?: Date, scheduledEnd?: Date) => {
if (!scheduledStart) {
return '';
Expand Down
38 changes: 37 additions & 1 deletion packages/common/src/utils/media.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CONTENT_TYPE } from '../constants';
import type { Playlist, PlaylistItem } from '../../types/playlist';
import { CONTENT_TYPE } from '../constants';

import { formatDuration, formatVideoSchedule } from './formatting';

type RequiredProperties<T, P extends keyof T> = T & Required<Pick<T, P>>;

Expand Down Expand Up @@ -49,3 +51,37 @@ export const getLegacySeriesPlaylistIdFromEpisodeTags = (item: PlaylistItem | un

export const isLiveChannel = (item: PlaylistItem): item is RequiredProperties<PlaylistItem, 'contentType' | 'liveChannelsId'> =>
item.contentType?.toLowerCase() === CONTENT_TYPE.liveChannel && !!item.liveChannelsId;

export const createVideoMetadata = (media: PlaylistItem, episodesLabel?: string) => {
const metaData = [];

if (media.pubdate) metaData.push(String(new Date(media.pubdate * 1000).getFullYear()));
if (!episodesLabel && media.duration) metaData.push(formatDuration(media.duration));
if (episodesLabel) metaData.push(episodesLabel);
if (media.genre) metaData.push(media.genre);
if (media.rating) metaData.push(media.rating);

return metaData;
};

export const createPlaylistMetadata = (playlist: Playlist, episodesLabel?: string) => {
const metaData = [];

if (episodesLabel) metaData.push(episodesLabel);
if (playlist.genre) metaData.push(playlist.genre as string);
if (playlist.rating) metaData.push(playlist.rating as string);

return metaData;
};

export const createLiveEventMetadata = (media: PlaylistItem, locale: string) => {
const metaData = [];
const scheduled = formatVideoSchedule(locale, media.scheduledStart, media.scheduledEnd);

if (scheduled) metaData.push(scheduled);
if (media.duration) metaData.push(formatDuration(media.duration));
if (media.genre) metaData.push(media.genre);
if (media.rating) metaData.push(media.rating);

return metaData;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.icon {
margin-right: 8px;
}
4 changes: 3 additions & 1 deletion packages/ui-react/src/components/StatusIcon/StatusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Today from '@jwp/ott-theme/assets/icons/today.svg?react';
import Tag from '../Tag/Tag';
import Icon from '../Icon/Icon';

import styles from './StatusIcon.module.scss';

type Props = {
mediaStatus?: MediaStatus;
};
Expand All @@ -13,7 +15,7 @@ export default function StatusIcon({ mediaStatus }: Props) {
const { t } = useTranslation('video');

if (mediaStatus === MediaStatus.SCHEDULED || mediaStatus === MediaStatus.VOD) {
return <Icon icon={Today} />;
return <Icon icon={Today} className={styles.icon} />;
} else if (mediaStatus === MediaStatus.LIVE) {
return <Tag isLive>{t('live')}</Tag>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,6 @@
line-height: variables.$base-line-height;
letter-spacing: 0.15px;

> :first-child {
margin-right: 8px;
}

@include responsive.mobile-only() {
order: 2;
font-size: 14px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.separator {
margin: 0 4px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import { render } from '@testing-library/react';

import VideoMetaData from './VideoMetaData';

describe('<VideoMetaData>', () => {
test('renders and matches snapshot', () => {
const attributes = ['2023', 'Comedy', 'something'];
const { container } = render(<VideoMetaData attributes={attributes} separator="|" />);

expect(container).toMatchSnapshot();
});
});
27 changes: 27 additions & 0 deletions packages/ui-react/src/components/VideoMetaData/VideoMetaData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';

import styles from './VideoMetaData.module.scss';

type Props = {
attributes: string[];
separator?: string;
};

const VideoMetaData: React.FC<Props> = ({ attributes, separator = '•' }: Props) => {
return (
<>
{attributes.map((value, index) => (
<>
<span key={index}>{value}</span>
{index < attributes.length - 1 && (
<span className={styles.separator} aria-hidden="true">
{separator}
</span>
)}
</>
))}
</>
);
};

export default VideoMetaData;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`<VideoMetaData> > renders and matches snapshot 1`] = `
<div>
<span>
2023
</span>
<span
aria-hidden="true"
class="_separator_1267df"
>
|
</span>
<span>
Comedy
</span>
<span
aria-hidden="true"
class="_separator_1267df"
>
|
</span>
<span>
something
</span>
</div>
`;
12 changes: 8 additions & 4 deletions packages/ui-react/src/pages/LegacySeries/LegacySeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { PlaylistItem } from '@jwp/ott-common/types/playlist';
import { useWatchHistoryStore } from '@jwp/ott-common/src/stores/WatchHistoryStore';
import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore';
import { formatPlaylistMetaString, formatSeriesMetaString, formatVideoMetaString } from '@jwp/ott-common/src/utils/formatting';
import { createPlaylistMetadata, createVideoMetadata } from '@jwp/ott-common/src/utils/media';
import { formatSeriesMetaString } from '@jwp/ott-common/src/utils/formatting';
import { legacySeriesURL } from '@jwp/ott-common/src/utils/urlFormatting';
import useEntitlement from '@jwp/ott-hooks-react/src/useEntitlement';
import useMedia from '@jwp/ott-hooks-react/src/useMedia';
Expand All @@ -27,6 +28,7 @@ import FavoriteButton from '../../containers/FavoriteButton/FavoriteButton';
import Button from '../../components/Button/Button';
import Loading from '../Loading/Loading';
import Icon from '../../components/Icon/Icon';
import VideoMetaData from '../../components/VideoMetaData/VideoMetaData';

import { filterSeries, generateLegacyEpisodeJSONLD, getEpisodesInSeason, getFiltersFromSeries, getNextItem } from './utils';

Expand Down Expand Up @@ -114,9 +116,11 @@ const LegacySeries = () => {
const canonicalUrl = `${window.location.origin}${legacySeriesURL({ episodeId: episode?.mediaid, seriesId })}`;
const backgroundImage = (selectedItem.backgroundImage as string) || undefined;

const primaryMetadata = episode
? formatVideoMetaString(episode, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }))
: formatPlaylistMetaString(seriesPlaylist, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }));
const primaryMetadata = episode ? (
<VideoMetaData attributes={createVideoMetadata(episode, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }))} />
) : (
<VideoMetaData attributes={createPlaylistMetadata(seriesPlaylist, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }))} />
);
const secondaryMetadata = episodeMetadata && episode && (
<>
<strong>{formatSeriesMetaString(episodeMetadata.seasonNumber, episodeMetadata.episodeNumber)}</strong> - {episode.title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { PlaylistItem } from '@jwp/ott-common/types/playlist';
import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore';
import { MediaStatus } from '@jwp/ott-common/src/utils/liveEvent';
import { formatLiveEventMetaString } from '@jwp/ott-common/src/utils/formatting';
import { createLiveEventMetadata } from '@jwp/ott-common/src/utils/media';
import { mediaURL } from '@jwp/ott-common/src/utils/urlFormatting';
import { generateMovieJSONLD } from '@jwp/ott-common/src/utils/structuredData';
import useMedia from '@jwp/ott-hooks-react/src/useMedia';
Expand All @@ -28,6 +28,7 @@ import FavoriteButton from '../../../../containers/FavoriteButton/FavoriteButton
import Button from '../../../../components/Button/Button';
import InlinePlayer from '../../../../containers/InlinePlayer/InlinePlayer';
import StatusIcon from '../../../../components/StatusIcon/StatusIcon';
import VideoMetaData from '../../../../components/VideoMetaData/VideoMetaData';
import Icon from '../../../../components/Icon/Icon';

const MediaEvent: ScreenComponent<PlaylistItem> = ({ data: media, isLoading }) => {
Expand Down Expand Up @@ -92,7 +93,7 @@ const MediaEvent: ScreenComponent<PlaylistItem> = ({ data: media, isLoading }) =
const primaryMetadata = (
<>
<StatusIcon mediaStatus={media.mediaStatus} />
{formatLiveEventMetaString(media, i18n.language)}
<VideoMetaData attributes={createLiveEventMetadata(media, i18n.language)} />
</>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { shallow } from '@jwp/ott-common/src/utils/compare';
import type { PlaylistItem } from '@jwp/ott-common/types/playlist';
import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore';
import { formatVideoMetaString } from '@jwp/ott-common/src/utils/formatting';
import { createVideoMetadata } from '@jwp/ott-common/src/utils/media';
import { mediaURL } from '@jwp/ott-common/src/utils/urlFormatting';
import { generateMovieJSONLD } from '@jwp/ott-common/src/utils/structuredData';
import useMedia from '@jwp/ott-hooks-react/src/useMedia';
Expand All @@ -26,6 +26,7 @@ import FavoriteButton from '../../../../containers/FavoriteButton/FavoriteButton
import Button from '../../../../components/Button/Button';
import InlinePlayer from '../../../../containers/InlinePlayer/InlinePlayer';
import Icon from '../../../../components/Icon/Icon';
import VideoMetaData from '../../../../components/VideoMetaData/VideoMetaData';

const MediaMovie: ScreenComponent<PlaylistItem> = ({ data, isLoading }) => {
const { t } = useTranslation('video');
Expand Down Expand Up @@ -79,7 +80,7 @@ const MediaMovie: ScreenComponent<PlaylistItem> = ({ data, isLoading }) => {
const pageTitle = `${data.title} - ${siteName}`;
const canonicalUrl = data ? `${window.location.origin}${mediaURL({ media: data })}` : window.location.href;

const primaryMetadata = formatVideoMetaString(data);
const primaryMetadata = <VideoMetaData attributes={createVideoMetadata(data)} />;
const shareButton = <ShareButton title={data.title} description={data.description} url={canonicalUrl} />;
const startWatchingButton = <StartWatchingButton item={data} playUrl={mediaURL({ media: data, playlistId: feedId, play: true })} />;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore';
import { generateEpisodeJSONLD } from '@jwp/ott-common/src/utils/structuredData';
import { getEpisodesInSeason, getFiltersFromSeries } from '@jwp/ott-common/src/utils/series';
import { formatSeriesMetaString, formatVideoMetaString } from '@jwp/ott-common/src/utils/formatting';
import { createVideoMetadata } from '@jwp/ott-common/src/utils/media';
import { formatSeriesMetaString } from '@jwp/ott-common/src/utils/formatting';
import { buildLegacySeriesUrlFromMediaItem, mediaURL } from '@jwp/ott-common/src/utils/urlFormatting';
import { VideoProgressMinMax } from '@jwp/ott-common/src/constants';
import useEntitlement from '@jwp/ott-hooks-react/src/useEntitlement';
Expand All @@ -34,6 +35,7 @@ import FavoriteButton from '../../../../containers/FavoriteButton/FavoriteButton
import Button from '../../../../components/Button/Button';
import Loading from '../../../Loading/Loading';
import Icon from '../../../../components/Icon/Icon';
import VideoMetaData from '../../../../components/VideoMetaData/VideoMetaData';
import { createURLFromLocation } from '../../../../utils/location';

const MediaSeries: ScreenComponent<PlaylistItem> = ({ data: seriesMedia }) => {
Expand Down Expand Up @@ -188,7 +190,7 @@ const MediaSeries: ScreenComponent<PlaylistItem> = ({ data: seriesMedia }) => {
const pageTitle = `${selectedItem.title} - ${siteName}`;
const canonicalUrl = `${window.location.origin}${mediaURL({ media: seriesMedia, episodeId: episode?.mediaid })}`;

const primaryMetadata = formatVideoMetaString(selectedItem, t('video:total_episodes', { count: series.episode_count }));
const primaryMetadata = <VideoMetaData attributes={createVideoMetadata(selectedItem, t('video:total_episodes', { count: series.episode_count }))} />;
const secondaryMetadata = episodeMetadata && episode && (
<>
<strong>{formatSeriesMetaString(episodeMetadata.seasonNumber, episodeMetadata.episodeNumber)}</strong> - {episode.title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Tag from '../../../../components/Tag/Tag';
import Loading from '../../../Loading/Loading';
import VideoDetails from '../../../../components/VideoDetails/VideoDetails';
import Icon from '../../../../components/Icon/Icon';
import VideoMetaData from '../../../../components/VideoMetaData/VideoMetaData';

import styles from './PlaylistLiveChannels.module.scss';

Expand Down Expand Up @@ -87,15 +88,14 @@ const PlaylistLiveChannels: ScreenComponent<Playlist> = ({ data: { feedid, playl
const endTime = new Date(program.endTime);
const durationInSeconds = differenceInSeconds(endTime, startTime);
const duration = formatDurationTag(durationInSeconds);
const attributes = [t('on_channel', { name: channel.title }), duration].filter(Boolean);

return (
<>
<Tag className={styles.tag} isLive={isLive}>
{isLive ? t('common:live') : `${format(startTime, 'p')} - ${format(endTime, 'p')}`}
</Tag>
{t('on_channel', { name: channel.title })}
{' • '}
{duration}
<VideoMetaData attributes={attributes} />
</>
);
}, [channel, isLive, program, t]);
Expand Down

0 comments on commit d72aa2b

Please sign in to comment.