Skip to content

Commit 988e6e3

Browse files
authored
Merge pull request #1264 from mathjax/fix/explorer-refocus
Fix refocusing the selected node after a rerender.
2 parents f8c373d + 7674676 commit 988e6e3

8 files changed

Lines changed: 187 additions & 23 deletions

File tree

ts/a11y/complexity.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { Handler } from '../core/Handler.js';
2626
import { MathDocumentConstructor } from '../core/MathDocument.js';
2727
import { STATE, newState } from '../core/MathItem.js';
2828
import { MathML } from '../input/mathml.js';
29-
import { MmlNode } from '../core/MmlTree/MmlNode.js';
3029
import {
3130
EnrichHandler,
3231
EnrichedMathItem,
@@ -69,6 +68,11 @@ newState('COMPLEXITY', 40);
6968
* @template D The Document class
7069
*/
7170
export interface ComplexityMathItem<N, T, D> extends EnrichedMathItem<N, T, D> {
71+
/**
72+
* The starting collapse ID for this expression
73+
*/
74+
initialID: number;
75+
7276
/**
7377
* @param {ComplexityMathDocument} document The MathDocument for the MathItem
7478
* @param {boolean} force True to force the computation even if enableComplexity is false
@@ -80,7 +84,7 @@ export interface ComplexityMathItem<N, T, D> extends EnrichedMathItem<N, T, D> {
8084
* The mixin for adding complexity to MathItems
8185
*
8286
* @param {B} BaseMathItem The MathItem class to be extended
83-
* @param {function(MmlNode): void} computeComplexity Method of complexity computation.
87+
* @param {(math: ComplexityMathItem<N,T,D>) => void} computeComplexity Method of complexity computation.
8488
* @returns {ComplexityMathItem} The complexity MathItem class
8589
*
8690
* @template N The HTMLElement node class
@@ -90,9 +94,14 @@ export interface ComplexityMathItem<N, T, D> extends EnrichedMathItem<N, T, D> {
9094
*/
9195
export function ComplexityMathItemMixin<N, T, D, B extends EMItemC<N, T, D>>(
9296
BaseMathItem: B,
93-
computeComplexity: (node: MmlNode) => void
97+
computeComplexity: (math: ComplexityMathItem<N, T, D>) => void
9498
): CMItemC<N, T, D> & B {
9599
return class extends BaseMathItem {
100+
/**
101+
* The starting collapse ID for this expression
102+
*/
103+
public initialID: number = null;
104+
96105
/**
97106
* @param {ComplexityMathDocument} document The MathDocument for the MathItem
98107
* @param {boolean} force True to force the computation even if enableComplexity is false
@@ -104,7 +113,7 @@ export function ComplexityMathItemMixin<N, T, D, B extends EMItemC<N, T, D>>(
104113
if (this.state() >= STATE.COMPLEXITY) return;
105114
if (!this.isEscaped && (document.options.enableComplexity || force)) {
106115
this.enrich(document, true);
107-
computeComplexity(this.root);
116+
computeComplexity(this);
108117
}
109118
this.state(STATE.COMPLEXITY);
110119
}
@@ -184,8 +193,12 @@ export function ComplexityMathDocumentMixin<N, T, D, B extends EMDocC<N, T, D>>(
184193
this.mmlFactory,
185194
visitorOptions
186195
);
187-
const computeComplexity = (node: MmlNode) =>
188-
this.complexityVisitor.visitTree(node);
196+
const computeComplexity = (math: ComplexityMathItem<N, T, D>) => {
197+
math.initialID = this.complexityVisitor.visitTree(
198+
math.root,
199+
math.initialID
200+
);
201+
};
189202
this.options.MathItem = ComplexityMathItemMixin<
190203
N,
191204
T,

ts/a11y/complexity/collapse.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,16 +531,29 @@ export class Collapse {
531531
/**
532532
* Add maction nodes to the nodes in the tree that can collapse
533533
*
534-
* @param {MmlNode} node The root of the tree to check
534+
* @param {MmlNode} node The root of the tree to check
535+
* @param {number|null} id The initial id to use
536+
* @returns {number} The initial id used
535537
*/
536-
public makeCollapse(node: MmlNode) {
538+
public makeCollapse(node: MmlNode, id: number | null): number {
539+
let oldCount = null;
540+
if (id === null) {
541+
id = this.idCount;
542+
} else {
543+
oldCount = this.idCount;
544+
this.idCount = id;
545+
}
537546
const nodes: MmlNode[] = [];
538547
node.walkTree((child: MmlNode) => {
539548
if (child.getProperty('collapse-marker')) {
540549
nodes.push(child);
541550
}
542551
});
543552
this.makeActions(nodes);
553+
if (oldCount !== null) {
554+
this.idCount = oldCount;
555+
}
556+
return id;
544557
}
545558

546559
/**

ts/a11y/complexity/visitor.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,12 @@ export class ComplexityVisitor extends MmlVisitor {
107107
/**
108108
* @override
109109
*/
110-
public visitTree(node: MmlNode) {
110+
public visitTree(node: MmlNode, id: number) {
111111
super.visitTree(node, true);
112112
if (this.options.makeCollapsible) {
113-
this.collapse.makeCollapse(node);
113+
id = this.collapse.makeCollapse(node, id);
114114
}
115+
return id;
115116
}
116117

117118
/**

ts/a11y/explorer.ts

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,27 @@ export interface ExplorerMathItem extends HTMLMATHITEM {
8585
*/
8686
explorers: ExplorerPool;
8787

88+
/**
89+
* Semantic id of the rerendered element that should regain the focus.
90+
*/
91+
refocus: string;
92+
8893
/**
8994
* @param {HTMLDocument} document The document where the Explorer is being added
9095
* @param {boolean} force True to force the explorer even if enableExplorer is false
9196
*/
9297
explorable(document: HTMLDOCUMENT, force?: boolean): void;
98+
99+
/**
100+
* @param {ExplorerMathDocument} document The explorer document being used
101+
* @returns {HTMLElement} The temporary focus element, if any
102+
*/
103+
setTemporaryFocus(document: ExplorerMathDocument): HTMLElement;
104+
105+
/**
106+
* @param {HTMLElement} focus The temporary focus element, if any
107+
*/
108+
clearTemporaryFocus(focus: HTMLElement): void;
93109
}
94110

95111
/**
@@ -143,9 +159,9 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
143159
public explorers: ExplorerPool;
144160

145161
/**
146-
* Semantic id of the rerendered element that should regain the focus.
162+
* @override
147163
*/
148-
protected refocus: string = null;
164+
public refocus: string = null;
149165

150166
/**
151167
* @override
@@ -192,21 +208,64 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
192208
this.state(STATE.EXPLORER);
193209
}
194210

211+
/**
212+
* @override
213+
*/
214+
public state(state: number = null, restore: boolean = false) {
215+
if (state < STATE.EXPLORER && this.explorers) {
216+
for (const explorer of Object.values(this.explorers.explorers)) {
217+
if (explorer.active) {
218+
explorer.Stop();
219+
}
220+
}
221+
}
222+
return super.state(state, restore);
223+
}
224+
195225
/**
196226
* @override
197227
*/
198228
public rerender(
199229
document: ExplorerMathDocument,
200230
start: number = STATE.RERENDER
201231
) {
232+
const focus = this.setTemporaryFocus(document);
233+
super.rerender(document, start);
234+
this.clearTemporaryFocus(focus);
235+
}
236+
237+
/**
238+
* Focuses a temporary element during rerendering
239+
*
240+
* @param {ExplorerMathDocument} document The explorer document to use
241+
* @returns {HTMLElement} The temporary focus element, if any
242+
*/
243+
public setTemporaryFocus(document: ExplorerMathDocument): HTMLElement {
244+
let focus = null;
202245
if (this.explorers) {
203246
const speech = this.explorers.speech;
204-
if (speech && speech.attached) {
247+
focus = speech?.attached ? document.tmpFocus : null;
248+
if (focus) {
205249
this.refocus = speech.semanticFocus() ?? null;
250+
const adaptor = document.adaptor;
251+
adaptor.append(adaptor.body(), focus);
206252
}
207253
this.explorers.reattach();
254+
focus?.focus();
255+
}
256+
return focus;
257+
}
258+
259+
/**
260+
* Removes the temporary element after rerendering
261+
*
262+
* @param {HTMLElement} focus The temporary focus element, if any
263+
*/
264+
public clearTemporaryFocus(focus: HTMLElement) {
265+
if (focus) {
266+
const promise = this.outputData.speechPromise ?? Promise.resolve();
267+
promise.then(() => setTimeout(() => focus.remove(), 100));
208268
}
209-
super.rerender(document, start);
210269
}
211270
};
212271
}
@@ -220,11 +279,21 @@ export interface ExplorerMathDocument extends HTMLDOCUMENT {
220279
*/
221280
infoIcon: HTMLElement;
222281

282+
/**
283+
* An element ot use for temporary focus during rerendering
284+
*/
285+
tmpFocus: HTMLElement;
286+
223287
/**
224288
* The objects needed for the explorer
225289
*/
226290
explorerRegions: RegionPool;
227291

292+
/**
293+
* The MathItem with the active KeyExplorer, if any
294+
*/
295+
activeItem: ExplorerMathItem;
296+
228297
/**
229298
* Add the Explorer to the MathItems in the MathDocument
230299
*
@@ -401,11 +470,21 @@ export function ExplorerMathDocumentMixin<
401470
*/
402471
public infoIcon: HTMLElement;
403472

473+
/**
474+
* An element ot use for temporary focus during rerendering
475+
*/
476+
public tmpFocus: HTMLElement;
477+
404478
/**
405479
* The objects needed for the explorer
406480
*/
407481
public explorerRegions: RegionPool = null;
408482

483+
/**
484+
* The MathItem with the active KeyExplorer, if any
485+
*/
486+
public activeItem: ExplorerMathItem = null;
487+
409488
/**
410489
* Extend the MathItem class used for this MathDocument
411490
* and create the visitor and explorer objects needed for the explorer
@@ -425,8 +504,11 @@ export function ExplorerMathDocumentMixin<
425504
if (!options.a11y.speechRules) {
426505
options.a11y.speechRules = `${options.sre.domain}-${options.sre.style}`;
427506
}
428-
options.MathItem = ExplorerMathItemMixin(options.MathItem, toMathML);
429-
options.MathItem.roleDescription = options.roleDescription;
507+
const mathItem = (options.MathItem = ExplorerMathItemMixin(
508+
options.MathItem,
509+
toMathML
510+
));
511+
mathItem.roleDescription = options.roleDescription;
430512
this.explorerRegions = new RegionPool(this);
431513
if ('addStyles' in this) {
432514
(this as any).addStyles(
@@ -448,6 +530,22 @@ export function ExplorerMathDocumentMixin<
448530
SVGNS
449531
),
450532
]);
533+
this.tmpFocus = this.adaptor.node('mjx-focus', {
534+
tabIndex: 0,
535+
style: {
536+
outline: 'none',
537+
display: 'block',
538+
position: 'absolute',
539+
top: 0,
540+
left: '-10px',
541+
width: '1px',
542+
height: '1px',
543+
overflow: 'hidden',
544+
},
545+
role: mathItem.ariaRole,
546+
'aria-label': mathItem.none,
547+
'aria-roledescription': mathItem.none,
548+
});
451549
}
452550

453551
/**
@@ -467,6 +565,17 @@ export function ExplorerMathDocumentMixin<
467565
return this;
468566
}
469567

568+
/**
569+
* @override
570+
*/
571+
public rerender(start?: number) {
572+
const active = this.activeItem;
573+
const focus = active?.setTemporaryFocus(this);
574+
super.rerender(start);
575+
active?.clearTemporaryFocus(focus);
576+
return this;
577+
}
578+
470579
/**
471580
* @override
472581
*/

ts/a11y/explorer/KeyExplorer.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,9 @@ export class SpeechExplorer
850850
if (this.speech) {
851851
this.speech.remove();
852852
this.speech = null;
853-
this.node.append(this.img);
853+
if (this.img) {
854+
this.node.append(this.img);
855+
}
854856
this.node.setAttribute('tabindex', '0');
855857
}
856858
}
@@ -937,6 +939,13 @@ export class SpeechExplorer
937939
}
938940
}
939941

942+
/**
943+
* Set focus on the current node
944+
*/
945+
public focus() {
946+
this.node.focus();
947+
}
948+
940949
/********************************************************************/
941950
/*
942951
* Utility functions
@@ -1132,6 +1141,7 @@ export class SpeechExplorer
11321141
// If we aren't attached or already active, return
11331142
//
11341143
if (!this.attached || this.active) return;
1144+
this.document.activeItem = this.item;
11351145
//
11361146
// If there is no speech, request the speech and wait for it
11371147
//
@@ -1322,14 +1332,17 @@ export class SpeechExplorer
13221332
public semanticFocus(): string {
13231333
const focus = [];
13241334
let name = 'data-semantic-id';
1325-
let node = this.current || this.node;
1335+
let node = this.current || this.refocus || this.node;
13261336
const action = this.actionable(node);
13271337
if (action) {
13281338
name = action.hasAttribute('data-maction-id') ? 'data-maction-id' : 'id';
13291339
node = action;
13301340
focus.push(nav);
13311341
}
1332-
focus.unshift(`[${name}="${node.getAttribute(name)}"]`);
1342+
const attr = node.getAttribute(name);
1343+
if (attr) {
1344+
focus.unshift(`[${name}="${attr}"]`);
1345+
}
13331346
return focus.join(' ');
13341347
}
13351348
}

ts/a11y/explorer/Region.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,10 @@ export class HoverRegion extends AbstractRegion<HTMLElement> {
640640
if (!this.div) return;
641641
this.Clear();
642642
const mjx = this.cloneNode(node);
643+
const selected = mjx.querySelector('[data-mjx-clone]') as HTMLElement;
644+
this.inner.style.backgroundColor = node.style.backgroundColor;
645+
selected.style.backgroundColor = '';
646+
selected.classList.remove('mjx-selected');
643647
this.inner.appendChild(mjx);
644648
this.position(node);
645649
}
@@ -652,6 +656,7 @@ export class HoverRegion extends AbstractRegion<HTMLElement> {
652656
*/
653657
private cloneNode(node: HTMLElement): HTMLElement {
654658
let mjx = node.cloneNode(true) as HTMLElement;
659+
mjx.setAttribute('data-mjx-clone', 'true');
655660
if (mjx.nodeName !== 'MJX-CONTAINER') {
656661
// remove element spacing (could be done in CSS)
657662
if (mjx.nodeName !== 'g') {

0 commit comments

Comments
 (0)