Skip to content

Commit 19ba412

Browse files
fix: Improve VoiceOver experience when using Safari (#10073)
* fix: avoid appending element to focus on re-order * fix: remove VoiceOver flicker when moving between stacks * fix: compute aria context for rendered conns at draw time Otherwise this only happens on focus and the first focus in Safari with VoiceOver fails to output anything. * fix: reduce performance load by only setting role and roledesc The full aria context for rendered conns is only calculated on focus * fix: ensure move mode icon is still at the front * fix: rendered conns are always visible Connection previews are maintained by toggling a class. Connectino reordering does not append element to be focused which fixes VoiceOver in Safari behaviour.
1 parent 2247c46 commit 19ba412

7 files changed

Lines changed: 52 additions & 30 deletions

File tree

packages/blockly/core/block_svg.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,23 +1393,29 @@ export class BlockSvg
13931393
* @internal
13941394
*/
13951395
moveSvgRootToFront(blockOnly = false) {
1396-
/* eslint-disable-next-line @typescript-eslint/no-this-alias */
1397-
let block: this | null = this;
1398-
if (block.isDeadOrDying()) {
1396+
if (this.isDeadOrDying()) {
13991397
return;
14001398
}
1401-
do {
1402-
const root = block.getSvgRoot();
1403-
const parent = root.parentNode;
1404-
if (!parent) return;
1405-
const childNodes = parent.childNodes;
1406-
// Avoid moving the block if it's already at the bottom.
1407-
if (childNodes[childNodes.length - 1] !== root) {
1408-
parent.appendChild(root);
1409-
}
1410-
if (blockOnly) break;
1411-
block = block.getParent();
1412-
} while (block);
1399+
requestAnimationFrame(() => {
1400+
if (this.dragging) return;
1401+
/* eslint-disable-next-line @typescript-eslint/no-this-alias */
1402+
let block: this | null = this;
1403+
do {
1404+
if (block.isDeadOrDying()) return;
1405+
const root = block.getSvgRoot();
1406+
const parent = root.parentNode;
1407+
if (!parent) return;
1408+
const childNodes = parent.childNodes;
1409+
// Avoid moving the block if it's already at the bottom.
1410+
if (childNodes[childNodes.length - 1] !== root) {
1411+
while (root.nextSibling) {
1412+
parent.insertBefore(root.nextSibling, root);
1413+
}
1414+
}
1415+
if (blockOnly) break;
1416+
block = block.getParent();
1417+
} while (block);
1418+
});
14131419
}
14141420

14151421
/**

packages/blockly/core/css.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ const content = `
162162
163163
.blocklyHighlightedConnectionPath {
164164
fill: none;
165+
}
166+
167+
.blocklyHighlightedConnectionPathVisible {
165168
stroke: #fc3;
166169
stroke-width: 4px;
167170
}
@@ -604,12 +607,6 @@ input[type=number] {
604607
stroke-width: var(--blockly-selection-width);
605608
}
606609
607-
/* Workaround for unexpectedly hidden connection path due to core style. */
608-
.blocklyKeyboardNavigation
609-
.blocklyPassiveFocus.blocklyHighlightedConnectionPath {
610-
display: unset !important;
611-
}
612-
613610
/* Different ways for toolbox/flyout to be the active tree: */
614611
/* Active focus in the flyout. */
615612
.blocklyKeyboardNavigation .blocklyFlyout:has(.blocklyActiveFocus),

packages/blockly/core/rendered_connection.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import * as internalConstants from './internal_constants.js';
2828
import {Msg} from './msg.js';
2929
import * as aria from './utils/aria.js';
3030
import {Coordinate} from './utils/coordinate.js';
31+
import * as dom from './utils/dom.js';
3132
import * as svgMath from './utils/svg_math.js';
3233
import {WorkspaceSvg} from './workspace_svg.js';
3334

@@ -322,11 +323,11 @@ export class RenderedConnection
322323
}
323324

324325
/**
325-
* Sets the aria role, label, and other state for this connection.
326+
* Sets the aria role and role description for this connection.
326327
*
327328
* @param highlightSvg The focusable element for this connection.
328329
*/
329-
private recomputeAriaContext(highlightSvg: SVGElement) {
330+
setAriaRole(highlightSvg: SVGElement) {
330331
// Note that output connections don't have highlights so this doesn't need to take them into account.
331332
const roleDescription =
332333
this.type === ConnectionType.INPUT_VALUE
@@ -335,6 +336,15 @@ export class RenderedConnection
335336

336337
aria.setRole(highlightSvg, aria.Role.FIGURE);
337338
aria.setState(highlightSvg, aria.State.ROLEDESCRIPTION, roleDescription);
339+
}
340+
341+
/**
342+
* Sets the aria role, label, and other state for this connection.
343+
*
344+
* @param highlightSvg The focusable element for this connection.
345+
*/
346+
private recomputeAriaContext(highlightSvg: SVGElement) {
347+
this.setAriaRole(highlightSvg);
338348

339349
// 'Next' connections are only focusable if they're the last connection
340350
// inside a statement input. The label for these connections comes from
@@ -385,8 +395,14 @@ export class RenderedConnection
385395
// draw pass).
386396
const highlightSvg = this.findHighlightSvg();
387397
if (highlightSvg) {
388-
highlightSvg.style.display = '';
389-
highlightSvg.parentElement?.appendChild(highlightSvg);
398+
dom.addClass(highlightSvg, 'blocklyHighlightedConnectionPathVisible');
399+
requestAnimationFrame(() => {
400+
const parent = highlightSvg.parentElement;
401+
if (!parent) return;
402+
while (highlightSvg.nextSibling) {
403+
parent.insertBefore(highlightSvg.nextSibling, highlightSvg);
404+
}
405+
});
390406
this.recomputeAriaContext(highlightSvg);
391407
}
392408
}
@@ -398,7 +414,7 @@ export class RenderedConnection
398414
// Note that this is done synchronously for parity with highlight().
399415
const highlightSvg = this.findHighlightSvg();
400416
if (highlightSvg) {
401-
highlightSvg.style.display = 'none';
417+
dom.removeClass(highlightSvg, 'blocklyHighlightedConnectionPathVisible');
402418
}
403419
}
404420

packages/blockly/core/renderers/common/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,7 @@ export class ConstantProvider {
12081208
`}`,
12091209

12101210
// Connection highlight.
1211-
`${selector} .blocklyHighlightedConnectionPath {`,
1211+
`${selector} .blocklyHighlightedConnectionPathVisible {`,
12121212
`stroke: #fc3;`,
12131213
`}`,
12141214

packages/blockly/core/renderers/common/drawer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,11 @@ export class Drawer {
440440

441441
const highlightSvg = this.drawConnectionHighlightPath(elem);
442442
if (highlightSvg) {
443-
highlightSvg.style.display = elem.highlighted ? '' : 'none';
443+
elem.connectionModel.setAriaRole(highlightSvg);
444+
highlightSvg.classList.toggle(
445+
'blocklyHighlightedConnectionPathVisible',
446+
elem.highlighted,
447+
);
444448
}
445449
}
446450
}

packages/blockly/core/renderers/common/path_object.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,6 @@ export class PathObject implements IPathObject {
226226
{
227227
'id': connection.id,
228228
'class': 'blocklyHighlightedConnectionPath',
229-
'style': 'display: none;',
230229
'd': connectionPath,
231230
'transform': transformation,
232231
},

packages/blockly/core/renderers/zelos/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ export class ConstantProvider extends BaseConstantProvider {
789789
`}`,
790790

791791
// Connection highlight.
792-
`${selector} .blocklyHighlightedConnectionPath {`,
792+
`${selector} .blocklyHighlightedConnectionPathVisible {`,
793793
`stroke: ${this.SELECTED_GLOW_COLOUR};`,
794794
`}`,
795795

0 commit comments

Comments
 (0)