From e232ed28dc51b6ad6ebcf8126f62d4eb974272c8 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Sun, 23 Oct 2016 18:15:07 +0000 Subject: [PATCH] Accessibility: Improve the Tags meta box accessibility. - changes the "X" links in buttons, improves their color contrast ratio and focus style - adds screen reader text "Remove item: + tagname" - uses `wp.a11y.speak()` to give screen reader users feedback when adding/removing tags - makes the `tagcloud-link` toggle a button, with an `aria-expanded` attribute to indicate the tag cloud collapsed/expanded state - changes colors for the autocomplete highlighted option in order to have a better color contrast ratio - reduces the font size for the autocomplete on Press This - removes CSS related to the old `suggest.js` from Press This Props joedolson, cgrymala, azaozz, afercia. Fixes #27555. git-svn-id: https://develop.svn.wordpress.org/trunk@38880 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 33 +++++- src/wp-admin/css/customize-controls.css | 2 + src/wp-admin/css/edit.css | 45 +++++--- src/wp-admin/css/forms.css | 11 +- src/wp-admin/css/ie.css | 6 +- src/wp-admin/css/press-this.css | 104 ++++++++---------- src/wp-admin/includes/class-wp-press-this.php | 2 +- src/wp-admin/includes/meta-boxes.php | 2 +- src/wp-admin/js/tags-box.js | 90 ++++++++++++--- src/wp-admin/js/tags-suggest.js | 66 ++++++----- src/wp-includes/script-loader.php | 4 + 11 files changed, 244 insertions(+), 121 deletions(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 0676888a39..572af3a1d8 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -759,7 +759,7 @@ img.emoji { /* @todo can we combine these into a class or use an existing dashicon one? */ .welcome-panel .welcome-panel-close:before, -.tagchecklist span a:before, +.tagchecklist .ntdelbutton .remove-tag-icon:before, #bulk-titles div a:before, .notice-dismiss:before { background: none; @@ -779,18 +779,41 @@ img.emoji { margin: 0; } -.tagchecklist span a:before, #bulk-titles div a:before { margin: 1px 0; } +.tagchecklist .ntdelbutton .remove-tag-icon:before { + margin-left: 2px; + -webkit-border-radius: 50%; + border-radius: 50%; + color: #0073aa; + /* vertically center the icon cross browsers */ + line-height: 1.28; +} + +.tagchecklist .ntdelbutton:focus { + outline: 0; +} + .welcome-panel .welcome-panel-close:hover:before, .welcome-panel .welcome-panel-close:focus:before, -.tagchecklist span a:hover:before, -#bulk-titles div a:hover:before { +.tagchecklist .ntdelbutton:hover .remove-tag-icon:before, +.tagchecklist .ntdelbutton:focus .remove-tag-icon:before, +#bulk-titles div a:hover:before, +#bulk-titles div a:focus:before { color: #c00; } +.tagchecklist .ntdelbutton:focus .remove-tag-icon:before { + -webkit-box-shadow: + 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); + box-shadow: + 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); +} + .key-labels label { line-height: 24px; } @@ -3571,9 +3594,7 @@ img { .sidebar-name-arrow, .sidebar-name:hover .sidebar-name-arrow, .meta-box-sortables .postbox:hover .handlediv, - .tagchecklist span a, #bulk-titles div a, - .tagchecklist span a:hover, #bulk-titles div a:hover { background: none !important; } diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index 39dbb94316..553a3ca853 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -276,7 +276,9 @@ body { box-sizing: border-box; -webkit-transition: 0.18s -webkit-transform cubic-bezier(0.645, 0.045, 0.355, 1); transition: 0.18s -webkit-transform cubic-bezier(0.645, 0.045, 0.355, 1); + -webkit-transition: 0.18s transform cubic-bezier(0.645, 0.045, 0.355, 1); transition: 0.18s transform cubic-bezier(0.645, 0.045, 0.355, 1); + -webkit-transition: 0.18s transform cubic-bezier(0.645, 0.045, 0.355, 1), 0.18s -webkit-transform cubic-bezier(0.645, 0.045, 0.355, 1); transition: 0.18s transform cubic-bezier(0.645, 0.045, 0.355, 1), 0.18s -webkit-transform cubic-bezier(0.645, 0.045, 0.355, 1); /* easeInOutCubic */ } diff --git a/src/wp-admin/css/edit.css b/src/wp-admin/css/edit.css index bbb0f3544b..5ee57f3051 100644 --- a/src/wp-admin/css/edit.css +++ b/src/wp-admin/css/edit.css @@ -582,10 +582,9 @@ span.wp-media-buttons-icon:before { position: absolute; } -.tagchecklist span { - margin-right: 25px; - display: block; +.tagchecklist > span { float: left; + margin-right: 25px; font-size: 13px; line-height: 1.8em; cursor: default; @@ -594,16 +593,16 @@ span.wp-media-buttons-icon:before { text-overflow: ellipsis; } -.tagchecklist span a { - margin: 1px 0 0 -17px; - cursor: pointer; - width: 20px; - height: 20px; - display: block; - float: left; - text-indent: 0; - overflow: hidden; +.tagchecklist .ntdelbutton { position: absolute; + width: 24px; + height: 24px; + border: none; + margin: 0 0 0 -19px; + padding: 0; + background: none; + cursor: pointer; + text-indent: 0; } #poststuff h3.hndle, /* Back-compat for pre-4.4 */ @@ -1027,6 +1026,25 @@ span.description, width: 260px; } +.tagcloud-link.button-link { + color: #0073aa; + text-decoration: underline; +} + +.tagcloud-link.button-link:hover { + color: #00a0d2; +} + +.tagcloud-link.button-link:focus { + color: #124964; + -webkit-box-shadow: + 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); + box-shadow: + 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); +} + #post-body-content .tagsdiv .the-tags { margin: 0 5px; } @@ -1052,6 +1070,7 @@ p.popular-tags a { margin: 2px 0 12px; } +/* Suggest.js autocomplete, no more used by core. */ .ac_results { display: none; margin: -1px 0 0; @@ -1438,7 +1457,7 @@ table.links-table { margin: 25px 10px; } - .tagchecklist span { + .tagchecklist > span { font-size: 16px; line-height: 1.4; } diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index 7cc0801b6a..f0b1b372ea 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -594,11 +594,18 @@ ul#add-to-blog-users { padding: 4px 10px; white-space: nowrap; text-align: left; + cursor: pointer; } -.ui-autocomplete li.ui-state-focus { +/* Colors for the wplink toolbar autocomplete. */ +.ui-autocomplete .ui-state-focus { background-color: #ddd; - cursor: pointer; +} + +/* Colors for the tags autocomplete. */ +.wp-tags-autocomplete .ui-state-focus { + background-color: #0073aa; + color: #fff; } /*------------------------------------------------------------------------------ diff --git a/src/wp-admin/css/ie.css b/src/wp-admin/css/ie.css index d98f31bbcc..df8d792494 100644 --- a/src/wp-admin/css/ie.css +++ b/src/wp-admin/css/ie.css @@ -441,11 +441,15 @@ div#dashboard-widgets { padding-right: 1px; } -.tagchecklist span, .tagchecklist span a { +.tagchecklist > span, .tagchecklist .ntdelbutton { display: inline-block; display: block; } +.tagchecklist .ntdelbutton:focus .remove-tag-icon:before { + outline: 1px solid #5b9dd9; +} + .tablenav .button, .nav .button { padding-top: 2px; diff --git a/src/wp-admin/css/press-this.css b/src/wp-admin/css/press-this.css index 1ec3ba06bf..36b988814d 100644 --- a/src/wp-admin/css/press-this.css +++ b/src/wp-admin/css/press-this.css @@ -770,38 +770,6 @@ dd { margin: 10px 0 6px 16px; } - -/* Tag hint TODO needed? */ -/* Tag suggestions */ -.ac_results { - padding: 0; - margin: -1px 0 0 -1px; - list-style: none; - position: absolute; - z-index: 10000; - display: none; - border: 1px solid #d8d8d8; - background-color: #fff; - font-size: 14px; -} - -.ac_results li { - padding: 6px 16px; - white-space: nowrap; - text-align: left; -} - -.ac_results .ac_over { - background-color: #e5e5e5; - background-color: #00a0d2; - color: #fff; - cursor: pointer; -} - -.ac_match { - text-decoration: underline; -} - /* Tags */ .tagchecklist { padding: 16px 28px 5px; @@ -817,10 +785,9 @@ dd { clear: both; } -.tagchecklist span { - display: block; - margin-right: 25px; +.tagchecklist > span { float: left; + margin-right: 25px; font-size: 13px; line-height: 1.8; white-space: nowrap; @@ -828,7 +795,7 @@ dd { } @media (max-width: 600px) { - .tagchecklist span { + .tagchecklist > span { margin-bottom: 15px; font-size: 16px; line-height: 1.3; @@ -836,36 +803,51 @@ dd { } .tagchecklist .ntdelbutton { - margin: 1px 0 0 -17px; - cursor: pointer; - width: 20px; - height: 20px; - display: block; - float: left; - text-indent: 0; - overflow: hidden; position: absolute; - outline: 0; + width: 24px; + height: 24px; + border: none; + margin: 0 0 0 -19px; + padding: 0; + background: none; + cursor: pointer; + text-indent: 0;; + position: absolute; } -.tagchecklist .ntdelbutton:before { +.tagchecklist .ntdelbutton .remove-tag-icon:before { content: "\f153"; display: block; - margin: 2px 0; + margin-left: 2px; height: 20px; width: 20px; - background: 0 0; - color: #9ea7af; - font: 400 16px/1 dashicons; + -webkit-border-radius: 50%; + border-radius: 50%; + background: transparent; + color: #0073aa; + /* line-height tweak to vertically center the icon cross browsers */ + font: 400 16px/1.28 dashicons; text-align: center; - speak: none; -webkit-font-smoothing: antialiased; } -.tagchecklist .ntdelbutton:focus:before { - color: #00a0d2; +.tagchecklist .ntdelbutton:focus { + outline: 0; } +.tagchecklist .ntdelbutton:hover .remove-tag-icon:before, +.tagchecklist .ntdelbutton:focus .remove-tag-icon:before { + color: #c00; +} + +.tagchecklist .ntdelbutton:focus .remove-tag-icon:before { + -webkit-box-shadow: + 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); + box-shadow: + 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); +} /* THE TAG CLOUD. */ .tagsdiv + p { @@ -2194,6 +2176,7 @@ html { -webkit-box-shadow: 0 1px 2px rgba( 30, 140, 190, 0.8 ); box-shadow: 0 1px 2px rgba( 30, 140, 190, 0.8 ); background-color: #fff; + font-size: 14px; } .ui-autocomplete li { @@ -2201,9 +2184,16 @@ html { padding: 4px 10px; white-space: nowrap; text-align: left; -} - -.ui-autocomplete li.ui-state-focus { - background-color: #ddd; cursor: pointer; } + +/* Colors for the wplink toolbar autocomplete. */ +.ui-autocomplete .ui-state-focus { + background-color: #ddd; +} + +/* Colors for the tags autocomplete. */ +.wp-tags-autocomplete .ui-state-focus { + background-color: #0073aa; + color: #fff; +} diff --git a/src/wp-admin/includes/class-wp-press-this.php b/src/wp-admin/includes/class-wp-press-this.php index e275b16e5b..71ce5153d3 100644 --- a/src/wp-admin/includes/class-wp-press-this.php +++ b/src/wp-admin/includes/class-wp-press-this.php @@ -940,7 +940,7 @@ class WP_Press_This { if ( $user_can_assign_terms ) { ?> - + -

labels->choose_from_most_used; ?>

+

X' ); + /* + * Build the X buttons, hide the X icon with aria-hidden and + * use visually hidden text for screen readers. + */ + xbutton = $( '' ); xbutton.on( 'click keypress', function( e ) { - // Trigger function if pressed Enter - keyboard navigation - if ( e.type === 'click' || e.keyCode === 13 ) { - // When using keyboard, move focus back to the new tag field. - if ( e.keyCode === 13 ) { - $( this ).closest( '.tagsdiv' ).find( 'input.newtag' ).focus(); - } + // On click or when using the Enter/Spacebar keys. + if ( 'click' === e.type || 13 === e.keyCode || 32 === e.keyCode ) { + /* + * When using the keyboard, move focus back to the + * add new tag field. Note: when releasing the pressed + * key this will fire the `keyup` event on the input. + */ + if ( 13 === e.keyCode || 32 === e.keyCode ) { + $( this ).closest( '.tagsdiv' ).find( 'input.newtag' ).focus(); + } + tagBox.userAction = 'remove'; tagBox.parseTags( this ); } }); @@ -106,6 +118,8 @@ var tagBox, array_unique_noempty; // Append the span to the tag list. tagchecklist.append( span ); }); + // The buttons list is built now, give feedback to screen reader users. + tagBox.screenReadersMessage(); }, flushTags : function( el, a, f ) { @@ -117,7 +131,14 @@ var tagBox, array_unique_noempty; text = a ? $(a).text() : newtag.val(); - if ( 'undefined' == typeof( text ) ) { + /* + * Return if there's no new tag or if the input field is empty. + * Note: when using the keyboard to add tags, focus is moved back to + * the input field and the `keyup` event attached on this field will + * fire when releasing the pressed key. Checking also for the field + * emptiness avoids to set the tags and call quickClicks() again. + */ + if ( 'undefined' == typeof( text ) || '' === text ) { return false; } @@ -148,6 +169,7 @@ var tagBox, array_unique_noempty; r = $( '

' + r + '

' ); $( 'a', r ).click( function() { + tagBox.userAction = 'add'; tagBox.flushTags( $( '#' + tax ), this ); return false; }); @@ -156,6 +178,37 @@ var tagBox, array_unique_noempty; }); }, + /** + * Track the user's last action. + * + * @since 4.7.0 + */ + userAction: '', + + /** + * Dispatch an audible message to screen readers. + * + * @since 4.7.0 + */ + screenReadersMessage: function() { + var message; + + switch ( this.userAction ) { + case 'remove': + message = window.tagsSuggestL10n.termRemoved; + break; + + case 'add': + message = window.tagsSuggestL10n.termAdded; + break; + + default: + return; + } + + window.wp.a11y.speak( message, 'assertive' ); + }, + init : function() { var ajaxtag = $('div.ajaxtag'); @@ -164,11 +217,13 @@ var tagBox, array_unique_noempty; }); $( '.tagadd', ajaxtag ).click( function() { + tagBox.userAction = 'add'; tagBox.flushTags( $( this ).closest( '.tagsdiv' ) ); }); $( 'input.newtag', ajaxtag ).keyup( function( event ) { if ( 13 == event.which ) { + tagBox.userAction = 'add'; tagBox.flushTags( $( this ).closest( '.tagsdiv' ) ); event.preventDefault(); event.stopPropagation(); @@ -189,14 +244,19 @@ var tagBox, array_unique_noempty; }); }); - // tag cloud + // Fetch and toggle the Tag cloud. $('.tagcloud-link').click(function(){ - tagBox.get( $(this).attr('id') ); - $(this).unbind().click(function(){ - $(this).siblings('.the-tagcloud').toggle(); - return false; - }); - return false; + // On the first click, fetch the tag cloud and insert it in the DOM. + tagBox.get( $( this ).attr( 'id' ) ); + // Update button state, remove previous click event and attach a new one to toggle the cloud. + $( this ) + .attr( 'aria-expanded', 'true' ) + .unbind() + .click( function() { + $( this ) + .attr( 'aria-expanded', 'false' === $( this ).attr( 'aria-expanded' ) ? 'true' : 'false' ) + .siblings( '.the-tagcloud' ).toggle(); + }); }); } }; diff --git a/src/wp-admin/js/tags-suggest.js b/src/wp-admin/js/tags-suggest.js index b9e1632120..7323a35f28 100644 --- a/src/wp-admin/js/tags-suggest.js +++ b/src/wp-admin/js/tags-suggest.js @@ -1,6 +1,13 @@ +/** + * Default settings for jQuery UI Autocomplete for use with non-hierarchical taxonomies. + */ ( function( $ ) { + if ( typeof window.tagsSuggestL10n === 'undefined' || typeof window.uiAutocompleteL10n === 'undefined' ) { + return; + } + var tempID = 0; - var separator = ( window.tagsSuggestL10n && window.tagsSuggestL10n.tagDelimiter ) || ','; + var separator = window.tagsSuggestL10n.tagDelimiter || ','; function split( val ) { return val.split( new RegExp( separator + '\\s*' ) ); @@ -10,19 +17,33 @@ return split( term ).pop(); } + /** + * Add UI Autocomplete to an input or textarea element with presets for use + * with non-hierarchical taxonomies. + * + * Example: `$( element ).wpTagsSuggest( options )`. + * + * The taxonomy can be passed in a `data-wp-taxonomy` attribute on the element or + * can be in `options.taxonomy`. + * + * @since 4.7 + * + * @param {object} options Options that are passed to UI Autocomplete. Can be used to override the default settings. + * @returns {object} jQuery instance. + */ $.fn.wpTagsSuggest = function( options ) { var cache; var last; var $element = $( this ); options = options || {}; - + var taxonomy = options.taxonomy || $element.attr( 'data-wp-taxonomy' ) || 'post_tag'; - + delete( options.taxonomy ); options = $.extend( { - source: function( request, response ) { + source: function( request, response ) { var term; if ( last === request.term ) { @@ -39,25 +60,25 @@ } ).always( function() { $element.removeClass( 'ui-autocomplete-loading' ); // UI fails to remove this sometimes? } ).done( function( data ) { - var value; - var terms = []; + var tagName; + var tags = []; if ( data ) { data = data.split( '\n' ); - for ( value in data ) { + for ( tagName in data ) { var id = ++tempID; - terms.push({ + tags.push({ id: id, - name: data[value] + name: data[tagName] }); } - cache = terms; - response( terms ); + cache = tags; + response( tags ); } else { - response( terms ); + response( tags ); } } ); @@ -80,11 +101,8 @@ $element.val( tags.join( separator + ' ' ) ); if ( $.ui.keyCode.TAB === event.keyCode ) { - if ( typeof window.uiAutocompleteL10n !== 'undefined' ) { - // Audible confirmation message when a tag has been selected. - window.wp.a11y.speak( window.uiAutocompleteL10n.itemSelected ); - } - + // Audible confirmation message when a tag has been selected. + window.wp.a11y.speak( window.tagsSuggestL10n.termSelected, 'assertive' ); event.preventDefault(); } else if ( $.ui.keyCode.ENTER === event.keyCode ) { // Do not close Quick Edit / Bulk Edit @@ -105,15 +123,13 @@ my: 'left top+2' }, messages: { - noResults: ( typeof window.uiAutocompleteL10n !== 'undefined' ) ? window.uiAutocompleteL10n.noResults : '', + noResults: window.uiAutocompleteL10n.noResults, results: function( number ) { - if ( typeof window.uiAutocompleteL10n !== 'undefined' ) { - if ( number > 1 ) { - return window.uiAutocompleteL10n.manyResults.replace( '%d', number ); - } - - return window.uiAutocompleteL10n.oneResult; + if ( number > 1 ) { + return window.uiAutocompleteL10n.manyResults.replace( '%d', number ); } + + return window.uiAutocompleteL10n.oneResult; } } }, options ); @@ -160,7 +176,7 @@ // so we need to find the active item with other means. $( this ).find( '[aria-selected="true"]' ).removeAttr( 'aria-selected' ); }); - + return this; }; diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 205cfffdc2..143bfa14ff 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -538,6 +538,10 @@ function wp_default_scripts( &$scripts ) { $scripts->add( 'tags-suggest', "/wp-admin/js/tags-suggest$suffix.js", array( 'jquery-ui-autocomplete', 'wp-a11y' ), false, 1 ); did_action( 'init' ) && $scripts->localize( 'tags-suggest', 'tagsSuggestL10n', array( 'tagDelimiter' => _x( ',', 'tag delimiter' ), + 'removeTerm' => __( 'Remove term:' ), + 'termSelected' => __( 'Term selected.' ), + 'termAdded' => __( 'Term added.' ), + 'termRemoved' => __( 'Term removed.' ), ) ); $scripts->add( 'post', "/wp-admin/js/post$suffix.js", array( 'suggest', 'wp-lists', 'postbox', 'tags-box', 'underscore', 'word-count', 'wp-a11y' ), false, 1 );