Skip to content

Commit 621d8ee

Browse files
authored
Fix VideoPlayer accessibility issues (#703)
* improve contrast of VideoPlayer focus styles * add changeset * remove overlay from playing video * improve contrast of VideoPlayer * fix issue of range tooltip overflowing container * fix color of custom play icon * update changeset * address pr feedback * fix issue where time tooltip can go out of range * fix colors after rebase * hide ControlsBar when video doesn't have focus or mouse * update snapshots * hide controls on inactivity
1 parent b18d390 commit 621d8ee

20 files changed

+148
-52
lines changed

.changeset/hot-laws-hug.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@primer/react-brand': patch
3+
---
4+
5+
`VideoPlayer` accessibility improvements
6+
7+
- Improved contrast of play overlay focus styles.
8+
- Improved contrast of controls and title.
9+
- The title bar now hides while the video is playing.
10+
- The controls bar now hides when the cursor or keyboard focus leaves the video player, or after a few seconds of inactivity, and reappears when the cursor or keyboard focus returns.

packages/design-tokens/src/tokens/functional/components/video-player/colors.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ module.exports = {
1616
},
1717
title: {
1818
bgColor: {
19-
value: 'linear-gradient(#01040966, var(--base-color-scale-transparent))',
20-
dark: 'linear-gradient(#01040966, var(--base-color-scale-transparent))',
19+
value: 'linear-gradient(180deg, #000000e6, #00000073 66%, transparent)',
20+
dark: 'linear-gradient(180deg, #000000e6, #00000073 66%, transparent)',
2121
},
2222
fgColor: {
2323
value: 'var(--base-color-scale-gray-0)',
@@ -26,8 +26,8 @@ module.exports = {
2626
},
2727
controls: {
2828
bgColor: {
29-
value: 'linear-gradient(var(--base-color-scale-transparent), #01040966)',
30-
dark: 'linear-gradient(var(--base-color-scale-transparent), #01040966)',
29+
value: '#000000bf',
30+
dark: '#000000bf',
3131
},
3232
fgColor: {
3333
value: 'var(--base-color-scale-gray-0)',
@@ -65,8 +65,8 @@ module.exports = {
6565
dark: 'var(--base-color-scale-gray-0)',
6666
},
6767
progress: {
68-
value: 'var(--base-color-scale-blue-5)',
69-
dark: 'var(--base-color-scale-blue-5)',
68+
value: 'var(--base-color-scale-blue-4)',
69+
dark: 'var(--base-color-scale-blue-4)',
7070
},
7171
},
7272
},

packages/react/src/VideoPlayer/VideoPlayer.features.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {VideoPlayer} from '.'
77
import {Stack} from '../Stack'
88
import {Button} from '../Button'
99
import {useVideo} from './hooks'
10+
import styles from './VideoPlayer.stories.module.css'
1011

1112
export default {
1213
title: 'Components/VideoPlayer/Features',
@@ -98,7 +99,7 @@ export const ControlledProgrammatically = () => {
9899
}
99100

100101
export const CustomPlayIcon = () => (
101-
<VideoPlayer title="GitHub media player" playIcon={() => <PlayIcon size={96} />}>
102+
<VideoPlayer title="GitHub media player" playIcon={() => <PlayIcon size={96} className={styles.customPlayIcon} />}>
102103
<VideoPlayer.Source src="./example.mp4" type="video/mp4" />
103104
<VideoPlayer.Track src="./example.vtt" default />
104105
</VideoPlayer>

packages/react/src/VideoPlayer/VideoPlayer.module.css

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
.VideoPlayer__container {
2020
width: 100%;
2121
position: relative;
22-
overflow: hidden;
2322
border-radius: var(--brand-borderRadius-medium);
2423
}
2524

@@ -37,7 +36,7 @@
3736
/* 3. Center Play Button */
3837
/* ---------------------------------------------------------- */
3938

40-
.VideoPlayer__playButton {
39+
.VideoPlayer__playButtonOverlay {
4140
position: absolute;
4241
top: 0;
4342
left: 0;
@@ -48,20 +47,21 @@
4847
z-index: 1;
4948
}
5049

51-
.VideoPlayer__playButtonOverlay {
50+
.VideoPlayer__playButtonOverlay.VideoPlayer__playButtonOverlay--transparent {
51+
background: transparent;
52+
}
53+
54+
.VideoPlayer__playButtonOverlay:focus-visible {
55+
outline: var(--brand-borderWidth-thicker) solid var(--brand-color-focus);
56+
outline-offset: var(--base-size-2);
57+
}
58+
59+
.VideoPlayer__playButton {
5260
width: 25%;
5361
height: 25%;
5462
max-width: var(--brand-VideoPlayer-playButton-width);
5563
max-height: var(--brand-VideoPlayer-playButton-height);
5664
opacity: 0.8;
57-
}
58-
59-
.VideoPlayer__playButton:focus {
60-
border: var(--brand-borderWidth-thick) solid var(--brand-color-focus);
61-
box-shadow: 0 0 0 0.125rem var(--brand-color-focus);
62-
}
63-
64-
.VideoPlayer__playButton svg {
6565
color: var(--brand-videoPlayer-playButton-fgColor-rest);
6666
}
6767

@@ -73,7 +73,6 @@
7373
transition: var(--brand-VideoPlayer-transition);
7474
top: 0;
7575
position: absolute;
76-
border-radius: var(--brand-borderRadius-medium);
7776
left: 0;
7877
width: 100%;
7978
z-index: 2;
@@ -85,6 +84,12 @@
8584
grid-gap: var(--base-size-12);
8685
grid-template-columns: auto auto;
8786
background: var(--brand-videoPlayer-title-bgColor);
87+
transition: all var(--brand-animation-duration-fast) var(--brand-animation-easing-default);
88+
}
89+
90+
.VideoPlayer__title.VideoPlayer__title--hidden {
91+
opacity: 0;
92+
visibility: hidden;
8893
}
8994

9095
/* ---------------------------------------------------------- */
@@ -115,8 +120,14 @@
115120
left: 0;
116121
width: 100%;
117122
background: var(--brand-videoPlayer-controls-bgColor);
118-
padding: var(--base-size-16) var(--base-size-24);
123+
padding: var(--base-size-12) var(--base-size-16);
119124
pointer-events: all;
125+
opacity: 1;
126+
}
127+
128+
.VideoPlayer__controlsBar--fade {
129+
transition: opacity var(--brand-animation-duration-default) var(--brand-animation-easing-default);
130+
opacity: 0;
120131
}
121132

122133
.VideoPlayer__controls:focus,
@@ -228,9 +239,8 @@
228239
}
229240

230241
.VideoPlayer__rangeInput:focus-visible {
231-
border-color: var(--brand-color-focus);
232-
outline: none;
233-
box-shadow: 0 0 0 0.125rem var(--brand-color-focus);
242+
outline: var(--brand-borderWidth-thick) solid var(--brand-color-focus);
243+
outline-offset: var(--base-size-4);
234244
opacity: 1;
235245
}
236246

packages/react/src/VideoPlayer/VideoPlayer.module.css.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
declare const styles: {
22
readonly "VideoPlayer__container": string;
33
readonly "VideoPlayer": string;
4-
readonly "VideoPlayer__playButton": string;
54
readonly "VideoPlayer__playButtonOverlay": string;
5+
readonly "VideoPlayer__playButtonOverlay--transparent": string;
6+
readonly "VideoPlayer__playButton": string;
67
readonly "VideoPlayer__title": string;
8+
readonly "VideoPlayer__title--hidden": string;
79
readonly "VideoPlayer__controls": string;
810
readonly "VideoPlayer__controls--hidden": string;
911
readonly "VideoPlayer__controlsBar": string;
12+
readonly "VideoPlayer__controlsBar--fade": string;
1013
readonly "VideoPlayer__iconControl": string;
1114
readonly "VideoPlayer__tooltip": string;
1215
readonly "VideoPlayer__seek": string;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.customPlayIcon {
2+
color: var(--base-color-scale-white-0);
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare const styles: {
2+
readonly "customPlayIcon": string;
3+
};
4+
export = styles;
5+

packages/react/src/VideoPlayer/VideoPlayer.tsx

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useRef, forwardRef, useContext, type HTMLProps, type FunctionComponent} from 'react'
1+
import React, {useEffect, useState, useRef, forwardRef, useContext, type HTMLProps, type FunctionComponent} from 'react'
22
import clsx from 'clsx'
33
import {Text} from '../Text'
44
import {type AnimateProps} from '../animation'
@@ -54,7 +54,7 @@ const Root = ({
5454
showMuteButton = true,
5555
showVolumeControl = true,
5656
showFullScreenButton = true,
57-
playIcon: PlayIcon = () => <DefaultPlayIcon className={styles.VideoPlayer__playButtonOverlay} />,
57+
playIcon: PlayIcon = () => <DefaultPlayIcon className={styles.VideoPlayer__playButton} />,
5858
...rest
5959
}: VideoPlayerProps) => {
6060
const videoWrapperRef = useRef<HTMLDivElement>(null)
@@ -63,41 +63,84 @@ const Root = ({
6363
const useVideoContext = useVideo()
6464
const {ccEnabled, isPlaying, ref, togglePlaying} = useVideoContext
6565

66-
const hideControls = !isPlaying && !showControlsWhenPaused
66+
const [isInteracting, setIsInteracting] = useState(false)
67+
68+
useEffect(() => {
69+
const videoWrapper = videoWrapperRef.current
70+
let hideControlsTimeout: NodeJS.Timeout
71+
const inactivityTimeout = 3000
72+
73+
if (!videoWrapper) {
74+
return
75+
}
76+
77+
const showControls = () => {
78+
setIsInteracting(true)
79+
80+
clearTimeout(hideControlsTimeout)
81+
82+
hideControlsTimeout = setTimeout(() => {
83+
setIsInteracting(false)
84+
}, inactivityTimeout)
85+
}
86+
87+
const hideControls = () => {
88+
setIsInteracting(false)
89+
}
90+
91+
videoWrapper.addEventListener('mousemove', showControls)
92+
videoWrapper.addEventListener('mouseleave', hideControls)
93+
videoWrapper.addEventListener('focusin', showControls)
94+
videoWrapper.addEventListener('focusout', hideControls)
95+
96+
return () => {
97+
videoWrapper.removeEventListener('mousemove', showControls)
98+
videoWrapper.removeEventListener('mouseleave', hideControls)
99+
videoWrapper.removeEventListener('focusin', showControls)
100+
videoWrapper.removeEventListener('focusout', hideControls)
101+
102+
clearTimeout(hideControlsTimeout)
103+
}
104+
}, [videoWrapperRef])
105+
106+
const showControls = isInteracting || (showControlsWhenPaused && !isPlaying)
67107

68108
return (
69109
<div className={styles.VideoPlayer__container} ref={videoWrapperRef}>
70110
<video ref={ref} title={title} controls={false} className={clsx(styles.VideoPlayer, className)} {...rest}>
71111
{children}
72112
<track kind="captions" />
73113
</video>
74-
<div className={styles.VideoPlayer__title}>
75-
{showBranding && <MarkGithubIcon size={40} />}
76-
{!visuallyHiddenTitle && (
77-
<Text size="400" weight="medium" className={styles.VideoPlayer__controlTextColor}>
78-
{title}
79-
</Text>
80-
)}
81-
</div>
114+
{showBranding || !visuallyHiddenTitle ? (
115+
<div className={clsx(styles.VideoPlayer__title, isPlaying && styles['VideoPlayer__title--hidden'])}>
116+
{showBranding && <MarkGithubIcon size={40} />}
117+
{!visuallyHiddenTitle && (
118+
<Text size="400" weight="medium" className={styles.VideoPlayer__controlTextColor}>
119+
{title}
120+
</Text>
121+
)}
122+
</div>
123+
) : null}
82124
<button
83-
className={styles.VideoPlayer__playButton}
125+
className={clsx(
126+
styles.VideoPlayer__playButtonOverlay,
127+
isPlaying && styles['VideoPlayer__playButtonOverlay--transparent'],
128+
)}
84129
onClick={togglePlaying}
85130
aria-label={isPlaying ? 'Pause' : 'Play'}
86131
>
87132
{!isPlaying && <PlayIcon />}
88133
</button>
89134
<div className={styles.VideoPlayer__controls}>
90135
{ccEnabled && <Captions />}
91-
{!hideControls && (
92-
<ControlsBar>
93-
{showPlayPauseButton && <PlayPauseButton />}
94-
{showSeekControl && <SeekControl />}
95-
{showCCButton && <CCButton />}
96-
{showMuteButton && <MuteButton />}
97-
{showVolumeControl && !isSmall && <VolumeControl />}
98-
{showFullScreenButton && <FullScreenButton />}
99-
</ControlsBar>
100-
)}
136+
<ControlsBar className={clsx(!showControls && styles['VideoPlayer__controlsBar--fade'])}>
137+
{showPlayPauseButton && <PlayPauseButton />}
138+
{showSeekControl && <SeekControl />}
139+
{showCCButton && <CCButton />}
140+
{showMuteButton && <MuteButton />}
141+
{showVolumeControl && !isSmall && <VolumeControl />}
142+
{showFullScreenButton && <FullScreenButton />}
143+
</ControlsBar>
101144
</div>
102145
</div>
103146
)
Loading
Loading

0 commit comments

Comments
 (0)