@@ -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><</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