From 0e6f802db23323c6d3d11b5d9e982bbedafe43d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 02:16:13 +0000 Subject: [PATCH 1/2] Add keyboard accessibility to drag listeners using RichDragListener Replace DragListener with RichDragListener in CoordinateSystemNode and CalibrationToolNode to support both pointer and keyboard dragging. Add PDOM attributes (tagName, focusable, accessibleName) to all draggable nodes. Add MoveDraggableItemsKeyboardHelpSection to the keyboard shortcuts panel. - CoordinateSystemNode: position drag uses positionProperty for automatic keyboard support; rotation handle uses left/right arrow keys with dragSpeed-based angle increments - CalibrationToolNode: both endpoint drags use positionProperty - AutoTrackerNode: add accessibleName to hit area for screen readers - KeyboardShortcutsNode: add draggable items help section https://claude.ai/code/session_013ee5HrubofyFGo2vWrBZB8 --- src/screen-name/view/AutoTrackerNode.ts | 2 + src/screen-name/view/CalibrationToolNode.ts | 29 +++++------ src/screen-name/view/CoordinateSystemNode.ts | 52 ++++++++++++------- src/screen-name/view/KeyboardShortcutsNode.ts | 13 +++-- 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/screen-name/view/AutoTrackerNode.ts b/src/screen-name/view/AutoTrackerNode.ts index 8d469e0..b38bd64 100644 --- a/src/screen-name/view/AutoTrackerNode.ts +++ b/src/screen-name/view/AutoTrackerNode.ts @@ -53,6 +53,8 @@ export class AutoTrackerNode extends Node { const hitArea = new Rectangle( 0, 0, VIDEO_W, VIDEO_H, { fill: 'transparent', cursor: 'crosshair', + tagName: 'div', + accessibleName: 'Video tracking area — drag to select object to track', } ); this.addChild( hitArea ); diff --git a/src/screen-name/view/CalibrationToolNode.ts b/src/screen-name/view/CalibrationToolNode.ts index 9f4ee0b..73dd5e0 100644 --- a/src/screen-name/view/CalibrationToolNode.ts +++ b/src/screen-name/view/CalibrationToolNode.ts @@ -1,7 +1,7 @@ -import { Circle, DragListener, HBox, Line, Node, Text } from "scenerystack/scenery"; +import { Circle, HBox, Line, Node, RichDragListener, Text } from "scenerystack/scenery"; import { Keypad, PhetFont } from "scenerystack/scenery-phet"; import { KeypadDialog } from "scenerystack/sim"; -import { Range, Vector2 } from "scenerystack/dot"; +import { Range, type Vector2 } from "scenerystack/dot"; import { DerivedProperty, NumberProperty, Property, type TReadOnlyProperty } from "scenerystack/axon"; import { ComboBox, type ComboBoxItem, Panel, TextPushButton } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; @@ -43,14 +43,17 @@ export class CalibrationToolNode extends Node { this.addChild( calibrationLine ); // ── Endpoint circles ────────────────────────────────────────────────── - const makeEndpoint = () => new Circle( ENDPOINT_RADIUS, { + const makeEndpoint = ( accessibleName: string ) => new Circle( ENDPOINT_RADIUS, { fill: TrackLabColors.calibrationFillProperty, stroke: TrackLabColors.textOnDarkProperty, lineWidth: 1.5, cursor: 'crosshair', + tagName: 'div', + focusable: true, + accessibleName: accessibleName, } ); - const endpoint1 = makeEndpoint(); - const endpoint2 = makeEndpoint(); + const endpoint1 = makeEndpoint( 'Calibration Point 1' ); + const endpoint2 = makeEndpoint( 'Calibration Point 2' ); this.addChild( endpoint1 ); this.addChild( endpoint2 ); @@ -135,17 +138,13 @@ export class CalibrationToolNode extends Node { // ── Drag listeners for endpoints ────────────────────────────────────── const makeDragListener = ( pointProperty: Property ) => { - let startPos = pointProperty.value.copy(); - let startPtr = new Vector2( 0, 0 ); - return new DragListener( { - start: ( event ) => { - startPos = pointProperty.value.copy(); - startPtr = this.globalToLocalPoint( event.pointer.point ); - }, - drag: ( event ) => { - const ptr = this.globalToLocalPoint( event.pointer.point ); - pointProperty.value = startPos.plus( ptr.minus( startPtr ) ); + return new RichDragListener( { + positionProperty: pointProperty, + keyboardDragListenerOptions: { + dragSpeed: 200, + shiftDragSpeed: 40, }, + tandem: Tandem.OPT_OUT, } ); }; endpoint1.addInputListener( makeDragListener( this.point1Property ) ); diff --git a/src/screen-name/view/CoordinateSystemNode.ts b/src/screen-name/view/CoordinateSystemNode.ts index 1720ac9..09a8363 100644 --- a/src/screen-name/view/CoordinateSystemNode.ts +++ b/src/screen-name/view/CoordinateSystemNode.ts @@ -1,7 +1,8 @@ -import { Circle, DragListener, Node, Text } from "scenerystack/scenery"; +import { Circle, Node, RichDragListener, Text } from "scenerystack/scenery"; import { ArrowNode, PhetFont } from "scenerystack/scenery-phet"; -import { Vector2 } from "scenerystack/dot"; +import type { Vector2 } from "scenerystack/dot"; import { NumberProperty, Property, type TReadOnlyProperty } from "scenerystack/axon"; +import { Tandem } from "scenerystack/tandem"; import TrackLabColors from "../../TrackLabColors.js"; const ARROW_LENGTH = 120; @@ -61,6 +62,9 @@ export class CoordinateSystemNode extends Node { stroke: TrackLabColors.textOnDarkProperty, lineWidth: 1.5, cursor: 'crosshair', + tagName: 'div', + focusable: true, + accessibleName: 'Rotation Handle', } ); rotatingNode.addChild( handleDisk ); @@ -72,7 +76,13 @@ export class CoordinateSystemNode extends Node { } ); // ── Position wrapper: translates with viewPositionProperty ─────────── - const positionNode = new Node( { children: [ rotatingNode, originMarker ], cursor: 'move' } ); + const positionNode = new Node( { + children: [ rotatingNode, originMarker ], + cursor: 'move', + tagName: 'div', + focusable: true, + accessibleName: 'Coordinate System', + } ); this.addChild( positionNode ); // ── Property → scene-graph linkage ──────────────────────────────────── @@ -80,28 +90,34 @@ export class CoordinateSystemNode extends Node { this.rotationAngleProperty.link( angle => { rotatingNode.rotation = angle; } ); // ── Drag: translate the entire coordinate system ────────────────────── - let startPos = initialPosition.copy(); - let startPtr = new Vector2( 0, 0 ); - - positionNode.addInputListener( new DragListener( { - start: ( event ) => { - startPos = this.viewPositionProperty.value.copy(); - startPtr = this.globalToLocalPoint( event.pointer.point ); - }, - drag: ( event ) => { - const ptr = this.globalToLocalPoint( event.pointer.point ); - this.viewPositionProperty.value = startPos.plus( ptr.minus( startPtr ) ); + positionNode.addInputListener( new RichDragListener( { + positionProperty: this.viewPositionProperty, + keyboardDragListenerOptions: { + dragSpeed: 300, + shiftDragSpeed: 50, }, + tandem: Tandem.OPT_OUT, } ) ); // ── Drag: rotate around origin ──────────────────────────────────────── // Dragging the handle disk updates the rotation angle based on the // pointer's angle relative to the coordinate-system origin. - handleDisk.addInputListener( new DragListener( { - drag: ( event ) => { - const p = positionNode.globalToLocalPoint( event.pointer.point ); - this.rotationAngleProperty.value = Math.atan2( p.y, p.x ); + handleDisk.addInputListener( new RichDragListener( { + dragListenerOptions: { + drag: ( event ) => { + const p = positionNode.globalToLocalPoint( event.pointer.point ); + this.rotationAngleProperty.value = Math.atan2( p.y, p.x ); + }, + }, + keyboardDragListenerOptions: { + keyboardDragDirection: 'leftRight', + dragSpeed: 100, + shiftDragSpeed: 20, + drag: ( _event, listener ) => { + this.rotationAngleProperty.value += listener.modelDelta.x * ( Math.PI / 180 ); + }, }, + tandem: Tandem.OPT_OUT, } ) ); // ── Visibility: only shown once a video with a finite duration is loaded diff --git a/src/screen-name/view/KeyboardShortcutsNode.ts b/src/screen-name/view/KeyboardShortcutsNode.ts index 70b3ebd..2a78bee 100644 --- a/src/screen-name/view/KeyboardShortcutsNode.ts +++ b/src/screen-name/view/KeyboardShortcutsNode.ts @@ -1,10 +1,17 @@ -import { Node } from "scenerystack/scenery"; -import { BasicActionsKeyboardHelpSection } from "scenerystack/scenery-phet"; +import { Node, VBox } from "scenerystack/scenery"; +import { BasicActionsKeyboardHelpSection, MoveDraggableItemsKeyboardHelpSection } from "scenerystack/scenery-phet"; export class KeyboardShortcutsNode extends Node { public constructor() { super(); - this.addChild( new BasicActionsKeyboardHelpSection() ); + this.addChild( new VBox( { + children: [ + new BasicActionsKeyboardHelpSection(), + new MoveDraggableItemsKeyboardHelpSection(), + ], + spacing: 16, + align: 'left', + } ) ); } } From f0f4c4634a76bb0420bfe34f521c80beb1d36ad8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 04:09:18 +0000 Subject: [PATCH 2/2] Update package-lock.json https://claude.ai/code/session_013ee5HrubofyFGo2vWrBZB8 --- package-lock.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ead3eb9..588d857 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3246,7 +3245,6 @@ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3284,7 +3282,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3509,7 +3506,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6155,7 +6151,6 @@ "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -6221,7 +6216,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -6482,7 +6476,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6879,7 +6872,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" },