-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Add view transitions theme support in abstracted way with sensible defaults #8370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
felixarntz
wants to merge
35
commits into
WordPress:trunk
Choose a base branch
from
felixarntz:add/view-transitions-theme-support
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
2ba8487
Implement view transitions as theme support featured, opt in by defau…
1c6592f
Opt in to using view transitions with default configuration for all c…
0d7b15e
Handle view transitions nicely for header.
fb44bb7
Merge branch 'trunk' into add/view-transitions-theme-support
6369845
Implement support for slide view transitions based on pagination and …
67076d6
Flip global-transition-names and post-transition-names as that makes …
9ad61c9
Clarify why JS needs to be included in the head.
felixarntz 22bc6f2
Merge branch 'trunk' into add/view-transitions-theme-support
86b6438
Clarify now obsolete inline documentation.
felixarntz bb00c24
Merge branch 'add/view-transitions-theme-support' of github.com:felix…
e2e6e5a
Move view transitions JS logic into a function to call for initializa…
8163159
Move CSS and JS assets to more appropriate location and support minif…
d05c521
Merge branch 'trunk' into add/view-transitions-theme-support
73337dc
Fix missing switch break.
0751966
Fix JSHint violations.
8c101b2
Fix failing PHPUnit test due to missing built JS file.
220d1b8
Add docblocks for view transition JS functions.
9058ed5
Fix JS doc.
felixarntz 864bcea
Simplify JS logic.
felixarntz 9683425
Merge branch 'trunk' into add/view-transitions-theme-support
dd837c9
Merge branch 'trunk' into add/view-transitions-theme-support
675e398
Simplify JS code.
55c543d
[WIP] Explore options to make horizontal slide animation look good by…
206551a
Vertically align scroll position for horizontal slide view transitions.
260b377
Allow specifying a default animation and implement a gentle wipe anim…
d628aac
Automation: Updating built files with changes. [dependabot skip]
6c1a23b
Merge branch 'trunk' into add/view-transitions-theme-support
6f33080
Use CSS registered custom property for animation angle (props @weston…
eb89cfc
Merge branch 'add/view-transitions-theme-support' of github.com:felix…
e4798d3
Implement classes for view transition animations and allow registerin…
88766c2
Complete decoupling view transition animations from view transition t…
dd04b78
Remove now obsolete view transitions CSS.
f90a336
Fix PHPCS violation.
499445a
Merge branch 'trunk' into add/view-transitions-theme-support
11a8ef1
Merge branch 'trunk' into add/view-transitions-theme-support
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,310 @@ | ||
| /** | ||
| * @output wp-includes/js/wp-view-transitions.js | ||
| */ | ||
|
|
||
| window.wp = window.wp || {}; | ||
| window.wp.viewTransitions = {}; | ||
|
|
||
| /** | ||
| * Initializes view transitions for the current URL. | ||
| * | ||
| * @param {object} config The view transitions configuration. | ||
| * @param {string} config.postSelector General selector for post elements in the DOM. | ||
| * @param {object} config.globalTransitionNames Map of selectors for global elements (queried relative to 'body') | ||
| * and their view transition names. | ||
| * @param {object} config.postTransitionNames Map of selectors for post elements (queried relative to an element | ||
| * identified by config.postSelector) and their view transition names. | ||
| * @param {boolean} config.chronologicalSlideInOut Whether slide in/out animation for chronological URL relationship | ||
| * (date- or pagination-based) should be enabled. | ||
| */ | ||
| window.wp.viewTransitions.init = ( config ) => { | ||
| if ( ! window.navigation || ! ( 'CSSViewTransitionRule' in window ) ) { | ||
| window.console.warn( 'View transitions not loaded as the browser is lacking support.' ); | ||
| return; | ||
| } | ||
|
|
||
| /** | ||
| * Gets all view transition entries relevant for a view transition. | ||
| * | ||
| * @param {string} transitionType View transition type (e.g. 'default', 'chronological-forwards', 'chronological-backwards'). | ||
| * @param {Element} bodyElement The body element. | ||
| * @param {Element|null} articleElement The post element relevant for the view transition, if any. | ||
| * @return {Array[]} View transition entries with each one containing the element and its view transition name. | ||
| */ | ||
| const getViewTransitionEntries = ( transitionType, bodyElement, articleElement ) => { | ||
| const globalEntries = config.animations[ transitionType ].useGlobalTransitionNames ? | ||
| Object.entries( config.globalTransitionNames || {} ).map( ( [ selector, name ] ) => { | ||
| const element = bodyElement.querySelector( selector ); | ||
| return [ element, name ]; | ||
| } ) : []; | ||
|
|
||
| const postEntries = config.animations[ transitionType ].usePostTransitionNames && articleElement ? | ||
| Object.entries( config.postTransitionNames || {} ).map( ( [ selector, name ] ) => { | ||
| const element = articleElement.querySelector( selector ); | ||
| return [ element, name ]; | ||
| } ) : []; | ||
|
|
||
| return [ | ||
| ...globalEntries, | ||
| ...postEntries, | ||
| ]; | ||
| }; | ||
|
|
||
| /** | ||
| * Temporarily sets view transition names for the given entries until the view transition has been completed. | ||
| * | ||
| * @param {Array[]} entries View transition entries as received from `getViewTransitionEntries()`. | ||
| * @param {Promise<void>} vtPromise Promise that resolves after the view transition has been completed. | ||
| * @return {Promise<void>} Promise that resolves after the view transition names were reset. | ||
| */ | ||
| const setTemporaryViewTransitionNames = async ( entries, vtPromise ) => { | ||
| for ( const [ element, name ] of entries ) { | ||
| if ( ! element ) { | ||
| continue; | ||
| } | ||
| element.style.viewTransitionName = name; | ||
| } | ||
|
|
||
| await vtPromise; | ||
|
|
||
| for ( const [ element ] of entries ) { | ||
| if ( ! element ) { | ||
| continue; | ||
| } | ||
| element.style.viewTransitionName = ''; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Appends a selector to another selector. | ||
| * | ||
| * This supports selectors which technically include multiple selectors (separated by comma). | ||
| * | ||
| * @param {string} selectors Main selector. | ||
| * @param {string} append Selector to append to the main selector. | ||
| * @return {string} Combined selector. | ||
| */ | ||
| const appendSelectors = ( selectors, append ) => { | ||
| return selectors.split( ',' ).map( subselector => subselector.trim() + ' ' + append ).join( ',' ); | ||
| }; | ||
|
|
||
| /** | ||
| * Gets a post element (the first on the page, in case there are multiple). | ||
| * | ||
| * @return {Element|null} Post element, or null if none is found. | ||
| */ | ||
| const getArticle = () => { | ||
| if ( ! config.postSelector ) { | ||
| return null; | ||
| } | ||
| return document.querySelector( config.postSelector ); | ||
| }; | ||
|
|
||
| /** | ||
| * Gets the post element for a specific post URL. | ||
| * | ||
| * @param {string} url Post URL (permalink) to find post element. | ||
| * @return {Element|null} Post element, or null if none is found. | ||
| */ | ||
| const getArticleForUrl = ( url ) => { | ||
| if ( ! config.postSelector ) { | ||
| return null; | ||
| } | ||
| const postLinkSelector = appendSelectors( config.postSelector, 'a[href="' + url + '"]' ); | ||
| const articleLink = document.querySelector( postLinkSelector ); | ||
| if ( ! articleLink ) { | ||
| return null; | ||
| } | ||
| return articleLink.closest( config.postSelector ); | ||
| }; | ||
|
|
||
| /** | ||
| * Determines the view transition type to use, given an old and new navigation history entry. | ||
| * | ||
| * @param {NavigationHistoryEntry} 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 = false; | ||
| let newPageMatches = false; | ||
| 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'; | ||
| }; | ||
|
|
||
| /** | ||
| * Gets the view transition name for the element that receives a slide animation based on the transition type determined, if any. | ||
| * | ||
| * @param {string} transitionType View transition type as received from `determineTransitionType()`. | ||
| * @return {string|null} View transition name, or null if none is relevant for the transition type. | ||
| */ | ||
| const getViewTransitionNameForSlideAnimation = ( transitionType ) => { | ||
| if ( config.animations[ transitionType ] && config.animations[ transitionType ].targetName !== '*' ) { | ||
| return config.animations[ transitionType ].targetName; | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| /** | ||
| * Customizes view transition behavior on the URL that is being navigated from. | ||
| * | ||
| * @param {PageSwapEvent} event Event fired as the previous URL is about to unload. | ||
| */ | ||
| window.addEventListener( 'pageswap', ( event ) => { | ||
| if ( event.viewTransition ) { | ||
| const transitionType = determineTransitionType( event.activation.from, event.activation.entry ); | ||
| event.viewTransition.types.add( transitionType ); | ||
|
|
||
| let viewTransitionEntries; | ||
| if ( document.body.classList.contains( 'single' ) ) { | ||
| viewTransitionEntries = getViewTransitionEntries( | ||
| transitionType, | ||
| document.body, | ||
| getArticle() | ||
| ); | ||
| } else if ( document.body.classList.contains( 'home' ) || document.body.classList.contains( 'archive' ) ) { | ||
| viewTransitionEntries = getViewTransitionEntries( | ||
| transitionType, | ||
| document.body, | ||
| getArticleForUrl( event.activation.entry.url ) | ||
| ); | ||
| } | ||
| if ( viewTransitionEntries ) { | ||
| setTemporaryViewTransitionNames( viewTransitionEntries, event.viewTransition.finished ); | ||
|
|
||
| const slideViewTransitionName = getViewTransitionNameForSlideAnimation( transitionType ); | ||
| if ( slideViewTransitionName ) { | ||
| // Consider a scroll offset if defined (e.g. due to fixed navigation bars being in the way). | ||
| const scrollYOffset = document.documentElement.style.getPropertyValue( '--wp-scroll-y-offset' ); | ||
| const currentScrollY = window.scrollY - ( scrollYOffset ? parseInt( scrollYOffset, 10 ) : 0 ); | ||
| sessionStorage.setItem( 'wpViewTransitionsOldScrollY', currentScrollY ); | ||
| } | ||
| } | ||
| } | ||
| } ); | ||
|
|
||
| /** | ||
| * Customizes view transition behavior on the URL that is being navigated to. | ||
| * | ||
| * @param {PageRevealEvent} event Event fired as the new URL being navigated to is loaded. | ||
| */ | ||
| window.addEventListener( 'pagereveal', ( event ) => { | ||
| if ( event.viewTransition ) { | ||
| const transitionType = determineTransitionType( window.navigation.activation.from, window.navigation.activation.entry ); | ||
| event.viewTransition.types.add( transitionType ); | ||
|
|
||
| let viewTransitionEntries; | ||
| if ( document.body.classList.contains( 'single' ) ) { | ||
| viewTransitionEntries = getViewTransitionEntries( | ||
| transitionType, | ||
| document.body, | ||
| getArticle() | ||
| ); | ||
| } else if ( document.body.classList.contains( 'home' ) || document.body.classList.contains( 'archive' ) ) { | ||
| viewTransitionEntries = getViewTransitionEntries( | ||
| transitionType, | ||
| document.body, | ||
| window.navigation.activation.from ? getArticleForUrl( window.navigation.activation.from.url ) : null | ||
| ); | ||
| } | ||
| if ( viewTransitionEntries ) { | ||
| setTemporaryViewTransitionNames( viewTransitionEntries, event.viewTransition.ready ); | ||
|
|
||
| const slideViewTransitionName = getViewTransitionNameForSlideAnimation( transitionType ); | ||
| if ( slideViewTransitionName ) { | ||
| const oldScrollY = sessionStorage.getItem( 'wpViewTransitionsOldScrollY' ); | ||
| if ( oldScrollY !== null ) { | ||
| // Align vertical scroll position. | ||
| if ( oldScrollY ) { | ||
| window.scrollTo( 0, parseInt( oldScrollY, 10 ) ); | ||
| } | ||
| sessionStorage.removeItem( 'wpViewTransitionsOldScrollY' ); | ||
| } else { | ||
| // Skip view transition to avoid an odd diagonal slide. | ||
| event.viewTransition.skipTransition(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } ); | ||
| }; | ||
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
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
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
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.