diff --git a/testsuite/tests/a11y/explorer/KeyExplorer.test.ts b/testsuite/tests/a11y/explorer/KeyExplorer.test.ts new file mode 100644 index 000000000..83f7bf9cb --- /dev/null +++ b/testsuite/tests/a11y/explorer/KeyExplorer.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from '@jest/globals'; +import { SpeechExplorer } from '#js/a11y/explorer/KeyExplorer.js'; + +class TestSpeechExplorer extends SpeechExplorer { + public setRole(node: HTMLElement, description: string) { + this.setRoleDescription(node, description); + } + + public setBrailleRole(node: HTMLElement, description: string) { + this.setBrailleRoleDescription(node, description); + } +} + +function makeExplorer() { + return new TestSpeechExplorer( + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + { none: '\u0091', brailleNone: '\u2800' } as any + ); +} + +function makeNode() { + const attributes = new Map(); + return { + attributes, + node: { + setAttribute: (name: string, value: string) => + attributes.set(name, value), + removeAttribute: (name: string) => attributes.delete(name), + } as HTMLElement, + }; +} + +describe('SpeechExplorer role descriptions', () => { + test('omits placeholder role descriptions', () => { + const explorer = makeExplorer(); + const { attributes, node } = makeNode(); + + explorer.setRole(node, 'math'); + expect(attributes.get('aria-roledescription')).toBe('math'); + + explorer.setRole(node, '\u0091'); + expect(attributes.has('aria-roledescription')).toBe(false); + + explorer.setRole(node, '\u2800'); + expect(attributes.has('aria-roledescription')).toBe(false); + }); + + test('omits placeholder braille role descriptions', () => { + const explorer = makeExplorer(); + const { attributes, node } = makeNode(); + + explorer.setBrailleRole(node, 'math'); + expect(attributes.get('aria-brailleroledescription')).toBe('math'); + + explorer.setBrailleRole(node, '\u0091'); + expect(attributes.has('aria-brailleroledescription')).toBe(false); + + explorer.setBrailleRole(node, '\u2800'); + expect(attributes.has('aria-brailleroledescription')).toBe(false); + }); +}); diff --git a/testsuite/tests/a11y/explorer/Region.test.ts b/testsuite/tests/a11y/explorer/Region.test.ts new file mode 100644 index 000000000..32ec1d15d --- /dev/null +++ b/testsuite/tests/a11y/explorer/Region.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from '@jest/globals'; +import { HoverRegion, LiveRegion, ToolTip } from '#js/a11y/explorer/Region.js'; + +describe('Explorer region stylesheet IDs', () => { + test('uses stable region class names', () => { + expect(ToolTip.sheetId).toBe('MJX-ToolTip-styles'); + expect(LiveRegion.sheetId).toBe('MJX-LiveRegion-styles'); + expect(HoverRegion.sheetId).toBe('MJX-HoverRegion-styles'); + }); +}); diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts index ce0f68883..299a9ab5c 100644 --- a/ts/a11y/explorer.ts +++ b/ts/a11y/explorer.ts @@ -544,7 +544,6 @@ export function ExplorerMathDocumentMixin< }, role: mathItem.ariaRole, 'aria-label': mathItem.none, - 'aria-roledescription': mathItem.none, }); } diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts index 2e14fef34..32a653d71 100644 --- a/ts/a11y/explorer/KeyExplorer.ts +++ b/ts/a11y/explorer/KeyExplorer.ts @@ -234,8 +234,8 @@ export class SpeechExplorer

Support for tactile Braille devices varies across screen readers, browsers, and operative systems. If you are using a Braille output device, you may need to select the "Combine with Speech" option in the - contextual menu's Braille submenu in order to obtain Nemeth or Euro - Braille output rather than the speech text on your Braille + contextual menu's Braille submenu in order to obtain Nemeth, UEB, or + Euro Braille output rather than the speech text on your Braille device. ${braille}

The contextual menu also provides options for viewing or copying a @@ -387,6 +387,42 @@ export class SpeechExplorer return this.item.brailleNone; } + /** + * Sets an ARIA role description if it is a real description. + * + * @param {HTMLElement} node The element to modify + * @param {string} description The role description + */ + protected setRoleDescription(node: HTMLElement, description: string) { + if ( + description && + description !== this.item.none && + description !== this.item.brailleNone + ) { + node.setAttribute('aria-roledescription', description); + } else { + node.removeAttribute('aria-roledescription'); + } + } + + /** + * Sets an ARIA braille role description if it is a real description. + * + * @param {HTMLElement} node The element to modify + * @param {string} description The braille role description + */ + protected setBrailleRoleDescription(node: HTMLElement, description: string) { + if ( + description && + description !== this.item.none && + description !== this.item.brailleNone + ) { + node.setAttribute('aria-brailleroledescription', description); + } else { + node.removeAttribute('aria-brailleroledescription'); + } + } + /** * The currently focused element. */ @@ -1373,7 +1409,7 @@ export class SpeechExplorer const speechNode = (this.speech = document.createElement('mjx-speech')); speechNode.setAttribute('role', this.role); speechNode.setAttribute('aria-label', speech || this.none); - speechNode.setAttribute('aria-roledescription', description || this.none); + this.setRoleDescription(speechNode, description); speechNode.setAttribute(SemAttr.SPEECH, speech); if (ssml) { speechNode.setAttribute(SemAttr.PREFIX_SSML, ssml[0] || ''); @@ -1383,10 +1419,10 @@ export class SpeechExplorer if (braille) { if (this.document.options.a11y.brailleSpeech) { speechNode.setAttribute('aria-label', braille); - speechNode.setAttribute('aria-roledescription', this.brailleNone); + this.setRoleDescription(speechNode, this.brailleNone); } speechNode.setAttribute('aria-braillelabel', braille); - speechNode.setAttribute('aria-brailleroledescription', this.brailleNone); + this.setBrailleRoleDescription(speechNode, this.brailleNone); if (this.document.options.a11y.brailleCombine) { speechNode.setAttribute( 'aria-label', @@ -1398,8 +1434,8 @@ export class SpeechExplorer if (isWindows) { const container = document.createElement('mjx-speech-container'); container.setAttribute('role', 'application'); - container.setAttribute('aria-roledescription', this.none); - container.setAttribute('aria-brailleroledescription', this.brailleNone); + this.setRoleDescription(container, this.none); + this.setBrailleRoleDescription(container, this.brailleNone); container.append(speechNode); this.node.append(container); speechNode.setAttribute('role', 'img'); @@ -1442,16 +1478,15 @@ export class SpeechExplorer this.img = this.document.adaptor.node('mjx-speech', { 'aria-label': speech, role: 'img', - 'aria-roledescription': item.none, }); const braille = container.getAttribute(SemAttr.BRAILLE); if (braille) { if (this.document.options.a11y.brailleSpeech) { this.img.setAttribute('aria-label', braille); - this.img.setAttribute('aria-roledescription', this.brailleNone); + this.setRoleDescription(this.img, this.brailleNone); } this.img.setAttribute('aria-braillelabel', braille); - this.img.setAttribute('aria-brailleroledescription', this.brailleNone); + this.setBrailleRoleDescription(this.img, this.brailleNone); if (this.document.options.a11y.brailleCombine) { this.img.setAttribute('aria-label', braille + BRAILLE_PADDING + speech); } diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts index 4a4f757ee..f1c3a98ce 100644 --- a/ts/a11y/explorer/Region.ts +++ b/ts/a11y/explorer/Region.ts @@ -113,14 +113,15 @@ export abstract class AbstractRegion implements Region { * @returns {string} The stylesheet ID */ public static get sheetId(): string { - return 'MJX-' + this.name + '-styles'; + const id = this.className ? this.className.replace(/^MJX_/, '') : this.name; + return 'MJX-' + id + '-styles'; } /** * @returns {HTMLStyleElement} The stylesheet for this region */ public static get styleSheet(): HTMLStyleElement { - return document.head.querySelector('#' + this.sheetId) as HTMLStyleElement; + return document.getElementById(this.sheetId) as HTMLStyleElement; } /** @@ -130,7 +131,7 @@ export abstract class AbstractRegion implements Region { const id = this.CLASS.sheetId; if ( !this.CLASS.style || - this.document.adaptor.head().querySelector('#' + id) + this.document.adaptor.document.getElementById(id) ) { return; } diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts index 78b100f31..5008175c4 100644 --- a/ts/ui/menu/Menu.ts +++ b/ts/ui/menu/Menu.ts @@ -587,6 +587,8 @@ export class Menu { jax.options.displayOverflow.substring(1).toLowerCase(); } this.settings.breakInline = jax.options.linebreaks?.inline; + this.settings.brailleCode = + this.document.options.sre?.braille || this.settings.brailleCode; this.defaultSettings = Object.assign( {}, this.document.options.a11y, @@ -939,7 +941,6 @@ export class Menu { const menu = this.menu; menu.settings = this.settings; menu.findID('Settings', 'Overflow', 'Elide').disable(); - menu.findID('Braille', 'ueb').hide(); menu.setJax(this.jax); this.checkLoadableItems(); const cache: [string, string][] = []; @@ -1087,6 +1088,8 @@ export class Menu { protected applySettings() { this.setTabOrder(this.settings.inTabOrder); const options = this.document.options; + options.sre ||= {}; + options.sre.braille = this.settings.brailleCode; options.enableAssistiveMml = this.settings.assistiveMml; this.enableAccessibilityItems('Speech', this.settings.speech); this.enableAccessibilityItems('Braille', this.settings.braille); @@ -1325,7 +1328,7 @@ export class Menu { } /** - * @param {string} code The Braille code format (nemeth or euro) + * @param {string} code The Braille code format (nemeth, ueb, or euro) */ protected setBrailleCode(code: string) { this.document.options.sre.braille = code;