From 6ee0b0be079fc04769d67d9dbd983340bd027cbb Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 24 Jun 2026 09:22:34 -0700 Subject: [PATCH 1/2] fix: Make zoom to fit plugin keyboard and screenreader accessible --- plugins/zoom-to-fit/src/index.ts | 81 +++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/plugins/zoom-to-fit/src/index.ts b/plugins/zoom-to-fit/src/index.ts index 7176f58c9f..48ed708410 100644 --- a/plugins/zoom-to-fit/src/index.ts +++ b/plugins/zoom-to-fit/src/index.ts @@ -9,7 +9,9 @@ import * as Blockly from 'blockly/core'; /** * Class for zoom to fit control. */ -export class ZoomToFitControl implements Blockly.IPositionable { +export class ZoomToFitControl + implements Blockly.IComponent, Blockly.IPositionable, Blockly.IFocusableNode +{ /** * The unique id for this component. */ @@ -72,7 +74,10 @@ export class ZoomToFitControl implements Blockly.IPositionable { this.workspace.getComponentManager().addComponent({ component: this, weight: 2, - capabilities: [Blockly.ComponentManager.Capability.POSITIONABLE], + capabilities: [ + Blockly.ComponentManager.Capability.POSITIONABLE, + Blockly.ComponentManager.Capability.FOCUSABLE, + ], }); this.createDom(); this.initialized = true; @@ -97,15 +102,46 @@ export class ZoomToFitControl implements Blockly.IPositionable { * Creates DOM for ui element. */ private createDom() { - this.svgGroup = Blockly.utils.dom.createSvgElement( + this.svgGroup = Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G, { + height: `${this.height}px`, + width: `${this.width}px`, + class: 'zoomToFit', + id: Blockly.utils.idGenerator.getNextUniqueId(), + tabindex: '0', + }); + Blockly.utils.aria.setState( + this.svgGroup, + Blockly.utils.aria.State.LABEL, + 'Zoom to fit', + ); + Blockly.utils.aria.setRole(this.svgGroup, Blockly.utils.aria.Role.BUTTON); + + Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.RECT, + { + width: 40, + height: 40, + x: -4, + y: -4, + rx: 2, + ry: 2, + fill: 'none', + class: 'blocklyFocusRing', + }, + this.svgGroup, + ); + + const image = Blockly.utils.dom.createSvgElement( Blockly.utils.Svg.IMAGE, { height: `${this.height}px`, width: `${this.width}px`, - class: 'zoomToFit', + class: 'zoomToFitIcon', }, + this.svgGroup, ); - this.svgGroup.setAttributeNS( + + image.setAttributeNS( Blockly.utils.dom.XLINK_NS, 'xlink:href', zoomToFitSvgDataUri, @@ -130,7 +166,7 @@ export class ZoomToFitControl implements Blockly.IPositionable { * * @param e A pointer down event. */ - private onClick(e: PointerEvent) { + private onClick(e?: Event) { this.workspace.zoomToFit(); const uiEvent = new (Blockly.Events.get(Blockly.Events.CLICK))( null, @@ -138,8 +174,8 @@ export class ZoomToFitControl implements Blockly.IPositionable { 'zoom_reset_control', ); Blockly.Events.fire(uiEvent); - e.stopPropagation(); // avoid to also fire workspace click event - e.preventDefault(); + e?.stopPropagation(); // avoid to also fire workspace click event + e?.preventDefault(); } /** @@ -241,6 +277,29 @@ export class ZoomToFitControl implements Blockly.IPositionable { `translate(${this.left}, ${this.top})`, ); } + + getFocusableElement(): HTMLElement | SVGElement { + if (!this.svgGroup) { + throw new Error('Tried to focus an uninitialized zoom to fit control'); + } + return this.svgGroup; + } + + getFocusableTree(): Blockly.IFocusableTree { + return this.workspace; + } + + onNodeFocus(): void {} + + onNodeBlur(): void {} + + canBeFocused(): boolean { + return true; + } + + performAction(e?: Event): void { + this.onClick(e); + } } /** @@ -256,13 +315,13 @@ const zoomToFitSvgDataUri = 'E0LjVMNSAxNy41OVYxNUgzdjZoNnYtMkg2LjQybDMuMDgtMy4wOXoiLz48L3N2Zz4='; Blockly.Css.register(` -.zoomToFit { +.zoomToFitIcon { opacity: 0.4; } -.zoomToFit:hover { +.zoomToFitIcon:hover, .zoomToFit:focus .zoomToFitIcon { opacity: 0.6; } -.zoomToFit:active { +.zoomToFitIcon:active { opacity: 0.8; } `); From 041570eec2b1d90630075a1947d50c7e02f527f8 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 30 Jun 2026 08:42:30 -0700 Subject: [PATCH 2/2] fix: Use localized ARIA label --- plugins/zoom-to-fit/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/zoom-to-fit/src/index.ts b/plugins/zoom-to-fit/src/index.ts index 48ed708410..723aeb88c8 100644 --- a/plugins/zoom-to-fit/src/index.ts +++ b/plugins/zoom-to-fit/src/index.ts @@ -112,7 +112,7 @@ export class ZoomToFitControl Blockly.utils.aria.setState( this.svgGroup, Blockly.utils.aria.State.LABEL, - 'Zoom to fit', + Blockly.Msg['ZOOM_TO_FIT_ARIA_LABEL'], ); Blockly.utils.aria.setRole(this.svgGroup, Blockly.utils.aria.Role.BUTTON);