-
Notifications
You must be signed in to change notification settings - Fork 216
feat: keyboard annotation #7503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gmorador-tribu
wants to merge
16
commits into
main
Choose a base branch
from
fix/vpat-keyboard
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+6,296
−122
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
215e8f0
feat: keyboard annotation
61e1d18
Merge branch 'main' into fix/vpat-keyboard
gmorador-tribu af19902
fix: copilot code review
1b81efb
fix: linter
b1b9cb0
fix: linter
52556a4
fix: coverage
a1cf60f
fix:lit
37b92ab
fix: codecov
a5380a2
fix: lint
b978043
fix: add test
6db43b9
fix: add test
04cdfbb
feat: add feature flag
5bd4a84
fix: santi code review
1533e03
fix: linter
04df59e
fix: code review
0873efd
fix: coverage
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import type { KeyboardMode, PinnedCorner } from '../../types/annotator'; | ||
| import { pinnedCornerToLabel } from '../util/pinned-corner'; | ||
|
|
||
| /** | ||
| * Component that announces changes to pin/rectangle position and size | ||
| * to screen readers using a live region. | ||
| */ | ||
| export type DrawToolAnnouncerProps = { | ||
| /** Current mode: 'move' for moving, 'resize' for resizing, 'rect' for rectangle, null when inactive */ | ||
| mode: KeyboardMode; | ||
|
|
||
| /** Current tool type: 'point' for pin, 'rect' for rectangle */ | ||
| tool: 'point' | 'rect' | null; | ||
|
|
||
| /** Which corner is pinned during resize mode */ | ||
| pinnedCorner?: PinnedCorner; | ||
|
|
||
| /** Current X coordinate (for point) or left position (for rect) */ | ||
| x?: number; | ||
|
|
||
| /** Current Y coordinate (for point) or top position (for rect) */ | ||
| y?: number; | ||
|
|
||
| /** Width of rectangle (only for rect tool) */ | ||
| width?: number; | ||
|
|
||
| /** Height of rectangle (only for rect tool) */ | ||
| height?: number; | ||
|
|
||
| /** Whether keyboard mode is active */ | ||
| keyboardActive: boolean; | ||
| }; | ||
|
|
||
| /** | ||
| * Announce current position/size of drawing tool to screen readers. | ||
| * | ||
| * This component renders a hidden live region that announces changes | ||
| * to the position or size of pin/rectangle annotations when using | ||
| * keyboard navigation. | ||
| */ | ||
| export default function DrawToolAnnouncer({ | ||
| mode, | ||
| tool, | ||
| x, | ||
| y, | ||
| width, | ||
| height, | ||
| keyboardActive, | ||
| pinnedCorner, | ||
| }: DrawToolAnnouncerProps) { | ||
| if (!keyboardActive || !tool) { | ||
| return null; | ||
| } | ||
|
|
||
| let announcement = ''; | ||
|
|
||
| if (tool === 'point') { | ||
| if (mode === 'move' && typeof x === 'number' && typeof y === 'number') { | ||
| announcement = `Pin position: ${Math.round(x)}, ${Math.round(y)}`; | ||
| } else if (mode === 'resize') { | ||
| // Pin doesn't support resize, but announce if mode is set incorrectly | ||
| announcement = 'Pin annotation mode. Use arrow keys to move.'; | ||
| } else { | ||
| announcement = | ||
| 'Pin annotation mode. Use arrow keys to move, Enter to confirm.'; | ||
| } | ||
| } else if (tool === 'rect') { | ||
| if ( | ||
| mode === 'move' && | ||
| typeof x === 'number' && | ||
| typeof y === 'number' && | ||
| typeof width === 'number' && | ||
| typeof height === 'number' | ||
| ) { | ||
| announcement = `Rectangle position: ${Math.round(x)}, ${Math.round(y)}. Size: ${Math.round(width)} by ${Math.round(height)} pixels`; | ||
| } else if ( | ||
| mode === 'resize' && | ||
| typeof width === 'number' && | ||
| typeof height === 'number' | ||
| ) { | ||
| const cornerText = pinnedCornerToLabel(pinnedCorner, 'long'); | ||
| announcement = `Rectangle size: ${Math.round(width)} by ${Math.round(height)} pixels. ${cornerText}. Press Tab to change pinned corner.`; | ||
| } else if (mode === 'rect') { | ||
| announcement = | ||
| 'Rectangle annotation mode. Click the mode button to switch to Move or Resize mode.'; | ||
| } else { | ||
| announcement = | ||
| 'Rectangle annotation mode. Use arrow keys to move, Ctrl+Shift+J to resize, Enter to confirm.'; | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| aria-live="polite" | ||
| aria-atomic="true" | ||
| role="status" | ||
| className="sr-only" | ||
| data-testid="draw-tool-announcer" | ||
| > | ||
| {announcement && <span>{announcement}</span>} | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import type { KeyboardMode, PinnedCorner } from '../../types/annotator'; | ||
| import { pinnedCornerToLabel } from '../util/pinned-corner'; | ||
|
|
||
| /** | ||
| * Visual indicator showing the current keyboard mode (move/resize) for drawing tools. | ||
| * This provides visual feedback to users when keyboard mode is active. | ||
| */ | ||
| export type DrawToolKeyboardIndicatorProps = { | ||
| /** Current mode: 'move' for moving, 'resize' for resizing, 'rect' for rectangle, null when inactive */ | ||
| mode: KeyboardMode; | ||
|
|
||
| /** Whether keyboard mode is active */ | ||
| keyboardActive: boolean; | ||
|
|
||
| /** Which corner is pinned during resize mode */ | ||
| pinnedCorner?: PinnedCorner; | ||
| }; | ||
|
|
||
| /** | ||
| * Visual indicator for keyboard drawing mode. | ||
| * | ||
| * Displays a small overlay showing the current mode (move/resize) when | ||
| * keyboard mode is active. This helps users understand the current state | ||
| * without relying solely on screen reader announcements. | ||
| */ | ||
| export default function DrawToolKeyboardIndicator({ | ||
| mode, | ||
| keyboardActive, | ||
| pinnedCorner, | ||
| }: DrawToolKeyboardIndicatorProps) { | ||
| if (!keyboardActive || !mode) { | ||
| return null; | ||
| } | ||
|
|
||
| const modeText = | ||
| mode === 'move' ? 'Move' : mode === 'resize' ? 'Resize' : 'Rectangle'; | ||
| let instructions: string; | ||
| if (mode === 'move') { | ||
| instructions = | ||
| 'Use arrow keys to move, click mode button to switch modes, Enter to confirm'; | ||
| } else if (mode === 'resize') { | ||
| const cornerText = pinnedCornerToLabel(pinnedCorner, 'short'); | ||
| instructions = `Use arrow keys to resize (${cornerText} corner pinned), Tab to change corner, click mode button to switch modes, Enter to confirm`; | ||
| } else { | ||
| instructions = | ||
| 'Rectangle mode. Click the mode button to switch to Move or Resize mode.'; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className="fixed bottom-4 left-4 bg-white border border-grey-3 rounded shadow-lg p-3 z-50 pointer-events-none" | ||
| data-testid="draw-tool-keyboard-indicator" | ||
| role="status" | ||
| > | ||
| <div className="text-sm font-semibold text-grey-9"> | ||
| Keyboard mode: {modeText} | ||
| </div> | ||
| <div className="text-xs text-grey-6 mt-1">{instructions}</div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| import type { KeyboardMode, PinnedCorner, Shape } from '../../types/annotator'; | ||
| import { normalizeRect } from '../util/draw-tool-position'; | ||
| import { getActiveEdges } from '../util/rect-resize'; | ||
|
|
||
| export type DrawToolSurfaceProps = { | ||
| shape: Shape | undefined; | ||
| waitingForSecondClick: boolean; | ||
| firstClickPoint: { x: number; y: number } | undefined; | ||
| keyboardMode: KeyboardMode; | ||
| keyboardActive: boolean; | ||
| pinnedCorner: PinnedCorner; | ||
| }; | ||
|
|
||
| const ACTIVE_EDGE_COLOR = '#374151'; | ||
| const INACTIVE_EDGE_COLOR = 'grey'; | ||
|
|
||
| /** | ||
| * Renders the current draw-tool shape (rect or point) or the two-click indicator | ||
| * into the SVG surface. | ||
| */ | ||
| export function DrawToolSurface({ | ||
| shape, | ||
| waitingForSecondClick, | ||
| firstClickPoint, | ||
| keyboardMode, | ||
| keyboardActive, | ||
| pinnedCorner, | ||
| }: DrawToolSurfaceProps) { | ||
|
Comment on lines
21
to
28
|
||
| if (shape?.type === 'rect') { | ||
| if (waitingForSecondClick && firstClickPoint) { | ||
| const { x, y } = firstClickPoint; | ||
| return ( | ||
| <> | ||
| <circle | ||
| stroke="grey" | ||
| stroke-width="2px" | ||
| fill="none" | ||
| cx={x} | ||
| cy={y} | ||
| r={8} | ||
| /> | ||
| <line | ||
| stroke="grey" | ||
| stroke-width="1px" | ||
| x1={x - 12} | ||
| y1={y} | ||
| x2={x + 12} | ||
| y2={y} | ||
| /> | ||
| <line | ||
| stroke="grey" | ||
| stroke-width="1px" | ||
| x1={x} | ||
| y1={y - 12} | ||
| x2={x} | ||
| y2={y + 12} | ||
| /> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| const rect = normalizeRect(shape); | ||
| const width = rect.right - rect.left; | ||
| const height = rect.bottom - rect.top; | ||
| const activeEdges = | ||
| keyboardMode === 'resize' && keyboardActive | ||
| ? getActiveEdges(pinnedCorner) | ||
| : { top: false, right: false, bottom: false, left: false }; | ||
|
|
||
| return ( | ||
| <> | ||
| {/* Background fill - white dashed stroke so grey dashes show through from next rect */} | ||
| <rect | ||
| stroke="white" | ||
| stroke-dasharray="5" | ||
| stroke-width="1px" | ||
| fill="grey" | ||
| fill-opacity="0.5" | ||
| x={rect.left} | ||
| y={rect.top} | ||
| width={width} | ||
| height={height} | ||
| /> | ||
| {/* Base border - grey dashed, offset to alternate with white */} | ||
| <rect | ||
| stroke={INACTIVE_EDGE_COLOR} | ||
| stroke-dasharray="5" | ||
| stroke-dashoffset="5" | ||
| stroke-width="1px" | ||
| fill="none" | ||
| x={rect.left} | ||
| y={rect.top} | ||
| width={width} | ||
| height={height} | ||
| /> | ||
| {/* Active edges overlay in resize mode - dashed dark grey on active edges only */} | ||
| {keyboardMode === 'resize' && keyboardActive && ( | ||
| <> | ||
| <line | ||
| x1={rect.left} | ||
| y1={rect.top} | ||
| x2={rect.right} | ||
| y2={rect.top} | ||
| stroke={activeEdges.top ? ACTIVE_EDGE_COLOR : 'transparent'} | ||
| stroke-width="3px" | ||
| stroke-dasharray="5" | ||
| stroke-dashoffset="5" | ||
| /> | ||
| <line | ||
| x1={rect.right} | ||
| y1={rect.top} | ||
| x2={rect.right} | ||
| y2={rect.bottom} | ||
| stroke={activeEdges.right ? ACTIVE_EDGE_COLOR : 'transparent'} | ||
| stroke-width="3px" | ||
| stroke-dasharray="5" | ||
| stroke-dashoffset="5" | ||
| /> | ||
| <line | ||
| x1={rect.left} | ||
| y1={rect.bottom} | ||
| x2={rect.right} | ||
| y2={rect.bottom} | ||
| stroke={activeEdges.bottom ? ACTIVE_EDGE_COLOR : 'transparent'} | ||
| stroke-width="3px" | ||
| stroke-dasharray="5" | ||
| stroke-dashoffset="5" | ||
| /> | ||
| <line | ||
| x1={rect.left} | ||
| y1={rect.top} | ||
| x2={rect.left} | ||
| y2={rect.bottom} | ||
| stroke={activeEdges.left ? ACTIVE_EDGE_COLOR : 'transparent'} | ||
| stroke-width="3px" | ||
| stroke-dasharray="5" | ||
| stroke-dashoffset="5" | ||
| /> | ||
| </> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| if (shape?.type === 'point') { | ||
| const point = shape; | ||
| return ( | ||
| <circle | ||
| stroke="black" | ||
| stroke-width="1px" | ||
| fill="yellow" | ||
| cx={point.x} | ||
| cy={point.y} | ||
| r={5} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.