Make WordPress Core

Changeset 59948


Ignore:
Timestamp:
03/06/2025 11:46:47 PM (17 months ago)
Author:
joedolson
Message:

Menus: Validate custom links and add accessible error messages.

Add URL validation in the admin navigation menu manager that matches the validation in the customizer when adding custom links. Improve accessibility of both custom link forms by adding aria-invalid and aria-describedby attributes with visible error messages and announcing the error using wp.a11y.speak().

Props joedolson, nikitasolanki1812, akrocks, pathan-amaankhan, rcreators, ironprogrammer, audrasjb, ankit-k-gupta, chaion07, rinkalpagdar, snehapatil02, jainil07, parthvataliya.
Fixes #60619, #60969.

Location:
trunk/src
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/lib/nav-menu.js

    r59431 r59948  
    11031103
    11041104                        $('#add-custom-links input[type="text"]').on( 'keypress', function(e){
    1105                                 $('#customlinkdiv').removeClass('form-invalid');
     1105                                $( '#customlinkdiv' ).removeClass( 'form-invalid' );
     1106                                $( '#custom-menu-item-url' ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
     1107                                $( '#custom-url-error' ).hide();
    11061108
    11071109                                if ( e.keyCode === 13 ) {
    11081110                                        e.preventDefault();
    11091111                                        $( '#submit-customlinkdiv' ).trigger( 'click' );
     1112                                }
     1113                        });
     1114
     1115                        $( '#submit-customlinkdiv' ).on( 'click', function (e) {
     1116                                var urlInput = $( '#custom-menu-item-url' ),
     1117                                        url = urlInput.val().trim(),
     1118                                        errorMessage = $( '#custom-url-error' ),
     1119                                        urlWrap = $( '#menu-item-url-wrap' ),
     1120                                        urlRegex;
     1121
     1122                                // Hide the error message initially
     1123                                errorMessage.hide();
     1124                                urlWrap.removeClass( 'has-error' );
     1125
     1126                                /*
     1127                                 * Allow URLs including:
     1128                                 * - http://example.com/
     1129                                 * - //example.com
     1130                                 * - /directory/
     1131                                 * - ?query-param
     1132                                 * - #target
     1133                                 * - mailto:foo@example.com
     1134                                 *
     1135                                 * Any further validation will be handled on the server when the setting is attempted to be saved,
     1136                                 * so this pattern does not need to be complete.
     1137                                 */
     1138                                urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
     1139                                if ( ! urlRegex.test( url ) ) {
     1140                                        e.preventDefault();
     1141                                        urlInput.addClass( 'form-invalid' )
     1142                                                .attr( 'aria-invalid', 'true' )
     1143                                                .attr( 'aria-describedby', 'custom-url-error' );
     1144
     1145                                        errorMessage.show();
     1146                                        var errorText = errorMessage.text();
     1147                                        urlWrap.addClass( 'has-error' );
     1148                                        // Announce error message via screen reader
     1149                                        wp.a11y.speak( errorText, 'assertive' );
    11101150                                }
    11111151                        });
     
    13901430                addCustomLink : function( processMethod ) {
    13911431                        var url = $('#custom-menu-item-url').val().toString(),
    1392                                 label = $('#custom-menu-item-name').val();
     1432                                label = $('#custom-menu-item-name').val(),
     1433                                urlRegex;
    13931434
    13941435                        if ( '' !== url ) {
     
    13981439                        processMethod = processMethod || api.addMenuItemToBottom;
    13991440
    1400                         if ( '' === url || 'https://' == url || 'http://' == url ) {
     1441                        /*
     1442                         * Allow URLs including:
     1443                         * - http://example.com/
     1444                         * - //example.com
     1445                         * - /directory/
     1446                         * - ?query-param
     1447                         * - #target
     1448                         * - mailto:foo@example.com
     1449                         *
     1450                         * Any further validation will be handled on the server when the setting is attempted to be saved,
     1451                         * so this pattern does not need to be complete.
     1452                         */
     1453                        urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
     1454                        if ( ! urlRegex.test( url ) ) {
    14011455                                $('#customlinkdiv').addClass('form-invalid');
    14021456                                return false;
  • trunk/src/js/_enqueues/wp/customize/nav-menus.js

    r59224 r59948  
    224224                        this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
    225225                                $( this ).removeClass( 'invalid' );
     226                                var errorMessageId = $( this ).attr( 'aria-describedby' );
     227                                $( '#' + errorMessageId ).hide();
     228                                $( this ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
    226229                        });
    227230
     
    547550                                itemName = $( '#custom-menu-item-name' ),
    548551                                itemUrl = $( '#custom-menu-item-url' ),
     552                                urlErrorMessage = $( '#custom-url-error' ),
     553                                nameErrorMessage = $( '#custom-name-error' ),
    549554                                url = itemUrl.val().trim(),
    550                                 urlRegex;
     555                                urlRegex,
     556                                errorText;
    551557
    552558                        if ( ! this.currentMenuControl ) {
     
    567573                         */
    568574                        urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
    569 
    570                         if ( '' === itemName.val() ) {
    571                                 itemName.addClass( 'invalid' );
     575                        if ( ! urlRegex.test( url ) || '' === itemName.val() ) {
     576                                if ( ! urlRegex.test( url ) ) {
     577                                        itemUrl.addClass( 'invalid' )
     578                                                .attr( 'aria-invalid', 'true' )
     579                                                .attr( 'aria-describedby', 'custom-url-error' );
     580                                        urlErrorMessage.show();
     581                                        errorText = urlErrorMessage.text();
     582                                        // Announce error message via screen reader
     583                                        wp.a11y.speak( errorText, 'assertive' );
     584                                }
     585                                if ( '' === itemName.val() ) {
     586                                        itemName.addClass( 'invalid' )
     587                                                .attr( 'aria-invalid', 'true' )
     588                                                .attr( 'aria-describedby', 'custom-name-error' );
     589                                        nameErrorMessage.show();
     590                                        errorText = ( '' === errorText ) ? nameErrorMessage.text() : errorText + nameErrorMessage.text();
     591                                        // Announce error message via screen reader
     592                                        wp.a11y.speak( errorText, 'assertive' );
     593                                }
    572594                                return;
    573                         } else if ( ! urlRegex.test( url ) ) {
    574                                 itemUrl.addClass( 'invalid' );
    575                                 return;
    576                         }
     595                        }
     596
     597                        urlErrorMessage.hide();
     598                        nameErrorMessage.hide();
     599                        itemName.removeClass( 'invalid' )
     600                                .removeAttr( 'aria-invalid', 'true' )
     601                                .removeAttr( 'aria-describedby', 'custom-name-error' );
     602                        itemUrl.removeClass( 'invalid' )
     603                                .removeAttr( 'aria-invalid', 'true' )
     604                                .removeAttr( 'aria-describedby', 'custom-name-error' );
    577605
    578606                        menuItem = {
  • trunk/src/wp-admin/css/nav-menus.css

    r59265 r59948  
    378378/* Add Menu Item Boxes */
    379379.postbox .howto input,
    380 .customlinkdiv .menu-item-textbox {
     380.customlinkdiv .menu-item-textbox,
     381.customlinkdiv .error-message {
    381382        width: 180px;
    382383        float: right;
     384}
     385
     386.customlinkdiv .error-message {
     387        clear: right;
    383388}
    384389
  • trunk/src/wp-admin/includes/nav-menu.php

    r59462 r59948  
    352352                                class="code menu-item-textbox form-required" placeholder="https://"
    353353                        />
     354                        <span id="custom-url-error" class="error-message" style="display: none;"><?php _e( 'Please provide a valid link.' ); ?></span>
    354355                </p>
    355356
  • trunk/src/wp-includes/class-wp-customize-nav-menus.php

    r59825 r59948  
    12691269                                        <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
    12701270                                        <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" placeholder="https://">
     1271                                        <span id="custom-url-error" class="error-message" style="display: none;"><?php _e( 'Please provide a valid link.' ); ?></span>
    12711272                                </p>
    12721273                                <p id="menu-item-name-wrap" class="wp-clearfix">
    12731274                                        <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
    12741275                                        <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
     1276                                        <span id="custom-name-error" class="error-message" style="display: none;"><?php _e( 'The link text cannot be empty.' ); ?></span>
    12751277                                </p>
    12761278                                <p class="button-controls">
Note: See TracChangeset for help on using the changeset viewer.