class-admin.php
1 month ago
class-menu-aria-labels.php
1 month ago
class-review-notice.php
1 month ago
index.php
1 year ago
class-menu-aria-labels.php
158 lines
| 1 | <?php |
| 2 | if ( ! defined( 'ABSPATH' ) ) exit; |
| 3 | |
| 4 | /** |
| 5 | * Conditional ARIA label for selected menus only. |
| 6 | * - Shows the field on nav-menus.php ONLY when ?menu=<ID> is in ariaat_aria_menus. |
| 7 | * - Injects aria-label on the frontend ONLY for items in selected menus. |
| 8 | */ |
| 9 | class ARIAAT_Menu_Aria_Label { |
| 10 | const META_KEY = '_ariaat_aria_label'; |
| 11 | const OPTION_MENUS = 'ariaat_aria_menus'; |
| 12 | const OVERRIDE_VISIBLE_TEXT = false; // override visible text even if meaningful |
| 13 | const FALLBACK_TO_TITLE_ATTRIBUTE = true; // use Title Attribute when custom field empty |
| 14 | |
| 15 | public function __construct() { |
| 16 | add_action( 'wp_nav_menu_item_custom_fields', [ $this, 'render_field' ], 10, 4 ); |
| 17 | add_action( 'wp_update_nav_menu_item', [ $this, 'save_field' ], 10, 3 ); |
| 18 | add_filter( 'nav_menu_link_attributes', [ $this, 'maybe_add_aria_label' ], 10, 3 ); |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * Render ARIA Label input ONLY if the currently edited menu (?menu=ID) is selected. |
| 23 | */ |
| 24 | public function render_field( $item_id, $item, $depth, $args ) { |
| 25 | if ( ! $this->current_menu_is_selected_strict() ) { |
| 26 | return; |
| 27 | } |
| 28 | |
| 29 | $value = get_post_meta( $item_id, self::META_KEY, true ); |
| 30 | $field_id = 'edit-' . self::META_KEY . '-' . $item_id; |
| 31 | ?> |
| 32 | <p class="field-ariaat-aria-label description description-wide"> |
| 33 | <label for="<?php echo esc_attr( $field_id ); ?>"> |
| 34 | <?php esc_html_e( 'ARIA Label', 'aria-accessibility-toolkit' ); ?><br> |
| 35 | <input |
| 36 | type="text" |
| 37 | id="<?php echo esc_attr( $field_id ); ?>" |
| 38 | class="widefat code edit-menu-item-ariaat-aria-label" |
| 39 | name="<?php echo esc_attr( self::META_KEY . '[' . $item_id . ']' ); ?>" |
| 40 | value="<?php echo esc_attr( $value ); ?>" |
| 41 | placeholder="<?php esc_attr_e( '', 'aria-accessibility-toolkit' ); ?>" |
| 42 | > |
| 43 | </label> |
| 44 | <span class="description"> |
| 45 | <?php esc_html_e( 'Screen readers will announce this instead of the link text. Leave blank for clear text like “About” or “Contact”.', 'aria-accessibility-toolkit' ); ?> |
| 46 | </span> |
| 47 | </p> |
| 48 | <?php |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Save ARIA Label meta only when editing a selected menu. |
| 53 | */ |
| 54 | public function save_field( $menu_id, $menu_item_db_id, $args ) { |
| 55 | if ( ! $this->current_menu_is_selected_strict() ) { |
| 56 | // If the menu is not selected, ensure we don't keep stale values. |
| 57 | delete_post_meta( $menu_item_db_id, self::META_KEY ); |
| 58 | return; |
| 59 | } |
| 60 | |
| 61 | if ( isset( $_POST[ self::META_KEY ][ $menu_item_db_id ] ) ) { |
| 62 | $val = sanitize_text_field( wp_unslash( $_POST[ self::META_KEY ][ $menu_item_db_id ] ) ); |
| 63 | if ( $val !== '' ) { |
| 64 | update_post_meta( $menu_item_db_id, self::META_KEY, $val ); |
| 65 | } else { |
| 66 | delete_post_meta( $menu_item_db_id, self::META_KEY ); |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Frontend: add aria-label only for items in selected menus and when we have a label. |
| 73 | */ |
| 74 | public function maybe_add_aria_label( $atts, $item, $args ) { |
| 75 | if ( ! $this->item_in_selected_menus( $item->ID ) ) { |
| 76 | return $atts; |
| 77 | } |
| 78 | |
| 79 | $custom = get_post_meta( $item->ID, self::META_KEY, true ); |
| 80 | $candidate = trim( (string) $custom ); |
| 81 | |
| 82 | if ( $candidate === '' && self::FALLBACK_TO_TITLE_ATTRIBUTE && ! empty( $item->attr_title ) ) { |
| 83 | $candidate = trim( $item->attr_title ); |
| 84 | } |
| 85 | if ( $candidate === '' ) { |
| 86 | return $atts; |
| 87 | } |
| 88 | |
| 89 | $link_text = isset( $item->title ) ? wp_strip_all_tags( $item->title ) : ''; |
| 90 | $has_visible_text = ( $link_text !== '' ); |
| 91 | |
| 92 | if ( $has_visible_text && ! self::OVERRIDE_VISIBLE_TEXT ) { |
| 93 | if ( ! $this->is_ambiguous_text( $link_text ) ) { |
| 94 | return $atts; // Don’t override clear text like “About” |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | $atts['aria-label'] = $candidate; |
| 99 | return $atts; |
| 100 | } |
| 101 | |
| 102 | /* ========================= |
| 103 | * Helpers |
| 104 | * =======================*/ |
| 105 | |
| 106 | /** |
| 107 | * STRICT: Only returns true if we're on nav-menus.php AND there is a ?menu=<id> param |
| 108 | * AND that id is in ariaat_aria_menus. No sticky fallbacks. |
| 109 | */ |
| 110 | private function current_menu_is_selected_strict() { |
| 111 | if ( ! is_admin() ) return false; |
| 112 | if ( ! function_exists( 'get_current_screen' ) ) return false; |
| 113 | |
| 114 | $screen = get_current_screen(); |
| 115 | if ( empty( $screen ) || $screen->base !== 'nav-menus' ) return false; |
| 116 | |
| 117 | // Require explicit ?menu=<ID> in the URL. If it's missing, don't show. |
| 118 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 119 | $menu_id = isset( $_GET['menu'] ) ? absint( $_GET['menu'] ) : 0; |
| 120 | if ( ! $menu_id ) return false; |
| 121 | |
| 122 | return in_array( $menu_id, $this->get_selected_menus(), true ); |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Is this menu item attached to any of the selected menus? (used on the frontend) |
| 127 | */ |
| 128 | private function item_in_selected_menus( $item_id ) { |
| 129 | $selected = $this->get_selected_menus(); |
| 130 | if ( empty( $selected ) ) return false; |
| 131 | |
| 132 | $terms = wp_get_post_terms( $item_id, 'nav_menu', [ 'fields' => 'ids' ] ); |
| 133 | if ( is_wp_error( $terms ) || empty( $terms ) ) return false; |
| 134 | |
| 135 | return (bool) array_intersect( $terms, $selected ); |
| 136 | } |
| 137 | |
| 138 | private function get_selected_menus() { |
| 139 | $selected = get_option( self::OPTION_MENUS, [] ); |
| 140 | if ( ! is_array( $selected ) ) return []; |
| 141 | // Normalize strings like "19" to integers. |
| 142 | return array_map( 'intval', $selected ); |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Consider short/ambiguous texts as candidates for aria-label replacement. |
| 147 | */ |
| 148 | private function is_ambiguous_text( $text ) { |
| 149 | $t = strtolower( trim( preg_replace( '/\s+/', ' ', $text ) ) ); |
| 150 | if ( $t === '' ) return true; |
| 151 | if ( mb_strlen( $t ) <= 4 ) return true; // e.g., “More”, “Info”, arrow-only |
| 152 | $phrases = [ 'read more', 'learn more', 'more', 'details', 'info', '→', '»' ]; |
| 153 | return in_array( $t, $phrases, true ); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | return new ARIAAT_Menu_Aria_Label(); |
| 158 |