Files
Andy(김규회) f1c0468f70 feat(UI): Improve shaka player UI accessibility (#10023)
### ARIA attributes
  - Add `aria-hidden="true"` to all decorative SVG icons (`icon.js`)
- Implement missing `aria-label` for ad controls (`skip_ad_button.js`,
`ad_info.js`)
- Add `aria-pressed` to toggle buttons (mute, fullscreen, pip, loop,
statistics, remote, stereoscopic)
- Remove incorrect `aria-pressed` from one-shot action button
(`recenter_vr.js`)
- Add `role="menu"`, `aria-haspopup`, `aria-expanded` to menus
(`overflow_menu.js`, `settings_menu.js`, `context_menu.js`)
- Add `role="menuitemradio"` and `aria-checked` to selection menus
(resolution, language, text, playback rate, etc.)
- Add `role="toolbar"` to control panel (`controls.js`)
- Add `role="heading"` to content title (`content_title.js`)
  
### Focus management
- Restore focus to trigger button when menus close
  - Maintain focus on fullscreen button after toggle     

 ### CSS
- Add `forced-colors` media query for Windows high contrast mode
  ### Other
- Add `alt=""` to seek bar thumbnail image
- Add `aria-hidden="true"` to watermark canvas
  - Add accessibility unit tests for ARIA attributes
- Remove redundant `aria-hidden` from checkmark icon (`ui_utils.js`)
  - Add ARIA terms to project spell-check dictionary  

Issue https://github.com/shaka-project/shaka-player/issues/3146
2026-04-23 14:53:23 +02:00

142 lines
3.3 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ui.Icon');
goog.require('shaka.util.Dom');
/**
* @final
* @export
*/
shaka.ui.Icon = class {
/**
* @param {?HTMLElement} parent
* @param {?(shaka.extern.UIIcon | string)=} icon
*/
constructor(parent, icon) {
this.parent = parent;
/** @private {!SVGElement} */
this.svg_ = shaka.util.Dom.createSVGElement('svg');
this.svg_.classList.add('shaka-ui-icon');
// Screen reader should ignore icon text.
// all icons should have this attribute
this.svg_.ariaHidden = 'true';
this.svg_.setAttribute('viewBox', '0 -960 960 960');
if (icon) {
this.use(icon);
}
if (this.parent) {
parent.appendChild(this.svg_);
}
}
/**
* If a single string is passed, it is treated as an SVG path
* @param {shaka.extern.UIIcon | string} icon
* @export
*/
use(icon) {
// check if it is empty string or null or undefined
if (!icon) {
return;
}
// remove all previous path elements
this.emptyChildNodes_();
if (typeof icon == 'string') {
this.svg_.style.setProperty('font-size', '');
this.applyInlinedSVG_(icon, null);
} else if (typeof icon == 'object') {
const url = icon['url'];
const path = icon['path'];
const viewBox = icon['viewBox'];
const size = icon['size'];
this.svg_.style.setProperty('font-size', size ? size + 'px': '');
if (url) {
// let handle the background-color (icon color) by CSS
this.svg_.style.setProperty('background-color', 'currentColor');
this.svg_.style.setProperty('mask-image', `url("${url}")`);
} else if (path) {
this.applyInlinedSVG_(path, viewBox);
}
}
}
/**
* @return {!SVGElement}
* @export
*/
getSvgElement() {
return this.svg_;
}
/**
* @return {string}
* @export
*/
getDataUrl() {
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(this.svg_);
const encoded = window.btoa(svgString);
return `url("data:image/svg+xml;base64,${encoded}")`;
}
/**
* @param {string | Array<string>} path
* @param {?string} viewBox
* @private
*/
applyInlinedSVG_(path, viewBox) {
// do not need a background color if mask-image isn't using
this.svg_.style.setProperty('background-color', 'transparent');
this.svg_.style.setProperty('mask-image', '');
this.svg_.setAttribute('viewBox', viewBox || '0 -960 960 960');
if (Array.isArray(path)) {
for (let i = 0, l = path.length; i < l; i++) {
this.addPath_(path[i]);
}
} else if (path) {
this.addPath_(path);
}
}
/**
* Add a path element, call `emptyChildNodes()` first to clean previous
* path elements.
* @param {string} path
* @private
*/
addPath_(path) {
const el = shaka.util.Dom.createSVGElement('path');
el.setAttribute('d', path);
this.svg_.appendChild(el);
}
/**
* Remove all the child nodes from svg element
* @private
*/
emptyChildNodes_() {
const childNodes = this.svg_.childNodes;
for (let i = 0, l = childNodes.length, child; i < l; i++) {
child = childNodes[i];
if (child instanceof SVGPathElement) {
child.remove();
}
}
}
};