From 3b84e7da9ee4ffdabd83ac92c2450d5a5adf9919 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 19 Dec 2023 09:44:20 -0800 Subject: [PATCH 1/5] Replace TT1 custom JS frontend code with Interactivity API (if available). --- .../assets/js/interactivity.js | 206 ++++++++++++++++++ .../class-twenty-twenty-one-dark-mode.php | 33 ++- .../themes/twentytwentyone/functions.php | 12 + .../themes/twentytwentyone/header.php | 12 +- .../twentytwentyone/inc/menu-functions.php | 9 +- .../template-parts/header/site-nav.php | 20 +- 6 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 src/wp-content/themes/twentytwentyone/assets/js/interactivity.js diff --git a/src/wp-content/themes/twentytwentyone/assets/js/interactivity.js b/src/wp-content/themes/twentytwentyone/assets/js/interactivity.js new file mode 100644 index 0000000000000..55570e2ba634a --- /dev/null +++ b/src/wp-content/themes/twentytwentyone/assets/js/interactivity.js @@ -0,0 +1,206 @@ +/** + * WordPress dependencies + */ +import { store, getContext, getElement } from '@wordpress/interactivity'; + +function checkClass( element, className ) { + if ( element.classList.contains( className ) ) { + return element; + } + if ( element.parentElement && element.parentElement.classList.contains( className ) ) { + return element.parentElement; + } + if ( element.parentElement.parentElement && element.parentElement.parentElement.classList.contains( className ) ) { + return element.parentElement.parentElement; + } + return null; +} + +const { state, actions } = store( 'twentytwentyone', { + state: { + isPrimaryMenuOpen: false, + windowWidth: 0, + prevScroll: 0, + isDarkMode: false, + isDarkModeTogglerHidden: false, + }, + actions: { + togglePrimaryMenu: () => { + state.isPrimaryMenuOpen = ! state.isPrimaryMenuOpen; + }, + + openPrimaryMenu: () => { + state.isPrimaryMenuOpen = true; + }, + + closePrimaryMenu: () => { + state.isPrimaryMenuOpen = false; + }, + + toggleDarkMode: () => { + state.isDarkMode = ! state.isDarkMode; + window.localStorage.setItem( 'twentytwentyoneDarkMode', state.isDarkMode ? 'yes' : 'no' ); + }, + + trapFocusInModal: ( event ) => { + if ( ! state.isPrimaryMenuOpen ) { + return; + } + + const ctx = getContext(); + + const escKey = event.keyCode === 27; + if ( escKey ) { + event.preventDefault(); + actions.closePrimaryMenu(); + if ( ctx.firstFocusable ) { + ctx.firstFocusable.focus(); + } + return; + } + + const tabKey = event.keyCode === 9; + const shiftKey = event.shiftKey; + const activeEl = document.activeElement; // eslint-disable-line @wordpress/no-global-active-element + + if ( ! shiftKey && tabKey && ctx.lastFocusable === activeEl ) { + event.preventDefault(); + if ( ctx.firstFocusable ) { + ctx.firstFocusable.focus(); + } + return; + } + + if ( shiftKey && tabKey && ctx.firstFocusable === activeEl ) { + event.preventDefault(); + if ( ctx.lastFocusable ) { + ctx.lastFocusable.focus(); + } + return; + } + + // If there are no elements in the menu, don't move the focus + if ( tabKey && ctx.firstFocusable === ctx.lastFocusable ) { + event.preventDefault(); + } + }, + + listenToSpecialClicks: ( event ) => { + const ctx = getContext(); + + // Check if this was a `.sub-menu-toggle` click. + const subMenuToggle = checkClass( event.target, 'sub-menu-toggle' ); + if ( subMenuToggle ) { + if ( ctx.activeSubmenu === subMenuToggle ) { + ctx.activeSubmenu = null; + } else { + ctx.activeSubmenu = subMenuToggle; + } + return; + } + + // Otherwise, check if this was an anchor link click. + if ( ! event.target.hash ) { + return; + } + + actions.closePrimaryMenu(); + + // Wait 550 and scroll to the anchor. + setTimeout( () => { + var anchor = document.getElementById( event.target.hash.slice( 1 ) ); + if ( anchor ) { + anchor.scrollIntoView(); + } + }, 550 ); + }, + }, + callbacks: { + determineFocusableElements: () => { + if ( ! state.isPrimaryMenuOpen ) { + return; + } + + const ctx = getContext(); + const { ref } = getElement(); + const elements = ref.querySelectorAll( 'input, a, button' ); + + ctx.firstFocusable = elements[ 0 ]; + ctx.lastFocusable = elements[ elements.length - 1 ]; + }, + + refreshSubmenus: () => { + const ctx = getContext(); + const { ref } = getElement(); + const elements = ref.querySelectorAll( '.sub-menu-toggle' ); + elements.forEach( ( subMenuToggle ) => { + if ( ctx.activeSubmenu === subMenuToggle ) { + subMenuToggle.setAttribute( 'aria-expanded', 'true' ); + } else { + subMenuToggle.setAttribute( 'aria-expanded', 'false' ); + } + } ); + }, + + makeIframesResponsive: () => { + const { ref } = getElement(); + + ref.querySelectorAll( 'iframe' ).forEach( function( iframe ) { + // Only continue if the iframe has a width & height defined. + if ( iframe.width && iframe.height ) { + // Calculate the proportion/ratio based on the width & height. + proportion = parseFloat( iframe.width ) / parseFloat( iframe.height ); + // Get the parent element's width. + parentWidth = parseFloat( window.getComputedStyle( iframe.parentElement, null ).width.replace( 'px', '' ) ); + // Set the max-width & height. + iframe.style.maxWidth = '100%'; + iframe.style.maxHeight = Math.round( parentWidth / proportion ).toString() + 'px'; + } + } ); + }, + + updateWindowWidthOnResize: () => { + // The following may be needed here since we can't use `data-wp-on--resize`? + const refreshWidth = () => { + state.windowWidth = window.innerWidth; + } + window.onresize = refreshWidth; + }, + + initDarkMode: () => { + let isDarkMode = window.matchMedia( '(prefers-color-scheme: dark)' ).matches; + + if ( 'yes' === window.localStorage.getItem( 'twentytwentyoneDarkMode' ) ) { + isDarkMode = true; + } else if ( 'no' === window.localStorage.getItem( 'twentytwentyoneDarkMode' ) ) { + isDarkMode = false; + } + + state.isDarkMode = isDarkMode; + + // The following may be needed here since we can't use `data-wp-on--scroll`? + const checkScroll = () => { + const currentScroll = window.scrollY || document.documentElement.scrollTop; + if ( + currentScroll + ( window.innerHeight * 1.5 ) > document.body.clientHeight || + currentScroll < state.prevScroll + ) { + state.isDarkModeTogglerHidden = false; + } else if ( currentScroll > state.prevScroll && 250 < currentScroll ) { + state.isDarkModeTogglerHidden = true; + } + state.prevScroll = currentScroll; + } + window.addEventListener( 'scroll', checkScroll ); + }, + + refreshHtmlElementDarkMode: () => { + // This hack may be needed since the HTML element cannot be controlled with the API attributes? + if ( state.isDarkMode ) { + document.documentElement.classList.add( 'is-dark-theme' ); + } else { + document.documentElement.classList.remove( 'is-dark-theme' ); + } + }, + }, +} ); diff --git a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php index 39b3fb726fc88..fe25a5540ecb6 100644 --- a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php +++ b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php @@ -52,6 +52,11 @@ public function __construct() { * @return void */ public function editor_custom_color_variables() { + // See potential bug that this fixes in https://core.trac.wordpress.org/ticket/60111. + if ( ! is_admin() ) { + return; + } + if ( ! $this->switch_should_render() ) { return; } @@ -306,15 +311,22 @@ public function the_switch() { * @return void */ public function the_html( $attrs = array() ) { - $attrs = wp_parse_args( - $attrs, - array( - 'id' => 'dark-mode-toggler', - 'class' => 'fixed-bottom', - 'aria-pressed' => 'false', - 'onClick' => 'toggleDarkMode()', - ) + $defaults = array( + 'id' => 'dark-mode-toggler', + 'class' => 'fixed-bottom', + 'aria-pressed' => 'false', ); + + // Extra attributes depending on whether or not the Interactivity API is being used. + if ( function_exists( 'gutenberg_register_module' ) ) { + $defaults['data-wp-on--click'] = 'actions.toggleDarkMode'; + $defaults['data-wp-bind--aria-pressed'] = 'state.isDarkMode'; + $defaults['data-wp-class--hide'] = 'state.isDarkModeTogglerHidden'; + } else { + $defaults['onClick'] = 'toggleDarkMode()'; + } + + $attrs = wp_parse_args( $attrs, $defaults ); echo ' $val ) { echo ' ' . esc_attr( $key ) . '="' . esc_attr( $val ) . '"'; @@ -363,6 +375,11 @@ public function the_html( $attrs = array() ) { * @return void */ public function the_script() { + // If the Interactivity API is being used, loading this JS code is not necessary. + if ( function_exists( 'gutenberg_register_module' ) ) { + return; + } + echo ''; diff --git a/src/wp-content/themes/twentytwentyone/functions.php b/src/wp-content/themes/twentytwentyone/functions.php index f163e394df734..b246e4bae79f5 100644 --- a/src/wp-content/themes/twentytwentyone/functions.php +++ b/src/wp-content/themes/twentytwentyone/functions.php @@ -442,6 +442,18 @@ function twenty_twenty_one_scripts() { ) ); + // Use WordPress Interactivity API if available. + if ( function_exists( 'gutenberg_register_module' ) ) { + gutenberg_register_module( + '@twentytwentyone/interactivity', + get_template_directory_uri() . '/assets/js/interactivity.js', + array( '@wordpress/interactivity' ), + wp_get_theme()->get( 'Version' ) + ); + gutenberg_enqueue_module( '@twentytwentyone/interactivity' ); + return; + } + // Main navigation scripts. if ( has_nav_menu( 'primary' ) ) { wp_enqueue_script( diff --git a/src/wp-content/themes/twentytwentyone/header.php b/src/wp-content/themes/twentytwentyone/header.php index a601a2aa6bca8..b3981140cc21e 100644 --- a/src/wp-content/themes/twentytwentyone/header.php +++ b/src/wp-content/themes/twentytwentyone/header.php @@ -20,7 +20,17 @@ -> + + data-wp-interactive='{"namespace": "twentytwentyone"}' + data-wp-class--primary-navigation-open="state.isPrimaryMenuOpen" + data-wp-class--lock-scrolling="state.isPrimaryMenuOpen" + data-wp-class--is-dark-theme="state.isDarkMode" + data-wp-init--iframes="callbacks.updateWindowWidthOnResize" + data-wp-watch--iframes="callbacks.makeIframesResponsive" + data-wp-init--darkmode="callbacks.initDarkMode" + data-wp-watch--darkmode="callbacks.refreshHtmlElementDarkMode" +>