React Forum
Blog & Guide Responses
Feb 01, 2023 · 04:00 PM

[Guide] Build a Photo Annotator (Keyboard Support Later) - implementation notes

Started by Soren Malik
guidephotoannotationkey bridgingfocus schedulingroute-local docsaccessibility posture

The Photo Annotator guide has a pragmatic take: ship a solid pointer-driven annotator first, then layer keyboard support later using key-bridging primitives and focus scheduling so you can add shortcuts without rewriting the canvas interaction model. The route-local annotation docs idea also seems useful for debuggability ("what did we think the annotation was?").

Read the guide

How did you structure the key-bridge so it stayed predictable (no global shortcut soup)? Where did you store annotation truth: route-local doc, vault, or component state tied to the canvas? What focus scheduling rules worked so keyboard support didn't feel janky or inaccessible? How did you render evidence so bug reports include the active tool/selection/anchor info?

Comments (20)

Back to latest
Marisol Vega
Feb 04, 2023 · 06:18 PM

Key bridging only worked once we treated it like a contract surface with a tiny vocabulary.

If every tool invents its own shortcuts, the bridge becomes chaos.

Dmitri Kline
Feb 09, 2023 · 09:09 AM

We implemented key bridging as a single dispatcher with a strict "reason" string and a focus schedule. The log format mattered because it made shortcut behavior auditable:

txt
[tips] keyBridge key=Escape handled=true action=selection:clear reason=user:key focusTarget=canvas
[tips] focusSchedule intent=canvas:focus at=now reason=selection:clear

If someone says "Escape didn't work", you can inspect whether it was handled and where focus was supposed to go.

Keira Santos
Feb 16, 2023 · 04:16 PM

Counterpoint: deferring keyboard support can paint you into a corner if the pointer model isn't accessible.

We still designed the tool model as if keyboard existed (tool, selection, anchor) even before wiring shortcuts.

Soren Malik
Feb 19, 2023 · 04:04 AM

Yes—"later" can't mean "never planned".

The reason key bridging works is because the tool model is already explicit and can be driven from multiple inputs.

Grace McCoy
Feb 28, 2023 · 10:22 PM

Route-local annotation docs were surprisingly helpful.

We store a doc per photo with activeTool, selectionId, and draftAnchor, and we render those as evidence in debug mode.

Jasper Nguyen
Mar 14, 2023 · 02:14 PM

We kept annotation truth as a route-local doc and treated the canvas as a renderer, not as the source of truth.

That made undo/redo and cross-panel UI (side list of annotations) consistent.

Nina Kapoor
Mar 29, 2023 · 03:03 AM

Focus scheduling was the hardest part: when do you move focus to the canvas vs the side panel vs a label input? We made it deterministic by encoding it as a schedule, not an immediate imperative call:

txt
[tips] focusSchedule intent=labelInput:focus reason=tool:rename selection=ann_42
[tips] focusSchedule intent=canvas:focus reason=tool:draw
Camille Duarte
Apr 18, 2023 · 06:18 PM

We also rendered data-tool, data-selection, data-anchor, data-key-bridge-scope on the shell.

It's not pretty, but it makes screenshots useful for diagnosing weird selection bugs.

Owen Price
May 10, 2023 · 10:10 AM

Counterpoint: logging key bridges is great until it becomes a performance problem (lots of key events).

We logged only handled keys + a small set of navigation keys. Everything else stays silent.

Benji Rios
Jun 01, 2023 · 06:06 AM

If the key bridge is global, it will eventually conflict with text inputs. We scoped the key bridge by focus target (canvas vs input) and logged the scope decision:

txt
[tips] keyBridge scope=input handled=false key=Backspace reason=typing
[tips] keyBridge scope=canvas handled=true key=Backspace action=selection:delete reason=user:key
Lina Ortega
Jun 25, 2023 · 10:22 PM

Docs tie-in: the focus scheduling idea maps to the accessibility guidance pretty well.

Related: Accessibility (Best Practices) and Tools and Ecosystem.

Theo Bennett
Aug 22, 2023 · 03:33 AM

Long-form take: key bridging is less about shortcuts and more about *input normalization*.

Once you normalize inputs into a common intent (draw, select, nudge, delete), the app stops caring whether it came from pointer, keyboard, or a toolbar.

The key is keeping intent strings stable and observable so new shortcuts don't change semantics by accident.

Salma Qureshi
Dec 31, 2023 · 09:09 AM

We also stored derived "selection summary" (type + count) so the side panel and canvas always agreed.

Without it, you end up with the canvas showing one thing and the panel showing another, which is trust-destroying.

Amina Farouk
Jul 21, 2023 · 07:07 AM

We used a key bridge integration layer that can be turned off per route.

That prevented shortcut leakage into other tools and made the bridge feel deliberate.

Evan Brooks
Sep 18, 2023 · 12:12 PM

Counterpoint: route-local docs can fragment if you have multi-photo workflows (compare, bulk annotate).

We introduced a vault that holds cross-route selection and made the route docs reference it by id.

Grace McCoy
Sep 22, 2023 · 04:04 AM

That split makes sense: route doc for local posture, vault for cross-route identity.

The important thing is that both are explicit and render evidence so you can see the split.

Harper Iqbal
Oct 15, 2023 · 03:15 PM

We found it helpful to log tool transitions too, because a lot of "bug" reports were actually "wrong tool" reports:

txt
[tips] tool=select -> tool=draw reason=toolbar:click
[tips] tool=draw -> tool=select reason=keyBridge:Escape
Rowan Pierce
Nov 12, 2023 · 04:04 AM

If you delay keyboard support, at least make sure your pointer interactions are consistent and discoverable.

The app should feel like it has a coherent model before you add more input channels.

Tomas Rivera
Dec 12, 2023 · 12:12 PM

Long-form counterpoint: focus scheduling can be used as a band-aid for a UI that's constantly stealing focus.

If you find yourself scheduling focus on every render tick, that's a smell: you're not respecting user control.

We ended up with a stricter contract: focus can move only on explicit intent boundaries (tool change, selection change, route change).

Mina Calder
Dec 20, 2023 · 06:18 PM

We also added a debug-only overlay that prints the route doc state in plain text.

It made it obvious when the canvas and the doc were drifting, which was the root cause of several bugs.

Jules Kwon
Dec 23, 2023 · 11:23 PM

The nice thing about key bridging is that it becomes testable.

Tests can assert on a log line / evidence key instead of trying to simulate pixel-perfect pointer movement.

Ari Novak
Dec 29, 2023 · 09:09 AM

If you're implementing this, start by deciding what evidence a screenshot must contain (tool, selection, anchor, focus target).

Once that exists, the rest of the architecture decisions get a lot easier to validate.