Skip to content

feat(Lightbox): a mouse, a pen and a trackpad use a finger's gestures - #8764

Closed
vjeux wants to merge 2312 commits into
meta:mainfrom
vjeux:uikit/lightbox-every-pointer
Closed

vjeux wants to merge 2312 commits into
meta:mainfrom
vjeux:uikit/lightbox-every-pointer

Conversation

@vjeux

@vjeux vjeux commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Depends on #8746. Review only this PR's change: vjeux/astryx@ec51222...7f75a16

Why

On a laptop, a Lightbox photo ignores a phone's gestures: a drag down does not close it, a trackpad's sideways scroll does not page, and a pinch does not zoom. spec:AST-094 FR43 says gestures attach for every pointer.

What led to it

Lightbox's Photos-style gestures, from the UIKit-parity work, attached to touch screens only. #8746 made a click toggle the controls for every pointer; the rest needed the zoom stage and pager to take every pointer, which #8676 (wheel and trackpad input) allows.

Options and tradeoffs

  • Mouse handlers beside the touch recognizers: smaller, but two models that drift (a double-click zoomed 2× at the centre, a double tap at the tapped point).
  • The recognizers for every pointer (this PR): one model. A double-click becomes the double tap, which fills the screen: on a 16:10 desktop a 3:2 photo zooms to 1.067×, not 2×. The keyboard keeps 2×.
  • Following FR43, one wheel notch toward the reader closes a fitted photo's viewer: a notch releases still, past the drag's 10 pt commit point.

Owner question: Should the drag to dismiss take only trackpad scrolling (allowedScrollTypes: ['continuous']), so a wheel notch over a photo never starts a dismiss? Today it follows FR43, notches included.

Why this choice

One gesture model is what FR43 asks; it removes the drift. Lightbox's own code shrinks; the size added is #8676's shared gesture modules.

How to check it

In the "Gallery with zoom" story, on a laptop:

  1. Drag the photo down and let go: the viewer closes.
  2. Drag sideways, or scroll sideways on a trackpad: the next photo shows.
  3. Pinch the trackpad: the photo zooms; pinch in at the fitted size: the viewer closes.
  4. Double-click the photo: it zooms at the click and the controls hide; again: back.
  5. Focus the photo, press +: it zooms 2× at the centre; the controls stay.
  6. With Reduce Motion on, drag the photo down and let go: the viewer closes at once.

Evidence

Desktop Chromium, 1280 × 800; before is #8746.

Input Before After
Mouse drag down nothing closes
Sideways drag or trackpad scroll nothing pages
Ctrl-wheel nothing zooms; out from fit, closes
One wheel notch toward the reader nothing closes
Double-click 2× at the centre fills, at the click
Pen drag down nothing closes

Ten new browser tests fail on #8746, pass here.

vjeux added 30 commits October 27, 2026 01:25
…from their pointer events

install() reads a finger from its pointer events when the window has no
ontouchstart, but the four touch handlers still took the Touch Events a
browser sends anyway (Chromium after touch emulation turns on), tracking
the finger twice, so a one-finger long press never recognized. Each touch
handler now returns while usesTouchEvents is false, after noting the
fingers down, as install()'s comment promised.
…box imports coarsePointer from utils/ (fZ moved it)
…x.tsx: this branch's imports with coarsePointer from utils/ (fZ moved it)
…n before the click guard

internal/motion.ts (meta#8562) re-exports UIKIT_CLICK from the barrel, so a
tree carrying both fails typecheck and the Storybook build since this
pull request dropped it. The barrel keeps it, with the click guard's
window, and the table of every gesture group still leaves it out. Fails
until the barrel exports it.
…'s window

This pull request dropped UIKIT_CLICK when the click guard took the
stray-click window over, but importers written before it read the barrel's
UIKIT_CLICK (internal/motion.ts in meta#8562 re-exports it), so a tree carrying
both failed typecheck and the Storybook build. UIKIT_CLICK stays in the
barrel, deprecated, with the guard's 400 ms (CLICK_GUARD_WINDOW_MS), and
UIKIT_GESTURES still leaves it out.
vjeux added 12 commits October 27, 2026 14:10
…arrives opens when it does) into meta#8197

# Conflicts:
#	packages/core/src/Lightbox/Lightbox.tsx
targetRef is not read in this PR's zoom stage (the drag to dismiss, in
meta#8215, is its first reader); its unused destructure was a lint warning.
Resolution: the zoom stage keeps targetRef in its destructure (meta#8197 drops
it as unused; the drag to dismiss here is its first reader).
Resolution: this PR's open/close effect (the zoom transition's) replaces
the plain one; it takes main's meta#8741 change: with no item there is no
dialog, so the effect waits for one (`hasItem` in its guard and its
dependencies) and a viewer opened before its first item arrives opens when
it does. meta#8741's test failed on this effect without it.
Resolution: the imports take meta#8231's (liftCopy and openingFadeStart; the
source portal helpers it deleted go) beside this PR's chrome curves.
…ls, and a single photo takes it too

astracat's review of meta#8746:
- A keyboard activation fires a click too, with no click count and at
  (0, 0): after a keyboard zoom, Enter on Next armed the toggle and the
  next photo opened with every control hidden while Next kept the focus.
  The toggle arms only for a click with a count, outside the controls, and
  a page change calls a pending one off.
- The hit test looked for a gallery page's current image only, so a single
  zoomable photo ignored the click; it now tests the photo the zoom stage
  moves, in a gallery or alone.
Each has a browser test in the chrome spec that fails on the head before.
…s (0, 0)

Enter on Next came while the zoom was still starting, before the photo
covered (0, 0), so the test passed on the head before the fix too; once it
waits for the zoom, it fails there (every control hidden) and passes with
the fix.
…: wheel and trackpad scrolling, still-released notches; c24-8147-base 3c7b077) into meta#8746 995bd48, the base of the FR43 branch
… (spec:AST-094 FR43)

The zoom stage's recognizers (pan, double tap, tap, touch-down, drag to
dismiss) and the gallery's pager attach for every pointer; the pinch stays
a finger's, and a ctrl-wheel reaches it through meta#8676. A mouse drag down
closes the viewer, a sideways drag or a trackpad's scroll pages, a
ctrl-wheel zooms and, out from the fitted size, closes, a wheel pans a
zoomed photo, and a released drag coasts. A double-click is the stage's
double tap (the zoom core's own mouse path goes); a pointer's zoom hides the
controls and the keyboard's 2x leaves them. A release follows the pointer
that began it: DR2 for a finger, DR3 for a mouse, a trackpad or a pen on a
fine primary pointer. A stage tap recognized after a page change toggles
nothing. The stage fills the viewer for every pointer, and meta#8242's
unguarded getAnimations() uses the motion system's capability check.

Tests: LightboxEveryPointer's ten proofs (each failing on meta#8746's build,
with the wheel deltas the page sees); the fine-pointer proofs that asserted
the old behaviour flip to FR43's; the chrome spec's page-change proof failed
before the tap guard. Docs in English and Chinese; a patch changeset.
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 28, 2026 12:11am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 28, 2026
@astracat-bot

astracat-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 28, 2026, 12:43 AM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 28, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 28, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request can't be approved yet: an engineering owner needs to provide or identify the governing spec for the Lightbox photo-viewer gestures it ships and extends to a mouse, a pen and a trackpad (the Lightbox component record is still a draft), and to approve or reject the layer-runtime INV13 and AST-057 wording this head adds. Any engineering owner, the author included, can record that decision by commenting /approve-spec followed by the full head SHA. Independently of that decision, these changes are needed:

  • Land #8746, #8676 and their bases first (or split them out) so this pull request covers its own 1,276-line change instead of a 59,918-line range.
  • Fix the Ctrl+wheel round trip that leaves the controls hidden and the background black at the fitted size: the Control keydown marks the last input as the keyboard, so the return to the fitted size never shows them.
  • Update scripts/touch-support-bundle.test.mjs, which fails because Core's root entry now bundles the gesture modules Lightbox imports.
  • Show fine-pointer before-and-after evidence for the single photo's stage now filling the viewer (its focus ring and zoom cursor), as AST-017 FR59 asks, and drop or implement the AST-057 motion-role wording, which the row's code does not follow yet.

[Automated review]

// Photos hides the controls as a gesture zooms, a mouse's, a pen's
// or a finger's alike (spec:AST-094 FR43); a zoom from the keyboard
// leaves them, so the keys that zoomed can reach them.
if (lastInputRef.current !== 'keyboard') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Ctrl+wheel zoom in and back out leaves the controls hidden and the background black at the fitted size. Holding Control fires a keydown in the dialog, so lastInputRef reads 'keyboard' (line 946) and this branch skips the show on the return, while onPinchZoomed (line 1140) hid them unconditionally; no wheel event updates lastInputRef. Ignoring modifier-only keys and recording a wheel as pointer input, or giving the hide and the show one gate, fixes it, and the ctrl-wheel proof in LightboxEveryPointer.interaction.browser.spec.ts could zoom back out and assert the controls show.

styles.imageWrapper,
isGallery && styles.imageWrapperGallery,
isZoomTarget && !isGallery && styles.imageWrapperZoomTarget,
isZoomTarget && !isGallery && styles.imageWrapperFill,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the touch gate gone, a single zoomable photo's stage fills the viewer under a fine pointer too, and this element also carries the :focus-visible outline and the zoom-in cursor (lines 1911–1912): the keyboard focus ring and the zoom cursor now cover the whole stage, including the dark area where a click closes the viewer. AST-017 FR59 asks for fine-pointer before-and-after evidence for that; keeping the outline and cursor on the photo's box would avoid the change.

- **Gesture tuning.** Axis-lock distance, commit ratio, fling velocity,
resistance past the panel: behavior constants in source, not public API and
not design tokens (the same ruling the touch press model received for its
clocks). The row's slide and spring motion plays on theme motion roles

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says the row's slide and spring play on theme motion roles, but packages/core/src/Item/useSwipeAction.ts still plays SWIPE_SETTLE_MS = 200, and nothing in this pull request changes it. Either drop this edit here or move the row's settle onto motion roles in the same change.

@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Lightbox (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 394B 394B 0B
Lines of Code 804 4147 -
Complexity Very High Very High (308) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.0KB 57.6KB +603B 12.4KB 12.4KB 0B
core/AppShell 58.0KB 58.6KB +547B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 40.1KB 40.7KB +631B 6.4KB 6.4KB 0B
core/Banner 48.0KB 48.5KB +592B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.5KB 55.0KB +554B 9.9KB 9.9KB 0B
core/BottomSheet 33.9KB 33.9KB 0B 7.9KB 7.9KB 0B
core/BottomSheetSwitcher 14.8KB 14.8KB 0B 3.3KB 3.3KB 0B
core/BreadcrumbItem 66.5KB 67.3KB +784B 11.0KB 11.0KB 0B
core/Button 43.4KB 44.0KB +602B 9.2KB 9.2KB 0B
core/Calendar 55.1KB 55.6KB +600B 10.0KB 10.0KB 0B
core/Carousel 50.4KB 50.9KB +586B 10.2KB 10.2KB 0B
core/ChatComposer 60.8KB 61.4KB +593B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.9KB 56.5KB +570B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.9KB 56.5KB +570B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.6KB 47.2KB +584B 9.5KB 9.5KB 0B
core/ChatLayout 49.0KB 49.6KB +610B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.5KB 47.1KB +592B 9.6KB 9.6KB 0B
core/ChatSendButton 46.0KB 46.5KB +575B 9.2KB 9.2KB 0B
core/CheckboxInput 50.1KB 50.6KB +560B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.2KB 54.8KB +546B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.4KB +626B 6.9KB 6.9KB 0B
core/CodeBlock 60.7KB 61.2KB +584B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.3KB +608B 4.2KB 4.2KB 0B
core/CommandPalette 51.0KB 51.6KB +618B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 9.0KB +596B 3.5KB 3.5KB 0B
core/ComplexSelector 56.0KB 56.5KB +580B 10.6KB 10.6KB 0B
core/ContextMenu 74.7KB 75.5KB +798B 14.6KB 14.6KB 0B
core/ContextMenuItem 53.0KB 53.6KB +557B 9.6KB 9.6KB 0B
core/DateInput 99.7KB 100.3KB +610B 14.8KB 14.8KB 0B
core/DateRangeInput 69.5KB 70.1KB +617B 11.2KB 11.2KB 0B
core/DateTimeInput 109.8KB 110.4KB +613B 15.2KB 15.2KB 0B
core/DialogHeader 49.0KB 49.5KB +580B 9.8KB 9.8KB 0B
core/DrawerHeader 49.1KB 49.6KB +552B 11.2KB 11.2KB 0B
core/DropdownMenu 72.8KB 73.6KB +759B 14.7KB 14.7KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.7KB +617B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 16.0KB +611B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.8KB +607B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.2KB 58.9KB +726B 9.5KB 9.5KB 0B
core/FileInput 54.3KB 54.8KB +546B 10.3KB 10.3KB 0B
core/IconButton 43.5KB 44.1KB +602B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.9KB +606B 4.7KB 4.7KB 0B
core/LayerProvider 63.4KB 64.0KB +600B 10.1KB 10.1KB 0B
core/Lightbox 51.4KB 102.4KB +51.1KB ⚠️ 9.8KB 10.2KB +463B
core/Link 43.7KB 44.2KB +590B 7.6KB 7.6KB 0B
core/ListItem 53.7KB 54.2KB +553B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 145.2KB +763B 17.4KB 17.4KB 0B
core/MobileNav 51.2KB 51.7KB +555B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.4KB 46.9KB +572B 9.4KB 9.4KB 0B
core/MoreMenu 73.2KB 73.9KB +745B 14.7KB 14.7KB 0B
core/MultiSelector 73.1KB 73.7KB +630B 14.6KB 14.6KB 0B
core/NumberInput 55.9KB 56.4KB +598B 10.7KB 10.7KB 0B
core/Outline 28.8KB 29.3KB +589B 4.0KB 4.0KB 0B
core/Pagination 84.9KB 85.7KB +775B 15.3KB 15.3KB 0B
core/Popover 50.0KB 50.6KB +604B 12.1KB 12.1KB 0B
core/PowerSearch 158.2KB 159.0KB +818B 19.9KB 19.9KB 0B
core/RadioListItem 52.4KB 52.9KB +571B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 9.2KB +597B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.4KB +623B 7.1KB 7.1KB 0B
core/Selector 76.5KB 77.2KB +779B 15.0KB 15.0KB 0B
core/SelectorOption 14.5KB 15.1KB +602B 5.1KB 5.1KB 0B
core/SideNav 58.1KB 58.7KB +588B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.8KB 47.3KB +573B 9.5KB 9.5KB 0B
core/SideNavHeading 54.9KB 55.4KB +564B 10.1KB 10.1KB 0B
core/SideNavItem 54.6KB 55.2KB +551B 10.1KB 10.1KB 0B
core/Step 32.6KB 33.1KB +582B 5.3KB 5.3KB 0B
core/Stepper 47.5KB 48.1KB +567B 9.6KB 9.6KB 0B
core/Switch 48.5KB 49.1KB +578B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.3KB +606B 4.1KB 4.1KB 0B
core/TabMenu 51.8KB 52.3KB +579B 10.0KB 10.0KB 0B
core/Table 83.9KB 84.7KB +797B 15.1KB 15.1KB 0B
core/TableCell 75.8KB 76.5KB +799B 14.8KB 14.8KB 0B
core/TableHeaderCell 75.5KB 76.3KB +822B 14.8KB 14.8KB 0B
core/TableSelectionToolbar 52.5KB 53.1KB +601B 11.2KB 11.2KB 0B
core/TextInput 53.7KB 54.3KB +579B 10.5KB 10.5KB 0B
core/Thumbnail 50.8KB 51.4KB +598B 11.0KB 11.0KB 0B
core/TimeInput 86.9KB 87.5KB +533B 13.8KB 13.8KB 0B
core/Timestamp 27.7KB 27.7KB +1B 9.5KB 9.5KB 0B
core/Toast 60.0KB 60.6KB +580B 9.7KB 9.7KB 0B
core/ToggleButton 44.3KB 44.8KB +563B 9.3KB 9.3KB 0B
core/Token 29.7KB 30.3KB +600B 4.5KB 4.5KB 0B
core/Tokenizer 68.8KB 69.3KB +611B 16.5KB 16.5KB 0B
core/TopNav 52.0KB 52.5KB +545B 10.4KB 10.4KB 0B
core/TopNavHeading 54.3KB 54.8KB +557B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.6KB +605B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.3KB 54.9KB +600B 10.8KB 10.8KB 0B
core/TopNavMegaMenuItem 9.1KB 9.7KB +603B 4.1KB 4.1KB 0B
core/TopNavMenu 53.7KB 54.3KB +580B 10.1KB 10.1KB 0B
core/TreeList 32.7KB 33.2KB +581B 4.7KB 4.7KB 0B
core/Typeahead 61.6KB 62.2KB +591B 11.3KB 11.3KB 0B
lab/ChatEmojiPicker N/A 65.8KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.4KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 94.7KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.1KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 267.4KB N/A N/A 2.4KB N/A
lab/Comments N/A 272.0KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.3KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.6KB N/A N/A 722B N/A
lab/ListInput N/A 75.8KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 158.8KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 91.7KB N/A N/A 775B N/A
lab/Schedule N/A 60.5KB N/A N/A 3.9KB N/A
lab/TourStep N/A 56.0KB N/A N/A 590B N/A
lab/TransferList N/A 67.4KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 73.0KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.4KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 213.3KB N/A N/A 454B N/A
richtext/RichTextView N/A 251.9KB N/A N/A 2.3KB N/A

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

⚠️ Touch-only size budget

These components grew past the touch-only budget (2 KB gzip JavaScript or 1 KB gzip CSS per component). If the growth runs only on a coarse pointer, load its JavaScript lazily so the import does not carry it, or justify the cost in the pull request description; CSS stays in the package stylesheet, so justify it.

Component JavaScript CSS
core/Lightbox +51.1KB ⚠️ +463B

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.6KB 34.8KB +201B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@vjeux

vjeux commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: its every-pointer change now lands in each feature's own PR, #8148 to #8252, on #8665 and #8676.

@vjeux vjeux closed this Oct 28, 2026
@vjeux
vjeux deleted the uikit/lightbox-every-pointer branch October 28, 2026 07:06

This branch was successfully deployed

1 active deployment
Preview — 7f75a163 Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant