Skip to content

Commit 1b1ba04

Browse files
OwenEdwardsgkatsev
authored andcommitted
fix: fix the structure of elements in menus to comply with ARIA requirements (#4034)
Fix the structure of elements in menus so that actionable elements are not children of actionable elements, as required by ARIA. Remove unnecessary aria-labels on menus.
1 parent 6208e4b commit 1b1ba04

File tree

10 files changed

+114
-32
lines changed

10 files changed

+114
-32
lines changed

src/js/clickable-component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ class ClickableComponent extends Component {
219219
// Support Space (32) or Enter (13) key operation to fire a click event
220220
if (event.which === 32 || event.which === 13) {
221221
event.preventDefault();
222-
this.handleClick(event);
222+
this.trigger('click');
223223
} else if (super.handleKeyPress) {
224224

225225
// Pass keypress handling up for unsupported keys

src/js/control-bar/audio-track-controls/audio-track-button.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ class AudioTrackButton extends TrackButton {
2525
options.tracks = player.audioTracks();
2626

2727
super(player, options);
28-
29-
this.el_.setAttribute('aria-label', 'Audio Menu');
3028
}
3129

3230
/**

src/js/control-bar/control-bar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ class ControlBar extends Component {
3939
className: 'vjs-control-bar',
4040
dir: 'ltr'
4141
}, {
42-
// The control bar is a group, so it can contain menuitems
42+
// The control bar is a group, but we don't aria-label it to avoid
43+
// over-announcing by JAWS
4344
role: 'group'
4445
});
4546
}

src/js/control-bar/text-track-controls/captions-button.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class CaptionsButton extends TextTrackButton {
2626
*/
2727
constructor(player, options, ready) {
2828
super(player, options, ready);
29-
this.el_.setAttribute('aria-label', 'Captions Menu');
3029
}
3130

3231
/**

src/js/control-bar/text-track-controls/chapters-button.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class ChaptersButton extends TextTrackButton {
2929
*/
3030
constructor(player, options, ready) {
3131
super(player, options, ready);
32-
this.el_.setAttribute('aria-label', 'Chapters Menu');
3332
}
3433

3534
/**

src/js/control-bar/text-track-controls/descriptions-button.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class DescriptionsButton extends TextTrackButton {
2626
*/
2727
constructor(player, options, ready) {
2828
super(player, options, ready);
29-
this.el_.setAttribute('aria-label', 'Descriptions Menu');
3029

3130
const tracks = player.textTracks();
3231
const changeHandler = Fn.bind(this, this.handleTracksChange);

src/js/control-bar/text-track-controls/subtitles-button.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ class SubtitlesButton extends TextTrackButton {
2525
*/
2626
constructor(player, options, ready) {
2727
super(player, options, ready);
28-
this.el_.setAttribute('aria-label', 'Subtitles Menu');
2928
}
3029

3130
/**

src/js/menu/menu-button.js

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/**
22
* @file menu-button.js
33
*/
4-
import ClickableComponent from '../clickable-component.js';
4+
import Button from '../button.js';
55
import Component from '../component.js';
66
import Menu from './menu.js';
77
import * as Dom from '../utils/dom.js';
88
import * as Fn from '../utils/fn.js';
9+
import * as Events from '../utils/events.js';
910
import toTitleCase from '../utils/to-title-case.js';
11+
import document from 'global/document';
1012

1113
/**
1214
* A `MenuButton` class for any popup {@link Menu}.
1315
*
14-
* @extends ClickableComponent
16+
* @extends Component
1517
*/
16-
class MenuButton extends ClickableComponent {
18+
class MenuButton extends Component {
1719

1820
/**
1921
* Creates an instance of this class.
@@ -27,12 +29,27 @@ class MenuButton extends ClickableComponent {
2729
constructor(player, options = {}) {
2830
super(player, options);
2931

32+
this.menuButton_ = new Button(player, options);
33+
34+
this.menuButton_.controlText(this.controlText_);
35+
this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
36+
37+
// Add buildCSSClass values to the button, not the wrapper
38+
const buttonClass = Button.prototype.buildCSSClass();
39+
40+
this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
41+
42+
this.addChild(this.menuButton_);
43+
3044
this.update();
3145

3246
this.enabled_ = true;
3347

34-
this.el_.setAttribute('aria-haspopup', 'true');
35-
this.el_.setAttribute('role', 'menuitem');
48+
this.on(this.menuButton_, 'tap', this.handleClick);
49+
this.on(this.menuButton_, 'click', this.handleClick);
50+
this.on(this.menuButton_, 'focus', this.handleFocus);
51+
this.on(this.menuButton_, 'blur', this.handleBlur);
52+
3653
this.on('keydown', this.handleSubmenuKeyPress);
3754
}
3855

@@ -56,7 +73,7 @@ class MenuButton extends ClickableComponent {
5673
* @private
5774
*/
5875
this.buttonPressed_ = false;
59-
this.el_.setAttribute('aria-expanded', 'false');
76+
this.menuButton_.el_.setAttribute('aria-expanded', 'false');
6077

6178
if (this.items && this.items.length === 0) {
6279
this.hide();
@@ -72,7 +89,7 @@ class MenuButton extends ClickableComponent {
7289
* The constructed menu
7390
*/
7491
createMenu() {
75-
const menu = new Menu(this.player_);
92+
const menu = new Menu(this.player_, { menuButton: this });
7693

7794
// Add a title list item to the top
7895
if (this.options_.title) {
@@ -113,10 +130,33 @@ class MenuButton extends ClickableComponent {
113130
*/
114131
createEl() {
115132
return super.createEl('div', {
116-
className: this.buildCSSClass()
133+
className: this.buildWrapperCSSClass()
134+
}, {
117135
});
118136
}
119137

138+
/**
139+
* Allow sub components to stack CSS class names for the wrapper element
140+
*
141+
* @return {string}
142+
* The constructed wrapper DOM `className`
143+
*/
144+
buildWrapperCSSClass() {
145+
let menuButtonClass = 'vjs-menu-button';
146+
147+
// If the inline option is passed, we want to use different styles altogether.
148+
if (this.options_.inline === true) {
149+
menuButtonClass += '-inline';
150+
} else {
151+
menuButtonClass += '-popup';
152+
}
153+
154+
// TODO: Fix the CSS so that this isn't necessary
155+
const buttonClass = Button.prototype.buildCSSClass();
156+
157+
return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
158+
}
159+
120160
/**
121161
* Builds the default DOM `className`.
122162
*
@@ -163,6 +203,47 @@ class MenuButton extends ClickableComponent {
163203
}
164204
}
165205

206+
/**
207+
* Set the focus to the actual button, not to this element
208+
*/
209+
focus() {
210+
this.menuButton_.focus();
211+
}
212+
213+
/**
214+
* Remove the focus from the actual button, not this element
215+
*/
216+
blur() {
217+
this.menuButton_.blur();
218+
}
219+
220+
/**
221+
* This gets called when a `MenuButton` gains focus via a `focus` event.
222+
* Turns on listening for `keydown` events. When they happen it
223+
* calls `this.handleKeyPress`.
224+
*
225+
* @param {EventTarget~Event} event
226+
* The `focus` event that caused this function to be called.
227+
*
228+
* @listens focus
229+
*/
230+
handleFocus() {
231+
Events.on(document, 'keydown', Fn.bind(this, this.handleKeyPress));
232+
}
233+
234+
/**
235+
* Called when a `MenuButton` loses focus. Turns off the listener for
236+
* `keydown` events. Which Stops `this.handleKeyPress` from getting called.
237+
*
238+
* @param {EventTarget~Event} event
239+
* The `blur` event that caused this function to be called.
240+
*
241+
* @listens blur
242+
*/
243+
handleBlur() {
244+
Events.off(document, 'keydown', Fn.bind(this, this.handleKeyPress));
245+
}
246+
166247
/**
167248
* Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
168249
* {@link ClickableComponent#handleKeyPress} for instances where this is called.
@@ -182,15 +263,15 @@ class MenuButton extends ClickableComponent {
182263
// Don't preventDefault for Tab key - we still want to lose focus
183264
if (event.which !== 9) {
184265
event.preventDefault();
266+
// Set focus back to the menu button's button
267+
this.menuButton_.el_.focus();
185268
}
186269
// Up (38) key or Down (40) key press the 'button'
187270
} else if (event.which === 38 || event.which === 40) {
188271
if (!this.buttonPressed_) {
189272
this.pressButton();
190273
event.preventDefault();
191274
}
192-
} else {
193-
super.handleKeyPress(event);
194275
}
195276
}
196277

@@ -213,6 +294,8 @@ class MenuButton extends ClickableComponent {
213294
// Don't preventDefault for Tab key - we still want to lose focus
214295
if (event.which !== 9) {
215296
event.preventDefault();
297+
// Set focus back to the menu button's button
298+
this.menuButton_.el_.focus();
216299
}
217300
}
218301
}
@@ -224,7 +307,7 @@ class MenuButton extends ClickableComponent {
224307
if (this.enabled_) {
225308
this.buttonPressed_ = true;
226309
this.menu.lockShowing();
227-
this.el_.setAttribute('aria-expanded', 'true');
310+
this.menuButton_.el_.setAttribute('aria-expanded', 'true');
228311
// set the focus into the submenu
229312
this.menu.focus();
230313
}
@@ -237,32 +320,30 @@ class MenuButton extends ClickableComponent {
237320
if (this.enabled_) {
238321
this.buttonPressed_ = false;
239322
this.menu.unlockShowing();
240-
this.el_.setAttribute('aria-expanded', 'false');
241-
// Set focus back to this menu button
242-
this.el_.focus();
323+
this.menuButton_.el_.setAttribute('aria-expanded', 'false');
243324
}
244325
}
245326

246327
/**
247328
* Disable the `MenuButton`. Don't allow it to be clicked.
248329
*/
249330
disable() {
250-
// Unpress, but don't force focus on this button
251-
this.buttonPressed_ = false;
252-
this.menu.unlockShowing();
253-
this.el_.setAttribute('aria-expanded', 'false');
331+
this.unpressButton();
254332

255333
this.enabled_ = false;
334+
this.addClass('vjs-disabled');
256335

257-
super.disable();
336+
this.menuButton_.disable();
258337
}
259338

260339
/**
261340
* Enable the `MenuButton`. Allow it to be clicked.
262341
*/
263342
enable() {
264343
this.enabled_ = true;
265-
super.enable();
344+
this.removeClass('vjs-disabled');
345+
346+
this.menuButton_.enable();
266347
}
267348
}
268349

src/js/menu/menu.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class Menu extends Component {
2727
constructor(player, options) {
2828
super(player, options);
2929

30+
if (options) {
31+
this.menuButton_ = options.menuButton;
32+
}
33+
3034
this.focusedChild_ = -1;
3135

3236
this.on('keydown', this.handleKeyPress);
@@ -42,8 +46,11 @@ class Menu extends Component {
4246
addItem(component) {
4347
this.addChild(component);
4448
component.on('click', Fn.bind(this, function(event) {
45-
this.unlockShowing();
46-
// TODO: Need to set keyboard focus back to the menuButton
49+
// Unpress the associated MenuButton, and move focus back to it
50+
if (this.menuButton_) {
51+
this.menuButton_.unpressButton();
52+
this.menuButton_.focus();
53+
}
4754
}));
4855
}
4956

@@ -67,7 +74,6 @@ class Menu extends Component {
6774
className: 'vjs-menu'
6875
});
6976

70-
el.setAttribute('role', 'presentation');
7177
el.appendChild(this.contentEl_);
7278

7379
// Prevent clicks from bubbling up. Needed for Menu Buttons,

test/unit/menu.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ QUnit.test('clicking should display the menu', function(assert) {
4545
const menuButton = new MenuButton(player, {
4646
title: 'testTitle'
4747
});
48-
const el = menuButton.el();
48+
const el = menuButton.menuButton_.el();
4949

5050
assert.ok(menuButton.menu !== undefined, 'menu is created');
5151

0 commit comments

Comments
 (0)