fix(expo): Android annotation gestures + drag-to-move placed shapes - #27
Merged
Merged
Conversation
Nothing could be drawn in the Annotate step on Android: strokes were ignored while the toolbar kept working, so it read as a broken canvas. iOS was fine. A react-native Modal on Android is a separate native window (ReactModalHostView.DialogRootViewGroup) that sits outside the host app's GestureHandlerRootView, so gesture-handler receives no touches inside it and the pan gesture never fires. Gesture-handler's own root view detects this case and activates a nested root for it, so the wizard has to carry its own. The launcher is unaffected because it renders in the app's own tree, which is why tapping the bug button still opened the wizard. Tests need react-native mounted under bun, which means stubbing the native modules; that harness lands in src/test-support/native-mocks.ts. bun test shares one module registry across files, so system-info.test.ts now takes react-native from the same stub instead of registering a partial one that decided what the component tests could import. test-sdk.sh grew *.test.tsx discovery — its find only matched *.test.ts, so component tests would never have run in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Annotations were write-once: a text label that landed in the wrong spot could only be undone and retyped, and the same went for arrows and boxes. The toolbar gains a cursor tool — tap a shape to select it, drag to move it, tap empty canvas to deselect. The geometry is pure and lives in @reprojs/sdk-utils (shapeBounds, hitTest, translateShape, clampTranslation) so the web widget can adopt the same behaviour later; nothing in packages/ui changes here. Shapes are grabbed by their ink — arrows and pen strokes by distance to the line, boxes and text anywhere inside — because a 2px border is not a finger-sized target. Text needs an estimated box: react-native offers no synchronous measurement, and SVG text is positioned by its baseline. "select" stays out of sdk-utils' Tool union, which means "a tool that produces a shape" and backs an exhaustive Record<Tool, ToolHandler> in the web canvas; the expo canvas takes a local CanvasMode instead. The store moves from append-only to snapshot history, because undo now has to reverse edits as well as additions — popping the last shape would delete the label you just nudged instead of putting it back. A whole drag commits once, on release, so it costs one history entry rather than one per frame. Drags are clamped to the canvas: a shape dragged past the edge would leave nothing to tap to get it back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cursor tool makes nine buttons in the first toolbar row. At 44pt each plus gaps that is ~416pt, which does not fit a 375pt phone, and Yoga defaults flexShrink to 0 — the row would have run off the edge and taken the trash button with it. The buttons now shrink to no less than 34pt, so they stay at 44pt wherever there is room. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Why
Reported from the field: in the Expo wizard's Annotate step nothing could be drawn on the screenshot. It reproduced in two different host apps, so it was ours, not a local setup. iOS drew fine — but on iOS a placed text label could not be moved.
1. Android: gestures were dead inside the wizard modal
The wizard is a react-native
Modal. On Android that is a separate native window (ReactModalHostView.DialogRootViewGroup) sitting outside the host app'sGestureHandlerRootView, so gesture-handler receives no touches inside it and the canvas' pan gesture never fires. The toolbar kept working (plainPressable), which is why it read as a broken canvas rather than a dead gesture. Gesture-handler's own root view detects this case and activates a nested root, so the wizard now carries its own.Evidence it was never the host app's setup: the launcher opens the wizard via an RNGH
Gesture.Tap()in the app's own tree — if gesture-handler were missing or unrooted, the bug button would not have opened anything.2. Annotations are now movable
The toolbar gains a cursor tool: tap a shape to select it (dashed outline), drag to move it, tap empty canvas to deselect. Works for text, arrows, rectangles, highlights and pen strokes.
shapeBounds,hitTest,translateShape,clampTranslation) is pure and lives in@reprojs/sdk-utils;packages/uiis untouched, andselectdeliberately stays out of the sharedToolunion.Not included: resize, rotate, multi-select, web parity.
Testing
sdk-utils.src/test-support/native-mocks.ts) underreact-test-renderer: asserts the gesture root wraps the canvas, and drives real tap/drag gestures to check selection, the committed move, single-step undo and clamping.scripts/test-sdk.shnow discovers*.test.tsx— itsfindonly matched*.test.ts, so component tests would have been skipped in CI silently.system-info.test.tstakes react-native from the shared stub;bun testshares one module registry, and its partial stub decided what other files could import.bun run check,bun run test:sdk,bun run expo:build,tsc --noEmitall green.🤖 Generated with Claude Code