Skip to content

feat(Lightbox): a click on the photo toggles the controls, as a tap does - #8746

Closed
vjeux wants to merge 1961 commits into
meta:mainfrom
vjeux:uikit/c27-lightbox-hide-tap-every-pointer
Closed

vjeux wants to merge 1961 commits into
meta:mainfrom
vjeux:uikit/c27-lightbox-hide-tap-every-pointer

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8252. Review only this PR's change: vjeux/astryx@bd7a9cb...995bd48

Why

With a mouse or a trackpad, nothing hides Lightbox's controls: #8242's tap works on touch screens only. spec:AST-094 FR43 says gestures attach for every pointer.

What led to it

The UIKit-parity work for Astryx: the first step to every pointer, before the gesture change that follows this PR.

Options and tradeoffs

  • Wait for that full change: one model, but much larger.
  • Answer a click in the viewer's click handler (this PR): small. It waits out the double-click interval (350 ms), so a double-click still zooms. Only a pointer's click counts: Enter on a button fires a click too, and that one is ignored.

Why this choice

It is the smallest change that gives every pointer the tap's behavior today. The cost is 87 bytes of gzip JavaScript (102,259 → 102,346).

How to check it

In the "Gallery with zoom" story, with a mouse:

  1. Click the photo: after a moment the controls hide. Click again: they return.
  2. Double-click: the photo zooms, and the controls stay.
  3. With the controls hidden, click beside the photo: they return.
  4. Focus the photo, press Enter to zoom, then press Enter on Next: the controls stay, with focus on Next.
  5. Open the "With zoom" story and click its single photo: the controls hide.

Evidence

After a click, before After a click, this PR
the controls stay the controls hidden, on black

CI's pr-interaction red here is #8148's double-tap zoom busy proof, inherited from #8148 and fixed there.

cixzhang and others added 30 commits October 26, 2026 20:19
…ta#8521) into meta#8270

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
… the lift, under real touch input

spec:AST-094 FR20's "follows the scroll to the lift" had only a jsdom
test with a synthetic takeover; the FR18-FR22 verification row asks for
real touch input. In Chromium with injected touches, a vertical drag on a
list a watcher pan is attached to scrolls the list natively, no touchmove
is cancelled, and the watcher goes from began to ended at the lift.
… at the edge stays the page's

spec:AST-094 FR10: two fingers before a claim are the page's pinch. The
scroll-edge hold (DEC-18) counted any finger, so with two fingers down it
cancelled the pinch's moves before any drag was claimed. It now holds only
while a single finger is down. The new two-finger test fails on the
previous head.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…s watcher browser test) into the arena fixes
… is down off the sheet (spec:AST-094 FR10)

The lone-finger hold counts only the fingers the pan tracks, so a second
finger on plain page content left the sheet's finger held, and the moves
of a pinch across the sheet's edge were cancelled. This test fails until
the host counts every finger on the screen.
…is down anywhere (spec:AST-094 FR10)

The lone-finger hold counted only the fingers the pan tracks, so a second
finger on plain page content left the sheet's finger held, and a pinch
across the sheet's edge had its moves cancelled. The host now applies a
recognizer's hold only while the move's event reports one finger on the
screen; the pan's own count still covers two fingers on the sheet.
An attach reads the element's own will-change in a task of its own. A
shared animator (animatorFor) is never detached, so its node can leave the
document with that read still waiting, and the read then forces a style
pass on a node with no style: a test run that mounts and unmounts many
pagers piled up such reads until a later wait timed out. The read now
skips a node out of the document; a promotion that needs it reads it then,
as one on a node attached in the same task does. A detach and dispose
already cancel it.

The new test fails on the previous code (one read of a removed node); the
in-task read test now puts its element in the document, where the read
happens.
@vercel

vercel Bot commented Oct 27, 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 27, 2026 10:40pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

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

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 27, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026
vjeux added 5 commits October 27, 2026 14:20
…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.

@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.

Requesting changes. Against main this branch also carries the whole open #8252 stack (about 49.7k changed lines, of which this change is 73), so please land #8252 first and rebase so this pull request is just its own commit. Two issues in the change itself:

  • After zooming with Enter, pressing Enter on Next hides every control while keyboard focus stays on the now-invisible Next button; arming the toggle only for clicks with detail >= 1 outside the controls, and clearing the timer when the photo changes, would fix it.
  • A single zoomable photo (no gallery) ignores the click, because the hit test only finds [data-current] img, which only gallery pages have, while the updated hasZoom docs say a click hides the controls.

[Automated review]

// second click of a double-click, which zooms, calls it off.
if (onPhoto && hasZoom && !isVideo && !stageTaps && !dragged) {
clearTimeout(clickToggleRef.current);
if (e.detail < 2) {

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 keyboard activation reaches this branch with detail 0 at (0, 0). After zooming with Enter, Enter on Next has already moved the track with pager.goTo, but the zoomed photo's box still covers (0, 0), so this arms the toggle and the next photo opens with every control hidden while Next keeps focus. Arming only for e.detail >= 1 and a target outside [data-lightbox-chrome], and clearing the timer when the item changes, avoids it.

Comment thread packages/core/src/Lightbox/Lightbox.tsx Outdated
// double-click zooms it): the photo takes no pointer events, so the
// click lands on its box.
const onPhoto = [
...e.currentTarget.querySelectorAll('[data-current] img'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Only gallery pages carry data-current, so for a single image with hasZoom this finds no image and a mouse click never toggles the controls, although the updated hasZoom docs say it does. Hit-testing the current image in both modes (the one stage.contentRef points at) would cover it.

vjeux added 4 commits October 27, 2026 14:29
…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.
@github-actions

github-actions Bot commented Oct 27, 2026 •

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 4247 -
Complexity Very High Very High (328) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.0KB 57.3KB +292B 12.4KB 12.4KB 0B
core/AppShell 58.0KB 58.3KB +239B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 40.1KB 40.3KB +304B 6.4KB 6.4KB 0B
core/Banner 48.0KB 48.2KB +290B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.5KB 54.7KB +242B 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 66.8KB +297B 11.0KB 11.0KB 0B
core/Button 43.4KB 43.7KB +267B 9.2KB 9.2KB 0B
core/Calendar 55.1KB 55.3KB +283B 10.0KB 10.0KB 0B
core/Carousel 50.4KB 50.6KB +269B 10.2KB 10.2KB 0B
core/ChatComposer 60.8KB 61.0KB +276B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.9KB 56.2KB +266B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.9KB 56.2KB +265B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.6KB 46.9KB +266B 9.5KB 9.5KB 0B
core/ChatLayout 49.0KB 49.2KB +272B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.5KB 46.8KB +255B 9.6KB 9.6KB 0B
core/ChatSendButton 46.0KB 46.2KB +263B 9.2KB 9.2KB 0B
core/CheckboxInput 50.1KB 50.3KB +268B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.2KB 54.5KB +256B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.1KB +289B 6.9KB 6.9KB 0B
core/CodeBlock 60.7KB 60.9KB +273B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.0KB +291B 4.2KB 4.2KB 0B
core/CommandPalette 51.0KB 51.3KB +280B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 8.7KB +280B 3.5KB 3.5KB 0B
core/ComplexSelector 56.0KB 56.3KB +279B 10.6KB 10.6KB 0B
core/ContextMenu 74.7KB 75.0KB +279B 14.6KB 14.6KB 0B
core/ContextMenuItem 53.0KB 53.3KB +270B 9.6KB 9.6KB 0B
core/DateInput 99.7KB 99.9KB +276B 14.8KB 14.8KB 0B
core/DateRangeInput 69.5KB 69.8KB +303B 11.2KB 11.2KB 0B
core/DateTimeInput 109.8KB 110.1KB +295B 15.2KB 15.2KB 0B
core/DialogHeader 49.0KB 49.2KB +272B 9.8KB 9.8KB 0B
core/DrawerHeader 49.1KB 49.3KB +248B 11.2KB 11.2KB 0B
core/DropdownMenu 72.8KB 73.1KB +278B 14.7KB 14.7KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.4KB +286B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 15.7KB +285B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.5KB +276B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.2KB 58.5KB +267B 9.5KB 9.5KB 0B
core/FileInput 54.3KB 54.5KB +262B 10.3KB 10.3KB 0B
core/IconButton 43.5KB 43.7KB +272B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.6KB +273B 4.7KB 4.7KB 0B
core/LayerProvider 63.4KB 63.7KB +285B 10.1KB 10.1KB 0B
core/Lightbox 51.4KB 99.9KB +48.6KB ⚠️ 9.8KB 10.2KB +463B
core/Link 43.7KB 43.9KB +281B 7.6KB 7.6KB 0B
core/ListItem 53.7KB 53.9KB +261B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 144.7KB +238B 17.4KB 17.4KB 0B
core/MobileNav 51.2KB 51.4KB +261B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.4KB 46.6KB +259B 9.4KB 9.4KB 0B
core/MoreMenu 73.2KB 73.4KB +257B 14.7KB 14.7KB 0B
core/MultiSelector 73.1KB 73.4KB +318B 14.6KB 14.6KB 0B
core/NumberInput 55.9KB 56.1KB +290B 10.7KB 10.7KB 0B
core/Outline 28.8KB 29.0KB +285B 4.0KB 4.0KB 0B
core/Pagination 84.9KB 85.2KB +258B 15.3KB 15.3KB 0B
core/Popover 50.0KB 50.3KB +285B 12.1KB 12.1KB 0B
core/PowerSearch 158.2KB 158.5KB +268B 19.9KB 19.9KB 0B
core/RadioListItem 52.4KB 52.6KB +264B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 8.9KB +285B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.1KB +294B 7.1KB 7.1KB 0B
core/Selector 76.5KB 76.7KB +275B 15.0KB 15.0KB 0B
core/SelectorOption 14.5KB 14.8KB +256B 5.1KB 5.1KB 0B
core/SideNav 58.1KB 58.4KB +277B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.8KB 47.0KB +248B 9.5KB 9.5KB 0B
core/SideNavHeading 54.9KB 55.1KB +248B 10.1KB 10.1KB 0B
core/SideNavItem 54.6KB 54.9KB +270B 10.1KB 10.1KB 0B
core/Step 32.6KB 32.8KB +279B 5.3KB 5.3KB 0B
core/Stepper 47.5KB 47.8KB +270B 9.6KB 9.6KB 0B
core/Switch 48.5KB 48.8KB +279B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.0KB +294B 4.1KB 4.1KB 0B
core/TabMenu 51.8KB 52.0KB +276B 10.0KB 10.0KB 0B
core/Table 83.9KB 84.2KB +280B 15.1KB 15.1KB 0B
core/TableCell 75.8KB 76.0KB +277B 14.8KB 14.8KB 0B
core/TableHeaderCell 75.5KB 75.8KB +282B 14.8KB 14.8KB 0B
core/TableSelectionToolbar 52.5KB 52.8KB +278B 11.2KB 11.2KB 0B
core/TextInput 53.7KB 54.0KB +274B 10.5KB 10.5KB 0B
core/Thumbnail 50.8KB 51.1KB +273B 11.0KB 11.0KB 0B
core/TimeInput 86.9KB 87.2KB +222B 13.8KB 13.8KB 0B
core/Timestamp 27.7KB 27.7KB 0B 9.5KB 9.5KB 0B
core/Toast 60.0KB 60.2KB +258B 9.7KB 9.7KB 0B
core/ToggleButton 44.3KB 44.5KB +245B 9.3KB 9.3KB 0B
core/Token 29.7KB 30.0KB +289B 4.5KB 4.5KB 0B
core/Tokenizer 68.8KB 69.0KB +284B 16.5KB 16.5KB 0B
core/TopNav 52.0KB 52.3KB +274B 10.4KB 10.4KB 0B
core/TopNavHeading 54.3KB 54.5KB +223B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.3KB +283B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.3KB 54.6KB +274B 10.8KB 10.8KB 0B
core/TopNavMegaMenuItem 9.1KB 9.4KB +281B 4.1KB 4.1KB 0B
core/TopNavMenu 53.7KB 54.0KB +258B 10.1KB 10.1KB 0B
core/TreeList 32.7KB 33.0KB +279B 4.7KB 4.7KB 0B
core/Typeahead 61.6KB 61.9KB +269B 11.3KB 11.3KB 0B
lab/ChatEmojiPicker N/A 65.5KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.0KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 94.4KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 47.8KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 264.9KB N/A N/A 2.4KB N/A
lab/Comments N/A 269.2KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.0KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.3KB N/A N/A 722B N/A
lab/ListInput N/A 75.5KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 158.5KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 91.4KB N/A N/A 775B N/A
lab/Schedule N/A 60.2KB N/A N/A 3.9KB N/A
lab/TourStep N/A 55.7KB N/A N/A 590B N/A
lab/TransferList N/A 67.1KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 72.7KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.1KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 212.8KB N/A N/A 454B N/A
richtext/RichTextView N/A 251.6KB 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 +48.6KB ⚠️ +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

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 27, 2026
@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Both fixed, each with a browser test that failed before: vjeux/astryx@7f4b4d3...995bd48

  • Keyboard: only a click with a count, outside the controls, arms the toggle now, and a page change calls a pending one off. The test zooms with Enter, waits until the photo covers (0, 0), and presses Enter on Next: the controls stay, and Next keeps the focus.
  • Single photo: the hit test uses the photo the zoom stage moves, so a single zoomable photo takes the click.

The stack lands bottom-up; until #8252 is in, the description's link shows this PR's own change.

@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.

Approval waits on an engineering owner's decision on architecture:layer-runtime INV13 ("One exit copy per closing layer"), which this branch carries from the #8252 stack and main does not yet state; an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Separately, please land #8252 first and rebase so this pull request is just its own commits: against main it carries about 54.3k changed lines, of which this change is 253. One issue remains in the change itself:

  • A click on the photo followed within 350 ms by Tab moves focus to Next and then hides every control, leaving keyboard focus on an invisible button; calling off the pending toggle on keydown, or when a control takes focus, with a click-then-Tab browser spec, would fix it.

The single-photo and keyboard-zoom issues raised earlier are fixed on this head.

[Automated review]

Comment on lines +836 to +839
if (e.detail < 2) {
clickToggleRef.current = setTimeout(() => {
toggleChromeRef.current();
}, UIKIT_TAP.maximumIntervalBetweenTaps.value);

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 arms the toggle, and nothing calls it off when a keyboard focus lands on a control: click the photo, press Tab within 350 ms, and focus sits on Next while every control fades to opacity 0 with no visible focus ring. Clearing clickToggleRef on keydown, or when a control inside [data-lightbox-chrome] takes focus, with a click-then-Tab case in the chrome spec, would fix it.

@vjeux

vjeux commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: the click now toggles the controls through the stage's tap in #8242. Closing in its favor; the branch stays.

@vjeux vjeux closed this Oct 28, 2026

This branch was successfully deployed

1 active deployment
Preview — 995bd483 Deployed Oct 27, 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.

2 participants