Last active
May 20, 2025 12:22
-
-
Save isuke01/a60c3da5127b84f20b18cde0a8e4790e to your computer and use it in GitHub Desktop.
Wordpress add extra checkbox to nav item to toggle to transform item into button. Button will act as an sub-menu toggle button, should cover aria-haspopup and aria-expanded
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
<?php | |
namespace SubmenuAccessibility; | |
/** | |
* Add a checkbox to the menu item settings to set it as a button. | |
* | |
* @param int $item_id The menu item ID. | |
* @param object $item The menu item object. | |
* @param int $depth The depth of the menu item. | |
* @param object $args The menu arguments. | |
*/ | |
\add_action( 'wp_nav_menu_item_custom_fields', function ( $item_id, $item, $depth, $args ) { | |
$is_checked = get_post_meta( $item_id, '_menu_item_as_button', true ) ? 'checked' : ''; | |
echo '<p class="field-toggle-button description description-wide"> | |
<label for="edit-menu-item-as-button-' . \esc_attr( (string) $item_id ) . '"> | |
<input type="checkbox" id="edit-menu-item-as-button-' . \esc_attr( (string) $item_id ) . '" name="menu-item-as-button[' . \esc_attr( (string) $item_id ) . ']" ' . $is_checked . ' /> | |
' . \esc_html__( 'Use as a button', 'textdomain' ) . '</br> | |
' . \esc_html__( 'This is usefully as an submenu toggle (button)', 'textdomain' ) . ' | |
</label> | |
</p>'; | |
}, 10, 4 ); | |
/** | |
* Save the checkbox value when the menu item is updated. | |
* | |
* @param int $menu_id The menu ID. | |
* @param int $menu_item_db_id The menu item database ID. | |
* @param object $args The menu arguments. | |
*/ | |
\add_action( 'wp_update_nav_menu_item', function ( $menu_id, $menu_item_db_id, $args ) { | |
$value = isset( $_POST['menu-item-as-button'][ $menu_item_db_id ] ) ? '1' : ''; | |
\update_post_meta( $menu_item_db_id, '_menu_item_as_button', $value ); | |
}, 10, 3 ); | |
\add_filter( 'wp_nav_menu_objects', function ( $items, $args ) { | |
foreach ( $items as &$item ) { | |
if ( get_post_meta( $item->ID, '_menu_item_as_button', true ) ) { | |
$item->is_button = true; | |
} | |
} | |
return $items; | |
}, 10, 2 ); | |
\add_filter( 'walker_nav_menu_start_el', function ( $item_output, $item, $depth, $args ) { | |
if ( empty( $item->is_button ) ) { | |
return $item_output; | |
} | |
// Suppress HTML5 warnings. | |
libxml_use_internal_errors( true ); | |
// Wrap with dummy HTML to avoid <html><body> boilerplate. | |
$html = '<div id="wrap">' . $item_output . '</div>'; | |
$doc = new \DOMDocument(); | |
$doc->loadHTML( mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' ) ); | |
$xpath = new \DOMXPath( $doc ); | |
$a = $xpath->query( '//div[@id="wrap"]//a' )->item(0); | |
if ( ! $a ) { | |
return $item_output; // no <a> found. | |
} | |
// Create <button> element. | |
$button = $doc->createElement( 'button' ); | |
$button->setAttribute( 'type', 'button' ); | |
$button->setAttribute( 'data-ally11AV-js', 'true' ); | |
$button->setAttribute( 'aria-haspopup', 'true' ); | |
$button->setAttribute( 'aria-expanded', 'false' ); | |
// Copy all attributes except those we want to remove. | |
foreach ( iterator_to_array( $a->attributes ) as $attr ) { | |
if ( in_array($attr->name, [ 'href', 'target', 'rel' ], true ) ) { | |
continue; | |
} | |
$button->setAttribute( $attr->name, $attr->value ); | |
} | |
// Move inner HTML. | |
while ($a->firstChild) { | |
$button->appendChild( $a->firstChild ); | |
} | |
// Replace <a> with <button>. | |
$a->parentNode->replaceChild( $button, $a ); | |
// Extract only the updated content inside our wrapper. | |
$new_html = ''; | |
foreach ( $doc->getElementById('wrap')->childNodes as $child ) { | |
$new_html .= $doc->saveHTML( $child ); | |
} | |
return $new_html; | |
}, 10, 4 ); | |
\add_action( 'wp_footer', function () { | |
?> | |
<script> | |
class AccessibleSubmenuToggle { | |
constructor(selector = '.menu-toggle-button') { | |
this.buttons = document.querySelectorAll(selector); | |
this.init(); | |
} | |
init() { | |
this.buttons.forEach((button) => { | |
const parentLi = button.closest('li'); | |
if (!parentLi || parentLi._submenuEventsBound) return; | |
parentLi._submenuEventsBound = true; | |
const submenu = parentLi.querySelector('.sub-menu'); | |
// Click to toggle | |
button.addEventListener('click', () => { | |
const expanded = button.getAttribute('aria-expanded') === 'true'; | |
if (expanded) { | |
this.close(button, parentLi); | |
} else { | |
this.open(button, parentLi); | |
} | |
}); | |
// Mouse enter opens | |
parentLi.addEventListener('mouseenter', () => { | |
this.open(button, parentLi); | |
}); | |
// Mouse leave closes | |
parentLi.addEventListener('mouseleave', () => { | |
this.close(button, parentLi); | |
}); | |
// Focusout closes when focus leaves the parent <li> | |
parentLi.addEventListener('focusout', (e) => { | |
if (!parentLi.contains(e.relatedTarget)) { | |
this.close(button, parentLi); | |
} | |
}); | |
}); | |
} | |
open(button, parentLi) { | |
const submenu = parentLi.querySelector('.sub-menu'); | |
button.setAttribute('aria-expanded', 'true'); | |
parentLi.classList.add('active'); | |
if (submenu) submenu.classList.add('open'); | |
} | |
close(button, parentLi) { | |
const submenu = parentLi.querySelector('.sub-menu'); | |
button.setAttribute('aria-expanded', 'false'); | |
parentLi.classList.remove('active'); | |
if (submenu) submenu.classList.remove('open'); | |
} | |
} | |
new AccessibleSubmenuToggle('[data-ally11AV-js="true"]'); | |
</script> | |
<?php | |
} ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment