Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f5d0d8c
feat(customizer): add global header and footer visibility toggles
Alexia-Soare Sep 18, 2026
4f2cac8
fix(amp): print the amp states outside the header
Alexia-Soare Sep 18, 2026
a883494
test: cover the global header and footer toggles
Alexia-Soare Sep 18, 2026
bf564de
test: restore the customizer global after each test
Alexia-Soare Sep 18, 2026
62d1f4f
refactor(customizer): keep the section labels next to their part
Alexia-Soare Sep 18, 2026
0c300e6
docs: trim the comments around the visibility filter
Alexia-Soare Sep 18, 2026
0955bd2
style(header): match the skip link markup in both templates
Alexia-Soare Sep 18, 2026
88baa1e
style(e2e): use the project spacing in the panel loop
Alexia-Soare Sep 18, 2026
9462899
fix(amp): keep the scroll to top animations off the header
Alexia-Soare Sep 21, 2026
15765b4
fix(amp): give the scroll to top observer an anchor to watch
Alexia-Soare Sep 21, 2026
65dbdb4
refactor(customizer): move the header and footer toggles into global …
selul Oct 7, 2026
4d3cd34
docs: add screenshots of the global header and footer settings
selul Oct 7, 2026
9d082a8
fix(customizer): keep Disable Header on the legacy builder
selul Oct 7, 2026
cf71d86
refactor(customizer): share the Disable Header setting arguments
selul Oct 7, 2026
a66fcdb
fix(customizer): name the sanitize callback in the legacy add_setting…
selul Oct 7, 2026
12ef688
refactor(customizer): add array return types to the Disable Header he…
selul Oct 7, 2026
cb58655
Merge branch 'development' into feat/4551-global-settings
selul Oct 8, 2026
19c7126
refactor(customizer): reuse existing strings for the toggles
selul Oct 8, 2026
d835ea3
docs: retake the screenshots after the string reuse
selul Oct 8, 2026
c82046d
Merge remote-tracking branch 'origin/feat/4551-global-settings' into …
selul Oct 8, 2026
9a48552
fix: keep content hooked around a disabled header, and AMP pagination…
selul Oct 8, 2026
70f69bf
fix(amp): hide the moved skip link on appended infinite scroll pages
selul Oct 8, 2026
4ced877
test: make the block template tests able to fail
selul Oct 8, 2026
22e9843
chore: remove the PR screenshots from docs
selul Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ ul[id^="sub-accordion-panel-hfg_"] {
}

#accordion-section-neve_header_presets,
#accordion-section-neve_footer_copyright_section {
#accordion-section-neve_footer_copyright_section,
#accordion-section-neve_global_footer_settings {
margin-top: 20px;
}

// Only some sites get the copyright section; when it is there, the two form one group.
#accordion-section-neve_footer_copyright_section
+ #accordion-section-neve_global_footer_settings {
margin-top: 0;
}
}
3 changes: 2 additions & 1 deletion assets/apps/customizer-controls/src/scss/_general.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@


#sub-accordion-panel-hfg_header #accordion-section-neve_header_presets,
#sub-accordion-panel-hfg_footer #accordion-section-neve_footer_copyright_section {
#sub-accordion-panel-hfg_footer #accordion-section-neve_footer_copyright_section,
#sub-accordion-panel-hfg_footer #accordion-section-neve_global_footer_settings {
display: list-item !important;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { test, expect } from '@playwright/test';
import { setCustomizeSettings } from '../../../utils';

const HEADER_WRAPPER = 'header.header';
const FOOTER_WRAPPER = '#site-footer';
const SKIP_LINK = 'a.neve-skip-link';

test.describe( 'Global header and footer visibility', function () {
test( 'Shows both toggles in the builder panels', async function ( { page } ) {
// The builder panels hide every section they do not know, so a registered
// control can still be invisible. Only the browser catches that.
await page.goto( '/wp-admin/customize.php' );
await page.waitForSelector( '#customize-theme-controls' );

const panels = {
hfg_header: {
section: 'neve_pro_global_header_settings',
control: 'neve_disable_header',
},
hfg_footer: {
section: 'neve_global_footer_settings',
control: 'neve_disable_footer',
},
};

for ( const [ panel, item ] of Object.entries( panels ) ) {
await page.click( `#accordion-panel-${ panel } h3` );

const section = page.locator( `#accordion-section-${ item.section }` );
await expect( section ).toBeVisible();

await section.locator( 'h3' ).click();
const control = page.locator( `#customize-control-${ item.control }` );
await expect( control ).toBeVisible();

// Assert on the structure. The label text is translated.
await expect(
control.locator( 'input[type="checkbox"]' )
).toHaveCount( 1 );

await page.goto( '/wp-admin/customize.php' );
await page.waitForSelector( '#customize-theme-controls' );
}
} );

test( 'Renders both parts when nothing is turned off', async function ( {
page,
request,
baseURL,
} ) {
await setCustomizeSettings(
'hf-visible',
{ neve_disable_header: false, neve_disable_footer: false },
{ request, baseURL }
);
await page.goto( '/hello-world/?test_name=hf-visible' );

await expect( page.locator( HEADER_WRAPPER )).toHaveCount( 1 );
await expect( page.locator( FOOTER_WRAPPER )).toHaveCount( 1 );
} );

test( 'Drops the header wrapper and keeps the skip link', async function ( {
page,
request,
baseURL,
} ) {
await setCustomizeSettings(
'hf-no-header',
{ neve_disable_header: true },
{ request, baseURL }
);
await page.goto( '/hello-world/?test_name=hf-no-header' );

// The wrapper used to stay behind, empty, holding only the skip link.
await expect( page.locator( HEADER_WRAPPER )).toHaveCount( 0 );
await expect( page.locator( SKIP_LINK )).toHaveCount( 1 );

// The rest of the page is untouched.
await expect( page.locator( 'main#content' )).toHaveCount( 1 );
await expect( page.locator( FOOTER_WRAPPER )).toHaveCount( 1 );
} );

test( 'Drops the footer and leaves the header alone', async function ( {
page,
request,
baseURL,
} ) {
await setCustomizeSettings(
'hf-no-footer',
{ neve_disable_footer: true },
{ request, baseURL }
);
await page.goto( '/hello-world/?test_name=hf-no-footer' );

await expect( page.locator( FOOTER_WRAPPER )).toHaveCount( 0 );
await expect( page.locator( HEADER_WRAPPER )).toHaveCount( 1 );
} );

test( 'Reaches an archive, where the page metabox never did', async function ( {
page,
request,
baseURL,
} ) {
await setCustomizeSettings(
'hf-archive',
{ neve_disable_header: true, neve_disable_footer: true },
{ request, baseURL }
);
await page.goto( '/?s=hello&test_name=hf-archive' );

await expect( page.locator( 'body.search' )).toHaveCount( 1 );
await expect( page.locator( HEADER_WRAPPER )).toHaveCount( 0 );
await expect( page.locator( FOOTER_WRAPPER )).toHaveCount( 0 );
} );
} );
41 changes: 40 additions & 1 deletion header-footer-grid/Core/Builder/Footer.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ function ( $value ) {
* @access public
*/
public function customize_register( WP_Customize_Manager $wp_customize ) {
$this->customize_global_footer( $wp_customize );

if ( $this->has_valid_addons() || ! Migration_Flags::is_new_user_after_v41() ) {
return parent::customize_register( $wp_customize );
}
Expand Down Expand Up @@ -192,10 +194,47 @@ public function customize_register( WP_Customize_Manager $wp_customize ) {
'setting' => 'footer_copyright_content',
]
);

return parent::customize_register( $wp_customize );
}

/**
* Registers the footer options section, the counterpart of the global header settings.
*
* @param WP_Customize_Manager $wp_customize The Customize Manager.
*
* @return void
*/
private function customize_global_footer( WP_Customize_Manager $wp_customize ) {
$wp_customize->add_section(
'neve_global_footer_settings',
[
'title' => __( 'Footer Options', 'neve' ),
'priority' => 201,
'panel' => 'hfg_footer',
]
);

$wp_customize->add_setting(
'neve_disable_footer',
[
'default' => false,
'transport' => 'refresh',
'sanitize_callback' => 'neve_sanitize_checkbox',
]
);

$wp_customize->add_control(
'neve_disable_footer',
[
'label' => esc_html__( 'Disable Footer', 'neve' ),
'section' => 'neve_global_footer_settings',
'type' => 'neve_toggle_control',
'priority' => 5,
]
);
}

/**
* Add footer component.
*
Expand Down
80 changes: 80 additions & 0 deletions header-footer-grid/Core/Builder/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Header extends Abstract_Builder {
const BACKGROUND_HEADING = 'background_heading';
const ADVANCED_STYLE = 'advanced_style';
const BACKGROUND_SETTING = 'background';
const DISABLE_HEADER = 'neve_disable_header';

/**
* Default color for global header background
Expand Down Expand Up @@ -111,6 +112,8 @@ public function init() {
*/
public function customize_register( WP_Customize_Manager $wp_customize ) {
if ( ! neve_is_new_builder() ) {
$this->customize_legacy_global_header( $wp_customize );

return parent::customize_register( $wp_customize );
}

Expand Down Expand Up @@ -206,12 +209,89 @@ private function move_pro_controls( WP_Customize_Manager $wp_customize, WP_Custo
ksort( $global_settings_tabs->tabs );
}

/**
* Arguments of the setting behind the toggle that hides the header on every page.
*
* @return array{default: false, transport: string, sanitize_callback: string}
*/
private function get_disable_header_setting_args(): array {
return [
'default' => false,
'transport' => 'refresh',
'sanitize_callback' => 'neve_sanitize_checkbox',
];
}

/**
* Arguments of the toggle that hides the header on every page.
*
* @return array{label: string, section: string, type: string, priority: int}
*/
private function get_disable_header_control_args(): array {
return [
'label' => esc_html__( 'Disable Header', 'neve' ),
'section' => 'neve_pro_global_header_settings',
'type' => 'neve_toggle_control',
'priority' => 5,
];
}

/**
* Registers the header toggle for the legacy builder.
*
* The legacy builder has no global header settings tabs, so the toggle is a plain control
* and any Neve Pro controls in the section keep showing as before.
*
* @param WP_Customize_Manager $wp_customize The Customize Manager.
*
* @return void
*/
private function customize_legacy_global_header( WP_Customize_Manager $wp_customize ) {
if ( $wp_customize->get_section( 'neve_pro_global_header_settings' ) === null ) {
$wp_customize->add_section(
'neve_pro_global_header_settings',
[
'title' => __( 'Global Header Settings', 'neve' ),
'priority' => 200,
'panel' => 'hfg_header',
]
);
}

$setting_args = $this->get_disable_header_setting_args();

// Theme Check reads the sanitize_callback key from the add_setting() call itself.
$wp_customize->add_setting(
self::DISABLE_HEADER,
[
'default' => $setting_args['default'],
'transport' => $setting_args['transport'],
'sanitize_callback' => $setting_args['sanitize_callback'],
]
);
$wp_customize->add_control( self::DISABLE_HEADER, $this->get_disable_header_control_args() );
}

/**
* Registers controls for global header background
*/
private function customize_global_header() {
$section_id = 'neve_pro_global_header_settings';

// Not a conditional header setting: it hides every header layout at once.
SettingsManager::get_instance()->add(
array_merge(
$this->get_disable_header_setting_args(),
$this->get_disable_header_control_args(),
[
'id' => self::DISABLE_HEADER,
'noformat' => true,
'group' => $section_id,
'tab' => 'general',
]
)
);

SettingsManager::get_instance()->add(
[
'id' => self::BACKGROUND_HEADING,
Expand Down
58 changes: 36 additions & 22 deletions header.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,31 +75,45 @@
do_action( 'neve_before_header_wrapper_hook' );
?>

<header class="<?php echo esc_attr( $header_classes ); ?>" <?php echo ( neve_is_amp() ) ? 'next-page-hide' : ''; ?> >
<a class="neve-skip-link show-on-focus" href="#content" >
<?php echo __( 'Skip to content', 'neve' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</a>
<?php
<a class="neve-skip-link show-on-focus" href="#content" <?php echo ( neve_is_amp() ) ? 'next-page-hide' : ''; ?>>
<?php echo __( 'Skip to content', 'neve' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</a>

<?php
$neve_has_header = apply_filters( 'neve_filter_toggle_content_parts', true, 'header' ) === true;

// The hooks around the header run even when it is off, so what they add (such as the
// Neve Pro page header) stays. The wrapper is printed only when it has something to wrap.
ob_start();

/**
* Executes actions before the header ( navigation ) area.
*
* @since 1.0.0
*/
do_action( 'neve_before_header_hook' );

/**
* Executes actions before the header ( navigation ) area.
*
* @since 1.0.0
*/
do_action( 'neve_before_header_hook' );

if ( apply_filters( 'neve_filter_toggle_content_parts', true, 'header' ) === true ) {
do_action( 'neve_do_header' );
}

/**
* Executes actions after the header ( navigation ) area.
*
* @since 1.0.0
*/
do_action( 'neve_after_header_hook' );
if ( $neve_has_header ) {
do_action( 'neve_do_header' );
}

/**
* Executes actions after the header ( navigation ) area.
*
* @since 1.0.0
*/
do_action( 'neve_after_header_hook' );

$neve_header_markup = (string) ob_get_clean();

if ( $neve_has_header || trim( $neve_header_markup ) !== '' ) {
?>
<header class="<?php echo esc_attr( $header_classes ); ?>" <?php echo ( neve_is_amp() ) ? 'next-page-hide' : ''; ?> >
<?php echo $neve_header_markup; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Printed by the header hooks, which escape their own output. ?>
</header>
<?php
}
?>

<?php
/**
Expand Down
10 changes: 9 additions & 1 deletion inc/compatibility/amp.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public function register_hooks() {
add_filter( 'walker_nav_menu_start_el', array( $this, 'wrap_content' ), 10, 4 );
add_filter( 'neve_sidebar_data_attrs', array( $this, 'add_woo_sidebar_attrs' ), 10, 2 );
add_filter( 'neve_search_menu_item_filter', array( $this, 'add_search_menu_item_attrs' ), 10, 2 );
add_action( 'neve_after_header_hook', array( $this, 'render_amp_states' ) );
// Printed outside the header, because one of the states drives the shop sidebar,
// which is on the page even when the header is turned off.
add_action( 'neve_after_header_wrapper_hook', array( $this, 'render_amp_states' ) );
add_filter( 'neve_nav_toggle_data_attrs', array( $this, 'add_nav_toggle_attrs' ) );
add_action( 'wp_head', array( $this, 'inline_styles' ) );

Expand Down Expand Up @@ -333,6 +335,12 @@ public function should_display_infinite_scroll() {
return false;
}

// The infinite scroll wraps the footer and replaces the pagination, so without
// the footer the page keeps its regular pagination.
if ( apply_filters( 'neve_filter_toggle_content_parts', true, 'footer' ) !== true ) {
return false;
}

$has_pagination = ! empty( get_the_posts_pagination() );
if ( ! $has_pagination ) {
return false;
Expand Down
Loading
Loading