Skip to content

Commit 8c11dd0

Browse files
committed
Administration: Refine the admin menu view transitions.
The admin menu's accordion animation sometimes degraded to a plain cross-fade, because a cross-document view transition snapshots the incoming page at its first render, which can happen before the admin menu has finished parsing. A render-blocking `expect` link now holds the first render until `#wpcontent`, the element right after the admin menu, is parsed, so the snapshot always has the complete menu without waiting for the rest of the page. The link is only printed once the View Transitions style has been printed, since an `expect` target that never appears blocks rendering until the whole document is parsed. The animation itself is also refined. Each menu item's snapshot is clipped to its animating box, so a submenu wipes open or closed in step with the items around it instead of overlapping them. The Collapse Menu button moves with the menu instead of cross-fading. An open flyout fades out while moving with its menu item, instead of being cut off by that clip. The style is now enqueued from `wp-admin/admin-header.php` instead of on the `admin_enqueue_scripts` action, which `iframe_header()` also fires, so screens without the admin menu no longer load it. As a result, unhooking `wp_enqueue_view_transitions_admin_css()` no longer turns off admin View Transitions. Dequeuing the `wp-view-transitions-admin` style does, and it works in 7.0 as well. Developed in #11912. Follow-up to r61491. Props westonruter, b1ink0, zieladam, mukesh27, dhrupo, audrasjb, joedolson, huzaifaalmesbah, jasonsa19, ozgursar, nikunj8866, darshitrajyaguru97. See #64470. Fixes #65032. git-svn-id: https://develop.svn.wordpress.org/trunk@64691 602fd350-edb4-49c9-b593-d223f7449a82
1 parent fb78023 commit 8c11dd0

6 files changed

Lines changed: 206 additions & 11 deletions

File tree

‎src/wp-admin/admin-header.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@
103103
wp_enqueue_script( 'utils' );
104104
wp_enqueue_script( 'svg-painter' );
105105

106+
/*
107+
* View transitions animate the admin menu, so they are only enqueued on screens that print it.
108+
* Iframe screens such as Thickbox modals do not load this header and so do not opt in.
109+
*/
110+
wp_enqueue_view_transitions_admin_css();
111+
106112
$admin_body_class = preg_replace( '/[^a-z0-9_-]+/i', '-', $hook_suffix );
107113

108114
// Print the global admin inline scripts through the script tag API so the

‎src/wp-admin/css/view-transitions.css‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,74 @@
33
navigation: auto;
44
}
55

6+
/*
7+
* Give each top-level menu item its own view transition name — its id,
8+
* such as "menu-posts" — so each one is captured as an independent group
9+
* and animates from its old position and size to its new ones across a
10+
* navigation. This produces the accordion effect: the previously open
11+
* section collapses as the newly current one expands, instead of the
12+
* whole menu simply cross-fading. attr() falls back to "none" for any
13+
* item whose id is missing or is not a valid custom identifier. The
14+
* shared wp-menu-top class lets the image-pair rule below address every
15+
* menu-top group at once.
16+
*/
617
#adminmenu > .menu-top {
718
view-transition-name: attr(id type(<custom-ident>), none);
19+
view-transition-class: wp-menu-top;
20+
}
21+
22+
/*
23+
* Name the collapse button too, so it slides into place with the menu
24+
* items instead of cross-fading as part of the page root.
25+
*/
26+
#collapse-menu {
27+
view-transition-name: collapse-menu;
28+
}
29+
30+
/*
31+
* Clip each menu-top's snapshot to its animating group box. By default a
32+
* captured snapshot keeps its full intrinsic height, so the new (expanded)
33+
* submenu paints at full height from the first frame and overlaps the
34+
* items below while they are still sliding down. Clipping the image pair
35+
* to the group — whose height animates from the link row to the full
36+
* expanded height — makes the submenu wipe open, and closed in reverse,
37+
* in step with those items.
38+
*/
39+
::view-transition-image-pair(.wp-menu-top) {
40+
overflow: clip;
41+
}
42+
43+
/*
44+
* Capture an open flyout submenu — one shown on hover or focus — as its own
45+
* group, so that it fades out instead of being cut off by the clip above,
46+
* since it extends outside its menu-top box. match-element gives each
47+
* flyout a unique name, so two open at once cannot collide and abort the
48+
* transition. view-transition-group nests the flyout's group inside its
49+
* menu-top's group, so it moves along with its menu item while fading out
50+
* instead of staying behind at its old position. Since the nested group is
51+
* a sibling of the menu-top's image pair, it is not cut off by the clip.
52+
*
53+
* In the expanded menu, the current item's submenu is in the normal flow,
54+
* so it is left to the clipped menu-top. When the menu is folded, either
55+
* explicitly or automatically on narrower screens, every submenu is a
56+
* flyout, including the current one. Below 783px the menu is responsive and
57+
* its submenus are in the normal flow, so none are named there.
58+
*/
59+
@media screen and (min-width: 783px) {
60+
#adminmenu > .wp-not-current-submenu.opensub > .wp-submenu,
61+
#adminmenu > .wp-not-current-submenu > a.menu-top:focus + .wp-submenu,
62+
.folded #adminmenu > .opensub > .wp-submenu,
63+
.folded #adminmenu > .menu-top > a.menu-top:focus + .wp-submenu {
64+
view-transition-name: match-element;
65+
view-transition-group: nearest;
66+
}
67+
}
68+
69+
@media screen and (min-width: 783px) and (max-width: 960px) {
70+
.auto-fold #adminmenu > .opensub > .wp-submenu,
71+
.auto-fold #adminmenu > .menu-top > a.menu-top:focus + .wp-submenu {
72+
view-transition-name: match-element;
73+
view-transition-group: nearest;
74+
}
875
}
976
}

‎src/wp-includes/default-filters.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@
634634
add_action( 'admin_enqueue_scripts', 'wp_localize_jquery_ui_datepicker', 1000 );
635635
add_action( 'admin_enqueue_scripts', 'wp_common_block_scripts_and_styles' );
636636
add_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
637-
add_action( 'admin_enqueue_scripts', 'wp_enqueue_view_transitions_admin_css' );
637+
add_action( 'admin_head', 'wp_print_view_transitions_render_blocking_link' );
638638
add_action( 'enqueue_block_assets', 'wp_enqueue_classic_theme_styles' );
639639
add_action( 'enqueue_block_assets', 'wp_enqueue_registered_block_scripts_and_styles' );
640640
add_action( 'enqueue_block_assets', 'enqueue_block_styles_assets', 30 );

‎src/wp-includes/view-transitions.php‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
/**
1111
* Enqueues View Transitions CSS for the admin.
1212
*
13+
* To disable admin View Transitions, dequeue the `wp-view-transitions-admin` style at the
14+
* {@see 'admin_enqueue_scripts'} action, rather than unhooking this function.
15+
*
1316
* @since 7.0.0
17+
* @since 7.2.0 Called from wp-admin/admin-header.php instead of hooked to {@see 'admin_enqueue_scripts'}.
1418
*/
1519
function wp_enqueue_view_transitions_admin_css(): void {
1620
wp_enqueue_style( 'wp-view-transitions-admin' );
@@ -26,5 +30,41 @@ function wp_enqueue_view_transitions_admin_css(): void {
2630
function wp_get_view_transitions_admin_css(): string {
2731
$affix = SCRIPT_DEBUG ? '' : '.min';
2832
$path = ABSPATH . "wp-admin/css/view-transitions{$affix}.css";
29-
return file_get_contents( $path );
33+
return (string) file_get_contents( $path );
34+
}
35+
36+
/**
37+
* Prints a render-blocking expectation so View Transitions capture a fully parsed admin menu.
38+
*
39+
* A cross-document view transition snapshots the incoming page at its first render, which can
40+
* happen before the `<body>` has finished parsing. The snapshot would then capture a partially
41+
* built page — for example an incomplete admin menu — and the transition animates to or from
42+
* the wrong state.
43+
*
44+
* The `expect` link blocks the first render until `#wpcontent` is parsed. Rendering is unblocked
45+
* as soon as the target's start tag is parsed, not its children, so the target is the element
46+
* right after the admin menu rather than the menu itself: once `#wpcontent` starts, the whole
47+
* menu has been parsed. Targeting an element near the end of the body would also work, but it
48+
* would delay the first render by the time it takes to parse all of the page content. The media
49+
* query limits the first-render delay to when view transitions can run, matching the
50+
* `@view-transition` rule in view-transitions.css. It does not detect whether a transition is
51+
* actually in progress, so a reload or a direct visit is also held until `#wpcontent` is parsed,
52+
* but since that element starts right after the admin menu, the delay is negligible.
53+
*
54+
* Nothing is printed unless the admin View Transitions style has already been printed, since
55+
* there is otherwise no transition to wait for. That style is only enqueued from
56+
* wp-admin/admin-header.php, so this also leaves out iframe screens such as Thickbox modals,
57+
* which have no `#wpcontent`: an `expect` link whose target never appears blocks the first
58+
* render until the whole document has been parsed.
59+
*
60+
* @since 7.2.0
61+
*
62+
* @link https://html.spec.whatwg.org/multipage/links.html#link-type-expect
63+
*/
64+
function wp_print_view_transitions_render_blocking_link(): void {
65+
if ( ! wp_style_is( 'wp-view-transitions-admin', 'done' ) ) {
66+
return;
67+
}
68+
69+
echo '<link rel="expect" href="#wpcontent" blocking="render" media="(prefers-reduced-motion: no-preference)">' . "\n";
3070
}

‎tests/phpunit/tests/view-transitions/wpEnqueueViewTransitionsAdminCss.php‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,6 @@ public function tear_down() {
2828
parent::tear_down();
2929
}
3030

31-
/**
32-
* Tests that the hook for enqueuing admin view transitions CSS is set up.
33-
*
34-
* @ticket 64470
35-
*/
36-
public function test_hook() {
37-
$this->assertSame( 10, has_action( 'admin_enqueue_scripts', 'wp_enqueue_view_transitions_admin_css' ) );
38-
}
39-
4031
/**
4132
* Tests that the admin view transitions style handle includes the inline CSS.
4233
*
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
/**
3+
* Tests for the wp_print_view_transitions_render_blocking_link() function.
4+
*
5+
* @package WordPress
6+
* @subpackage View Transitions
7+
*/
8+
9+
/**
10+
* @group view-transitions
11+
* @covers ::wp_print_view_transitions_render_blocking_link
12+
*/
13+
class Tests_View_Transitions_wpPrintViewTransitionsRenderBlockingLink extends WP_UnitTestCase {
14+
15+
private ?WP_Styles $original_wp_styles = null;
16+
17+
/**
18+
* Saves the global styles instance and resets it so each test starts with nothing enqueued.
19+
*
20+
* @global WP_Styles|null $wp_styles
21+
*/
22+
public function set_up() {
23+
global $wp_styles;
24+
25+
parent::set_up();
26+
$this->original_wp_styles = $wp_styles;
27+
$wp_styles = null;
28+
}
29+
30+
public function tear_down() {
31+
global $wp_styles;
32+
33+
$wp_styles = $this->original_wp_styles;
34+
parent::tear_down();
35+
}
36+
37+
/**
38+
* Tests that the hook for printing the render-blocking link is set up.
39+
*
40+
* @ticket 65032
41+
*/
42+
public function test_hook(): void {
43+
$this->assertSame( 10, has_action( 'admin_head', 'wp_print_view_transitions_render_blocking_link' ) );
44+
}
45+
46+
/**
47+
* Tests that a render-blocking expect link is printed.
48+
*
49+
* @ticket 65032
50+
*/
51+
public function test_prints_render_blocking_link(): void {
52+
wp_enqueue_view_transitions_admin_css();
53+
get_echo( 'wp_print_styles', array( 'wp-view-transitions-admin' ) );
54+
$this->assertTrue( wp_style_is( 'wp-view-transitions-admin', 'done' ), 'Expected the style to have been printed.' );
55+
56+
$output = get_echo( 'wp_print_view_transitions_render_blocking_link' );
57+
58+
$processor = new WP_HTML_Tag_Processor( $output );
59+
$this->assertTrue( $processor->next_tag( 'LINK' ), 'Expected a LINK tag to be printed.' );
60+
$this->assertSame( 'expect', $processor->get_attribute( 'rel' ), 'Expected the LINK to have a rel of "expect".' );
61+
$this->assertSame( '#wpcontent', $processor->get_attribute( 'href' ), 'Expected the LINK to point at the element after the admin menu.' );
62+
$this->assertSame( 'render', $processor->get_attribute( 'blocking' ), 'Expected the LINK to block rendering.' );
63+
$this->assertSame( '(prefers-reduced-motion: no-preference)', $processor->get_attribute( 'media' ), 'Expected the LINK to be scoped to the view transitions media query.' );
64+
$this->assertFalse( $processor->next_tag(), 'Expected only a single tag to be printed.' );
65+
}
66+
67+
/**
68+
* Tests that nothing is printed when the admin View Transitions style is not enqueued.
69+
*
70+
* @ticket 65032
71+
*/
72+
public function test_prints_nothing_when_style_not_enqueued(): void {
73+
$this->assertFalse( wp_style_is( 'wp-view-transitions-admin' ), 'Expected the style to not be enqueued.' );
74+
$this->assertSame( '', get_echo( 'wp_print_view_transitions_render_blocking_link' ) );
75+
}
76+
77+
/**
78+
* Tests that nothing is printed when the admin View Transitions style is enqueued but not yet printed.
79+
*
80+
* This happens when the style is enqueued too late for the head, so it is printed in the footer instead.
81+
*
82+
* @ticket 65032
83+
*/
84+
public function test_prints_nothing_when_style_not_printed(): void {
85+
wp_enqueue_view_transitions_admin_css();
86+
$this->assertTrue( wp_style_is( 'wp-view-transitions-admin' ), 'Expected the style to be enqueued.' );
87+
$this->assertFalse( wp_style_is( 'wp-view-transitions-admin', 'done' ), 'Expected the style to not be printed yet.' );
88+
89+
$this->assertSame( '', get_echo( 'wp_print_view_transitions_render_blocking_link' ) );
90+
}
91+
}

0 commit comments

Comments
 (0)