Skip to content

feat(Lightbox): a gallery's caption sits in a two-line band - #9810

Draft
vjeux wants to merge 17 commits into
meta:mainfrom
vjeux:uikit/c24-caption-row
Draft

vjeux wants to merge 17 commits into
meta:mainfrom
vjeux:uikit/c24-caption-row

Conversation

@vjeux

@vjeux vjeux commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #8197 → #9810

Stacked on #8197's 3ee5207, on main 1c142dc. This head merges #8197's head into the previous head c83f03d with no conflict, and this pull request's own diff is unchanged. Review this pull request's two commits: the caption band, vjeux@0fc994c, and the answers to CodeBunny's 10:27Z review, vjeux@c83f03d

The design owner's option C for a gallery's captions (card 46): a constant photo area; the caption in a fixed two-line band; a longer caption clamps, and "more" opens the full caption over the bottom of the photo on a scrim; the full caption is always in the accessible description; no hidden sizing copies.

It answers the design owner's re-check (card 46C (Cindy)): the caption sat too close to the bottom, its second line was indented, its size was unusual, and the Close button showed an outline on a tap's opening. The caption must also meet 4.5:1 over a mid-grey photo (Specs decision, Nov 5 2026). Lightbox renders through Astryx parts on a media surface:

  • The viewer's content is inside MediaTheme (dark), so its controls, caption and counter take the theme's dark surface tokens and its rules for them. No Lightbox color is left beyond the controls' and caption's backing and the on-dark color (AR9).
  • The caption and the counter are Text in the body type, so both caption lines start at one edge. Owner decision, Nov 5 2026 (Cindy): body text.
  • A page with no caption shows no band and no backing (card 46D), and its photo stays put: in a gallery with any caption, every page reserves the band's space, transparent where the page has no caption, so every page's photo box, and its top, is the same.
  • "More" and "less" are a ghost Button one caption line tall, at the end of the caption's last line, so the caption keeps the band's whole width and the clamp's "…" comes just before "more" (card 46D). Its hit area reaches out toward 44 px under a finger, sideways and down, never into the line above. An open caption that scrolls in its panel keeps the toggle's width clear at the end of every line.
  • The caption's backing is dark enough for 4.5:1 over a mid-grey photo in every arm of docs(Lightbox): caption type comparison story for review (not for merge) #10084: the body caption measures 17.2:1 there.
  • The band's padding clears the screen's bottom edge, plus the safe area there.
  • Focus the viewer moves follows the last input in the viewer's own document, a Picture-in-Picture window's included. So a tap's opening paints no ring on Close, and a key's still does.
  • In a gallery, the keyboard's zoom button is the current page's photo box, so the caption band and "more" sit outside it, and Enter or Space on "more" toggles the caption. Visible difference: a keyboard user's zoom focus ring outlines the photo box, not the whole viewer area.
  • Paging away from "more", or from the focused photo, by a key, a button or a swipe, moves focus to the new page's photo, or to Close, through Core's shared focus-return helpers, so the next arrow still pages. "Less" keeps its focus. Each page renders its caption once: "more" opens the same element in place.
  • Closing the viewer returns focus to its opener through Core's shared return, which never scrolls the page.

This round answers the review on the rebased head: the toggle no longer sits inside the zoom button, "less" keeps its focus, and FR17 names the caption's backing. Out of scope: the backdrop keeps the page theme's --color-overlay and blur, as today. ::backdrop inherits from the dialog element, outside the media surface.

User impact

In a Lightbox gallery, every page has a caption band two caption lines tall under its photo, whatever its caption. So the photo area is the same on every page, and paging never resizes it. A caption longer than two lines shows two lines and a "more" button. "More" opens the whole caption over the bottom of the photo, on a dark backing, and "less" or paging closes it. Under a finger the button takes taps out toward 44 px, and its tap is its own, not the photo's. A screen reader gets the whole caption as the viewer's description, clamped or open.

Contrast

Measured on #10084's exact preview (4c07f5c, stacked on this pull request) in Chromium: default theme, a 402 px phone viewport, coarse pointer, text against what lies under it:

Caption In the band Open over a white photo Over a mid-grey photo (rgb 127) Over a black photo
Body (16 px, on-dark) 17.2:1 14.7:1 17.2:1 19.2:1
Supporting (14 px, secondary) 6.4:1 5.5:1 6.4:1 7.2:1
Supporting, primary color 17.2:1 14.7:1 17.2:1 19.2:1

Across the shipped themes (butter, chocolate, gothic, matcha, stone), body stays at or above 7.3:1, matcha the lowest. Supporting in its secondary color falls to 2.7:1 in matcha and 3.8:1 in chocolate over a white photo; the other themes give 5.0–5.6:1.

Record change

  • component:Lightbox FR1 gains: "In a gallery, Caption sits in a band two caption lines tall under every page's Media. A longer caption shows two lines and a "more" button that opens it in full over the bottom of the Media, on a scrim. The full caption is always the viewer's accessible description. The caption band uses Astryx parts: the caption text is Text with the body type, and "more"/"less" is a ghost Button, not hand-styled elements."
  • New FR17: "The viewer is a media surface: it renders inside MediaTheme (dark), so its controls and caption take the theme's dark surface tokens and the active theme's component rules (architecture:theme-application INV8), with no Lightbox-owned colors beyond AR9's backing, the caption's backing and the on-dark color." Its basis reads "Specs decision, Nov 5 2026". Its verification row names the unit file and the browser interaction spec, and states the caption's contrast over a white, a mid-grey and a black photo (body 14.7:1 over white).
  • spec:AST-038's existing-code row now reads: "Lightbox's caption renders through Text under MediaTheme (component:Lightbox)", with the rest of the row unchanged.

Writing rubric results

  • R1 narration: none. The new sentences state present behavior. The body type stands until the owner's pick on the 46C re-check; if the pick is supporting, FR1 and the code change together before merge.
  • R2 research in the record: one measured value, FR17's verification row's "body 14.7:1 over white". The design owner's side asked for the shipped type's ratio in that row, so it stays.
  • R3 evidence naming a change: none.
  • R4 rejected alternatives: none in the record.
  • R5 pull request or issue references: none in the record.
  • R6 answered open questions still listing options: none.

Visual authority

  • Existing component/module DD row reused or amended.
  • Durable component-local intent added to the optional Design decisions table by a team design owner.
  • Correction already settled by current family, design, theme, or objective accessibility authority.
  • New shared or cross-component direction settled in docs/design/.

The design owner chose option C on card 46 and asked for these fixes on card 46C; the record states them in FR1 and FR17.

How to check

  1. Open "Gallery with captions" on a phone. The first caption shows two lines at the body size, both starting at one edge, with "more" after the last line. Its band sits clear of the screen's bottom edge.
  2. Swipe through the four photos. The photo keeps one size on every page, and the band stays two lines tall. The third photo has no caption.
  3. On the first photo, tap "more". The whole caption opens over the bottom of the photo, on a dark backing, readable over the photo's light parts. Tap "less", or swipe to the next photo, to close it.
  4. Close the viewer and open it again with a tap: Close shows no outline. Open it from the keyboard: Close shows its focus ring.
  5. With VoiceOver on, the viewer's description reads the whole first caption.

State matrix

  • Captions: none, short, about two lines, longer than two lines. A page with no caption keeps the band's height empty, once any item in the gallery has a caption. A gallery with no captions has no band.
  • Pointers: a finger (the toggle's own tap, its 44 px target) and a mouse; both open and close the full caption, and the viewer stays open.
  • Opening: by a tap (no ring on Close) and by a key (Close's ring).
  • An open full caption too long for its panel (60 % of the viewport's height) scrolls under a finger.
  • Paging closes an open full caption and moves focus off "more".

Minimal DOM

Element counts for the current page's caption, measured in Chromium on Storybook builds. Main renders a gallery's caption as one Text under the photo, with no clamp and no toggle.

State main c56f4e1 this head
A caption that fits its two lines 1 4 7
A clamped caption, "more" n/a 12 11
The open caption, "less" n/a 12 11
A page with no caption, in a gallery with captions 0 1 1

Every captioned page renders the same elements in every state. The toggle is the only element that comes and goes, and only while the caption runs past two lines. Each element's job:

  1. The band: the page's caption row. It keeps the same space on every page, so every photo box matches, and it carries the backing.
  2. The caption panel: the element "more" opens over the photo, and the target of the toggle's aria-controls.
  3. The caption body: the toggle's containing block. It does not scroll with an open caption's lines, so the toggle stays in view.
  4. The caption Text: the caption itself, also the viewer's description.
  5. The line block: a block inside the clamp's box, so the clamp counts its lines and its floats wrap them. WebKit's clamp box ignores floats.
  6. and 7. Two floats: the toggle's place at the end of the last line. Without a toggle they have no size.
  7. The toggle: a ghost Button (4 elements with its label), only while the caption runs past two lines. It hosts its own position, so the slot that wrapped it is gone.

Testing

  • At this head (ced33fe): the same own diff as c83f03d (patch-id unchanged), merged onto feat(Lightbox): swipe between gallery photos like Photos #8197's 3ee5207 without conflict.

  • At this head (c83f03d), CodeBunny's 10:27Z items and two pre-review follow-ups (vjeux@c83f03d):

    • A gallery caption whose text grows while its box keeps two lines now gets "more". The clamp comes from Text's own truncation check, through useHeldTruncation, a variant of useTruncation internal to core. It keeps its answer while the caption is open, so the toggle stays mounted, with its focus, as the caption closes. useTruncation's public options don't change, and plain useTruncation runs no new effect, so Text and Heading pay nothing new.
    • A closed dialog lays its caption out at no size, so Lightbox measures the caption again in the commit that opens the dialog, before the first paint. The first frame that shows a clamped caption already ends it in "more".
    • The changeset says that a gallery's caption keeps 4.5:1. A single photo's caption has no backing and measures 4.00:1 over a white page, on main too, so the record lists it as a gap in FR17, and fix(Lightbox): a single photo's caption sits on the caption's backing #10284 gives it the band's backing.
    • Red first: "a caption whose text grows while its box keeps two lines gets "more" at the end of its second line" fails on 0fc994c's build. "The first frame that shows the clamped caption at open already ends it in "more"" records every frame from before the tap, and fails 3/3 in Chromium and 3/3 in WebKit without the opening measure. The unit "adds a layout effect only to the held variant, never to useTruncation" fails with a useTruncation that adds one.
    • At this head on main bef866e: pnpm install --frozen-lockfile 0; Lightbox, Text and Heading units 476/476; pnpm check:knowledge and pnpm check:changesets pass; core tsc 0; strict ESLint 0; comment sentence lengths 0 over. Leak and public-text checks clean.
    • Pixel diff against a build of 0fc994c's own lines on main 2151c88: pages 1 to 4 of "Gallery with captions", with page 1 clamped and open, at 1440 × 900, 402 × 874 and 360 × 740, left to right and right to left. 60 of 60 screenshots are identical (30 Chromium, 30 WebKit).
    • On a Storybook build of the same commit on main 2151c88 (3e2d912; main has changed none of these files since): Chromium Lightbox a11y specs 40/40, WebKit 19/19; Chromium Lightbox Paging and EveryPointer interaction specs 40 passed (3 WebKit-only skips); WebKit 37 passed, and LightboxPaging "the page settle keeps advancing every frame while the main thread is blocked" fails as on 0fc994c: feat(Lightbox): swipe between gallery photos like Photos #8197's tempo commit fixes it below this pull request.
  • Red first:

    • The media surface unit cases fail on ded4664's product.
    • The Picture-in-Picture ring case fails on 01bc90d's product, before the viewer tracked its own document's input: "expected undefined to deeply equal { focusVisible: false }".
    • On a Storybook build of 4dded66 with the fix round's specs: the caption's contrast case measures 4.00:1, under 4.5; the arrow key on "more", the swipe from "more", the caption rendered once, and the on-dark color cases fail.
    • On a Storybook build of 5a2ba52: a button sits inside the zoom button (1, expected 0); Enter on "more" zooms the photo instead of opening the caption; after "less" the toggle loses focus. The case that pages from a focused photo passes on both builds and guards the new zoom button's focus.
  • At this head (0fc994c): Lightbox units 139/139; core tsc 0, strict ESLint 0, comment sentence lengths 0 over. On a Storybook build of this head: Chromium Lightbox a11y specs 38/38; WebKit LightboxViewport and LightboxKeyboard 17/17; Lightbox Paging, Zoom and EveryPointer specs 78 passed in Chromium (3 WebKit-only skips); WebKit Paging, EveryPointer and Keyboard 38 passed (5 skips).

    • Red first: "renders the same caption elements with or without its toggle, clamped or open" fails on c56f4e1's Lightbox.tsx (the clamped caption is DIV(DIV(SPAN(),SPAN())), the one that fits DIV()) and passes here.
    • Pixel diff against c56f4e1's build: 60 of 60 screenshots are identical, byte for byte, in Chromium and WebKit, at 1440 × 900, 402 × 874 and 360 × 740, left to right and right to left. The states: "more" collapsed, the caption open, two lines that fit, no caption, and one short line.
  • At c56f4e1: Lightbox units 138/138; core tsc 0, strict ESLint 0, comment sentence lengths 0 over, Prettier clean; pnpm check:knowledge and check:repo pass. On a Storybook build of this head: Chromium Lightbox a11y specs 38/38; Lightbox Paging, Zoom and EveryPointer specs 55 passed in Chromium (3 WebKit-only skips); WebKit Paging and EveryPointer plus the LightboxViewport caption cases 34 passed (5 skips).

    • Red first, LightboxViewport.a11y.browser.spec.ts: "a gallery page without a caption shows no caption backing, and every page's photo keeps its top" and "a clamped caption's "more" ends its second line, with no column of its own" (402 × 874 and 1440 × 900), and "… still ends its second line after the viewport narrows (402 → 360)" fail on 053f321's build and pass here. "An open caption scrolled to its end keeps every line beside "less" clear of it" (360 × 150, 390 × 150) fails on a build that keeps the inline place while the caption scrolls, and passes here. The unit case "follows the caption in reading order, and its place in the last line is hidden from assistive technology" fails on 053f321's product.
  • iPhone 17 Pro simulator (iOS 26.2, Safari), on c56f4e1's Storybook build, every frame viewed: every page's photo top is 189 pt; page 1's two lines end in "…" and "more" (its last line ends at 318 pt, "more" sits at 328–390 pt on that line); page 2's two lines fit with no toggle; page 3 has no caption and no backing (rgba(0, 0, 0, 0)); page 4's one line sits on its backing.

    pages 1 to 4 on the iOS 26.2 simulator

  • c56f4e1's preview on the iOS 26.2 simulator (Safari, every frame viewed), pages 1 to 4:

    pages 1 to 4 of the exact-head preview on the iOS 26.2 simulator

  • Desktop, Chromium at 1440 × 900. Before (main, which has no captioned gallery story: "Gallery" page 1 and its captionless page 2, and "With caption" for a long caption):

    main: a short caption under the photo main: a page with no caption main: a long caption on one line

    After (c56f4e1, "Gallery with captions"): "more" collapsed, the same caption expanded, the captionless page, and a short caption:

    a two-line caption ending in "…" and "more" the caption open over the photo, with "less" a page with no caption: no band a short caption on its backing

  • At 053f321: core tsc 0, strict ESLint 0, comment sentence lengths 0 over, Lightbox units 137/137. Red first: the new case "returns focus to the opener without scrolling the page when the viewer closes" fails on 57425e7's product, whose close focused the opener without preventScroll. On a Storybook build of this head: Chromium Lightbox a11y specs 31/31; Paging and EveryPointer 28 passed in Chromium (3 WebKit-only skips); Paging, EveryPointer and Keyboard 26 passed in WebKit (5 skips); the a11y audit finds 0 new violations; pnpm check:knowledge and check:repo pass.

  • At 57425e7 (053f321 without the shared focus return): the LightboxKeyboard spec 9/9 in Chromium; Lightbox Paging and EveryPointer specs 28 passed in Chromium (3 WebKit-only skips), and Paging, EveryPointer and Keyboard 26 passed in WebKit (5 skips).

  • At 40e8001, the same behavior before the focus handoff moved to the shared helpers (the head also differs by the FR17 basis cell and two comments):

    • core tsc 0; strict ESLint on every changed file 0; comment sentence lengths 0 over; pnpm check:knowledge and check:repo pass (check:changesets, check:i18n-catalog, check:i18n-runtime, check:use-client and check:package-boundaries at 65534b6, the same code without the viewport hook).
    • Lightbox units 136/136.
    • On a Storybook build: Chromium Lightbox interaction specs 78 passed (3 WebKit-only skips); WebKit 73 passed (8 skips); Chromium Lightbox a11y specs 31/31; the LightboxKeyboard spec in WebKit 9/9; the a11y audit finds 0 new violations.
  • Existing cases: the fine-pointer touch-action lists gain the media surface's auto; the scroll case's viewport is 240 px tall so the body-size caption still overflows its panel; the paging specs find the pager's viewport by data-lightbox-viewport, since the zoom button is now the photo box.

  • iPhone 17 Pro simulator (iOS 26.2, Safari), on 4dded66's build, every frame viewed: a tap's opening leaves Close without a ring, and a key's opening shows it; the caption is body Text (16 px in the default theme), its lines start at one edge, and the band keeps 12 px under them.

@vercel

vercel Bot commented Nov 3, 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 Nov 6, 2026 6:58pm UTC

Request Review

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

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 6, 2026, 7:48 PM UTC

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

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs an owner decision before it can be approved, and it has independent fixes to make either way.

Owner decision: no Lightbox requirement yet covers gallery pages sharing one caption row, and this change pursues that direction while the design decision is open. The head also adds new requirements to the Lightbox record: AR9, the controls' backing and contrast with zoom on; and the zoom-settle formula with its motion roles. Approval needs a spec-gate owner to settle these: either approve the shared caption row and how it reserves its height (which today conflicts with the style-authoring rule that content renders once), or keep each caption under its own photo. An engineering owner records the decision by commenting /approve-spec <full head SHA>, and the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden copies of the unmounted pages' captions to reserve its height, which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • Against main the diff is 8,108 authored lines, because the three stacked pull requests ride along with this 250-line change, so land them first or retarget so this pull request carries only its own commit.
  • In a gallery with hasZoom, a click or tap on the reserved caption band under a photo without a caption no longer closes the viewer, while main and a gallery without zoom close it (useZoomStage.ts, the stage's tap only counts points inside the stage).
  • .changeset/lightbox-gallery-paging.md still says each caption sits "right under its photo", but it now sits at the bottom of the stage, 252 px below a landscape photo on a phone.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.

[Automated review]

Comment thread .changeset/lightbox-gallery-paging.md Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs an owner decision before it can be approved, and it has independent fixes to make either way.

Owner decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head also adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on) and the zoom-settle formula with its motion roles. A spec-gate owner needs to settle these: approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo. An engineering owner records the decision by commenting /approve-spec <full head SHA>, and the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden copies of the unmounted pages' captions to reserve its height (Lightbox.tsx, the caption sizer), which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • Desktop behavior change needs an owner decision: a mouse drag or a trackpad's sideways scroll now pages every gallery, with default props (Lightbox.tsx, the pager's allowedPointerTypes), so gate it to touch input or cite the decision that covers it.
  • Against main the diff is 8,175 authored lines, because the three pull requests below ride along with this 294-line change, and their bodies carry a different stack line, so land them first or make the stack declaration identical in all four.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs a spec owner's decision before it can be approved, and it has independent fixes to make either way.

Spec decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on), the zoom-settle formula, and its motion roles. A spec-gate owner needs to approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo, and approve or reject the new record text. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden, inert copies of the unrendered pages' captions to reserve its height (Lightbox.tsx, the caption sizer), which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • Desktop behavior change needs an owner decision: with default props a mouse drag or a trackpad's sideways scroll now pages every gallery (Lightbox.tsx, the pager's allowedPointerTypes), and with zoom a ctrl-wheel zooms the photo and a released mouse drag coasts (useZoomStage.ts), so gate these to touch input or cite the decision that covers them.
  • Against main the diff is 8,172 authored lines, because the three pull requests below ride along with this 294-line change and the stack line is not identical in every listed pull request, so land them first or make the stack declaration hold.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs a spec owner's decision before it can be approved, and it has independent fixes to make either way.

Spec decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on), the zoom-settle formula and its motion roles. A spec-gate owner needs to approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo, and approve or reject the new record text. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden, inert copies of the unrendered pages' captions only to reserve its height (Lightbox.tsx, the caption sizer), which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.
  • Against main the diff is 8,442 authored lines, because the three pull requests below ride along with this 294-line change and #8215 does not contain #8197's head, so land the lower pull requests first or make every listed head contain the one below it.
  • Regression: the Chinese hasZoom description still says every zoomed drag pans with momentum, while a mouse drag now stops at the release, so narrow it as the English description was narrowed.

Drag paging is now limited to touch input, so a mouse and a trackpad page a gallery as they do on main.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs a spec owner's decision before it can be approved, and it has independent fixes to make either way.

Spec decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on), the zoom-settle formula and its motion roles, beside unrecorded video and index behaviours. A spec-gate owner needs to approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo, and approve or reject the new record text. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Blocking findings:

  • In a captioned gallery without zoom, a tap or a mouse click on the dark band between a photo and its caption, which now sits in the bottom row, leaves the viewer open, while main and a zoomable gallery close on it, because the hit test in Lightbox.tsx treats one box spanning photo and caption as the media.
  • The caption row renders hidden, inert copies of the unrendered pages' captions only to reserve its height, which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.
  • Against main the diff is 8,622 authored lines, because the three pull requests below ride along and #8215 does not contain #8197's head, so land the lower pull requests first or declare a stack line that every listed head satisfies.
  • Miss: the caption sizer picks captions with a truthiness test (item.caption && …) instead of isRenderable, so a 0 caption reserves no height and a true caption reserves an empty box.

The Chinese hasZoom description now matches the English one.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
@vjeux

vjeux commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

On the rebase: the merge worker restacks this stack onto main, so the branches here don't rebase themselves. #8148 onto main without #8147's commit is staged for it, with the pull requests above following.

vjeux added 2 commits November 5, 2026 22:19
Each page of a Lightbox gallery laid out its own photo and caption, so
a neighbour with a longer caption arrived with a smaller photo. Every
page now has a caption band two caption lines tall under its photo,
whatever the caption, so the photo area is the same on every page and
paging never resizes it.

A page with no caption keeps the band's space, so its photo sits where
every other page's does, but it shows no backing there.

A longer caption shows two lines, and its second line ends in a "more"
button, so the caption keeps the band's whole width. The toggle is one
line tall, so its fill stays on that line. The button opens the whole
caption over the bottom of the photo, on the scrim the controls use, and
its "less" closes it. An open caption too long for its panel scrolls,
and it keeps the toggle's width clear at the end of every line. Paging
closes it too. The whole caption is always the viewer's accessible
description, clamped or open. The viewer lays out no hidden copies of
other captions. Each caption renders the same elements in every state:
without a toggle, the toggle's place in its last line has no size.

The viewer renders inside MediaTheme, dark, so its controls, caption and
counter take the theme's dark surface tokens and its rules for them,
with no Lightbox-owned colors beyond the controls' and the caption's
backing. The caption is body Text, both of its lines start at one edge,
and "more" and "less" are a ghost Button. The band clears the screen's
bottom edge and its safe area. Focus the viewer moves follows the last
input, so a tap that opens it paints no ring.

The caption sits on its own backing, the controls' backing colour at
90 %, so it keeps 4.5:1 over any photo or backdrop behind it. With
zoom, the controls' icons and the Counter's text keep the on-dark
colour, whatever a theme does to buttons or Text. An open caption is
the clamped caption itself, unclamped over the photo's bottom, so the
caption renders once. In a gallery the keyboard's zoom button is the
current page's photo box, so the caption band and its button sit
outside it, and Enter or Space on "more" toggles the caption. Paging
from a focused "more", "less" or photo moves focus to the new page's
photo, or to Close without zoom, so the arrow keys keep paging. "Less"
keeps its focus.
The zoom pan's begin threshold was set by Lightbox's own capture
pointerdown listener on the stage. The shared PanGestureRecognizer
takes a per-press hysteresis function, so the threshold moves there
and the listener goes. A mouse's or a pen's drag on a zoomed photo
still pans from its first move, and a finger keeps UIKit's threshold.

The zoom stage's header now says that the return past a limit springs
from the pinch's velocity.

A fine-pointer test's comment splits one 26-word sentence in two, under
the 25-word limit.
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec c56f4e1

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 0fc994c

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: the record text is approved for this head, and four blocking issues remain.

This pull request was reviewed on its own because its Stack: line no longer holds: #8148 has merged, and #8197's current head is not contained in this branch.

  • Miss: A gallery caption that fills its two lines and then grows while the viewer is open never shows "more", because Lightbox re-measures the clamp only on resize, attach and open or close instead of using useTruncation, which also watches content changes (Lightbox.tsx:862-899).
  • Miss: useZoomStage.ts:288-311 adds its own capture pointerdown listener to set the pan's begin threshold, which the shared PanGestureRecognizer already takes as a per-press hysteresis function, so pass it there and drop the listener and its gap sentence, as #8197's current head does.
  • Miss: A single photo's caption has no backing and measures 4.0:1 on the default backdrop over a white page, under the 4.5:1 that FR17's verification row requires, while the caption-row changeset claims 4.5:1 over any photo, so give it the caption's backing or narrow the row and the changeset to gallery captions.
  • Regression: The branch no longer merges into main (13 conflicting files, including Lightbox.tsx and useZoomStage.ts) and, with the stack broken, carries 10,763 authored lines as one change, so rebase #8197 onto main and this commit onto #8197.

CI has not built or tested this head.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
vjeux added 5 commits November 6, 2026 07:44
This merge takes main at bef866e, with the Popover group, and
this pull request's own diff is unchanged.
The zoom stage's pan refused a finger's drag that headed past a zoomed
photo's edge in every Lightbox, so a single photo resting at its edge
no longer rubber-banded there. Only a gallery's touch pager takes that
drag now: the stage yields at the edge only when Lightbox says an
outer pager is there. A single photo gives past its edge as on main.

The record's press-tracker line no longer names the stage's own
pointerdown listener, which the pan's per-press threshold replaced.
The spec owners ruled this a consistency fix: AST-108 plays motion at
the theme's tempo, and AST-094 requires the touch presentations to
follow it, but the pager's settle after a release ignored it.
solvePaging now takes the tempo. Each step lasts tempo times as long
and carries tempo times the release's distance, so a settle from rest
keeps its shape and takes tempo times as long, and a release still
starts at 96 % of the finger's speed. At tempo 1 the settle is
unchanged. No motion role names the paging table, so usePager reads
the tempo of the theme the track is in. Every pager follows it: a
Lightbox gallery and a Carousel with hasSnap.

The busy-main-thread proof of the gallery's settle now plays at tempo
3, as the zoom's does. A swipe at tempo 1 checks that the slowed settle
is three times as long, and a frozen settle and one that the main
thread drives both fail the proof. A refused drag's spring-back is now
read from its keyframes rather than counted in main-thread frames,
which a loaded runner thins out.
The merge worker's reference for restacking meta#9810 on meta#8197's head
22da53d, on main bef866e. No conflict; this pull request's own
diff is unchanged.
A gallery caption re-measured its clamp only when its box resized, a
page attached, or the caption opened or closed. New text in a box that
kept its two lines never got "more". The clamp now comes from Text's
own truncation check, which also follows new text: useHeldTruncation,
a variant of useTruncation internal to core, keeps the answer while the
caption is open, so the toggle stays mounted, with its focus, as the
caption closes. useTruncation's public options do not change, and only
the held variant runs a layout effect, so Text and Heading run no new
effect. Lightbox still measures the toggle's place and the open
caption's scroll, now on new text too.

A closed dialog lays its caption out at no size, so Lightbox measures
the caption again as the dialog opens, before the first paint: the
first frame that shows a clamped caption already ends it in "more".

The changeset now says that a gallery's caption keeps 4.5:1, and the
record lists a single photo's caption, which has no backing, as a gap
in FR17.
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec c83f03d

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: the record text is approved for this head, and two blocking issues remain.

  • The Stack: line does not hold, because #8147's and #8148's bodies carry a different line, #8148's head is not in this branch after its squash merge, and the line branches at #8197, so this pull request is reviewed on its own and carries #8197's increment as 5,870 authored lines with no exemption; put one identical, linear Stack: line naming only the open pull requests (for example #8197 → #9810 → #10284) in every listed body.
  • Regression: since #10301 landed on main, the branch conflicts in packages/core/src/Lightbox/LightboxFinePointer.a11y.browser.spec.ts on the expectSpringZoom comment this pull request rewords (lines 429-430), so merge main and keep its comment.

The caption band itself is unchanged since the last round and has no other blocking finding. Two non-blocking notes: spec:AST-094's FR45 verification row still lists tempo as a pager gap, and FR17's review state should read settled; implementation gap now that a gap names it. CI has not run on this head: it skips every job while the pull request is a draft.

[Automated review]

| FR14 | Gestures, paging, motion, and reduced motion run through Core's one shared set of internal touch modules; Lightbox bundles no copy of a module's primitive and adds no gesture listener outside them. | `architecture:react-component-runtime` INV13 | settled; implementation gap |
| FR15 | Lightbox supports a Picture-in-Picture window as its only second document; there it ranks Escape and platform close among that window's members. | `family:overlay-dismissal` FR8 | settled |
| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on a spring from rest, `springs.settle` unless the motion table names another role, with the backdrop and controls fading on its pace (`spec:AST-094` FR7, FR45). `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap |
| FR17 | The viewer is a media surface: it renders inside `MediaTheme` (dark), so its controls and caption take the theme's dark surface tokens and the active theme's component rules (`architecture:theme-application` INV8), with no Lightbox-owned colors beyond AR9's backing, the caption's backing and the on-dark color. | Specs decision, Nov 5 2026 | settled |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: the current-state impact now lists "Single photo's caption contrast (FR17)", so this row's review state should read settled; implementation gap, as FR4, FR14 and FR16 do.

vjeux added 4 commits November 6, 2026 09:02
The release note named a Carousel with hasSnap, but a plain hasSnap is
native CSS scroll snap. Only hasSnap={useSnapPaging()} runs the pager
whose settle now follows tempo, so the note names that value. The
AST-094 FR45 verification row no longer lists tempo as a gap of the
pager's settle.
This merge takes main at 1c142dc. meta#10301 rewrote the spring-zoom
check in LightboxFinePointer.a11y.browser.spec.ts onto the shared
interaction harness, and this pull request had split a sentence of
that check's old comment. The merge keeps meta#10301's check and its
comment as they are on main.
The check that Next and the arrow key show the next photo at once
samples the track every frame for 600 ms after the press, and it needs
more than 10 frames. WebKit in a 4-CPU container painted 3 to 7 frames
in that time under load, so the check failed there. The sampler now
runs for 600 ms and at least 12 frames. At half the test's timeout it
gives up, and too few frames fail by name. The one-place check is the
same.
The restack of meta#9810 on meta#8197's head 3ee5207. That head merges
main, fixes its release note and waits for 12 frames in its
instant-page check. No conflict; this pull request's own diff is
unchanged.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved pending CI: CI has not run on this head yet. As a draft from a fork, it runs once the pull request is marked ready and a maintainer adds the ci:run label.

This pull request's own increment over #8197 has no blocking findings: its record text is approved for this head, and the Stack: line now holds, so the earlier size finding no longer applies. A pull request in the stack lands only when it and every pull request below it are approved.

  • Non-blocking: FR17's verification row names a black photo, but the contrast case runs only mid-grey and white; add it or drop it from the row.
  • Non-blocking: FR17's review state should read settled; implementation gap now that the single-caption gap names it.
  • Note: the branch's conflict with main in LightboxFinePointer.a11y.browser.spec.ts comes from #8197's reworded expectSpringZoom comment and resolves when #8197 is restacked.

[Automated review]

| FR11 | Fine-pointer browser tests | Double-click, click on the photo, drag, ctrl-wheel, trackpad scroll, mouse wheel notch | A double-click that does not zoom 2× about the point, a click that hides the controls, a fine-pointer close that animates, or a mouse wheel's notch that pans a zoomed photo fails. | `audit:Lightbox/touch` |
| FR6, AR1 | Touch test double-tapping a photo shaped like the screen and a landscape photo | 9:16 photo on a phone viewport, 3:2 photo | A double tap that zooms less than 2×, or not about the tapped point, fails. | `audit:Lightbox/touch` |
| FR16 | Touch tests closing by Close, Escape, platform close, and a tap beside the photo with the controls shown; Reduce Motion | With and without `experimental`; controls shown and hidden | A touch close without the exit, a literal timing, input reaching the viewer during the exit, or a tap beside the photo that closes while the controls are hidden fails. | `audit:Lightbox/touch` |
| FR17 | Unit: `Lightbox.test.tsx` media surface cases; browser: `LightboxPaging` and `LightboxZoom` interaction specs, Chromium and WebKit | Gallery with captions, with and without zoom; a single photo's caption; over a white, a mid-grey and a black photo (body 14.7:1 over white) | A viewer outside `MediaTheme`, a Lightbox-set text color, a caption or counter that is not `Text`, a toggle that is not a ghost `Button`, two start edges, or a caption under 4.5:1 fails. | `audit:Lightbox/theming` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: this row names a caption over "a white, a mid-grey and a black photo", but the contrast case in LightboxZoom.interaction.browser.spec.ts runs only mid-grey and white. Add the black photo to that loop, or drop it from the row.

| FR14 | Gestures, paging, motion, and reduced motion run through Core's one shared set of internal touch modules; Lightbox bundles no copy of a module's primitive and adds no gesture listener outside them. | `architecture:react-component-runtime` INV13 | settled; implementation gap |
| FR15 | Lightbox supports a Picture-in-Picture window as its only second document; there it ranks Escape and platform close among that window's members. | `family:overlay-dismissal` FR8 | settled |
| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on a spring from rest, `springs.settle` unless the motion table names another role, with the backdrop and controls fading on its pace (`spec:AST-094` FR7, FR45). `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap |
| FR17 | The viewer is a media surface: it renders inside `MediaTheme` (dark), so its controls and caption take the theme's dark surface tokens and the active theme's component rules (`architecture:theme-application` INV8), with no Lightbox-owned colors beyond AR9's backing, the caption's backing and the on-dark color. | Specs decision, Nov 5 2026 | settled |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: the new "Single photo's caption contrast (FR17)" gap says FR17 is not met for a single photo, so this row's review state reads settled; implementation gap, as FR4, FR14 and FR16 do.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec ced33fe

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: the record text is approved for this head, and one blocking issue remains.

  • Regression: the Stack: line #8197 → #9810 → #10284 no longer holds, because this head merges #8197's new head and #10284's head does not contain it, so this pull request is judged on its own and carries #8197's increment as 5,865 authored lines with no exemption; update #10284 so its head contains this one.

The earlier conflict with main is resolved. Non-blocking: FR17's verification row names a black photo that no contrast case runs, and FR17's review state should read settled; implementation gap now that a gap names it.

[Automated review]

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved pending CI: CI has not run on this head yet, and as a draft from a fork it runs once the pull request is marked ready for review and a maintainer adds the ci:run label.

#8197

Approved at its current head, with no blocking findings.

#9810

Approved, with no blocking findings: the record text is approved for this head, and the Stack: #8197 → #9810 line now holds.

  • Miss (non-blocking): this pull request deletes the GalleryKeepsIndex story's description that #8197 adds (Lightbox.stories.tsx:191), so Storybook's docs show that story unexplained.
  • Non-blocking: FR17's verification row names a black photo that no contrast case runs, and FR17's review state should read settled; implementation gap now that a gap names it.

Whole stack

No blocking issue across the two pull requests; each lands only when it and every pull request below it are approved.

[Automated review]

},
};

export const GalleryKeepsIndex: Story = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Miss (non-blocking): #8197 gives this story a description ("A controlled gallery whose parent keeps its index, paging only with its own buttons, say while a change is confirmed. A swipe returns to the photo the parent shows."), and this pull request deletes it, so Storybook's docs show the story with no explanation. Restore the comment above this line.

Comment on lines +1677 to +1678
<div
{...stylex.props(styles.captionFlow)}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: component:Text says "Block-level content … is not supported inside a clamped Text, because it breaks the clamp." This block keeps the clamp in Chromium, and the browser specs measure it in WebKit; record the case with Text's owner so a change to Text does not break the caption silently.

This branch was successfully deployed

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

Labels

ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants