View Transitions (3.2.0): animated query swaps + MPA theme opt-in - #12
Open
dabowman wants to merge 11 commits into
Open
View Transitions (3.2.0): animated query swaps + MPA theme opt-in#12dabowman wants to merge 11 commits into
dabowman wants to merge 11 commits into
Conversation
Interview-driven spec for the 6.4 cycle, narrowed from the five-primitive modern-primitives pack to View Transitions only. Two layers: router region swaps (default-on, prefetch-then-wrap around navigate(), crossfade plus server-injected per-item morph names scoped by queryId) and cross-document MPA transitions (theme-support opt-in emitting the @view-transition rule, plus the ba-vt-post class helper for per-post names that connect archive thumbnails to single-post heroes). No editor UI; reduced-motion disables both layers; core router verified to have no native VT support (2.52.0), so the wrap is ours and patches nothing. Roadmap: tick shipped 6.1-6.3, record the 6.4 scope decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7d5iHfz3R4qucXkVSWajG
Verified against router 2.52.0: fetchPage() never rejects (failures resolve the cache entry to false, which navigate() answers with a hard reload, not a re-fetch) and it has no timeout. The real hazard is a hung fetch, so the wrap now races the prefetch against a 3s bound and only starts a transition once the fetch has settled — the rendering freeze can never include network time. Hung fetches fall back to a plain navigate under core's own 10s timeout. Accessibility note and test plan updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7d5iHfz3R4qucXkVSWajG
Plugin-side 400ms loading announcement (core's can't fire on the transition path), popstate declared a non-goal, accurate hard-reload freeze semantics, token-exact post-ID matching, and version-citation wording for router 2.52.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Query_Action::post_process_html() names the region root
(ba-query-{queryId}) and each post item (ba-q{queryId}-p{postId},
token-exact post-{ID} match, first occurrence only) so surviving posts
morph on router swaps. enqueue_view_script() delivers the
block_actions_query_view_transitions kill switch and the translated
loading announcement via wp_interactivity_config.
New View_Transitions class: theme opt-in
(add_theme_support 'block-actions-view-transitions') prints the
reduced-motion-guarded @view-transition rule in wp_head; a render_block
filter injects ba-post-{ID} on ba-vt-post-classed blocks (first-tag
exact-class guard, context postId with main-loop fallback).
Adversarially reviewed (pass; one nice-to-have noted for nested-query
regions). 122 PHP tests / 283 assertions green, PHPCS clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
navigateTo() gains the prefetch-then-wrap branch: warm the router cache (raced against a 3s guard) so startViewTransition only ever freezes a cache-hit swap; types signature (block-actions-query) with callback-form fallback; updateCallbackDone awaited so the animation never blocks the generator. A 400ms timer restores the screen-reader loading announcement core's navigate can no longer emit on this path. Review fix: getConfig() is called with the explicit 'block-actions/query' namespace — runGenerator() resumptions (debounced live search) run with an empty namespace stack, and a bare getConfig() there silently bypassed the kill switch. The interactivity mock is now namespace-keyed so that regression stays catchable. 235 JS tests / 12 suites green, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New docs/view-transitions.md: default router animations, kill switch, MPA opt-in, the ba-vt-post thumbnail-to-hero recipe (with the one-per-post-per-page caveat), static-name CSS recipes, and the ::view-transition-* / transition-type styling hooks. Pointer added to the query-loop guide; CLAUDE.md gains the new class, guide, and a two-layer architecture summary. Adversarial review fixed a never-matching scoping selector and a wrong Firefox support claim before commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Ordering. Prefetch-first made navigations enter the router in
network-settle order, defeating core's navigatingTo guard (which only
orders by entry) — a slow earlier search could land after and overwrite
a faster later one. A latestNavigation map, claimed before anything
awaits and re-checked after the prefetch race, restores latest-wins:
superseded navigations bail before burning a snapshot, and only the
owner clears the loading flag.
Duplicate names. Duplicate page-level view-transition-names make the
browser skip the WHOLE transition, and the server can only dedupe
within one render — a sticky post repeats across pages by default, so
loadMore() now strips names off infinite-scroll appends. Per-request
seen-maps guard ba-post-{ID} and ba-query-{id}; already-named items are
skipped so a nested query keeps its own identity.
Escape hatches. clientNavigationDisabled short-circuits canTransition()
(the router hard-reloads from a never-settling promise there, freezing
the page until Chrome's timeout); the kill switch now gates name
injection too (the names aren't inert with the store off — they drive
the MPA layer and create a stacking context at all times); matchMedia
is guarded and the whole transition branch wrapped so any throw falls
through to a plain navigate rather than leaving a preventDefault()ed
dead link; vt.ready / vt.finished get no-op catches; try/catch on the
new render_block filter per the repo's error-handling rule.
Quality. The .is-loading dim was baked into both snapshots — suppressed
inside a query transition via a transition-type-scoped rule with an
invalid-selector fallback. Guard timer made cancellable, append_style()
deduplicated, post_id_from_classes() uses core's class_list(), dead
pending flag removed. Docs corrected: the spec's "latest wins is
inherited from core" rationale was wrong, and the guide's "any block
carrying it" claim doesn't hold for Group/Columns.
244 JS tests / 12 suites green, ESLint and PHPCS clean, build succeeds.
The nine added or adjusted PHP tests still need Docker/wp-env to run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second 6.4 slice, interview-driven. Two actions on the native Popover API: popover-toggle (dropdown/disclosure targeting a new Popover variation of core/group, popover=auto injected at render time via the force_dialog precedent) and tooltip (text-only inspector field; the renderer splices a plugin-owned themable hint-popover element with role=tooltip and aria-describedby — no target at all). CSS anchor positioning only, with a placement enum compiled to position-area and flip fallbacks; non-supporting browsers get the UA centered default. Default-click triggers on real <button> elements compile to the native popovertarget invoker (works with JS off, browser-managed aria-expanded); everything else rides the store with toggle-event state sync. Button + Group hosting, no editor preview, 3.3.0. Roadmap: record the popover slice, drop the details accordion-group primitive (core shipped an accordion block), and capture the editor state-indication need as new Task 6.7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7d5iHfz3R4qucXkVSWajG
Capture the three-persona framing (authors as a constraint, designers as the worst-served persona who defines the seam, developers gated on the refactor cost of the first action — agents included) as the fixed reference point for future specs, with the delineation razor (we ship platform-defined primitives plus the grammar; extension is app logic), the upstream posture, and the open wedge-vs-keystone sequencing call. Define the two Phase 0 spikes that gate the states-and-effects direction: context multiplexing (multi-namespace hydration and co-armed handlers on one element, via a standalone interactivity-runtime harness) and overlay recording (whether the existing inspector can record into a state overlay — write-path census, interception candidates including diff-on-exit, merged rendering, core-dependency register). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7d5iHfz3R4qucXkVSWajG
In-browser verification (interactivity 6.52.0, standalone Playwright harness) plus runtime-source corroboration: repeated suffixed namespaced directives on one element hydrate per-namespace, and co-armed entries each evaluate in their own store scope. Recommended plumbing: the transformer injects each interaction's context and qualified directives side by side; the engine stays a pure dispatcher; single-interaction output stays byte-identical to today's. Constraints discovered: same-namespace contexts merge (grammar caps at one interaction per action per block), a bare ba-fire broadcast hits every armed entry (engine must dispatch per-tuple event names), co-armed entries cannot cancel each other, and multi-tuple engine config moves into the engine's own suffixed context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7d5iHfz3R4qucXkVSWajG
Source analysis of block-editor 16.1.0 vs the WP 7.0 bundle (15.13.2): Gutenberg >= 15.21.0 (June 2026) contains block style states — a per-block selected state that routes every core supports panel through getStyleForState/setStyleForState into a nested path inside the style attribute, with instance-scoped canvas preview CSS, a state-reframed inspector, and state-scoped reset-all. State keys pass through unvalidated, so custom named states work end to end and core generates no frontend CSS for them. Recommended mechanism: piggyback via the private setSelectedBlockStyleState action plus an upstream proposal for registerable named states; diff-on-exit (guardrailed) as the fallback. The prop-swap BlockEdit HOC is rejected — supports panels read attributes from the store, so prop surgery never reaches the read path. Grammar consequences: overlays are style-only and live in the style attribute under a collision-proof state key; interactionSettings only names and wires states; frontend CSS is ours; the designer surface is version-gated on the 15.21+ machinery (feature-detect, don't shim). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7d5iHfz3R4qucXkVSWajG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the View Transitions spec (
docs/specs/view-transitions.md, roadmap 6.4 — scope narrowed to View Transitions only). Two independent layers, zero configuration, zero editor surface.What ships
Layer 1 — router swaps (default-on). Filter / live-search / pagination region updates crossfade and post items morph to their new grid positions, via the native View Transitions API.
navigateTo()warms the router cache first and only wraps a settled cache-hit swap instartViewTransition(3 s prefetch guard), so the rendering freeze can never include network time. Feature-detected,prefers-reduced-motion-aware, kill switch:apply_filters( 'block_actions_query_view_transitions', true ).Query_Action::post_process_html()injects deterministic names server-side (ba-query-{queryId}on the region,ba-q{queryId}-p{postId}per item, token-exactpost-{ID}matching, first occurrence only). A 400 ms timer restores the screen-reader loading announcement that core'snavigate()can no longer emit on the prefetched path.Layer 2 — cross-document/MPA (theme opt-in).
add_theme_support( 'block-actions-view-transitions' )prints one reduced-motion-guarded@view-transition { navigation: auto; }rule inwp_head. Theba-vt-postclass (typed into Additional CSS Class(es)) gets arender_block-injectedview-transition-name: ba-post-{ID}for archive-thumbnail → single-hero morphs. The two layers compose.No
startViewTransitionexists anywhere in@wordpress/interactivity-router(verified against 2.52.0 source) — both layers are ours; neither patches core. No REST, no nonces; the query engine's page-cache posture is untouched. Graceful degradation: no API → today's instant swap, byte-identical.Process
Spec was validated against the actual router 2.52.0 package source and repo code before implementation (validation findings folded in as
d239ca0). Implementation ran as three ownership-scoped chunks, each adversarially reviewed before its commit. The reviews caught real defects pre-commit, including a kill-switch bypass on the debounced live-search path (runGenerator()resumptions run with an empty namespace stack, so a baregetConfig()read the empty config) — fixed with an explicit namespace and locked in by a fails-on-revert regression test.Testing
ba-vt-postinjection)@wordpress/a11yexternalized as a module dep🤖 Generated with Claude Code