Skip to content

Commit c44c37d

Browse files
committed
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. git-svn-id: https://develop.svn.wordpress.org/trunk@59948 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 408a01a commit c44c37d

File tree

5 files changed

+101
-11
lines changed

5 files changed

+101
-11
lines changed

src/js/_enqueues/lib/nav-menu.js

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,13 +1102,53 @@
11021102
}, 500 ) );
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' );
11101112
}
11111113
});
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:[email protected]
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' );
1150+
}
1151+
});
11121152
},
11131153

11141154
/**
@@ -1389,15 +1429,29 @@
13891429

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 ) {
13951436
url = url.trim();
13961437
}
13971438

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:[email protected]
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;
14031457
}

src/js/_enqueues/wp/customize/nav-menus.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@
223223

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

228231
// Load available items if it looks like we'll need them.
@@ -546,8 +549,11 @@
546549
var menuItem,
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 ) {
553559
return;
@@ -566,15 +572,37 @@
566572
* so this pattern does not need to be complete.
567573
*/
568574
urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
569-
570-
if ( '' === itemName.val() ) {
571-
itemName.addClass( 'invalid' );
572-
return;
573-
} else if ( ! urlRegex.test( url ) ) {
574-
itemUrl.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+
}
575594
return;
576595
}
577596

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' );
605+
578606
menuItem = {
579607
'title': itemName.val(),
580608
'url': url,

src/wp-admin/css/nav-menus.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,16 @@ input.bulk-select-switcher:focus + .bulk-select-button-label {
377377

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;
383384
}
384385

386+
.customlinkdiv .error-message {
387+
clear: right;
388+
}
389+
385390
.accordion-container .outer-border {
386391
margin: 0;
387392
}

src/wp-admin/includes/nav-menu.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ function wp_nav_menu_item_link_meta_box() {
351351
type="text"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
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

356357
<p id="menu-item-name-wrap" class="wp-clearfix">

src/wp-includes/class-wp-customize-nav-menus.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,10 +1268,12 @@ protected function print_custom_links_available_menu_item() {
12681268
<p id="menu-item-url-wrap" class="wp-clearfix">
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">
12771279
<span class="add-to-menu">

0 commit comments

Comments
 (0)