From 113b5f66c8865b0489371043c4b0cb77a6061aa9 Mon Sep 17 00:00:00 2001 From: Ridwan Sanusi Date: Wed, 10 Jun 2026 17:22:46 -0400 Subject: [PATCH 1/2] =?UTF-8?q?a11y(2.1.1):=20ZoomSvg=20=E2=80=94=20add=20?= =?UTF-8?q?keyboard=20pan/zoom=20controls=20(closes=202.1.1,=202.5.1,=202.?= =?UTF-8?q?5.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pan was mouse-drag only; zoom was wheel only — no keyboard or single-pointer alternative. Adds panBy/zoomBy helpers, a tab-reachable button cluster (Pan Up/Down/Left/Right, Zoom In/Out, Center), and arrow-key/+/- shortcuts on the focused container. Mouse paths are unchanged. Also adds plus/minus icons. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/holocene/icon/paths.ts | 4 + src/lib/holocene/icon/svg/minus.svelte | 11 +++ src/lib/holocene/icon/svg/plus.svelte | 11 +++ src/lib/holocene/zoom-svg.svelte | 117 +++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 src/lib/holocene/icon/svg/minus.svelte create mode 100644 src/lib/holocene/icon/svg/plus.svelte diff --git a/src/lib/holocene/icon/paths.ts b/src/lib/holocene/icon/paths.ts index d92b45ef13..c211d81a42 100644 --- a/src/lib/holocene/icon/paths.ts +++ b/src/lib/holocene/icon/paths.ts @@ -91,6 +91,7 @@ import merge from './svg/merge.svelte'; import microchip from './svg/microchip.svelte'; import microsoft from './svg/microsoft.svelte'; import minimize from './svg/minimize.svelte'; +import minus from './svg/minus.svelte'; import moon from './svg/moon.svelte'; import namespaceSwitcher from './svg/namespace-switcher.svelte'; import namespace from './svg/namespace.svelte'; @@ -102,6 +103,7 @@ import pencil from './svg/pencil.svelte'; import pinFilled from './svg/pin-filled.svelte'; import pin from './svg/pin.svelte'; import play from './svg/play.svelte'; +import plus from './svg/plus.svelte'; import regions from './svg/regions.svelte'; import relationship from './svg/relationship.svelte'; import retention from './svg/retention.svelte'; @@ -249,6 +251,7 @@ export const icons = { microchip, microsoft, minimize, + minus, moon, 'namespace-switcher': namespaceSwitcher, namespace, @@ -256,6 +259,7 @@ export const icons = { 'office-buildings': officeBuildings, overview, play, + plus, pause, pencil, 'pin-filled': pinFilled, diff --git a/src/lib/holocene/icon/svg/minus.svelte b/src/lib/holocene/icon/svg/minus.svelte new file mode 100644 index 0000000000..30c96e8c33 --- /dev/null +++ b/src/lib/holocene/icon/svg/minus.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/holocene/icon/svg/plus.svelte b/src/lib/holocene/icon/svg/plus.svelte new file mode 100644 index 0000000000..9699549f54 --- /dev/null +++ b/src/lib/holocene/icon/svg/plus.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/holocene/zoom-svg.svelte b/src/lib/holocene/zoom-svg.svelte index 0f78cc462f..da39fcc9fa 100644 --- a/src/lib/holocene/zoom-svg.svelte +++ b/src/lib/holocene/zoom-svg.svelte @@ -28,6 +28,27 @@ let panOffsetX = 0; let panOffsetY = 0; + const PAN_STEP_RATIO = 0.1; + const ZOOM_STEP = 0.1; + + function panBy(dx: number, dy: number) { + if (!pannable) return; + viewBox.x += dx * viewBox.width; + viewBox.y += dy * viewBox.height; + } + + function zoomBy(factor: number, centerX = width / 2, centerY = height / 2) { + if (!zoomable) return; + const newZoomLevel = zoomLevel + factor; + if (newZoomLevel < maxZoomIn || newZoomLevel > maxZoomOut) return; + const zoomRatio = newZoomLevel / zoomLevel; + viewBox.x = centerX - (centerX - viewBox.x) * zoomRatio; + viewBox.y = centerY - (centerY - viewBox.y) * zoomRatio; + viewBox.width *= zoomRatio; + viewBox.height *= zoomRatio; + zoomLevel = newZoomLevel; + } + const handleWheel = (event: WheelEvent) => { if (!zoomable) return; event.preventDefault(); @@ -87,22 +108,118 @@ viewBox.height = height; zoomLevel = initialZoom; } + + function handleKeydown(event: KeyboardEvent) { + switch (event.key) { + case 'ArrowUp': + event.preventDefault(); + panBy(0, -PAN_STEP_RATIO); + break; + case 'ArrowDown': + event.preventDefault(); + panBy(0, PAN_STEP_RATIO); + break; + case 'ArrowLeft': + event.preventDefault(); + panBy(-PAN_STEP_RATIO, 0); + break; + case 'ArrowRight': + event.preventDefault(); + panBy(PAN_STEP_RATIO, 0); + break; + case '+': + case '=': + event.preventDefault(); + zoomBy(-ZOOM_STEP); + break; + case '-': + case '_': + event.preventDefault(); + zoomBy(ZOOM_STEP); + break; + } + }
+ {#if pannable} + +