From 08e7cae7911352aa47cf90d43921eafe90858002 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Fri, 9 Jan 2026 02:03:44 +0530 Subject: [PATCH 1/9] Add support for chronological and pagination transitions Co-authored-by: Felix Arntz --- plugins/view-transitions/includes/theme.php | 123 ++++++++++++- plugins/view-transitions/js/types.ts | 4 + .../view-transitions/js/view-transitions.js | 162 +++++++++++++++++- 3 files changed, 279 insertions(+), 10 deletions(-) diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index fb2ad15ada..f1d7dae417 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -66,18 +66,22 @@ function plvt_sanitize_view_transitions_theme_support(): void { $args = $_wp_theme_features['view-transitions']; $defaults = array( - 'post-selector' => '.wp-block-post.post, article.post, body.single main', - 'global-transition-names' => array( + 'post-selector' => '.wp-block-post.post, article.post, body.single main', + 'global-transition-names' => array( 'header' => 'header', 'main' => 'main', ), - 'post-transition-names' => array( + 'post-transition-names' => array( '.wp-block-post-title, .entry-title' => 'post-title', '.wp-post-image' => 'post-thumbnail', '.wp-block-post-content, .entry-content' => 'post-content', ), - 'default-animation' => 'fade', - 'default-animation-duration' => 400, + 'default-animation' => 'fade', + 'default-animation-duration' => 400, + 'chronological-forwards-animation' => false, + 'chronological-backwards-animation' => false, + 'pagination-forwards-animation' => false, + 'pagination-backwards-animation' => false, ); // If no specific `$args` were provided, simply use the defaults. @@ -102,8 +106,21 @@ function plvt_sanitize_view_transitions_theme_support(): void { if ( ! is_array( $args['post-transition-names'] ) ) { $args['post-transition-names'] = array(); } - } + // If specific transition animations match the default animations, they are irrelevant. + if ( $args['chronological-forwards-animation'] === $args['default-animation'] ) { + $args['chronological-forwards-animation'] = false; + } + if ( $args['chronological-backwards-animation'] === $args['default-animation'] ) { + $args['chronological-backwards-animation'] = false; + } + if ( $args['pagination-forwards-animation'] === $args['default-animation'] ) { + $args['pagination-forwards-animation'] = false; + } + if ( $args['pagination-backwards-animation'] === $args['default-animation'] ) { + $args['pagination-backwards-animation'] = false; + } + } // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $_wp_theme_features['view-transitions'] = $args; } @@ -324,7 +341,11 @@ function plvt_load_view_transitions(): void { */ if ( ( ! is_array( $theme_support['global-transition-names'] ) || count( $theme_support['global-transition-names'] ) === 0 ) && - ( ! is_array( $theme_support['post-transition-names'] ) || count( $theme_support['post-transition-names'] ) === 0 ) + ( ! is_array( $theme_support['post-transition-names'] ) || count( $theme_support['post-transition-names'] ) === 0 ) && + ! (bool) $theme_support['chronological-forwards-animation'] && + ! (bool) $theme_support['chronological-backwards-animation'] && + ! (bool) $theme_support['pagination-forwards-animation'] && + ! (bool) $theme_support['pagination-backwards-animation'] ) { return; } @@ -336,6 +357,34 @@ function plvt_load_view_transitions(): void { ), ); + $additional_transition_types = array( + 'chronological-forwards', + 'chronological-backwards', + 'pagination-forwards', + 'pagination-backwards', + ); + + foreach ( $additional_transition_types as $transition_type ) { + if ( isset( $theme_support[ $transition_type . '-animation' ] ) ) { + $additional_animation_args = isset( $theme_support[ $transition_type . '-animation-args' ] ) ? (array) $theme_support[ $transition_type . '-animation-args' ] : array(); + $additional_animation_stylesheet = $animation_registry->get_animation_stylesheet( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ); + if ( '' !== $additional_animation_stylesheet ) { + wp_add_inline_style( + 'plvt-view-transitions', + plvt_scope_animation_stylesheet_to_transition_type( $additional_animation_stylesheet, $transition_type ) + ); + } + + $animations_js_config[ $transition_type ] = array( + 'useGlobalTransitionNames' => $animation_registry->use_animation_global_transition_names( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ), + 'usePostTransitionNames' => $animation_registry->use_animation_post_transition_names( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ), + 'targetName' => isset( $additional_animation_args['target-name'] ) ? $additional_animation_args['target-name'] : '*', // Special argument. + ); + } else { + $animations_js_config[ $transition_type ] = false; + } + } + $config = array( 'postSelector' => $theme_support['post-selector'], 'globalTransitionNames' => $theme_support['global-transition-names'], @@ -389,3 +438,63 @@ function plvt_inject_animation_duration( string $css, int $animation_duration ): return $css; } + +/** + * Scopes the given view transition animation CSS to apply only to a specific transition type. + * + * @since n.e.x.t + * @access private + * + * @param string $css Animation stylesheet as inline CSS. + * @param string $transition_type Transition type to scope the CSS to. + * @return string Scoped animation stylesheet. + */ +function plvt_scope_animation_stylesheet_to_transition_type( string $css, string $transition_type ): string { + $indent = static function ( string $input, $indent_tabs = 1 ): string { + return implode( + "\n", + array_map( + static function ( string $line ) use ( $indent_tabs ): string { + return str_repeat( "\t", $indent_tabs ) . $line; + }, + explode( "\n", $input ) + ) + ); + }; + + // This is very fragile, but it works well enough for now. TODO: Find a better solution to scope the CSS selectors. + if ( (bool) preg_match_all( '/(\s*)([^{}]+)\{[^{}]*?\}/m', $css, $matches ) ) { + // Wrap all `::view-transition-*` selectors to scope them to the transition type. + $view_transition_rule_pattern = '/::view-transition-/'; + + foreach ( $matches[0] as $index => $match ) { + $rule = $match; + $rule_name = $matches[2][ $index ]; + if ( (bool) preg_match( $view_transition_rule_pattern, $rule_name ) ) { + $rule_whitespace = $matches[1][ $index ]; + $prefixed_rule_name = preg_replace( $view_transition_rule_pattern, '&\0', $rule_name ); + if ( null === $prefixed_rule_name ) { + continue; + } + + $rule = str_replace( $rule_name, $prefixed_rule_name, $rule ); + + if ( str_contains( $rule, "\n" ) ) { // Non-minified. + $rule = $rule_whitespace . + "html:active-view-transition-type({$transition_type}) {\n" . + $indent( substr( $rule, strlen( $rule_whitespace ) ), 1 ) . + "\n}"; + } else { // Minified. + $rule = $rule_whitespace . + "html:active-view-transition-type({$transition_type}){" . + substr( $rule, strlen( $rule_whitespace ) ) . + '}'; + } + + // Replace the original rule with the wrapped/scoped one. + $css = str_replace( $match, $rule, $css ); + } + } + } + return $css; +} diff --git a/plugins/view-transitions/js/types.ts b/plugins/view-transitions/js/types.ts index c1509d37d4..1653b841c2 100644 --- a/plugins/view-transitions/js/types.ts +++ b/plugins/view-transitions/js/types.ts @@ -14,6 +14,10 @@ export type InitViewTransitionsFunction = ( config: ViewTransitionsConfig ) => void; +export type NavigationHistoryEntry = { + url: string; +}; + declare global { interface Window { plvtInitViewTransitions?: InitViewTransitionsFunction; diff --git a/plugins/view-transitions/js/view-transitions.js b/plugins/view-transitions/js/view-transitions.js index 485e7bba10..fe98730486 100644 --- a/plugins/view-transitions/js/view-transitions.js +++ b/plugins/view-transitions/js/view-transitions.js @@ -3,6 +3,7 @@ * @typedef {import("./types.ts").InitViewTransitionsFunction} InitViewTransitionsFunction * @typedef {import("./types.ts").PageSwapListenerFunction} PageSwapListenerFunction * @typedef {import("./types.ts").PageRevealListenerFunction} PageRevealListenerFunction + * @typedef {import("./types.ts").NavigationHistoryEntry} NavigationHistoryEntry */ /** @@ -133,6 +134,156 @@ window.plvtInitViewTransitions = ( config ) => { return articleLink.closest( config.postSelector ); }; + /** + * Determines the view transition type to use, given an old and new navigation history entry. + * + * @param {NavigationHistoryEntry|null} oldEntry Navigation history entry for the URL navigated from. + * @param {NavigationHistoryEntry} newEntry Navigation history entry for the URL navigated to. + * @return {string} View transition type (e.g. 'default', 'chronological-forwards', 'chronological-backwards'). + */ + const determineTransitionType = ( oldEntry, newEntry ) => { + if ( ! oldEntry || ! newEntry ) { + return 'default'; + } + + // Use 'default' transition type if all other transition types are disabled. + if ( + ! config.animations[ 'chronological-forwards' ] && + ! config.animations[ 'chronological-backwards' ] && + ! config.animations[ 'pagination-forwards' ] && + ! config.animations[ 'pagination-backwards' ] + ) { + return 'default'; + } + + const oldURL = new URL( oldEntry.url ); + const newURL = new URL( newEntry.url ); + + const oldPathname = oldURL.pathname; + const newPathname = newURL.pathname; + + if ( oldPathname === newPathname ) { + return 'default'; + } + + let oldPageMatches = null; + let newPageMatches = null; + let prefix = ''; + + // If enabled, check if the URLs are for a chronologically paginated archive. + if ( + config.animations[ 'chronological-forwards' ] || + config.animations[ 'chronological-backwards' ] + ) { + oldPageMatches = oldPathname.match( /\/page\/(\d+)\/?$/ ); + newPageMatches = newPathname.match( /\/page\/(\d+)\/?$/ ); + prefix = 'chronological-'; + } + + // If not, check if the URLs are for a multi-page post. + if ( + ! oldPageMatches && + ! newPageMatches && + ( config.animations[ 'pagination-forwards' ] || + config.animations[ 'pagination-backwards' ] ) + ) { + oldPageMatches = oldPathname.match( /\/(\d+)\/?$/ ); + newPageMatches = newPathname.match( /\/(\d+)\/?$/ ); + prefix = 'pagination-'; + } + // If there is a match on at least one of the URLs, compare whether their roots before the page segment match. + if ( oldPageMatches || newPageMatches ) { + const oldPageBase = oldPageMatches + ? oldPathname.substring( + 0, + oldPathname.length - oldPageMatches[ 0 ].length + ) + : oldPathname.replace( /\/$/, '' ); + const newPageBase = newPageMatches + ? newPathname.substring( + 0, + newPathname.length - newPageMatches[ 0 ].length + ) + : newPathname.replace( /\/$/, '' ); + + if ( oldPageBase === newPageBase ) { + // They belong to the same archive or post. + // Return the appropriate transition type, or 'default' if no particular animation is specified. + if ( oldPageMatches && newPageMatches ) { + if ( + Number( oldPageMatches[ 1 ] ) < + Number( newPageMatches[ 1 ] ) + ) { + return config.animations[ `${ prefix }forwards` ] + ? `${ prefix }forwards` + : 'default'; + } + return config.animations[ `${ prefix }backwards` ] + ? `${ prefix }backwards` + : 'default'; + } + if ( newPageMatches && Number( newPageMatches[ 1 ] ) > 1 ) { + return config.animations[ `${ prefix }forwards` ] + ? `${ prefix }forwards` + : 'default'; + } + if ( oldPageMatches && Number( oldPageMatches[ 1 ] ) > 1 ) { + return config.animations[ `${ prefix }backwards` ] + ? `${ prefix }backwards` + : 'default'; + } + } + } + + // If enabled, check if the URLs are for content labelled by date (e.g. navigation to previous/next post). + if ( + config.animations[ 'chronological-forwards' ] || + config.animations[ 'chronological-backwards' ] + ) { + const oldDateMatches = oldPathname.match( + /\/(\d{4})\/(\d{2})\/(\d{2})\/[^\/]+\/?$/ + ); + const newDateMatches = newPathname.match( + /\/(\d{4})\/(\d{2})\/(\d{2})\/[^\/]+\/?$/ + ); + if ( oldDateMatches && newDateMatches ) { + const oldPageBase = oldPathname.substring( + 0, + oldPathname.length - oldDateMatches[ 0 ].length + ); + const newPageBase = newPathname.substring( + 0, + newPathname.length - newDateMatches[ 0 ].length + ); + if ( oldPageBase === newPageBase ) { + // They belong to the same hierarchy. + const oldDate = new Date( + parseInt( oldDateMatches[ 1 ] ), + parseInt( oldDateMatches[ 2 ] ) - 1, + parseInt( oldDateMatches[ 3 ] ) + ); + const newDate = new Date( + parseInt( newDateMatches[ 1 ] ), + parseInt( newDateMatches[ 2 ] ) - 1, + parseInt( newDateMatches[ 3 ] ) + ); + if ( oldDate < newDate ) { + return config.animations[ 'chronological-forwards' ] + ? 'chronological-forwards' + : 'default'; + } + if ( oldDate > newDate ) { + return config.animations[ 'chronological-backwards' ] + ? 'chronological-backwards' + : 'default'; + } + } + } + } + + return 'default'; + }; + /** * Customizes view transition behavior on the URL that is being navigated from. * @@ -143,9 +294,11 @@ window.plvtInitViewTransitions = ( config ) => { 'pageswap', ( /** @type {PageSwapEvent} */ event ) => { if ( event.viewTransition ) { - const transitionType = 'default'; // Only 'default' is supported so far, but more to be added. + const transitionType = determineTransitionType( + event.activation.from, + event.activation.entry + ); event.viewTransition.types.add( transitionType ); - let viewTransitionEntries; if ( document.body.classList.contains( 'single' ) ) { viewTransitionEntries = getViewTransitionEntries( @@ -184,7 +337,10 @@ window.plvtInitViewTransitions = ( config ) => { 'pagereveal', ( /** @type {PageRevealEvent} */ event ) => { if ( event.viewTransition ) { - const transitionType = 'default'; // Only 'default' is supported so far, but more to be added. + const transitionType = determineTransitionType( + window.navigation.activation.from, + window.navigation.activation.entry + ); event.viewTransition.types.add( transitionType ); let viewTransitionEntries; From f51830674a55dd2b08fd358afbfd873c22d1b241 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Fri, 9 Jan 2026 02:42:05 +0530 Subject: [PATCH 2/9] Add options to settings for chronological and pagination transitions --- .../view-transitions/includes/settings.php | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/plugins/view-transitions/includes/settings.php b/plugins/view-transitions/includes/settings.php index 4242dfc6de..f4d0df4d50 100644 --- a/plugins/view-transitions/includes/settings.php +++ b/plugins/view-transitions/includes/settings.php @@ -21,19 +21,22 @@ */ function plvt_get_view_transition_animation_labels(): array { return array( - 'fade' => _x( 'Fade (default)', 'animation label', 'view-transitions' ), - 'slide-from-right' => _x( 'Slide (from right)', 'animation label', 'view-transitions' ), - 'slide-from-left' => _x( 'Slide (from left)', 'animation label', 'view-transitions' ), - 'slide-from-bottom' => _x( 'Slide (from bottom)', 'animation label', 'view-transitions' ), - 'slide-from-top' => _x( 'Slide (from top)', 'animation label', 'view-transitions' ), - 'swipe-from-right' => _x( 'Swipe (from right)', 'animation label', 'view-transitions' ), - 'swipe-from-left' => _x( 'Swipe (from left)', 'animation label', 'view-transitions' ), - 'swipe-from-bottom' => _x( 'Swipe (from bottom)', 'animation label', 'view-transitions' ), - 'swipe-from-top' => _x( 'Swipe (from top)', 'animation label', 'view-transitions' ), - 'wipe-from-right' => _x( 'Wipe (from right)', 'animation label', 'view-transitions' ), - 'wipe-from-left' => _x( 'Wipe (from left)', 'animation label', 'view-transitions' ), - 'wipe-from-bottom' => _x( 'Wipe (from bottom)', 'animation label', 'view-transitions' ), - 'wipe-from-top' => _x( 'Wipe (from top)', 'animation label', 'view-transitions' ), + 'fade' => _x( 'Fade (default)', 'animation label', 'view-transitions' ), + 'slide-from-right' => _x( 'Slide (from right)', 'animation label', 'view-transitions' ), + 'slide-from-left' => _x( 'Slide (from left)', 'animation label', 'view-transitions' ), + 'slide-from-bottom' => _x( 'Slide (from bottom)', 'animation label', 'view-transitions' ), + 'slide-from-top' => _x( 'Slide (from top)', 'animation label', 'view-transitions' ), + 'slide-chronological-pagination' => _x( 'Slide (Chronological and Pagination)', 'animation label', 'view-transitions' ), + 'swipe-from-right' => _x( 'Swipe (from right)', 'animation label', 'view-transitions' ), + 'swipe-from-left' => _x( 'Swipe (from left)', 'animation label', 'view-transitions' ), + 'swipe-from-bottom' => _x( 'Swipe (from bottom)', 'animation label', 'view-transitions' ), + 'swipe-from-top' => _x( 'Swipe (from top)', 'animation label', 'view-transitions' ), + 'swipe-chronological-pagination' => _x( 'Swipe (Chronological and Pagination)', 'animation label', 'view-transitions' ), + 'wipe-from-right' => _x( 'Wipe (from right)', 'animation label', 'view-transitions' ), + 'wipe-from-left' => _x( 'Wipe (from left)', 'animation label', 'view-transitions' ), + 'wipe-from-bottom' => _x( 'Wipe (from bottom)', 'animation label', 'view-transitions' ), + 'wipe-from-top' => _x( 'Wipe (from top)', 'animation label', 'view-transitions' ), + 'wipe-chronological-pagination' => _x( 'Wipe (Chronological and Pagination)', 'animation label', 'view-transitions' ), ); } @@ -231,7 +234,22 @@ function plvt_apply_settings_to_theme_support(): void { // Apply the settings. $args['default-animation'] = $options['default_transition_animation']; $args['default-animation-duration'] = absint( $options['default_transition_animation_duration'] ); - $selector_options = array( + + // Automatically enable chronological and pagination animations for special animation options. + $chronological_pagination_animations = array( + 'slide-chronological-pagination' => 'slide', + 'swipe-chronological-pagination' => 'swipe', + 'wipe-chronological-pagination' => 'wipe', + ); + if ( isset( $chronological_pagination_animations[ $args['default-animation'] ] ) ) { + $base_animation = $chronological_pagination_animations[ $args['default-animation'] ]; + $args['chronological-forwards-animation'] = $base_animation . '-from-right'; + $args['chronological-backwards-animation'] = $base_animation . '-from-left'; + $args['pagination-forwards-animation'] = $base_animation . '-from-right'; + $args['pagination-backwards-animation'] = $base_animation . '-from-left'; + } + + $selector_options = array( 'global' => array( 'header_selector' => 'header', 'main_selector' => 'main', From d3b4785a98666a402be07d42b1b569536b982e99 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 26 Feb 2026 00:12:19 +0530 Subject: [PATCH 3/9] Wrap animation stylesheet in media query for reduced motion preference --- plugins/view-transitions/includes/theme.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index f1d7dae417..a68b99d10b 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -371,7 +371,7 @@ function plvt_load_view_transitions(): void { if ( '' !== $additional_animation_stylesheet ) { wp_add_inline_style( 'plvt-view-transitions', - plvt_scope_animation_stylesheet_to_transition_type( $additional_animation_stylesheet, $transition_type ) + '@media (prefers-reduced-motion: no-preference) {' . plvt_scope_animation_stylesheet_to_transition_type( $additional_animation_stylesheet, $transition_type ) . '}' ); } From 5c43cc22420c90f56fab36db90b9fe1dee439d39 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 26 Feb 2026 22:08:40 -0800 Subject: [PATCH 4/9] Apply some static analysis improvements --- plugins/view-transitions/includes/theme.php | 16 ++++++++-------- plugins/view-transitions/js/view-transitions.js | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index a68b99d10b..4b884353e6 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -141,7 +141,7 @@ function plvt_register_view_transition_animations( PLVT_View_Transition_Animatio * animation. */ $is_specific_target_name = static function ( string $alias, array $args ): bool { - return '*' === $args['target-name'] ? false : true; + return ! ( '*' === $args['target-name'] ); }; /* @@ -342,10 +342,10 @@ function plvt_load_view_transitions(): void { if ( ( ! is_array( $theme_support['global-transition-names'] ) || count( $theme_support['global-transition-names'] ) === 0 ) && ( ! is_array( $theme_support['post-transition-names'] ) || count( $theme_support['post-transition-names'] ) === 0 ) && - ! (bool) $theme_support['chronological-forwards-animation'] && - ! (bool) $theme_support['chronological-backwards-animation'] && - ! (bool) $theme_support['pagination-forwards-animation'] && - ! (bool) $theme_support['pagination-backwards-animation'] + false === $theme_support['chronological-forwards-animation'] && + false === $theme_support['chronological-backwards-animation'] && + false === $theme_support['pagination-forwards-animation'] && + false === $theme_support['pagination-backwards-animation'] ) { return; } @@ -378,7 +378,7 @@ function plvt_load_view_transitions(): void { $animations_js_config[ $transition_type ] = array( 'useGlobalTransitionNames' => $animation_registry->use_animation_global_transition_names( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ), 'usePostTransitionNames' => $animation_registry->use_animation_post_transition_names( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ), - 'targetName' => isset( $additional_animation_args['target-name'] ) ? $additional_animation_args['target-name'] : '*', // Special argument. + 'targetName' => $additional_animation_args['target-name'] ?? '*', // Special argument. ); } else { $animations_js_config[ $transition_type ] = false; @@ -481,12 +481,12 @@ static function ( string $line ) use ( $indent_tabs ): string { if ( str_contains( $rule, "\n" ) ) { // Non-minified. $rule = $rule_whitespace . - "html:active-view-transition-type({$transition_type}) {\n" . + "html:active-view-transition-type($transition_type) {\n" . $indent( substr( $rule, strlen( $rule_whitespace ) ), 1 ) . "\n}"; } else { // Minified. $rule = $rule_whitespace . - "html:active-view-transition-type({$transition_type}){" . + "html:active-view-transition-type($transition_type){" . substr( $rule, strlen( $rule_whitespace ) ) . '}'; } diff --git a/plugins/view-transitions/js/view-transitions.js b/plugins/view-transitions/js/view-transitions.js index fe98730486..2f60283404 100644 --- a/plugins/view-transitions/js/view-transitions.js +++ b/plugins/view-transitions/js/view-transitions.js @@ -180,7 +180,7 @@ window.plvtInitViewTransitions = ( config ) => { prefix = 'chronological-'; } - // If not, check if the URLs are for a multi-page post. + // If not, check if the URLs are for a multipage post. if ( ! oldPageMatches && ! newPageMatches && @@ -235,7 +235,7 @@ window.plvtInitViewTransitions = ( config ) => { } } - // If enabled, check if the URLs are for content labelled by date (e.g. navigation to previous/next post). + // If enabled, check if the URLs are for content labeled by date (e.g. navigation to previous/next post). if ( config.animations[ 'chronological-forwards' ] || config.animations[ 'chronological-backwards' ] From b6dfded7f1bd9306b8ba71969af173c6e1ab8de2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 26 Feb 2026 22:27:17 -0800 Subject: [PATCH 5/9] Export pagination_base from PHP --- plugins/view-transitions/includes/theme.php | 5 +++++ plugins/view-transitions/js/types.ts | 1 + plugins/view-transitions/js/view-transitions.js | 11 +++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index 4b884353e6..23d6aaa938 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -310,8 +310,12 @@ function plvt_register_view_transition_animations( PLVT_View_Transition_Animatio * Loads view transitions based on the current configuration. * * @since 1.0.0 + * + * @global WP_Rewrite $wp_rewrite */ function plvt_load_view_transitions(): void { + global $wp_rewrite; + if ( ! current_theme_supports( 'view-transitions' ) ) { return; } @@ -390,6 +394,7 @@ function plvt_load_view_transitions(): void { 'globalTransitionNames' => $theme_support['global-transition-names'], 'postTransitionNames' => $theme_support['post-transition-names'], 'animations' => $animations_js_config, + 'paginationBase' => $wp_rewrite->pagination_base, ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents diff --git a/plugins/view-transitions/js/types.ts b/plugins/view-transitions/js/types.ts index 1653b841c2..7d53eb1cbb 100644 --- a/plugins/view-transitions/js/types.ts +++ b/plugins/view-transitions/js/types.ts @@ -8,6 +8,7 @@ export type ViewTransitionsConfig = { globalTransitionNames?: Record< string, string >; postTransitionNames?: Record< string, string >; animations?: Record< string, ViewTransitionAnimationConfig >; + paginationBase: string; }; export type InitViewTransitionsFunction = ( diff --git a/plugins/view-transitions/js/view-transitions.js b/plugins/view-transitions/js/view-transitions.js index 2f60283404..a3daf9cbe9 100644 --- a/plugins/view-transitions/js/view-transitions.js +++ b/plugins/view-transitions/js/view-transitions.js @@ -159,6 +159,7 @@ window.plvtInitViewTransitions = ( config ) => { const oldURL = new URL( oldEntry.url ); const newURL = new URL( newEntry.url ); + // TODO: Handle non-pretty permalinks. const oldPathname = oldURL.pathname; const newPathname = newURL.pathname; @@ -175,8 +176,12 @@ window.plvtInitViewTransitions = ( config ) => { config.animations[ 'chronological-forwards' ] || config.animations[ 'chronological-backwards' ] ) { - oldPageMatches = oldPathname.match( /\/page\/(\d+)\/?$/ ); - newPageMatches = newPathname.match( /\/page\/(\d+)\/?$/ ); + const pagedRegEx = new RegExp( + '/' + config.paginationBase + '/(\\d+)/?$' // TODO: Escape. + ); + // TODO: Handle non-pretty permalinks. + oldPageMatches = oldPathname.match( pagedRegEx ); + newPageMatches = newPathname.match( pagedRegEx ); prefix = 'chronological-'; } @@ -187,6 +192,7 @@ window.plvtInitViewTransitions = ( config ) => { ( config.animations[ 'pagination-forwards' ] || config.animations[ 'pagination-backwards' ] ) ) { + // TODO: Handle non-pretty permalinks. oldPageMatches = oldPathname.match( /\/(\d+)\/?$/ ); newPageMatches = newPathname.match( /\/(\d+)\/?$/ ); prefix = 'pagination-'; @@ -240,6 +246,7 @@ window.plvtInitViewTransitions = ( config ) => { config.animations[ 'chronological-forwards' ] || config.animations[ 'chronological-backwards' ] ) { + // TODO: Handle non-pretty permalinks. const oldDateMatches = oldPathname.match( /\/(\d{4})\/(\d{2})\/(\d{2})\/[^\/]+\/?$/ ); From 9f38a994888f2f1cd9ee08ae66d34d554dc9746a Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 28 Apr 2026 22:07:30 +0530 Subject: [PATCH 6/9] Fix theme support validation --- plugins/view-transitions/includes/theme.php | 56 ++++++++++++++------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index 6ce66bee5f..1d0e296aac 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -100,7 +100,6 @@ function plvt_sanitize_view_transitions_theme_support(): void { } $args = wp_parse_args( $args, $defaults ); - // Enforce correct types. if ( ! is_array( $args['global-transition-names'] ) ) { $args['global-transition-names'] = array(); @@ -108,6 +107,22 @@ function plvt_sanitize_view_transitions_theme_support(): void { if ( ! is_array( $args['post-transition-names'] ) ) { $args['post-transition-names'] = array(); } + if ( ! is_string( $args['default-animation'] ) ) { + $args['default-animation'] = 'fade'; + } + + $transition_animation_keys = array( + 'chronological-forwards-animation', + 'chronological-backwards-animation', + 'pagination-forwards-animation', + 'pagination-backwards-animation', + ); + + foreach ( $transition_animation_keys as $transition_animation_key ) { + if ( ! is_string( $args[ $transition_animation_key ] ) || '' === $args[ $transition_animation_key ] ) { + $args[ $transition_animation_key ] = false; + } + } // If specific transition animations match the default animations, they are irrelevant. if ( $args['chronological-forwards-animation'] === $args['default-animation'] ) { @@ -355,7 +370,6 @@ function plvt_load_view_transitions(): void { ) { return; } - $animations_js_config = array( 'default' => array( 'useGlobalTransitionNames' => $animation_registry->use_animation_global_transition_names( $theme_support['default-animation'], $default_animation_args ), @@ -371,24 +385,32 @@ function plvt_load_view_transitions(): void { ); foreach ( $additional_transition_types as $transition_type ) { - if ( isset( $theme_support[ $transition_type . '-animation' ] ) ) { - $additional_animation_args = isset( $theme_support[ $transition_type . '-animation-args' ] ) ? (array) $theme_support[ $transition_type . '-animation-args' ] : array(); - $additional_animation_stylesheet = $animation_registry->get_animation_stylesheet( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ); - if ( '' !== $additional_animation_stylesheet ) { - wp_add_inline_style( - 'plvt-view-transitions', - '@media (prefers-reduced-motion: no-preference) {' . plvt_scope_animation_stylesheet_to_transition_type( $additional_animation_stylesheet, $transition_type ) . '}' - ); - } + $transition_animation_key = $transition_type . '-animation'; + if ( ! isset( $theme_support[ $transition_animation_key ] ) ) { + $animations_js_config[ $transition_type ] = false; + continue; + } - $animations_js_config[ $transition_type ] = array( - 'useGlobalTransitionNames' => $animation_registry->use_animation_global_transition_names( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ), - 'usePostTransitionNames' => $animation_registry->use_animation_post_transition_names( $theme_support[ $transition_type . '-animation' ], $additional_animation_args ), - 'targetName' => $additional_animation_args['target-name'] ?? '*', // Special argument. - ); - } else { + $transition_animation_alias = $theme_support[ $transition_animation_key ]; + if ( ! is_string( $transition_animation_alias ) || '' === $transition_animation_alias ) { $animations_js_config[ $transition_type ] = false; + continue; } + + $additional_animation_args = isset( $theme_support[ $transition_type . '-animation-args' ] ) ? (array) $theme_support[ $transition_type . '-animation-args' ] : array(); + $additional_animation_stylesheet = $animation_registry->get_animation_stylesheet( $transition_animation_alias, $additional_animation_args ); + if ( '' !== $additional_animation_stylesheet ) { + wp_add_inline_style( + 'plvt-view-transitions', + '@media (prefers-reduced-motion: no-preference) {' . plvt_scope_animation_stylesheet_to_transition_type( $additional_animation_stylesheet, $transition_type ) . '}' + ); + } + + $animations_js_config[ $transition_type ] = array( + 'useGlobalTransitionNames' => $animation_registry->use_animation_global_transition_names( $transition_animation_alias, $additional_animation_args ), + 'usePostTransitionNames' => $animation_registry->use_animation_post_transition_names( $transition_animation_alias, $additional_animation_args ), + 'targetName' => $additional_animation_args['target-name'] ?? '*', // Special argument. + ); } $config = array( From c8ba51b4f52d5bd57d1c22042217ea0a4285f9df Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 12 May 2026 18:24:28 +0530 Subject: [PATCH 7/9] Add support for animation duration in additional transition animation --- plugins/view-transitions/includes/theme.php | 1 + plugins/view-transitions/tests/test-theme.php | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index 1d0e296aac..54b798e710 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -399,6 +399,7 @@ function plvt_load_view_transitions(): void { $additional_animation_args = isset( $theme_support[ $transition_type . '-animation-args' ] ) ? (array) $theme_support[ $transition_type . '-animation-args' ] : array(); $additional_animation_stylesheet = $animation_registry->get_animation_stylesheet( $transition_animation_alias, $additional_animation_args ); + $additional_animation_stylesheet = plvt_inject_animation_duration( $additional_animation_stylesheet, absint( $theme_support['default-animation-duration'] ) ); if ( '' !== $additional_animation_stylesheet ) { wp_add_inline_style( 'plvt-view-transitions', diff --git a/plugins/view-transitions/tests/test-theme.php b/plugins/view-transitions/tests/test-theme.php index 7879c72f73..e1b092abcb 100644 --- a/plugins/view-transitions/tests/test-theme.php +++ b/plugins/view-transitions/tests/test-theme.php @@ -48,4 +48,37 @@ public function test_plvt_load_view_transitions(): void { $this->assertTrue( wp_style_is( 'plvt-view-transitions', 'registered' ) ); $this->assertTrue( wp_style_is( 'plvt-view-transitions', 'enqueued' ) ); } + + /** + * @covers ::plvt_load_view_transitions + * @covers ::plvt_sanitize_view_transitions_theme_support + * @covers ::plvt_inject_animation_duration + */ + public function test_plvt_load_view_transitions_injects_duration_for_additional_transition_stylesheets(): void { + // Clear up style if it is already registered. + if ( wp_style_is( 'plvt-view-transitions', 'registered' ) ) { + unset( wp_styles()->registered['plvt-view-transitions'] ); + } + + remove_theme_support( 'view-transitions' ); + add_theme_support( + 'view-transitions', + array( + 'default-animation' => 'fade', + 'default-animation-duration' => 500, + 'chronological-forwards-animation' => 'slide-from-right', + 'chronological-backwards-animation' => 'slide-from-left', + ) + ); + + plvt_sanitize_view_transitions_theme_support(); + plvt_load_view_transitions(); + + $styles = wp_styles()->registered['plvt-view-transitions']->extra['after'] ?? array(); + + $this->assertIsArray( $styles ); + $this->assertNotEmpty( $styles ); + $this->assertStringContainsString( '--plvt-view-transition-animation-duration: 0.5s;', implode( '', $styles ) ); + $this->assertStringContainsString( 'html:active-view-transition-type(chronological-forwards)', implode( '', $styles ) ); + } } From b11ae3674211106525642a71f3bf2d51d5a553b0 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 12 May 2026 18:31:58 +0530 Subject: [PATCH 8/9] Update types and add checks for animation config --- plugins/view-transitions/js/types.ts | 7 ++++++- plugins/view-transitions/js/view-transitions.js | 12 +++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/view-transitions/js/types.ts b/plugins/view-transitions/js/types.ts index e587d73598..b65d89a543 100644 --- a/plugins/view-transitions/js/types.ts +++ b/plugins/view-transitions/js/types.ts @@ -1,13 +1,18 @@ export type ViewTransitionAnimationConfig = { useGlobalTransitionNames: boolean; usePostTransitionNames: boolean; + targetName?: string; }; +export type ViewTransitionsAnimationMap = { + default: ViewTransitionAnimationConfig; +} & Record< string, ViewTransitionAnimationConfig | false >; + export type ViewTransitionsConfig = { postSelector?: string; globalTransitionNames?: Record< string, string >; postTransitionNames?: Record< string, string >; - animations?: Record< string, ViewTransitionAnimationConfig >; + animations?: ViewTransitionsAnimationMap; paginationBase: string; }; diff --git a/plugins/view-transitions/js/view-transitions.js b/plugins/view-transitions/js/view-transitions.js index e9b31e4bbc..a03a1404db 100644 --- a/plugins/view-transitions/js/view-transitions.js +++ b/plugins/view-transitions/js/view-transitions.js @@ -33,10 +33,13 @@ window.plvtInitViewTransitions = ( config ) => { bodyElement, articleElement ) => { - const animations = config.animations || {}; + const animationConfig = config.animations?.[ transitionType ]; - const globalEntries = animations[ transitionType ] - .useGlobalTransitionNames + if ( ! animationConfig ) { + return []; + } + + const globalEntries = animationConfig.useGlobalTransitionNames ? Object.entries( config.globalTransitionNames || {} ).map( ( [ selector, name ] ) => { const element = bodyElement.querySelector( selector ); @@ -46,8 +49,7 @@ window.plvtInitViewTransitions = ( config ) => { : []; const postEntries = - animations[ transitionType ].usePostTransitionNames && - articleElement + animationConfig.usePostTransitionNames && articleElement ? Object.entries( config.postTransitionNames || {} ).map( ( [ selector, name ] ) => { const element = From 8bc1d868681262bb8fe87b564bfdf66f71964f13 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 19 May 2026 18:38:15 +0530 Subject: [PATCH 9/9] Add separate checkbox for enabling chronological pagination transition --- .../view-transitions/includes/settings.php | 119 ++++++++++++------ plugins/view-transitions/includes/theme.php | 9 +- plugins/view-transitions/tests/test-theme.php | 53 ++++++++ 3 files changed, 139 insertions(+), 42 deletions(-) diff --git a/plugins/view-transitions/includes/settings.php b/plugins/view-transitions/includes/settings.php index 9b327a1003..84d63784a2 100644 --- a/plugins/view-transitions/includes/settings.php +++ b/plugins/view-transitions/includes/settings.php @@ -23,25 +23,41 @@ */ function plvt_get_view_transition_animation_labels(): array { return array( - 'fade' => _x( 'Fade (default)', 'animation label', 'view-transitions' ), - 'slide-from-right' => _x( 'Slide (from right)', 'animation label', 'view-transitions' ), - 'slide-from-left' => _x( 'Slide (from left)', 'animation label', 'view-transitions' ), - 'slide-from-bottom' => _x( 'Slide (from bottom)', 'animation label', 'view-transitions' ), - 'slide-from-top' => _x( 'Slide (from top)', 'animation label', 'view-transitions' ), - 'slide-chronological-pagination' => _x( 'Slide (Chronological and Pagination)', 'animation label', 'view-transitions' ), - 'swipe-from-right' => _x( 'Swipe (from right)', 'animation label', 'view-transitions' ), - 'swipe-from-left' => _x( 'Swipe (from left)', 'animation label', 'view-transitions' ), - 'swipe-from-bottom' => _x( 'Swipe (from bottom)', 'animation label', 'view-transitions' ), - 'swipe-from-top' => _x( 'Swipe (from top)', 'animation label', 'view-transitions' ), - 'swipe-chronological-pagination' => _x( 'Swipe (Chronological and Pagination)', 'animation label', 'view-transitions' ), - 'wipe-from-right' => _x( 'Wipe (from right)', 'animation label', 'view-transitions' ), - 'wipe-from-left' => _x( 'Wipe (from left)', 'animation label', 'view-transitions' ), - 'wipe-from-bottom' => _x( 'Wipe (from bottom)', 'animation label', 'view-transitions' ), - 'wipe-from-top' => _x( 'Wipe (from top)', 'animation label', 'view-transitions' ), - 'wipe-chronological-pagination' => _x( 'Wipe (Chronological and Pagination)', 'animation label', 'view-transitions' ), + 'fade' => _x( 'Fade (default)', 'animation label', 'view-transitions' ), + 'slide-from-right' => _x( 'Slide (from right)', 'animation label', 'view-transitions' ), + 'slide-from-left' => _x( 'Slide (from left)', 'animation label', 'view-transitions' ), + 'slide-from-bottom' => _x( 'Slide (from bottom)', 'animation label', 'view-transitions' ), + 'slide-from-top' => _x( 'Slide (from top)', 'animation label', 'view-transitions' ), + 'swipe-from-right' => _x( 'Swipe (from right)', 'animation label', 'view-transitions' ), + 'swipe-from-left' => _x( 'Swipe (from left)', 'animation label', 'view-transitions' ), + 'swipe-from-bottom' => _x( 'Swipe (from bottom)', 'animation label', 'view-transitions' ), + 'swipe-from-top' => _x( 'Swipe (from top)', 'animation label', 'view-transitions' ), + 'wipe-from-right' => _x( 'Wipe (from right)', 'animation label', 'view-transitions' ), + 'wipe-from-left' => _x( 'Wipe (from left)', 'animation label', 'view-transitions' ), + 'wipe-from-bottom' => _x( 'Wipe (from bottom)', 'animation label', 'view-transitions' ), + 'wipe-from-top' => _x( 'Wipe (from top)', 'animation label', 'view-transitions' ), ); } +/** + * Returns the supported animation for chronological and pagination transitions. + * + * @since n.e.x.t + * + * @param string $animation Animation slug or alias. + * @return string|false Animation, or false if the animation does not support chronological and pagination + * transitions. + */ +function plvt_get_supported_directional_animation( string $animation ) { + foreach ( array( 'slide', 'swipe', 'wipe' ) as $directional_animation ) { + if ( 0 === strpos( $animation, $directional_animation . '-' ) ) { + return $directional_animation; + } + } + + return false; +} + /** * Returns the default setting value for View Transitions configuration. * @@ -50,12 +66,14 @@ function plvt_get_view_transition_animation_labels(): array { * @since 1.0.0 * @see plvt_sanitize_view_transitions_theme_support() * - * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { + * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, enable_directional_transitions: bool, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { * Default setting value. * * @type bool $override_theme_config Whether to override the current theme's configuration. Otherwise, * the other frontend specific settings won't be applied. * @type string $default_transition_animation Default view transition animation. + * @type bool $enable_directional_transitions Whether to enable chronological and pagination transitions + * for supported animation. * @type int $default_transition_animation_duration Default transition animation duration in milliseconds. * @type string $header_selector CSS selector for the global header element. * @type string $main_selector CSS selector for the global main element. @@ -69,6 +87,7 @@ function plvt_get_setting_default(): array { return array( 'override_theme_config' => false, 'default_transition_animation' => 'fade', + 'enable_directional_transitions' => false, 'default_transition_animation_duration' => 400, 'header_selector' => 'header', 'main_selector' => 'main', @@ -84,12 +103,14 @@ function plvt_get_setting_default(): array { * * @since 1.0.0 * - * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { + * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, enable_directional_transitions: bool, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { * Stored setting value. * * @type bool $override_theme_config Whether to override the current theme's configuration. Otherwise, * the other frontend specific settings won't be applied. * @type string $default_transition_animation Default view transition animation. + * @type bool $enable_directional_transitions Whether to enable chronological and pagination transitions + * for supported animation families. * @type int $default_transition_animation_duration Default transition animation duration in milliseconds. * @type string $header_selector CSS selector for the global header element. * @type string $main_selector CSS selector for the global main element. @@ -109,12 +130,14 @@ function plvt_get_stored_setting_value(): array { * @since 1.0.0 * * @param mixed $input Setting to sanitize. - * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { + * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, enable_directional_transitions: bool, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { * Sanitized setting. * * @type bool $override_theme_config Whether to override the current theme's configuration. Otherwise, * the other frontend specific settings won't be applied. * @type string $default_transition_animation Default view transition animation. + * @type bool $enable_directional_transitions Whether to enable chronological and pagination transitions + * for supported animation families. * @type int $default_transition_animation_duration Default transition animation duration in milliseconds. * @type string $header_selector CSS selector for the global header element. * @type string $main_selector CSS selector for the global main element. @@ -163,6 +186,7 @@ function plvt_sanitize_setting( $input ): array { $checkbox_options = array( 'override_theme_config', + 'enable_directional_transitions', 'enable_admin_transitions', ); foreach ( $checkbox_options as $checkbox_option ) { @@ -171,6 +195,18 @@ function plvt_sanitize_setting( $input ): array { } } + if ( ! is_string( plvt_get_supported_directional_animation( $value['default_transition_animation'] ) ) ) { + if ( isset( $input['enable_directional_transitions'] ) && (bool) $input['enable_directional_transitions'] ) { + add_settings_error( + 'plvt_view_transitions', + 'plvt_directional_transitions_requires_supported_animation', + __( 'Chronological and pagination transitions require a supported default transition animation.', 'view-transitions' ), + 'warning' + ); + } + $value['enable_directional_transitions'] = false; + } + return $value; } @@ -234,17 +270,15 @@ function plvt_apply_settings_to_theme_support(): void { $args = $_wp_theme_features['view-transitions']; // Apply the settings. - $args['default-animation'] = $options['default_transition_animation']; - $args['default-animation-duration'] = absint( $options['default_transition_animation_duration'] ); - - // Automatically enable chronological and pagination animations for special animation options. - $chronological_pagination_animations = array( - 'slide-chronological-pagination' => 'slide', - 'swipe-chronological-pagination' => 'swipe', - 'wipe-chronological-pagination' => 'wipe', - ); - if ( isset( $chronological_pagination_animations[ $args['default-animation'] ] ) ) { - $base_animation = $chronological_pagination_animations[ $args['default-animation'] ]; + $args['default-animation'] = $options['default_transition_animation']; + $args['default-animation-duration'] = absint( $options['default_transition_animation_duration'] ); + $args['chronological-forwards-animation'] = false; + $args['chronological-backwards-animation'] = false; + $args['pagination-forwards-animation'] = false; + $args['pagination-backwards-animation'] = false; + + $base_animation = plvt_get_supported_directional_animation( $args['default-animation'] ); + if ( $options['enable_directional_transitions'] && is_string( $base_animation ) ) { $args['chronological-forwards-animation'] = $base_animation . '-from-right'; $args['chronological-backwards-animation'] = $base_animation . '-from-left'; $args['pagination-forwards-animation'] = $base_animation . '-from-right'; @@ -347,6 +381,11 @@ static function (): void { 'title' => __( 'Default Transition Animation', 'view-transitions' ), 'description' => __( 'Choose the animation that is used for the default view transition type.', 'view-transitions' ), ), + 'enable_directional_transitions' => array( + 'section' => 'plvt_view_transitions', + 'title' => __( 'Chronological And Pagination Transitions', 'view-transitions' ), + 'description' => __( 'Enable directional transitions for chronological and paginated navigation. This only applies when the selected default animation is supported.', 'view-transitions' ), + ), 'default_transition_animation_duration' => array( 'section' => 'plvt_view_transitions', 'title' => __( 'Transition Animation Duration', 'view-transitions' ), @@ -399,7 +438,7 @@ static function (): void { ); // Remove 'label_for' for checkbox fields to avoid duplicate label association. - if ( 'override_theme_config' === $slug || 'enable_admin_transitions' === $slug ) { + if ( 'override_theme_config' === $slug || 'enable_directional_transitions' === $slug || 'enable_admin_transitions' === $slug ) { unset( $additional_args['label_for'] ); } @@ -423,17 +462,18 @@ static function (): void { * @since 1.0.0 * @access private * - * @param array{ field: non-empty-string, title: non-empty-string, description: string, label_for: non-empty-string } $args { + * @param array{ field: non-empty-string, title: non-empty-string, description: string, label_for?: non-empty-string } $args { * Associative array of arguments. * * @type string $field The slug of the sub setting controlled by the field. * @type string $title The title for the field. * @type string $description Optional. A description to show for the field. - * @type string $label_for ID to use for the field control. + * @type string $label_for Optional. ID to use for the field control. * } */ function plvt_render_settings_field( array $args ): void { - $option = plvt_get_stored_setting_value(); + $option = plvt_get_stored_setting_value(); + $field_id = $args['label_for'] ?? "plvt-view-transitions-field-{$args['field']}"; switch ( $args['field'] ) { case 'default_transition_animation': @@ -445,6 +485,7 @@ function plvt_render_settings_field( array $args ): void { $choices = array(); // Defined just for consistency. break; case 'override_theme_config': + case 'enable_directional_transitions': case 'enable_admin_transitions': $type = 'checkbox'; $choices = array(); // Defined just for consistency. @@ -459,12 +500,12 @@ function plvt_render_settings_field( array $args ): void { if ( 'select' === $type ) { ?> - id="" + id="" name="" value="" class="regular-text code" - aria-describedby="" + aria-describedby="" @@ -518,7 +559,7 @@ class="regular-text code" if ( '' !== $args['description'] && 'checkbox' !== $type ) { ?>

diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index 54b798e710..1a4a4924d6 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -56,16 +56,19 @@ function plvt_polyfill_theme_support(): void { * @since 1.0.0 * @access private * - * @global array $_wp_theme_features Theme support features added and their arguments. + * @global bool|null $plvt_has_theme_support_with_args Whether the current theme explicitly supports view transitions with custom config. + * @global array $_wp_theme_features Theme support features added and their arguments. */ function plvt_sanitize_view_transitions_theme_support(): void { - global $_wp_theme_features; + global $plvt_has_theme_support_with_args, $_wp_theme_features; if ( ! isset( $_wp_theme_features['view-transitions'] ) ) { + $plvt_has_theme_support_with_args = false; return; } - $args = $_wp_theme_features['view-transitions']; + $args = $_wp_theme_features['view-transitions']; + $plvt_has_theme_support_with_args = true !== $args; $defaults = array( 'post-selector' => '.wp-block-post.post, article.post, body.single main', diff --git a/plugins/view-transitions/tests/test-theme.php b/plugins/view-transitions/tests/test-theme.php index e1b092abcb..c841ab7cb8 100644 --- a/plugins/view-transitions/tests/test-theme.php +++ b/plugins/view-transitions/tests/test-theme.php @@ -81,4 +81,57 @@ public function test_plvt_load_view_transitions_injects_duration_for_additional_ $this->assertStringContainsString( '--plvt-view-transition-animation-duration: 0.5s;', implode( '', $styles ) ); $this->assertStringContainsString( 'html:active-view-transition-type(chronological-forwards)', implode( '', $styles ) ); } + + /** + * @covers ::plvt_apply_settings_to_theme_support + * @covers ::plvt_sanitize_view_transitions_theme_support + */ + public function test_plvt_apply_settings_to_theme_support_enables_directional_animations_from_default_animation(): void { + remove_theme_support( 'view-transitions' ); + add_theme_support( 'view-transitions' ); + plvt_sanitize_view_transitions_theme_support(); + + update_option( + 'plvt_view_transitions', + array( + 'default_transition_animation' => 'wipe-from-top', + 'enable_directional_transitions' => true, + ) + ); + + plvt_apply_settings_to_theme_support(); + + $theme_support = get_theme_support( 'view-transitions' ); + + $this->assertSame( 'wipe-from-top', $theme_support['default-animation'] ); + $this->assertSame( 'wipe-from-right', $theme_support['chronological-forwards-animation'] ); + $this->assertSame( 'wipe-from-left', $theme_support['chronological-backwards-animation'] ); + $this->assertSame( 'wipe-from-right', $theme_support['pagination-forwards-animation'] ); + $this->assertSame( 'wipe-from-left', $theme_support['pagination-backwards-animation'] ); + + delete_option( 'plvt_view_transitions' ); + } + + /** + * @covers ::plvt_sanitize_setting + * @covers ::plvt_get_supported_directional_animation + */ + public function test_plvt_sanitize_setting_disables_directional_animations_for_unsupported_default_animation(): void { + global $wp_settings_errors; + + $wp_settings_errors = array(); + + $setting = plvt_sanitize_setting( + array( + 'default_transition_animation' => 'fade', + 'enable_directional_transitions' => true, + ) + ); + $settings_errors = get_settings_errors( 'plvt_view_transitions' ); + + $this->assertSame( 'fade', $setting['default_transition_animation'] ); + $this->assertFalse( $setting['enable_directional_transitions'] ); + $this->assertCount( 1, $settings_errors ); + $this->assertSame( 'plvt_directional_transitions_requires_supported_animation', $settings_errors[0]['code'] ); + } }