Repository navigation
fix(php-transformer): open a projected modal in its source open state - #2783
Conversation
Review of #2744 found that a projected captured modal could still open invisible or keep the browser's dialog box: - Id rules never matched the dialog during analysis. Its id is generated and the source panel id rides on its editor anchor class, so placement "source" missed a panel positioned only by id rules. The selector matcher and both candidate indexes now treat an element carrying an id's editor anchor class as that id's holder. - A rest rule that hides the panel (by id or by class) made the dialog hidden content and the dialog block was dropped. A captured dialog's own rest state no longer counts as hidden; the block owns its closed state. - Without a captured ancestor state, the rest rule won on the open dialog (`#panel{display:none;transform:translateX(-100%)}` against `body.nav-open #panel{display:block;transform:none}`). For each hiding or offsetting property that a state rule on the same panel reverses, the dialog now carries the state value inline. The block stylesheet's important closed-state rule still closes it. Refs #2726 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Please hold: our independent review found the open-state picker takes whichever state rule comes last (e.g. a .dark-theme or .no-js variant), and a centring translate(-50%,-50%) is treated as an at-rest offset, un-centring modals that were fine on trunk. Fix in progress on this branch. |
… a modal's open values Review of c0b3e89: - The last state rule in source order won: `.dark-theme #panel{opacity:.5}` after `body.nav-open #panel{opacity:1}` gave the dialog `opacity:.5`, and `.no-js #panel{display:block}` replaced `display:flex`. When the capture recorded an opened ancestor class, only state rules naming it are trusted; otherwise every state rule must agree on a property, or nothing is carried. - A centring `translate(-50%,-50%)` counted as a rest offset, so `.no-js #panel{transform:none}` un-centred a modal that was fine on trunk. Only a translation of a full panel (100%) or viewport (100vw, 100vh) or more now counts as moving the panel out of view. - A dialog none of whose triggers can ever show (no viewport shows the trigger or an ancestor) is dropped again instead of surviving as an orphaned closed dialog. Refs #2726 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hold lifted. Merged after three independent review rounds. Follow-up to #2744: id rules match a projected dialog through its editor-anchor class, so source placement sees id-only positioning; a dialog's own rest state no longer drops it; open-state values (display, visibility, opacity, out-of-view transforms) are carried inline only from state rules naming recorded ancestor state, or that all agree; centring transforms are left alone; a dialog whose triggers can never show is dropped. Known gaps, same as trunk: pixel-offset drawers, JS-added trigger classes. Refs #2726. cc @chubes4 |
Refs #2726. Follow-up to #2744.
What was wrong
Review of #2744 found that a projected captured modal can still open invisible, or keep the browser's dialog box.
idattribute. SoCapturedDialogConverter::modalPlacement(), which reads the cascade, missed a panel positioned only by id rules (#panel{position:fixed;top:0;left:10px;width:200px}), and the dialog kept the browser box.HtmlCompilation::sourceElementStartsHidden()readdisplay:noneat rest as hidden content, and the dialog was not emitted. This was already true for class rules on trunk (.panel{display:none}), and it would have become true for id rules once they match.#panel{display:none;transform:translateX(-100%)}andbody.nav-open #panel{display:block;transform:none}, the open dialog stayed hidden and off canvas. The existing closed-state repair (ClosedStateNormalizer) does not cover this. It only reacts to:hoverand:focusreveal states, its repair stylesheet is editor-only, and it exempts positioned layers.What changed
CssSelectorMatcher::matchesCompound(),CssSelectorMatchCache::styleRuleCandidates()andAuthorStyleAnalysis::selectorCandidates()andselectorCanMatch(): an element carrying an id's editor anchor class is treated as that id's holder. Only a projected captured dialog carries such a class in source HTML.HtmlCompilation::sourceElementStartsHidden(): a captured<dialog>'s own rest state is not hidden content; the dialog block owns its closed state.StyleResolver::capturedDialogOpenStateValues(): walks the raw author rules (the static analysis dropstransformand drops rules that match no element yet). A state rule is an unconditioned rule whose rightmost compound matches the dialog but whose full selector does not. Fordisplay,visibility,opacityandtransform, where the rest cascade hides or offsets the panel and a state rule reverses it, the state rule's value is returned. A transform the open state keeps, such as a centring translate, is not touched.CapturedDialogConverter: a modal carries those values in its inline style, without!important. That beats the rest rules, and the block stylesheet'sdialog[data-blocks-engine-triggers]:not([open]){display:none!important}still closes it.Tests
New
tests/unit/captured-dialog-open-state.php, 6 assertions, compiled throughArtifactCompilerwith a wired copy holding#panel:style="display:block;transform:none", placementsource;style="display:flex;opacity:1";sourceand no inline values;On trunk 4 of 6 fail.
captured-dialog-source-panel-box,captured-dialog-projector,captured-dialog-closed-visibility,captured-dialog-trigger-survives,captured-dialog-dropdown-panelandcss-selector-matcherstill pass. The layer direction contract passes, and phpstan is clean on the touched files.capture-evidence-equivalence: 349/349 unchanged.Full unit run (memory_limit -1, one process per test): 429 pass, 0 fail.
Review round 2 (second commit, 59a9430)
.dark-theme #panel{opacity:.5}afterbody.nav-open #panel{opacity:1}gaveopacity:.5, and.no-js #panel{display:block}replaceddisplay:flex.data-blocks-engine-ancestor-state), only state rules naming it are trusted. If none names it, nothing is carried.translate(-50%,-50%)counted as a rest offset, so.no-js #panel{transform:none}un-centred a modal that is fine on trunk. Only a translation of 100% (a full panel) or 100vw or 100vh or more along an axis now counts as moving the panel out of view.display:noneat its base with no conditional value that shows it, or carrieshidden) is dropped again. A burger hidden on desktop but shown in a phone media query keeps its dialog.Dialog probe matrix against trunk 2a963f0 (the reviewer's
dlg2.shcases plus two trigger cases):body.nav-opendisplay:block;transform:none, placementsourceopacity: nav-open 1, then dark .5sourcedisplay: nav-open flex, then no-js block@media.menu-opendisplay:block#menu:hover ~ #panelsourcedisplay:noneeverywheredisplay:blockcaptured-dialog-open-state.phpnow has 12 assertions: 7 fail on trunk, and 5 fail on the previous PR head (the review cases).capture-evidence-equivalence: 349/349 unchanged. Full unit run (memory_limit -1, one process per test): 429 pass, 0 fail.Import proof (workbench, new-vocabulary captures)
Studio CLI
create --fromwith the liberation workbench captures (DLA main 569c336). The baseline is SSI v1.31.2 with BE trunk 2a963f0 (wb-*-trunk). The proof package is the same ZIP with #2780, #2781 and #2783 merged on 2a963f0 (ssi-be-proof-2726-combined-e51205f.zip). iPhone user agent at 375; desktop at 1280. Quality gate passed on every import (quality_pass: true,fail_import: false). The proof sites are stopped, not deleted.sourcewith no inline open values (its panel has no state rule to replay). Box at x=27.5, 320px wide, beige layer, the same as trunk (localhost:8952).🤖 Generated with Claude Code