Skip to content
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

Add dark mode support in twenty twenty-one theme #6874

20 changes: 0 additions & 20 deletions includes/admin/class-amp-template-customizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,6 @@ public static function init( WP_Customize_Manager $wp_customize, ReaderThemeLoad
$self->set_refresh_setting_transport();
$self->remove_cover_template_section();
$self->remove_homepage_settings_section();
if ( get_template() === 'twentytwentyone' ) {
add_action( 'customize_controls_print_footer_scripts', [ $self, 'add_dark_mode_toggler_button_notice' ] );
}
return $self;
}

Expand Down Expand Up @@ -235,23 +232,6 @@ protected function remove_homepage_settings_section() {
}
}

/**
* Add notice that the dark mode toggler button is not currently available on AMP pages.
*/
public function add_dark_mode_toggler_button_notice() {
$message = __( 'While dark mode works on AMP pages, the toggle button is not currently available. It appears here only for preview purposes.', 'amp' );
?>
<script>
wp.customize.control( 'respect_user_color_preference', function ( control ) {
control.notifications.add( new wp.customize.Notification( 'amp_dark_mode_toggler_availability_notice', {
message: <?php echo wp_json_encode( $message ); ?>,
type: 'info'
} ) );
} );
</script>
<?php
}

/**
* Init Customizer preview for legacy.
*
Expand Down
127 changes: 85 additions & 42 deletions includes/sanitizers/class-amp-core-theme-sanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use AmpProject\Amp;
use AmpProject\Html\Attribute;
use AmpProject\Html\Tag;
use AmpProject\Html\Role;

/**
Expand Down Expand Up @@ -84,37 +85,32 @@ protected static function get_theme_features_config( $theme_slug, $args = [] ) {
switch ( $theme_slug ) {
case 'twentytwentyone':
$config = [
'dequeue_scripts' => [
'dequeue_scripts' => [
'twenty-twenty-one-responsive-embeds-script',
'twenty-twenty-one-primary-navigation-script',
],
'remove_actions' => [
'remove_actions' => [
'wp_print_footer_scripts' => [
'twenty_twenty_one_skip_link_focus_fix', // Unnecessary since part of the AMP runtime.
],
'wp_footer' => [
'twentytwentyone_add_ie_class',
'twenty_twenty_one_supports_js', // AMP is essentially no-js, with any interactivity added explicitly via amp-bind.
[
'Twenty_Twenty_One_Dark_Mode',
'the_switch',
10,
],
],
],
'amend_twentytwentyone_styles' => [],
'amend_twentytwentyone_styles' => [],
'amend_twentytwentyone_sub_menu_toggles' => [],
'add_twentytwentyone_mobile_modal' => [],
'add_twentytwentyone_sub_menu_fix' => [],
'add_twentytwentyone_mobile_modal' => [],
'add_twentytwentyone_sub_menu_fix' => [],
'add_twentytwentyone_dark_mode_toggle' => [],
'amend_twentytwentyone_dark_mode_styles' => [],
];

// Dark mode button toggle is only supported in the Customizer for now.
// A notice is added to the Customizer control in AMP_Template_Customizer::add_dark_mode_toggler_button_notice() via AMP_Template_Customizer::init().
if ( is_customize_preview() ) {
// Make dark mode toggle AMP compatible.
$config['add_twentytwentyone_dark_mode_toggle'] = [];
} else {
// Amend the dark mode stylesheet to only apply its rules when the user's system supports dark mode.
$config['amend_twentytwentyone_dark_mode_styles'] = [];
// Prevent the dark mode toggle and its accompanying script from being inlined.
$config['remove_actions']['wp_footer'][] = [ 'Twenty_Twenty_One_Dark_Mode', 'the_switch', 10 ];
}

return $config;

// Twenty Twenty.
Expand Down Expand Up @@ -1890,11 +1886,8 @@ public function add_twentytwenty_toggles() {
}

/**
* Amend the Twenty Twenty-One dark mode stylesheet to only apply the relevant rules when the user has requested
* the system use a dark color theme.
*
* Note: Dark mode will only be available when the user's system supports it. The dark mode toggle is not available
* on the frontend as yet since there is no feasible AMP-compatible way to store and unserialize user's preferences.
* Amend the Twenty Twenty-One dark mode stylesheet to only apply the relevant rules when the user has enables
* dark mode support from the customizer options.
*/
public static function amend_twentytwentyone_dark_mode_styles() {
add_action(
Expand Down Expand Up @@ -1924,13 +1917,10 @@ static function() {

$styles = file_get_contents( $dark_mode_css_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

// Restrict rules to only when the user has requested the system use a dark color theme.
$new_styles = str_replace( '@media only screen', '@media only screen and (prefers-color-scheme: dark)', $styles );
// Allow for rules to override the light theme related rules.
$new_styles = str_replace( '.is-dark-theme.is-dark-theme', ':root', $new_styles );
$new_styles = str_replace( '.respect-color-scheme-preference.is-dark-theme body', '.respect-color-scheme-preference:not(._) body', $new_styles );
$styles = str_replace( '.respect-color-scheme-preference.is-dark-theme body', '.respect-color-scheme-preference body.is-dark-theme', $styles );

wp_add_inline_style( $theme_style_handle, $new_styles );
wp_add_inline_style( $theme_style_handle, $styles );
},
11
);
Expand Down Expand Up @@ -1966,6 +1956,13 @@ static function() {

$styles = file_get_contents( $css_file ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

// AMP provides a global API "AMP.toggleTheme()" to support dark theme by toggling a BODY element class.
// However, for dark mode in the Twenty Twenty-One theme, the class needs to be toggled on HTML and BODY elements because the CSS variables are set at the root level.
// In AMP we cannot toggle HTML classes. So here we are changing the location where CSS variables are defined from `:root` to the BODY element.
if ( get_theme_mod( 'respect_user_color_preference', false ) ) {
$styles = str_replace( ':root {', 'body {', $styles );
}

// Append extra rules needed for nav menus according to changes made to the document during sanitization.
$styles .= '
/* Trap keyboard navigation within mobile menu when it\'s open */
Expand Down Expand Up @@ -2022,32 +2019,78 @@ static function() {

/**
* Make the dark mode toggle in the Twenty Twenty-One theme AMP compatible.
*
* Note: This is only shown within the Customizer preview for now, as there is no feasible way of persisting and
* unserializing the user's preference when they switch to dark (or light) mode.
*/
public function add_twentytwentyone_dark_mode_toggle() {
$button = $this->dom->getElementById( 'dark-mode-toggler' );
// First check if dark mode is enabled.
$should_respect_color_scheme = get_theme_mod( 'respect_user_color_preference', false );

if ( ! $button ) {
if ( ! $should_respect_color_scheme ) {
return;
}

$style = $this->dom->createElement( 'style' );
$style->textContent = '.no-js #dark-mode-toggler { display: block; }';
// Create button element for dark mode toggle.
// Dark mode toggle button html and js has been omitted due to removing `the_switch` function.
$button = AMP_DOM_Utils::create_node(
$this->dom,
Tag::BUTTON,
[
Attribute::ID => 'dark-mode-toggler',
Attribute::CLASS_ => 'fixed-bottom',
]
);

/* translators: %s: On/Off */
$dark_mode_label = __( 'Dark Mode: %s', 'twentytwentyone' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
$dark_mode_off = sprintf( $dark_mode_label, __( 'Off', 'twentytwentyone' ) ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
$dark_mode_on = sprintf( $dark_mode_label, __( 'On', 'twentytwentyone' ) ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch

// Create span tag to show `On` for dark mode.
$button_text_on = $this->dom->createElement( Tag::SPAN );
$button_text_on->setAttribute( Attribute::CLASS_, 'dark-mode-button-on' );
$button_text_on->nodeValue = $dark_mode_on;

// Create span tag to show `Off` for dark mode.
$button_text_off = $this->dom->createElement( Tag::SPAN );
$button_text_off->setAttribute( Attribute::CLASS_, 'dark-mode-button-off' );
$button_text_off->nodeValue = $dark_mode_off;

// Add button_text_{On, Off} to button.
$button->appendChild( $button_text_on );
$button->appendChild( $button_text_off );

// Add button to body.
$this->dom->body->appendChild( $button );

$style = $this->dom->createElement( Tag::STYLE );
$style->setAttribute( Attribute::ID, 'amp-twentytwentyone-dark-mode-toggle-styles' );
$style->textContent = sprintf( // We need to add these styles to show On and Off to the user.
'
.no-js #dark-mode-toggler {
display: block;
}
#dark-mode-toggler > span {
margin-%s: 5px;
}
.dark-mode-button-on {
display: none;
}
body.is-dark-theme .dark-mode-button-on {
display: inline-block;
}
body.is-dark-theme .dark-mode-button-off {
display: none;
}
',
is_rtl() ? 'right' : 'left'
);
$this->dom->head->appendChild( $style );

$toggle_class = 'is-dark-theme';
$state_id = str_replace( '-', '_', $toggle_class );

$body_id = $this->dom->getElementId( $this->dom->body );
$document_id = $this->dom->getElementId( $this->dom->documentElement );

AMP_DOM_Utils::add_amp_action( $button, 'tap', "AMP.setState({{$state_id}: !{$state_id}})" );
AMP_DOM_Utils::add_amp_action( $button, 'tap', "{$body_id}.toggleClass(class='{$toggle_class}')" );
AMP_DOM_Utils::add_amp_action( $button, 'tap', "{$document_id}.toggleClass(class='{$toggle_class}')" );
// Add data-prefers-dark-mode-class in body to use toggleTheme component.
$this->dom->body->setAttribute( 'data-prefers-dark-mode-class', $toggle_class );

$button->setAttribute( 'data-amp-bind-aria-pressed', "{$state_id} ? 'true' : 'false'" );
AMP_DOM_Utils::add_amp_action( $button, 'tap', 'AMP.toggleTheme()' );
}

/**
Expand Down
11 changes: 11 additions & 0 deletions includes/sanitizers/class-amp-style-sanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,17 @@ static function ( $match ) {
}
}

// If using the toggleTheme component, get the theme's dark mode class.
// See usage of toggleTheme in <https://github.com/ampproject/amphtml/pull/36958>.
$dark_mode_class = $this->dom->body->getAttribute( 'data-prefers-dark-mode-class' );

// Prevent dark mode class from being tree-shaken.
if ( $dark_mode_class ) {
$class_names[] = $dark_mode_class;
} else {
$class_names[] = 'amp-dark-mode';
}

$this->used_class_names = array_fill_keys( $class_names, true );
return $this->used_class_names;
}
Expand Down
61 changes: 61 additions & 0 deletions tests/php/test-amp-style-sanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,67 @@ public function test_class_amp_bind_preservation() {
);
}

/** @return array */
public function get_data_to_test_dark_mode_classes() {
return [
'default' => [
null,
[
'body.amp-dark-mode{background:black}',
'',
],
],
'custom' => [
'is-dark-theme',
[
'',
'body.is-dark-theme{background:black}',
],
],
];
}

/**
* Test that dark mode classes are not stripped out.
*
* @dataProvider get_data_to_test_dark_mode_classes
*
* @covers AMP_Style_Sanitizer::get_used_class_names()
* @covers AMP_Style_Sanitizer::finalize_stylesheet_group()
*
* @param null|string $dark_mode_class
* @param string[] $expected_stylesheets
*/
public function test_dark_mode_classes( $dark_mode_class, $expected_stylesheets ) {
ob_start();
?>
<html amp>
<head>
<meta charset="utf-8">
<style>body.amp-dark-mode { background:black; }</style>
<style>body.is-dark-theme { background:black; }</style>
</head>
<body
<?php if ( $dark_mode_class ) : ?>
data-prefers-dark-mode-class="<?php echo esc_attr( $dark_mode_class ); ?>"
<?php endif; ?>
>
</body>
</html>
<?php
$dom = Document::fromHtml( ob_get_clean(), Options::DEFAULTS );

$sanitizer = new AMP_Style_Sanitizer(
$dom,
[ 'use_document_element' => true ]
);
$sanitizer->sanitize();
$this->assertEquals(
$expected_stylesheets,
array_values( $sanitizer->get_stylesheets() )
);
}

/**
* Test that auto-removal is performed and that excessive CSS will be removed entirely.
*
Expand Down
Loading