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
16 changes: 16 additions & 0 deletions ts/a11y/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export interface ExplorerMathItem extends HTMLMATHITEM {
*/
none: string;

/**
* The string to use for when there is no Braille description;
*/
brailleNone: string;

/**
* The Explorer objects for this math item
*/
Expand Down Expand Up @@ -138,6 +143,11 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
*/
protected static none: string = '\u0091';

/**
* Braille decription to use when set to none
*/
protected static brailleNone: string = '\u2800';

public get ariaRole() {
return (this.constructor as typeof BaseClass).ariaRole;
}
Expand All @@ -153,6 +163,10 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
return (this.constructor as typeof BaseClass).none;
}

public get brailleNone() {
return (this.constructor as typeof BaseClass).brailleNone;
}

/**
* @override
*/
Expand Down Expand Up @@ -351,6 +365,8 @@ export function ExplorerMathDocumentMixin<
treeColoring: false, // tree color expression
viewBraille: false, // display Braille output as subtitles
voicing: false, // switch on speech output
brailleSpeech: false, // use aria-label for Braille
brailleCombine: false, // combine Braille with speech output
help: true, // include "press h for help" messages on focus
roleDescription: 'math', // the role description to use for math expressions
tabSelects: 'all', // 'all' for whole expression, 'last' for last explored node
Expand Down
90 changes: 71 additions & 19 deletions ts/a11y/explorer/KeyExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import { InfoDialog } from '../../ui/dialog/InfoDialog.js';

/**********************************************************************/

const isWindows = context.os === 'Windows';

const BRAILLE_PADDING = Array(40).fill('\u2800').join('');

/**
* Interface for keyboard explorers. Adds the necessary keyboard events.
*
Expand Down Expand Up @@ -348,7 +352,18 @@ export class SpeechExplorer
* @returns {string} The string to use for no description
*/
protected get none(): string {
return this.item.none;
return this.document.options.a11y.brailleSpeech
? this.item.brailleNone
: this.item.none;
}

/**
* Shorthand for the item's "brailleNone" indicator
*
* @returns {string} The string to use for no description
*/
protected get brailleNone(): string {
return this.item.brailleNone;
}

/**
Expand Down Expand Up @@ -665,6 +680,7 @@ export class SpeechExplorer
protected escapeKey(): boolean {
this.Stop();
this.focusTop();
this.setCurrent(null);
return true;
}

Expand Down Expand Up @@ -1219,7 +1235,7 @@ export class SpeechExplorer
*/
protected removeSpeech() {
if (this.speech) {
this.speech.remove();
this.unspeak(this.speech);
this.speech = null;
if (this.img) {
this.node.append(this.img);
Expand All @@ -1246,28 +1262,56 @@ export class SpeechExplorer
description: string = this.none
) {
const oldspeech = this.speech;
this.speech = document.createElement('mjx-speech');
this.speech.setAttribute('role', this.role);
this.speech.setAttribute('aria-label', speech);
this.speech.setAttribute(SemAttr.SPEECH, speech);
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);
speechNode.setAttribute(SemAttr.SPEECH, speech);
if (ssml) {
this.speech.setAttribute(SemAttr.PREFIX_SSML, ssml[0] || '');
this.speech.setAttribute(SemAttr.SPEECH_SSML, ssml[1] || '');
this.speech.setAttribute(SemAttr.POSTFIX_SSML, ssml[2] || '');
speechNode.setAttribute(SemAttr.PREFIX_SSML, ssml[0] || '');
speechNode.setAttribute(SemAttr.SPEECH_SSML, ssml[1] || '');
speechNode.setAttribute(SemAttr.POSTFIX_SSML, ssml[2] || '');
}
if (braille) {
this.speech.setAttribute('aria-braillelabel', braille);
if (this.document.options.a11y.brailleSpeech) {
speechNode.setAttribute('aria-label', braille);
speechNode.setAttribute('aria-roledescription', this.brailleNone);
}
speechNode.setAttribute('aria-braillelabel', braille);
speechNode.setAttribute('aria-brailleroledescription', this.brailleNone);
if (this.document.options.a11y.brailleCombine) {
speechNode.setAttribute(
'aria-label',
braille + BRAILLE_PADDING + speech
);
}
}
speechNode.setAttribute('tabindex', '0');
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);
container.append(speechNode);
this.node.append(container);
speechNode.setAttribute('role', 'img');
} else {
this.node.append(speechNode);
}
this.speech.setAttribute('aria-roledescription', description);
this.speech.setAttribute('tabindex', '0');
this.node.append(this.speech);
this.focusSpeech = true;
this.speech.focus();
speechNode.focus();
this.focusSpeech = false;
this.Update();
if (oldspeech) {
setTimeout(() => oldspeech.remove(), 100);
setTimeout(() => this.unspeak(oldspeech), 100);
}
}

public unspeak(node: HTMLElement) {
if (isWindows) {
node = node.parentElement;
}
node.remove();
}

/**
Expand All @@ -1292,6 +1336,18 @@ export class SpeechExplorer
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.img.setAttribute('aria-braillelabel', braille);
this.img.setAttribute('aria-brailleroledescription', this.brailleNone);
if (this.document.options.a11y.brailleCombine) {
this.img.setAttribute('aria-label', braille + BRAILLE_PADDING + speech);
}
}
container.appendChild(this.img);
this.adjustAnchors();
}
Expand Down Expand Up @@ -1779,10 +1835,6 @@ export class SpeechExplorer
*/
public Stop() {
if (this.active) {
const description = this.description;
if (this.node.getAttribute('aria-roledescription') !== description) {
this.node.setAttribute('aria-roledescription', description);
}
this.node.classList.remove('mjx-explorer-active');
if (this.document.options.enableExplorerHelp) {
this.document.infoIcon.remove();
Expand Down
59 changes: 55 additions & 4 deletions ts/ui/menu/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface MenuSettings {
backgroundOpacity: string;
braille: boolean;
brailleCode: string;
brailleSpeech: boolean;
brailleCombine: boolean;
foregroundColor: string;
foregroundOpacity: string;
highlight: string;
Expand Down Expand Up @@ -156,6 +158,8 @@ export class Menu {
speech: true,
braille: true,
brailleCode: 'nemeth',
brailleSpeech: false,
brailleCombine: false,
speechRules: 'clearspeak-default',
roleDescription: 'math',
tabSelects: 'all',
Expand Down Expand Up @@ -619,6 +623,12 @@ export class Menu {
this.variable<string>('brailleCode', (code) =>
this.setBrailleCode(code)
),
this.a11yVar<boolean>('brailleSpeech', (speech) =>
this.setBrailleSpeech(speech)
),
this.a11yVar<boolean>('brailleCombine', (speech) =>
this.setBrailleCombine(speech)
),
this.a11yVar<string>('highlight', (value) => this.setHighlight(value)),
this.a11yVar<string>('backgroundColor'),
this.a11yVar<string>('backgroundOpacity', (value) =>
Expand Down Expand Up @@ -804,6 +814,12 @@ export class Menu {
this.submenu('Braille', '\xA0 \xA0 Braille', [
this.checkbox('Generate', 'Generate', 'braille'),
this.checkbox('Subtitles', 'Show Subtitles', 'viewBraille'),
this.checkbox('BrailleSpeech', 'Replace Speech', 'brailleSpeech'),
this.checkbox(
'BrailleCombine',
'Combine with Speech',
'brailleCombine'
),
this.rule(),
this.label('Code', 'Code Format:'),
this.radioGroup('brailleCode', [
Expand Down Expand Up @@ -1246,10 +1262,17 @@ export class Menu {
protected setSpeech(speech: boolean) {
this.enableAccessibilityItems('Speech', speech);
this.document.options.enableSpeech = speech;
if (speech && this.settings.assistiveMml) {
this.noRerender(() =>
this.menu.pool.lookup('assistiveMml').setValue(false)
);
if (speech) {
if (this.settings.assistiveMml) {
this.noRerender(() =>
this.menu.pool.lookup('assistiveMml').setValue(false)
);
}
if (this.settings.brailleSpeech) {
this.noRerender(() =>
this.menu.pool.lookup('brailleSpeech').setValue(false)
);
}
}
if (!speech || MathJax._?.a11y?.explorer) {
this.rerender(STATE.COMPILED);
Expand Down Expand Up @@ -1284,6 +1307,34 @@ export class Menu {
this.rerender(STATE.COMPILED);
}

/**
* @param {boolean} speech Whether to use aria-label for Braille
*/
protected setBrailleSpeech(speech: boolean) {
if (speech && this.settings.speech) {
Menu.loading++; // pretend we're loading, to suppress rerendering for each variable change
this.menu.pool.lookup('speech').setValue(false);
Menu.loading--;
} else {
this.enableAccessibilityItems('Speech', true);
}
this.settings.brailleCombine = this.document.options.a11y.brailleCombine =
false;
this.rerender(STATE.COMPILED);
}

/**
* @param {boolean} _speech Whether to combine Braille into aria-label
*/
protected setBrailleCombine(_speech: boolean) {
if (this.settings.brailleSpeech) {
this.menu.pool.lookup('brailleSpeech').setValue(false);
}
this.settings.brailleSpeech = this.document.options.a11y.brailleSpeech =
false;
this.rerender(STATE.COMPILED);
}

/**
* @param {string} locale The speech locale
*/
Expand Down