Skip to content

Commit af44f98

Browse files
authored
Merge pull request #1396 from mathjax/fix/braille
Fix problems with Braille output devices
2 parents dc349f9 + 6663405 commit af44f98

3 files changed

Lines changed: 181 additions & 32 deletions

File tree

ts/a11y/explorer.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export interface ExplorerMathItem extends HTMLMATHITEM {
8080
*/
8181
none: string;
8282

83+
/**
84+
* The string to use for when there is no Braille description;
85+
*/
86+
brailleNone: string;
87+
8388
/**
8489
* The Explorer objects for this math item
8590
*/
@@ -138,6 +143,11 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
138143
*/
139144
protected static none: string = '\u0091';
140145

146+
/**
147+
* Braille decription to use when set to none
148+
*/
149+
protected static brailleNone: string = '\u2800';
150+
141151
public get ariaRole() {
142152
return (this.constructor as typeof BaseClass).ariaRole;
143153
}
@@ -153,6 +163,10 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
153163
return (this.constructor as typeof BaseClass).none;
154164
}
155165

166+
public get brailleNone() {
167+
return (this.constructor as typeof BaseClass).brailleNone;
168+
}
169+
156170
/**
157171
* @override
158172
*/
@@ -351,6 +365,8 @@ export function ExplorerMathDocumentMixin<
351365
treeColoring: false, // tree color expression
352366
viewBraille: false, // display Braille output as subtitles
353367
voicing: false, // switch on speech output
368+
brailleSpeech: false, // use aria-label for Braille
369+
brailleCombine: false, // combine Braille with speech output
354370
help: true, // include "press h for help" messages on focus
355371
roleDescription: 'math', // the role description to use for math expressions
356372
tabSelects: 'all', // 'all' for whole expression, 'last' for last explored node

ts/a11y/explorer/KeyExplorer.ts

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import { InfoDialog } from '../../ui/dialog/InfoDialog.js';
3434

3535
/**********************************************************************/
3636

37+
const isWindows = context.os === 'Windows';
38+
39+
const BRAILLE_PADDING = Array(40).fill('\u2800').join('');
40+
3741
/**
3842
* Interface for keyboard explorers. Adds the necessary keyboard events.
3943
*
@@ -125,11 +129,16 @@ export class SpeechExplorer
125129
/**
126130
* Creates a customized help dialog
127131
*
128-
* @param {string} title The title to use for the message
129-
* @param {string} select Additional ways to select the typeset math
130-
* @returns {string} The customized message
131-
*/
132-
protected static helpMessage(title: string, select: string): string {
132+
* @param {string} title The title to use for the message
133+
* @param {string} select Additional ways to select the typeset math
134+
* @param {string} braille Additional Braille information
135+
* @returns {string} The customized message
136+
*/
137+
protected static helpMessage(
138+
title: string,
139+
select: string,
140+
braille: string
141+
): string {
133142
return `
134143
<h2 role="heading" aria-level="2">Exploring expressions ${title}</h2>
135144
@@ -205,6 +214,10 @@ export class SpeechExplorer
205214
<li><kbd>&lt;</kbd> cycles through the verbosity levels
206215
for the current rule set.</li>
207216
217+
<li><kbd>b</kbd> toggles whether Braille notation is combined
218+
with speech text for tactile Braille devices, as discussed
219+
below.
220+
208221
<li><kbd>h</kbd> produces this help listing.</li>
209222
</ul>
210223
@@ -218,6 +231,13 @@ export class SpeechExplorer
218231
speech and Braille will disable the expression explorer, its
219232
highlighting, and its help icon.</p>
220233
234+
<p>Support for tactile Braille devices varies across screen readers,
235+
browsers, and operative systems. If you are using a Braille output
236+
device, you may need to select the "Combine with Speech" option in the
237+
contextual menu's Braille submenu in order to obtain Nemeth or Euro
238+
Braille output rather than the speech text on your Braille
239+
device. ${braille}</p>
240+
221241
<p>The contextual menu also provides options for viewing or copying a
222242
MathML version of the expression or its original source format,
223243
creating an SVG version of the expression, and viewing various other
@@ -239,12 +259,13 @@ export class SpeechExplorer
239259
/**
240260
* Help for the different OS versions
241261
*/
242-
protected static helpData: Map<string, [string, string]> = new Map([
262+
protected static helpData: Map<string, [string, string, string]> = new Map([
243263
[
244264
'MacOS',
245265
[
246266
'on MacOS and iOS using VoiceOver',
247267
', or the VoiceOver arrow keys to select an expression',
268+
'',
248269
],
249270
],
250271
[
@@ -258,6 +279,8 @@ export class SpeechExplorer
258279
the NVDA or JAWS key plus the arrow keys to explore the expression
259280
even in browse mode, and you can use NVDA+shift+arrow keys to
260281
navigate out of an expression that has the focus in NVDA`,
282+
`NVDA users need to select this option, while JAWS users should be able
283+
to get Braille output without changing this setting.`,
261284
],
262285
],
263286
[
@@ -267,9 +290,10 @@ export class SpeechExplorer
267290
`, and Orca should enter focus mode automatically. If not, use the
268291
Orca+a key to toggle focus mode on or off. Also note that you can use
269292
Orca+arrow keys to explore expressions even in browse mode`,
293+
'',
270294
],
271295
],
272-
['unknown', ['with a Screen Reader.', '']],
296+
['unknown', ['with a Screen Reader.', '', '']],
273297
]);
274298

275299
/*
@@ -304,6 +328,7 @@ export class SpeechExplorer
304328
['p', [(explorer) => explorer.prevMark(), false]],
305329
['u', [(explorer) => explorer.clearMarks(), false]],
306330
['s', [(explorer) => explorer.autoVoice(), false]],
331+
['b', [(explorer) => explorer.toggleBraille(), false]],
307332
...[...'0123456789'].map((n) => [
308333
n,
309334
[(explorer: SpeechExplorer) => explorer.numberKey(parseInt(n)), false],
@@ -348,7 +373,18 @@ export class SpeechExplorer
348373
* @returns {string} The string to use for no description
349374
*/
350375
protected get none(): string {
351-
return this.item.none;
376+
return this.document.options.a11y.brailleSpeech
377+
? this.item.brailleNone
378+
: this.item.none;
379+
}
380+
381+
/**
382+
* Shorthand for the item's "brailleNone" indicator
383+
*
384+
* @returns {string} The string to use for no description
385+
*/
386+
protected get brailleNone(): string {
387+
return this.item.brailleNone;
352388
}
353389

354390
/**
@@ -665,6 +701,7 @@ export class SpeechExplorer
665701
protected escapeKey(): boolean {
666702
this.Stop();
667703
this.focusTop();
704+
this.setCurrent(null);
668705
return true;
669706
}
670707

@@ -873,6 +910,15 @@ export class SpeechExplorer
873910
this.Update();
874911
}
875912

913+
protected toggleBraille() {
914+
const value = !this.document.options.a11y.brailleCombine;
915+
if (this.document.menu) {
916+
this.document.menu.menu.pool.lookup('brailleCombine').setValue(value);
917+
} else {
918+
this.document.options.a11y.brailleCombine = value;
919+
}
920+
}
921+
876922
/**
877923
* Get index for cell to jump to.
878924
*
@@ -1018,10 +1064,10 @@ export class SpeechExplorer
10181064
return;
10191065
}
10201066
const CLASS = this.constructor as typeof SpeechExplorer;
1021-
const [title, select] = CLASS.helpData.get(context.os);
1067+
const [title, select, braille] = CLASS.helpData.get(context.os);
10221068
InfoDialog.post({
10231069
title: 'MathJax Expression Explorer Help',
1024-
message: CLASS.helpMessage(title, select),
1070+
message: CLASS.helpMessage(title, select, braille),
10251071
node: this.node,
10261072
adaptor: this.document.adaptor,
10271073
styles: {
@@ -1226,7 +1272,7 @@ export class SpeechExplorer
12261272
*/
12271273
protected removeSpeech() {
12281274
if (this.speech) {
1229-
this.speech.remove();
1275+
this.unspeak(this.speech);
12301276
this.speech = null;
12311277
if (this.img) {
12321278
this.node.append(this.img);
@@ -1253,28 +1299,56 @@ export class SpeechExplorer
12531299
description: string = this.none
12541300
) {
12551301
const oldspeech = this.speech;
1256-
this.speech = document.createElement('mjx-speech');
1257-
this.speech.setAttribute('role', this.role);
1258-
this.speech.setAttribute('aria-label', speech);
1259-
this.speech.setAttribute(SemAttr.SPEECH, speech);
1302+
const speechNode = (this.speech = document.createElement('mjx-speech'));
1303+
speechNode.setAttribute('role', this.role);
1304+
speechNode.setAttribute('aria-label', speech || this.none);
1305+
speechNode.setAttribute('aria-roledescription', description || this.none);
1306+
speechNode.setAttribute(SemAttr.SPEECH, speech);
12601307
if (ssml) {
1261-
this.speech.setAttribute(SemAttr.PREFIX_SSML, ssml[0] || '');
1262-
this.speech.setAttribute(SemAttr.SPEECH_SSML, ssml[1] || '');
1263-
this.speech.setAttribute(SemAttr.POSTFIX_SSML, ssml[2] || '');
1308+
speechNode.setAttribute(SemAttr.PREFIX_SSML, ssml[0] || '');
1309+
speechNode.setAttribute(SemAttr.SPEECH_SSML, ssml[1] || '');
1310+
speechNode.setAttribute(SemAttr.POSTFIX_SSML, ssml[2] || '');
12641311
}
12651312
if (braille) {
1266-
this.speech.setAttribute('aria-braillelabel', braille);
1313+
if (this.document.options.a11y.brailleSpeech) {
1314+
speechNode.setAttribute('aria-label', braille);
1315+
speechNode.setAttribute('aria-roledescription', this.brailleNone);
1316+
}
1317+
speechNode.setAttribute('aria-braillelabel', braille);
1318+
speechNode.setAttribute('aria-brailleroledescription', this.brailleNone);
1319+
if (this.document.options.a11y.brailleCombine) {
1320+
speechNode.setAttribute(
1321+
'aria-label',
1322+
braille + BRAILLE_PADDING + speech
1323+
);
1324+
}
1325+
}
1326+
speechNode.setAttribute('tabindex', '0');
1327+
if (isWindows) {
1328+
const container = document.createElement('mjx-speech-container');
1329+
container.setAttribute('role', 'application');
1330+
container.setAttribute('aria-roledescription', this.none);
1331+
container.setAttribute('aria-brailleroledescription', this.brailleNone);
1332+
container.append(speechNode);
1333+
this.node.append(container);
1334+
speechNode.setAttribute('role', 'img');
1335+
} else {
1336+
this.node.append(speechNode);
12671337
}
1268-
this.speech.setAttribute('aria-roledescription', description);
1269-
this.speech.setAttribute('tabindex', '0');
1270-
this.node.append(this.speech);
12711338
this.focusSpeech = true;
1272-
this.speech.focus();
1339+
speechNode.focus();
12731340
this.focusSpeech = false;
12741341
this.Update();
12751342
if (oldspeech) {
1276-
setTimeout(() => oldspeech.remove(), 100);
1343+
setTimeout(() => this.unspeak(oldspeech), 100);
1344+
}
1345+
}
1346+
1347+
public unspeak(node: HTMLElement) {
1348+
if (isWindows) {
1349+
node = node.parentElement;
12771350
}
1351+
node.remove();
12781352
}
12791353

12801354
/**
@@ -1299,6 +1373,18 @@ export class SpeechExplorer
12991373
role: 'img',
13001374
'aria-roledescription': item.none,
13011375
});
1376+
const braille = container.getAttribute(SemAttr.BRAILLE);
1377+
if (braille) {
1378+
if (this.document.options.a11y.brailleSpeech) {
1379+
this.img.setAttribute('aria-label', braille);
1380+
this.img.setAttribute('aria-roledescription', this.brailleNone);
1381+
}
1382+
this.img.setAttribute('aria-braillelabel', braille);
1383+
this.img.setAttribute('aria-brailleroledescription', this.brailleNone);
1384+
if (this.document.options.a11y.brailleCombine) {
1385+
this.img.setAttribute('aria-label', braille + BRAILLE_PADDING + speech);
1386+
}
1387+
}
13021388
container.appendChild(this.img);
13031389
this.adjustAnchors();
13041390
}
@@ -1786,10 +1872,6 @@ export class SpeechExplorer
17861872
*/
17871873
public Stop() {
17881874
if (this.active) {
1789-
const description = this.description;
1790-
if (this.node.getAttribute('aria-roledescription') !== description) {
1791-
this.node.setAttribute('aria-roledescription', description);
1792-
}
17931875
this.node.classList.remove('mjx-explorer-active');
17941876
if (this.document.options.enableExplorerHelp) {
17951877
this.document.infoIcon.remove();

0 commit comments

Comments
 (0)