Skip to content

feat(lab): MobilePowerSearch's steps are standalone sheets that replace one another - #8476

Merged
imdreamrunner merged 1 commit into
mainfrom
feat/lab-mobile-powersearch-stacked-sheets
Oct 27, 2026
Merged

imdreamrunner merged 1 commit into
mainfrom
feat/lab-mobile-powersearch-stacked-sheets

Conversation

@imdreamrunner

@imdreamrunner imdreamrunner commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Maintainer impact

Lab's MobilePowerSearch was the last surface in the repository composing BottomSheetSwitcher, which component:BottomSheetSwitcher DEP-0015 deprecates in favour of standalone step sheets. The deprecation patch leaves Lab's migration optional (@astryxdesign/lab is private and published only under @canary, so spec:AST-017 FR29c's "maintained Astryx packages" does not reach it); this change completes it anyway, so that nothing in the repository still teaches or depends on the switcher and the Lab prototype exercises the same stack and replace machinery Core ships.

Targets main. It needs BottomSheet's open-order stack and same-update replace handoff, which landed with #7648; this PR is the one Lab commit on top of that merge.

Intended invariant

  • component:BottomSheet FR10–FR11 (a same-update close and open is a replace handoff: the entering sheet rises above the retained one under the one scrim, the retained one fades inert and hidden, focus follows the person to the entering step and returns to the flow's opener when the last step closes) and FR5–FR9 (stacking) — the behaviour the steps now rely on.
  • component:BottomSheetSwitcher DEP-0015 (Lab's MobilePowerSearch migrates "by its own change": this one).
  • spec:AST-017 FR59 (touch work changes fine-pointer behaviour only for a defect or deliberate consistency): the component is the same on every pointer, and the evidence below shows identical behaviour and element counts under a coarse and a fine pointer.
  • Everything MobilePowerSearch already promised stays as it was: focus moves and returns (feat(lab): MobilePowerSearch edits list values through MobileTokenizer #7979), Back, swipe and scrim dismissal by purpose, the sticky header and footer above the rows scrolling under them (fix(lab): MobilePowerSearch's sticky sheet header stays above the rows scrolling under it #8005), the trigger's aria-labelledby, the in-sheet role=status regions (data-testid="mobile-power-search-sheet-status"), the StrictMode count guard, joined pending messages and the ref resets on teardown (fix(lab): MobilePowerSearch names its trigger by what it shows and announces inside its sheets #8007).

Change

The three steps — Filters, Add filter and the value step — were children of one BottomSheetSwitcher driven by step. Each is now its own BottomSheet, isOpen={step === '<step>'}, controlled by the same step value, so every step change the handlers already made (setStep('fields'), setStep('manage'), …) is now a same-update close and open that BottomSheet treats as a replace handoff. The switcher's onActiveSheetChange (which only ever reported null, for an allowed dismissal) becomes one onOpenChange per step that ends the flow only when the request comes from the current step, so a stale request from a sheet that is leaving does nothing. openManager still focuses the trigger before the first step opens, and the stack carries that recorded opener from step to step, so focus returns to the trigger however the flow ends. The pending-focus effects (focusPendingSheetTarget) are untouched: they run after the entering sheet's own focus, as before.

Why maintenance: no prop, type, export or behaviour of MobilePowerSearch changes for a Lab caller; the component only stops composing a deprecated component. No Core or spec file is touched.

Announcements. spec:AST-086 FR14 (owners inside an open modal deliver inside it) is on main, but useAnnounce still writes only to the page-body channel (the record names that as the gap), so the in-sheet role=status regions are still the only route for a message while a step is open and are kept as they were. Nothing double-announces: announce() routes to the page channel only while no step is open. Once useAnnounce implements FR14, these regions and the component's own routing can go in one follow-up.

Follow-up record edit (not changed here): component:BottomSheetSwitcher's Intent gap note and DEP-0015 downstream row say Lab's MobilePowerSearch keeps the switcher until its own change migrates it; once this lands, that sentence and the remaining Implementation gap: line can be removed from the record in #7648 (the record is its PR's to amend).

Evidence

  • Failure before the change: not a defect — the switcher version works; the problem is that Lab still composed a deprecated component (DEP-0015's open gap).
  • Success after the change: grep -rn BottomSheetSwitcher packages/lab/src finds only the test that asserts its absence; the flow runs on BottomSheet's stack. Chromium, static Storybook of this head, lab-mobilepowersearch--touch-flow, 390×844: 32 of 32 checks pass under both the iPhone profile (pointer: coarse) and a desktop profile (fine pointer), among them — tap opens Filters as one modal sheet; during Filters → Add filter both steps are presented, Filters inert + aria-hidden with data-sheet-stack="replaced", Add filter interactive on top; the page dim is constant through the handoff (0.50 min/max over 79 frames: one scrim); the handoff completes with only Add filter open and focus inside it; choosing Status replaces Add filter with the value step; Cancel returns to Add filter with focus on the Status row; Back returns to Filters with focus on Add filter; Done closes and focus is on the trigger (the return target survived three handoffs); Escape closes to the trigger; a scrim press closes Filters (info) and does not close the value step (form); a finger swipe on the handle dismisses the flow (coarse); the sticky header has its own z-index inside an isolated layer (fix(lab): MobilePowerSearch's sticky sheet header stays above the rows scrolling under it #8005); the trigger's aria-labelledby resolves and the open step carries a role=status region (fix(lab): MobilePowerSearch names its trigger by what it shows and announces inside its sheets #8007).
  • Product/runtime behavior verified unchanged: the same 32 checks run against the switcher version (the parent commit's component in the same Storybook) also pass 32 of 32 — identical dim samples, identical focus outcomes, identical element counts per step (Filters 56, Add filter 110) under both pointers. The one DOM difference is the stack's own marking of the handoff on the hosts while the two steps overlap, which the switcher did not write and which nothing in this PR relies on. Screenshots of the Add filter step before and after, under both pointers, are in the evidence directory alongside the per-check results.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run packages/lab/src/MobilePowerSearch/ — 94 passed (the 86 existing tests unchanged apart from two helper comments that described the switcher, plus 8 new: Filters opens as one modal dialog named by the step; Filters → Add filter is a replace handoff (entering on top and exposed, leaving step presented but inert and aria-hidden, then gone after its fade — behavioural assertions only, no Core-internal attribute), a parity pin: the switcher version produces the same observable sequence, so this test guards the migration against regressions rather than distinguishing the two; a lone step is one open modal dialog (aria-modal="true"), interactive and exposed; focus returns to the trigger after a flow that crossed every step; Escape on Filters closes to the trigger; a scrim press closes Filters (info) and not the value step (form); a touch swipe on Filters dismisses; a stale dismissal from the leaving sheet — a scrim press on the replaced Filters dialog, the one dismissal path BottomSheet does not gate on its open state — cannot end the flow — the one test that fails with the per-step guard removed, the only new logic this migration adds).
  • Lab and BottomSheet suites: 86 files / 1,270 tests pass.
  • The seven CI typecheck steps (core typecheck, core typecheck:docs, lab typecheck:docs, storybook typecheck, a11y-spec typecheck, cli typecheck:strict, cli typecheck:template-docs): each exits 0.
  • ASTRYX_STRICT_LINT=1 eslint on the Lab component and its story: 0 problems.
  • pnpm check:repo ✓, pnpm check:knowledge ✓, pnpm build ✓, Storybook build ✓, build-previews 784 routes verified, git diff --check ✓.
  • No Changeset: @astryxdesign/lab is private and canary-only and ships none (as fix(lab): MobilePowerSearch's sticky sheet header stays above the rows scrolling under it #8005 and fix(lab): MobilePowerSearch names its trigger by what it shows and announces inside its sheets #8007 did).

@vercel

vercel Bot commented Oct 26, 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 3:07am UTC

Request Review

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

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 27, 2026, 4:48 PM UTC

@github-actions

github-actions Bot commented Oct 26, 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

MobilePowerSearch (@astryxdesign/lab) · View in Storybook
Metric Before After Delta
Entry File (ESM) 336B 336B 0B
Lines of Code 2492 2505 -
Complexity Very High Very High (319) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
lab/MobilePowerSearch 159.7KB 156.8KB -2.9KB 1.4KB 1.4KB 0B

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.

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/lab 4.6KB 1.9KB 1.9KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/lab 11.1KB 11.1KB 0B

Accessibility Audit

Status: No accessibility violations detected.


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

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

No blocking issues. Moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow renders, dismisses and returns focus the same way as before. Nothing more is needed beyond landing this after #7648, which provides the replace handoff the steps rely on. One optional tidy-up: the test titled "no BottomSheetSwitcher remains" only asserts that Filters opens as one modal dialog, so it also passes with the switcher, and a title saying what it checks would be more accurate.

[Automated review]

return Array.from(document.querySelectorAll<HTMLElement>('dialog[open]'));
}

it('no BottomSheetSwitcher remains: every step is a standalone sheet', () => {

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 test only asserts that Filters opens as one modal dialog named "Filters", so it also passes against the switcher version. The removed import proves the switcher is gone, not this test. Consider a title that says what it checks, for example "Filters opens as one modal dialog".

tapRow('Add filter');
// The leaving Filters dialog asks to close (as a late Escape would); the
// flow is on Add filter now, so nothing happens.
fireEvent(filters, new Event('cancel', {cancelable: true}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

BottomSheet already ignores a cancel from a sheet that is no longer open (dismissOnEscape requires isOpen), so this test passes even without the stepRef.current === sheetStep guard in handleStepOpenChange. Sending the stale request as a click on filters (the scrim path, which BottomSheet does not gate on isOpen) would make the test depend on that guard.

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

No blocking issues. Moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow's dim, dismissal and focus behave as before on fine and coarse pointers. Please land this only with or after #7648, which provides the replace handoff the steps rely on and can then drop its switcher record's note that MobilePowerSearch still uses the switcher. One optional tidy-up: the "a step on its own has one modal dialog and no stack attribute" test only checks that the stack attributes are absent, so asserting aria-modal and a single open dialog would make it match its title.

[Automated review]

expect(isSheetOpen()).toBe(true);
});

it('a step on its own has one modal dialog and no stack attribute', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: this test's title promises one modal dialog, but it only asserts that data-sheet-stack and data-sheet-stack-depth are absent. No current spec record makes those attributes contractual, and the test passes unchanged against the switcher version. Asserting aria-modal="true" and a single dialog[open] would make it check what its title says.

@imdreamrunner
imdreamrunner force-pushed the feat/lab-mobile-powersearch-stacked-sheets branch from 9b3436b to 9e39033 Compare October 26, 2026 03:30
@imdreamrunner
imdreamrunner force-pushed the feat/core-bottom-sheet-open-order branch from eeaa501 to 933e20e Compare October 26, 2026 03:30

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

No blocking issues. Moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow looks, dismisses and returns focus as before, with identical frames on fine and coarse pointers. Please land this only with or after #7648, which provides the replace handoff the steps rely on and can then drop its switcher record's note that MobilePowerSearch still uses the switcher. Optional: the handoff and lone-step tests also assert Core's internal data-sheet-stack marks, which no spec makes contractual, so they would break on a Core rename even while the behaviour holds.

[Automated review]

// accessibility tree; the entering step is the interactive one.
expect(filters).toHaveAttribute('inert');
expect(filters).toHaveAttribute('aria-hidden', 'true');
expect(filters).toHaveAttribute('data-sheet-stack', 'replaced');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: data-sheet-stack is Core's private mark, and no current spec record makes it contractual, so this assertion ties the Lab test to Core's DOM. It is also the only line that makes this test fail against the switcher version; the inert, aria-hidden and presented-dialog assertions around it already prove the handoff.

@imdreamrunner
imdreamrunner force-pushed the feat/lab-mobile-powersearch-stacked-sheets branch from 9e39033 to c91a138 Compare October 26, 2026 09:41
@imdreamrunner
imdreamrunner force-pushed the feat/core-bottom-sheet-open-order branch from 933e20e to c2b66f5 Compare October 26, 2026 09:41
@imdreamrunner
imdreamrunner force-pushed the feat/lab-mobile-powersearch-stacked-sheets branch from c91a138 to 4f4311e Compare October 26, 2026 10:08
@imdreamrunner
imdreamrunner force-pushed the feat/core-bottom-sheet-open-order branch from c2b66f5 to b8eef54 Compare October 26, 2026 10:08

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

No blocking issues. Moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow looks and hands off identically on fine and coarse pointers, and dismisses and returns focus as before. Please land this only with or after #7648, which provides the replace handoff the steps rely on and can then drop its switcher record's note that MobilePowerSearch still uses the switcher. One correction for the description: the new handoff test also passes against the switcher version, so it pins shared behaviour rather than the migration; only the stale-dismissal test is sensitive to the new guard.

[Automated review]

expect(sheet()).toHaveAttribute('aria-modal', 'true');
});

it('Filters → Add filter is a replace handoff: the entering step is on top, the leaving step stays presented, inert and hidden, then leaves', () => {

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 test also passes against the switcher version of the component (the base commit's MobilePowerSearch.tsx under this file's tests): both versions keep the two dialogs open with Filters inert and aria-hidden, then leave only Add filter. That makes it a good parity pin, but the description's "fails on the switcher version" no longer holds now that the data-sheet-stack assertion is gone; of the eight new tests, only the stale-dismissal one turns red, and only when the per-step guard is removed.

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

No blocking issues: moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow looks, hands off, dismisses and returns focus as before on fine and coarse pointers. Please land it only on top of #7648, which provides the replace handoff the steps rely on and can then drop its switcher record's note that MobilePowerSearch still uses the switcher. One small description fix: grep -rn BottomSheetSwitcher packages/lab/src now matches only the new sentence in MobilePowerSearch.doc.mjs, not a test.

[Automated review]

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

No blocking issues: moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow looks, hands off, dismisses and returns focus exactly as before on fine and coarse pointers. Please land it only on top of #7648, which provides the replace handoff the steps rely on and can then drop its switcher record's note that MobilePowerSearch still uses the switcher. One small description fix: grep -rn BottomSheetSwitcher packages/lab/src now matches only the new sentence in MobilePowerSearch.doc.mjs, not a test.

[Automated review]

@imdreamrunner
imdreamrunner force-pushed the feat/core-bottom-sheet-open-order branch from 1797865 to 20ed66a Compare October 26, 2026 23:58
@imdreamrunner
imdreamrunner force-pushed the feat/lab-mobile-powersearch-stacked-sheets branch from 525e267 to f95793f Compare October 26, 2026 23:58

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

Looks good to approve: moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow looks, hands off, dismisses and returns focus exactly as before by touch, mouse and keyboard. Please land it only on top of #7648, which provides the replace handoff the steps rely on. Two small description fixes: grep -rn BottomSheetSwitcher packages/lab/src now matches only the new sentence in MobilePowerSearch.doc.mjs (no test names the switcher), and Filters and Add filter each render one element fewer than the switcher version, so the element counts are not identical.

[Automated review]

…ce one another

The Filters, Add filter and value steps were children of the deprecated
BottomSheetSwitcher. Each is now its own BottomSheet controlled by the one
`step` value: a step change closes one sheet and opens the next in the same
update, which BottomSheet treats as a replace handoff — the entering step
rises above the leaving one under the one scrim, the leaving one fades out
inert and hidden, and the opener recorded for the first step carries to each
next step so a Done, Back, Escape, scrim press or swipe at the end returns
focus to the trigger. A sheet only ever asks to close; only the current
step's request ends the flow, so a stale request from a leaving sheet does
nothing.

Nothing else changes: the sticky header and footer, the trigger's
aria-labelledby, the in-sheet role=status regions (still needed while
useAnnounce's in-modal route is unimplemented), the StrictMode count guard,
the joined pending messages and the ref resets stay as they were. No
BottomSheetSwitcher import remains in Lab.

Completes the Lab migration DEP-0015 leaves optional (spec:AST-017 FR29c).
@imdreamrunner
imdreamrunner changed the base branch from feat/core-bottom-sheet-open-order to main October 27, 2026 02:57
@imdreamrunner
imdreamrunner force-pushed the feat/lab-mobile-powersearch-stacked-sheets branch from f95793f to 42eccc0 Compare October 27, 2026 02:57

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

Looks good to approve: moving MobilePowerSearch's Filters, Add filter and value steps onto standalone BottomSheets follows the documented replacement for the deprecated BottomSheetSwitcher, and the flow looks, hands off, dismisses and returns focus exactly as before by touch and with a mouse. Two small description fixes: grep -rn BottomSheetSwitcher packages/lab/src matches only the new sentence in MobilePowerSearch.doc.mjs (no test asserts the switcher's absence), and each open step renders one element fewer than the switcher version, so the element counts are not identical. Once this lands, the BottomSheetSwitcher record's sentences saying MobilePowerSearch still uses the switcher need their own edit, since #7648, where the description defers it, has already merged.

[Automated review]

@imdreamrunner
imdreamrunner merged commit b02fac6 into main Oct 27, 2026
162 of 166 checks passed
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…eta#8297, meta#8650, meta#8672) into meta#8203

# Conflicts:
#	packages/core/src/BottomSheet/BottomSheet.tsx
@github-actions
github-actions Bot deleted the feat/lab-mobile-powersearch-stacked-sheets branch October 28, 2026 06:17

This branch was successfully deployed

1 active deployment
Preview — 42eccc02 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant