Skip to content

Commit 7c3f3fd

Browse files
authored
Improve grouping of elements for a better description of AX tags in videos (#807)
* Improve how we group videos for AX descriptions * Delete dots from video indentifier * Create unique id for custom-controls * It does not render the reference to the alt id in video-replay-container if alt does not exit * Escape dots in Jest using a backslash
1 parent 14923e5 commit 7c3f3fd

File tree

6 files changed

+65
-25
lines changed

6 files changed

+65
-25
lines changed

src/components/ReplayableVideoAsset.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@
99
-->
1010

1111
<template>
12-
<div class="video-replay-container">
12+
<div
13+
class="video-replay-container"
14+
role="group"
15+
:aria-roledescription="$t('video.title')"
16+
:aria-labelledby="!showsDefaultControls ? ariaLabelledByContainer : null"
17+
>
18+
<span
19+
:id="`${id}-custom-controls`"
20+
hidden
21+
>
22+
{{ $t('video.custom-controls') }}
23+
</span>
1324
<VideoAsset
1425
ref="asset"
1526
:variants="variants"
@@ -92,6 +103,9 @@ export default {
92103
if (this.videoEnded) return this.$t('video.replay');
93104
return this.isPlaying ? this.$t('video.pause') : this.$t('video.play');
94105
},
106+
ariaLabelledByContainer: ({ id, alt }) => (alt
107+
? `${id}-custom-controls ${id}-alt`
108+
: `${id}-custom-controls`),
95109
},
96110
data() {
97111
return {

src/components/VideoAsset.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
:muted="muted"
2828
:width="optimalWidth"
2929
:aria-roledescription="$t('video.title')"
30-
:aria-label="!showsDefaultControls ? $t('video.custom-controls') : null"
31-
:aria-describedby="alt ? altTextId : null"
30+
:aria-labelledby="showsDefaultControls && alt ? altTextId : null"
3231
playsinline
3332
@loadedmetadata="setOrientation"
3433
@playing="$emit('playing')"
@@ -46,7 +45,7 @@
4645
<span
4746
v-if="alt"
4847
:id="altTextId"
49-
class="visuallyhidden"
48+
hidden
5049
>
5150
{{ $t('video.description', { alt }) }}
5251
</span>

src/lang/locales/en-US.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"play": "Play",
99
"pause": "Pause",
1010
"watch": "Watch intro video",
11-
"description": "Content description of this video: {alt}",
12-
"custom-controls": "Custom controls are available below"
11+
"description": "Content description: {alt}",
12+
"custom-controls": "Video with custom controls."
1313
},
1414
"tutorials": {
1515
"title": "Tutorial | Tutorials",

tests/unit/components/Asset.spec.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11+
/* eslint-disable no-useless-escape */
1112
import { shallowMount } from '@vue/test-utils';
1213
import Asset from 'docc-render/components/Asset.vue';
1314
import ImageAsset from 'docc-render/components/ImageAsset.vue';
@@ -20,7 +21,7 @@ const video = {
2021
alt: 'Text describing this video',
2122
variants: [
2223
{
23-
url: 'foo.mp4',
24+
url: 'foo\.mp4',
2425
traits: ['2x'],
2526
size: {
2627
width: 42,
@@ -63,7 +64,7 @@ describe('Asset', () => {
6364
alt: 'blah',
6465
variants: [
6566
{
66-
url: 'foo.png',
67+
url: 'foo\.png',
6768
traits: ['2x'],
6869
size: {
6970
width: 42,
@@ -84,7 +85,7 @@ describe('Asset', () => {
8485
type: 'image',
8586
variants: [
8687
{
87-
url: 'image.jpg',
88+
url: 'image\.jpg',
8889
traits: ['2x', 'light'],
8990
},
9091
],
@@ -226,7 +227,7 @@ describe('Asset', () => {
226227
alt: 'blah',
227228
variants: [
228229
{
229-
url: 'foo.png',
230+
url: 'foo\.png',
230231
traits: ['2x'],
231232
size: {
232233
width: 42,

tests/unit/components/ReplayableVideoAsset.spec.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const propsData = {
2424
posterVariants,
2525
showsDefaultControls: false,
2626
alt: 'Text describing this video',
27-
id: 'video.mp4',
27+
id: 'videomp4',
2828
};
2929
describe('ReplayableVideoAsset', () => {
3030
const mountWithProps = props => shallowMount(ReplayableVideoAsset, {
@@ -73,6 +73,39 @@ describe('ReplayableVideoAsset', () => {
7373
expect(wrapper.find('.control-button').exists()).toBe(true);
7474
});
7575

76+
it('renders a video-replay-container as a group with AX aria tags', () => {
77+
const wrapper = mountWithProps();
78+
const ariaLabelledByContainer = `${propsData.id}-custom-controls ${propsData.id}-alt`;
79+
80+
const container = wrapper.find('.video-replay-container');
81+
expect(container.attributes('role')).toBe('group');
82+
expect(container.attributes('aria-labelledby')).toBe(ariaLabelledByContainer);
83+
84+
const customControlsDescription = wrapper.find(`#${propsData.id}-custom-controls`);
85+
expect(customControlsDescription.exists()).toBe(true);
86+
expect(customControlsDescription.attributes('hidden')).toBe('hidden');
87+
expect(customControlsDescription.text()).toBe('video.custom-controls');
88+
});
89+
90+
it('does not render the reference to the alt id in video-replay-container if alt does not exit', () => {
91+
const wrapper = mountWithProps({
92+
alt: '',
93+
});
94+
const ariaLabelledByContainer = `${propsData.id}-custom-controls`;
95+
96+
const container = wrapper.find('.video-replay-container');
97+
expect(container.attributes('aria-labelledby')).toBe(ariaLabelledByContainer);
98+
});
99+
100+
it('renders a video-replay-container without "aria-labelledby" if showsDefaultControls is true', () => {
101+
const wrapper = mountWithProps({
102+
showsDefaultControls: true,
103+
});
104+
105+
const container = wrapper.find('.video-replay-container');
106+
expect(container.attributes()).not.toHaveProperty('aria-labelledby');
107+
});
108+
76109
it('does not show the `.control-button` if `showsDefaultControls` is `true`', () => {
77110
const wrapper = mountWithProps({
78111
showsDefaultControls: true,

tests/unit/components/VideoAsset.spec.js

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ const propsData = {
3939
{ traits: ['dark', '1x'], url: 'https://www.example.com/video~dark.mp4' },
4040
],
4141
alt: 'Text describing this video',
42-
id: 'video.mp4',
42+
id: 'videomp4',
43+
showsDefaultControls: false,
4344
};
4445

4546
const altTextId = `${propsData.id}-alt`;
@@ -61,21 +62,21 @@ describe('VideoAsset', () => {
6162
});
6263

6364
it('renders a hidden description with unique id for AX purposes if video provides an alt text', () => {
64-
const hiddenDesc = wrapper.find('span.visuallyhidden');
65+
const hiddenDesc = wrapper.find('span[hidden=hidden]');
6566
expect(hiddenDesc.exists()).toBe(true);
6667
expect(hiddenDesc.attributes('id')).toBe(altTextId);
6768
expect(hiddenDesc.text()).toBe(`video.description ${propsData.alt}`);
6869
});
6970

70-
it('adds a description reference to the `video` with altTextId', () => {
71+
it('adds a description reference to the `video` if showsDefaultControls is true', () => {
72+
wrapper.setProps({ showsDefaultControls: true });
7173
const video = wrapper.find('video');
72-
expect(video.attributes('aria-describedby')).toBe(altTextId);
74+
expect(video.attributes('aria-labelledby')).toBe(altTextId);
7375
});
7476

7577
it('does not add a description reference to the `video` if alt is not provided', () => {
7678
wrapper.setProps({ alt: null });
77-
const video = wrapper.find('video');
78-
expect(video.attributes()).not.toHaveProperty('aria-describedby');
79+
expect(wrapper.find('video').attributes()).not.toHaveProperty('aria-labelledby');
7980
});
8081

8182
it('adds a poster to the `video`, using light by default', async () => {
@@ -158,14 +159,6 @@ describe('VideoAsset', () => {
158159
expect(source.attributes('controls')).toBe(undefined);
159160
});
160161

161-
it('renders an aria-label to indicate how the user should interact with custom controls when `showsDefaultControls=false`', () => {
162-
wrapper.setProps({
163-
showControls: false,
164-
});
165-
const video = wrapper.find('video');
166-
expect(video.attributes('aria-label')).toBe('video.custom-controls');
167-
});
168-
169162
it('forwards `playing`, `pause` and `ended` events', () => {
170163
const video = wrapper.find('video');
171164

0 commit comments

Comments
 (0)