Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions testsuite/tests/a11y/explorer/KeyExplorer.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();
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);
});
});
10 changes: 10 additions & 0 deletions testsuite/tests/a11y/explorer/Region.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
1 change: 0 additions & 1 deletion ts/a11y/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,6 @@ export function ExplorerMathDocumentMixin<
},
role: mathItem.ariaRole,
'aria-label': mathItem.none,
'aria-roledescription': mathItem.none,
});
}

Expand Down
55 changes: 45 additions & 10 deletions ts/a11y/explorer/KeyExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ export class SpeechExplorer
<p>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}</p>

<p>The contextual menu also provides options for viewing or copying a
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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] || '');
Expand All @@ -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',
Expand All @@ -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');
Expand Down Expand Up @@ -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);
}
Expand Down
7 changes: 4 additions & 3 deletions ts/a11y/explorer/Region.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ export abstract class AbstractRegion<T> implements Region<T> {
* @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;
}

/**
Expand All @@ -130,7 +131,7 @@ export abstract class AbstractRegion<T> implements Region<T> {
const id = this.CLASS.sheetId;
if (
!this.CLASS.style ||
this.document.adaptor.head().querySelector('#' + id)
this.document.adaptor.document.getElementById(id)
) {
return;
}
Expand Down
7 changes: 5 additions & 2 deletions ts/ui/menu/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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][] = [];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down