Skip to content

Commit c8756f1

Browse files
authored
Merge pull request #1325 from mathjax/fix/navigation_issues
Fix navigation issues when semantic tree is not aligned with syntactic tree
2 parents 3167719 + 7486f28 commit c8756f1

1 file changed

Lines changed: 59 additions & 12 deletions

File tree

ts/a11y/explorer/KeyExplorer.ts

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ export class SpeechExplorer
582582
this.FocusOut(null);
583583
} else {
584584
this.stopEvent(event);
585-
this.refocus = this.firstNode(this.node);
585+
this.refocus = this.rootNode();
586586
this.Start();
587587
}
588588
}
@@ -649,7 +649,7 @@ export class SpeechExplorer
649649
* Select top-level of expression
650650
*/
651651
protected homeKey() {
652-
this.setCurrent(this.firstNode(this.node));
652+
this.setCurrent(this.rootNode());
653653
}
654654

655655
/**
@@ -673,7 +673,7 @@ export class SpeechExplorer
673673
protected moveUp(shift: boolean): boolean | void {
674674
return shift
675675
? this.moveToNeighborCell(-1, 0)
676-
: this.moveTo(this.current.parentElement.closest(nav));
676+
: this.moveTo(this.getParent(this.current));
677677
}
678678

679679
/**
@@ -760,7 +760,7 @@ export class SpeechExplorer
760760
protected prevMark() {
761761
if (this.currentMark < 0) {
762762
if (this.marks.length === 0) {
763-
this.setCurrent(this.lastMark || this.firstNode(this.node));
763+
this.setCurrent(this.lastMark || this.rootNode());
764764
return;
765765
}
766766
this.currentMark = this.marks.length - 1;
@@ -1293,15 +1293,50 @@ export class SpeechExplorer
12931293
}
12941294

12951295
/**
1296-
* Get an element's first speech child.
1296+
* Get an element's first speech child. This is computed by going through the
1297+
* owns list until the first speech element is found.
12971298
*
12981299
* @param {HTMLElement} node The parent element to get a child from
12991300
* @returns {HTMLElement} The first speech child of the node
13001301
*/
13011302
protected firstNode(node: HTMLElement): HTMLElement {
1303+
const owns = node.getAttribute('data-semantic-owns');
1304+
if (!owns) {
1305+
return node.querySelector(nav) as HTMLElement;
1306+
}
1307+
const ownsList = owns.split(/ /);
1308+
for (const id of ownsList) {
1309+
const node = this.getNode(id);
1310+
if (node?.hasAttribute('data-speech-node')) {
1311+
return node;
1312+
}
1313+
}
13021314
return node.querySelector(nav) as HTMLElement;
13031315
}
13041316

1317+
/**
1318+
* Get the element's semantic root node. We compute this from the root id
1319+
* given in the semantic structure. The semantic structure is an sexp either
1320+
* of the form `0` or `(0 1 (2 ...) ...)`. We can safely assume that the root
1321+
* node contains the speech for the entire structure.
1322+
*
1323+
* If for some reason the semantic structure is not available, we return the
1324+
* first speech node found in the expression.
1325+
*
1326+
* @returns {HTMLElement} The semantic root or first speech node.
1327+
*/
1328+
protected rootNode(): HTMLElement {
1329+
const base = this.node.querySelector('[data-semantic-structure]');
1330+
if (!base) {
1331+
return this.node.querySelector(nav) as HTMLElement;
1332+
}
1333+
const id = base
1334+
.getAttribute('data-semantic-structure')
1335+
.split(/ /)[0]
1336+
.replace('(', '');
1337+
return this.getNode(id);
1338+
}
1339+
13051340
/**
13061341
* Navigate one step to the right on the same level.
13071342
*
@@ -1311,10 +1346,16 @@ export class SpeechExplorer
13111346
protected nextSibling(node: HTMLElement): HTMLElement {
13121347
const id = this.parentId(node);
13131348
if (!id) return null;
1314-
const owns = this.getNode(id).getAttribute('data-semantic-owns')?.split(/ /);
1349+
const owns = this.getNode(id)
1350+
.getAttribute('data-semantic-owns')
1351+
?.split(/ /);
13151352
if (!owns) return null;
1316-
const i = owns.indexOf(this.nodeId(node));
1317-
return this.getNode(owns[i + 1]);
1353+
let i = owns.indexOf(this.nodeId(node));
1354+
let next;
1355+
do {
1356+
next = this.getNode(owns[++i]);
1357+
} while (next && !next.hasAttribute('data-speech-node'));
1358+
return next;
13181359
}
13191360

13201361
/**
@@ -1326,10 +1367,16 @@ export class SpeechExplorer
13261367
protected prevSibling(node: HTMLElement): HTMLElement {
13271368
const id = this.parentId(node);
13281369
if (!id) return null;
1329-
const owns = this.getNode(id).getAttribute('data-semantic-owns')?.split(/ /);
1370+
const owns = this.getNode(id)
1371+
.getAttribute('data-semantic-owns')
1372+
?.split(/ /);
13301373
if (!owns) return null;
1331-
const i = owns.indexOf(this.nodeId(node));
1332-
return this.getNode(owns[i - 1]);
1374+
let i = owns.indexOf(this.nodeId(node));
1375+
let prev;
1376+
do {
1377+
prev = this.getNode(owns[--i]);
1378+
} while (prev && !prev.hasAttribute('data-speech-node'));
1379+
return prev;
13331380
}
13341381

13351382
/**
@@ -1498,7 +1545,7 @@ export class SpeechExplorer
14981545
// current node (which creates the speech) and start the explorer.
14991546
//
15001547
const node = this.findStartNode();
1501-
this.setCurrent(node || this.firstNode(this.node), !node);
1548+
this.setCurrent(node || this.rootNode(), !node);
15021549
super.Start();
15031550
//
15041551
// Show any needed regions

0 commit comments

Comments
 (0)