summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorJoe Dolson <joedolson@git.wordpress.org>2025-03-06 23:46:47 +0000
committerJoe Dolson <joedolson@git.wordpress.org>2025-03-06 23:46:47 +0000
commitc44c37d996ae00164c733de98faad1212a57f5c6 (patch)
tree1c4ffef480a6ca9b81826769588573c0fe1c2047
parent408a01ab8d56136d1e6a4e1f24f429afb2053f8f (diff)
downloadwordpress-c44c37d996ae00164c733de98faad1212a57f5c6.tar.gz
wordpress-c44c37d996ae00164c733de98faad1212a57f5c6.zip
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
-rw-r--r--src/js/_enqueues/lib/nav-menu.js60
-rw-r--r--src/js/_enqueues/wp/customize/nav-menus.js42
-rw-r--r--src/wp-admin/css/nav-menus.css7
-rw-r--r--src/wp-admin/includes/nav-menu.php1
-rw-r--r--src/wp-includes/class-wp-customize-nav-menus.php2
5 files changed, 101 insertions, 11 deletions
diff --git a/src/js/_enqueues/lib/nav-menu.js b/src/js/_enqueues/lib/nav-menu.js
index 398f54ecfd..68e285d90d 100644
--- a/src/js/_enqueues/lib/nav-menu.js
+++ b/src/js/_enqueues/lib/nav-menu.js
@@ -1102,13 +1102,53 @@
}, 500 ) );
$('#add-custom-links input[type="text"]').on( 'keypress', function(e){
- $('#customlinkdiv').removeClass('form-invalid');
+ $( '#customlinkdiv' ).removeClass( 'form-invalid' );
+ $( '#custom-menu-item-url' ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
+ $( '#custom-url-error' ).hide();
if ( e.keyCode === 13 ) {
e.preventDefault();
$( '#submit-customlinkdiv' ).trigger( 'click' );
}
});
+
+ $( '#submit-customlinkdiv' ).on( 'click', function (e) {
+ var urlInput = $( '#custom-menu-item-url' ),
+ url = urlInput.val().trim(),
+ errorMessage = $( '#custom-url-error' ),
+ urlWrap = $( '#menu-item-url-wrap' ),
+ urlRegex;
+
+ // Hide the error message initially
+ errorMessage.hide();
+ urlWrap.removeClass( 'has-error' );
+
+ /*
+ * Allow URLs including:
+ * - http://example.com/
+ * - //example.com
+ * - /directory/
+ * - ?query-param
+ * - #target
+ * - mailto:foo@example.com
+ *
+ * Any further validation will be handled on the server when the setting is attempted to be saved,
+ * so this pattern does not need to be complete.
+ */
+ urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
+ if ( ! urlRegex.test( url ) ) {
+ e.preventDefault();
+ urlInput.addClass( 'form-invalid' )
+ .attr( 'aria-invalid', 'true' )
+ .attr( 'aria-describedby', 'custom-url-error' );
+
+ errorMessage.show();
+ var errorText = errorMessage.text();
+ urlWrap.addClass( 'has-error' );
+ // Announce error message via screen reader
+ wp.a11y.speak( errorText, 'assertive' );
+ }
+ });
},
/**
@@ -1389,7 +1429,8 @@
addCustomLink : function( processMethod ) {
var url = $('#custom-menu-item-url').val().toString(),
- label = $('#custom-menu-item-name').val();
+ label = $('#custom-menu-item-name').val(),
+ urlRegex;
if ( '' !== url ) {
url = url.trim();
@@ -1397,7 +1438,20 @@
processMethod = processMethod || api.addMenuItemToBottom;
- if ( '' === url || 'https://' == url || 'http://' == url ) {
+ /*
+ * Allow URLs including:
+ * - http://example.com/
+ * - //example.com
+ * - /directory/
+ * - ?query-param
+ * - #target
+ * - mailto:foo@example.com
+ *
+ * Any further validation will be handled on the server when the setting is attempted to be saved,
+ * so this pattern does not need to be complete.
+ */
+ urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
+ if ( ! urlRegex.test( url ) ) {
$('#customlinkdiv').addClass('form-invalid');
return false;
}
diff --git a/src/js/_enqueues/wp/customize/nav-menus.js b/src/js/_enqueues/wp/customize/nav-menus.js
index 74f79b6fee..f193d5463b 100644
--- a/src/js/_enqueues/wp/customize/nav-menus.js
+++ b/src/js/_enqueues/wp/customize/nav-menus.js
@@ -223,6 +223,9 @@
this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
$( this ).removeClass( 'invalid' );
+ var errorMessageId = $( this ).attr( 'aria-describedby' );
+ $( '#' + errorMessageId ).hide();
+ $( this ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
});
// Load available items if it looks like we'll need them.
@@ -546,8 +549,11 @@
var menuItem,
itemName = $( '#custom-menu-item-name' ),
itemUrl = $( '#custom-menu-item-url' ),
+ urlErrorMessage = $( '#custom-url-error' ),
+ nameErrorMessage = $( '#custom-name-error' ),
url = itemUrl.val().trim(),
- urlRegex;
+ urlRegex,
+ errorText;
if ( ! this.currentMenuControl ) {
return;
@@ -566,15 +572,37 @@
* so this pattern does not need to be complete.
*/
urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
-
- if ( '' === itemName.val() ) {
- itemName.addClass( 'invalid' );
- return;
- } else if ( ! urlRegex.test( url ) ) {
- itemUrl.addClass( 'invalid' );
+ if ( ! urlRegex.test( url ) || '' === itemName.val() ) {
+ if ( ! urlRegex.test( url ) ) {
+ itemUrl.addClass( 'invalid' )
+ .attr( 'aria-invalid', 'true' )
+ .attr( 'aria-describedby', 'custom-url-error' );
+ urlErrorMessage.show();
+ errorText = urlErrorMessage.text();
+ // Announce error message via screen reader
+ wp.a11y.speak( errorText, 'assertive' );
+ }
+ if ( '' === itemName.val() ) {
+ itemName.addClass( 'invalid' )
+ .attr( 'aria-invalid', 'true' )
+ .attr( 'aria-describedby', 'custom-name-error' );
+ nameErrorMessage.show();
+ errorText = ( '' === errorText ) ? nameErrorMessage.text() : errorText + nameErrorMessage.text();
+ // Announce error message via screen reader
+ wp.a11y.speak( errorText, 'assertive' );
+ }
return;
}
+ urlErrorMessage.hide();
+ nameErrorMessage.hide();
+ itemName.removeClass( 'invalid' )
+ .removeAttr( 'aria-invalid', 'true' )
+ .removeAttr( 'aria-describedby', 'custom-name-error' );
+ itemUrl.removeClass( 'invalid' )
+ .removeAttr( 'aria-invalid', 'true' )
+ .removeAttr( 'aria-describedby', 'custom-name-error' );
+
menuItem = {
'title': itemName.val(),
'url': url,
diff --git a/src/wp-admin/css/nav-menus.css b/src/wp-admin/css/nav-menus.css
index f34a014ac7..0c02cde9a4 100644
--- a/src/wp-admin/css/nav-menus.css
+++ b/src/wp-admin/css/nav-menus.css
@@ -377,11 +377,16 @@ input.bulk-select-switcher:focus + .bulk-select-button-label {
/* Add Menu Item Boxes */
.postbox .howto input,
-.customlinkdiv .menu-item-textbox {
+.customlinkdiv .menu-item-textbox,
+.customlinkdiv .error-message {
width: 180px;
float: right;
}
+.customlinkdiv .error-message {
+ clear: right;
+}
+
.accordion-container .outer-border {
margin: 0;
}
diff --git a/src/wp-admin/includes/nav-menu.php b/src/wp-admin/includes/nav-menu.php
index c3b1244f47..4d58832ede 100644
--- a/src/wp-admin/includes/nav-menu.php
+++ b/src/wp-admin/includes/nav-menu.php
@@ -351,6 +351,7 @@ function wp_nav_menu_item_link_meta_box() {
type="text"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
class="code menu-item-textbox form-required" placeholder="https://"
/>
+ <span id="custom-url-error" class="error-message" style="display: none;"><?php _e( 'Please provide a valid link.' ); ?></span>
</p>
<p id="menu-item-name-wrap" class="wp-clearfix">
diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php
index 327a71fbcb..f6d1c5af9e 100644
--- a/src/wp-includes/class-wp-customize-nav-menus.php
+++ b/src/wp-includes/class-wp-customize-nav-menus.php
@@ -1268,10 +1268,12 @@ final class WP_Customize_Nav_Menus {
<p id="menu-item-url-wrap" class="wp-clearfix">
<label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
<input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" placeholder="https://">
+ <span id="custom-url-error" class="error-message" style="display: none;"><?php _e( 'Please provide a valid link.' ); ?></span>
</p>
<p id="menu-item-name-wrap" class="wp-clearfix">
<label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
<input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
+ <span id="custom-name-error" class="error-message" style="display: none;"><?php _e( 'The link text cannot be empty.' ); ?></span>
</p>
<p class="button-controls">
<span class="add-to-menu">