Accessibility: Media: Improve accessibility of the status and error messages in the Image Editor.
- improves focus management by moving focus to the notices, if any, or to the first "tabbable" element - this avoids a focus loss and helps Braille-only and screen magnification users to be aware of the messages - adds an ARIA role `alert` to all the notices - uses `wp.a11y.speak()` to announce messages to assistive technology - this way, all visual users will see the messages while assistive technology users will get an audible message - uses `wp.i18n` for translatable strings in `wp-admin/js/image-edit.js` Props anevins, ryanshoover, antpb, SergeyBiryukov, afercia. See #20491. Fixes #47147. Built from https://develop.svn.wordpress.org/trunk@48375 git-svn-id: http://core.svn.wordpress.org/trunk@48144 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
parent
181717c9ab
commit
aee4324b9e
|
@ -2607,8 +2607,11 @@ function wp_ajax_image_editor() {
|
||||||
switch ( $_POST['do'] ) {
|
switch ( $_POST['do'] ) {
|
||||||
case 'save':
|
case 'save':
|
||||||
$msg = wp_save_image( $attachment_id );
|
$msg = wp_save_image( $attachment_id );
|
||||||
$msg = wp_json_encode( $msg );
|
if ( $msg->error ) {
|
||||||
wp_die( $msg );
|
wp_send_json_error( $msg );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( $msg );
|
||||||
break;
|
break;
|
||||||
case 'scale':
|
case 'scale':
|
||||||
$msg = wp_save_image( $attachment_id );
|
$msg = wp_save_image( $attachment_id );
|
||||||
|
@ -2618,8 +2621,25 @@ function wp_ajax_image_editor() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
wp_image_editor( $attachment_id, $msg );
|
wp_image_editor( $attachment_id, $msg );
|
||||||
wp_die();
|
$html = ob_get_clean();
|
||||||
|
|
||||||
|
if ( $msg->error ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array(
|
||||||
|
'message' => $msg,
|
||||||
|
'html' => $html,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'message' => $msg,
|
||||||
|
'html' => $html,
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,9 +38,9 @@ function wp_image_editor( $post_id, $msg = false ) {
|
||||||
|
|
||||||
if ( $msg ) {
|
if ( $msg ) {
|
||||||
if ( isset( $msg->error ) ) {
|
if ( isset( $msg->error ) ) {
|
||||||
$note = "<div class='error'><p>$msg->error</p></div>";
|
$note = "<div class='notice notice-error' tabindex='-1' role='alert'><p>$msg->error</p></div>";
|
||||||
} elseif ( isset( $msg->msg ) ) {
|
} elseif ( isset( $msg->msg ) ) {
|
||||||
$note = "<div class='updated'><p>$msg->msg</p></div>";
|
$note = "<div class='notice notice-success' tabindex='-1' role='alert'><p>$msg->msg</p></div>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ function wp_image_editor( $post_id, $msg = false ) {
|
||||||
<div class="imgedit-group">
|
<div class="imgedit-group">
|
||||||
<div class="imgedit-group-top">
|
<div class="imgedit-group-top">
|
||||||
<h2><?php _e( 'Scale Image' ); ?></h2>
|
<h2><?php _e( 'Scale Image' ); ?></h2>
|
||||||
<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);return false;" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Scale Image Help' ); ?></span></button>
|
<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Scale Image Help' ); ?></span></button>
|
||||||
<div class="imgedit-help">
|
<div class="imgedit-help">
|
||||||
<p><?php _e( 'You can proportionally scale the original image. For best results, scaling should be done before you crop, flip, or rotate. Images can only be scaled down, not up.' ); ?></p>
|
<p><?php _e( 'You can proportionally scale the original image. For best results, scaling should be done before you crop, flip, or rotate. Images can only be scaled down, not up.' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -141,7 +141,7 @@ function wp_image_editor( $post_id, $msg = false ) {
|
||||||
|
|
||||||
<div class="imgedit-group">
|
<div class="imgedit-group">
|
||||||
<div class="imgedit-group-top">
|
<div class="imgedit-group-top">
|
||||||
<h2><button type="button" onclick="imageEdit.toggleHelp(this);" class="button-link"><?php _e( 'Restore Original Image' ); ?> <span class="dashicons dashicons-arrow-down imgedit-help-toggle"></span></button></h2>
|
<h2><button type="button" onclick="imageEdit.toggleHelp(this);" class="button-link" aria-expanded="false"><?php _e( 'Restore original image' ); ?> <span class="dashicons dashicons-arrow-down imgedit-help-toggle"></span></button></h2>
|
||||||
<div class="imgedit-help imgedit-restore">
|
<div class="imgedit-help imgedit-restore">
|
||||||
<p>
|
<p>
|
||||||
<?php
|
<?php
|
||||||
|
@ -164,7 +164,7 @@ function wp_image_editor( $post_id, $msg = false ) {
|
||||||
<div class="imgedit-group">
|
<div class="imgedit-group">
|
||||||
<div class="imgedit-group-top">
|
<div class="imgedit-group-top">
|
||||||
<h2><?php _e( 'Image Crop' ); ?></h2>
|
<h2><?php _e( 'Image Crop' ); ?></h2>
|
||||||
<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);return false;" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Image Crop Help' ); ?></span></button>
|
<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Image Crop Help' ); ?></span></button>
|
||||||
|
|
||||||
<div class="imgedit-help">
|
<div class="imgedit-help">
|
||||||
<p><?php _e( 'To crop the image, click on it and drag to make your selection.' ); ?></p>
|
<p><?php _e( 'To crop the image, click on it and drag to make your selection.' ); ?></p>
|
||||||
|
@ -209,7 +209,7 @@ function wp_image_editor( $post_id, $msg = false ) {
|
||||||
<div class="imgedit-group imgedit-applyto">
|
<div class="imgedit-group imgedit-applyto">
|
||||||
<div class="imgedit-group-top">
|
<div class="imgedit-group-top">
|
||||||
<h2><?php _e( 'Thumbnail Settings' ); ?></h2>
|
<h2><?php _e( 'Thumbnail Settings' ); ?></h2>
|
||||||
<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);return false;" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Thumbnail Settings Help' ); ?></span></button>
|
<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Thumbnail Settings Help' ); ?></span></button>
|
||||||
<div class="imgedit-help">
|
<div class="imgedit-help">
|
||||||
<p><?php _e( 'You can edit the image while preserving the thumbnail. For example, you may wish to have a square thumbnail that displays just a section of the image.' ); ?></p>
|
<p><?php _e( 'You can edit the image while preserving the thumbnail. For example, you may wish to have a square thumbnail that displays just a section of the image.' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
* @output wp-admin/js/image-edit.js
|
* @output wp-admin/js/image-edit.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* global imageEditL10n, ajaxurl, confirm */
|
/* global ajaxurl, confirm */
|
||||||
|
|
||||||
(function($) {
|
(function($) {
|
||||||
|
var __ = wp.i18n.__;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains all the methods to initialise and control the image editor.
|
* Contains all the methods to initialise and control the image editor.
|
||||||
|
@ -137,28 +138,34 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$( document ).on( 'image-editor-image-loaded', this.focusManager );
|
$( document ).on( 'image-editor-ui-ready', this.focusManager );
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the wait/load icon in the editor.
|
* Toggles the wait/load icon in the editor.
|
||||||
*
|
*
|
||||||
* @since 2.9.0
|
* @since 2.9.0
|
||||||
|
* @since 5.5.0 Added the triggerUIReady parameter.
|
||||||
*
|
*
|
||||||
* @memberof imageEdit
|
* @memberof imageEdit
|
||||||
*
|
*
|
||||||
* @param {number} postid The post ID.
|
* @param {number} postid The post ID.
|
||||||
* @param {number} toggle Is 0 or 1, fades the icon in then 1 and out when 0.
|
* @param {number} toggle Is 0 or 1, fades the icon in when 1 and out when 0.
|
||||||
|
* @param {boolean} triggerUIReady Whether to trigger a custom event when the UI is ready. Default false.
|
||||||
*
|
*
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
toggleEditor : function(postid, toggle) {
|
toggleEditor: function( postid, toggle, triggerUIReady ) {
|
||||||
var wait = $('#imgedit-wait-' + postid);
|
var wait = $('#imgedit-wait-' + postid);
|
||||||
|
|
||||||
if ( toggle ) {
|
if ( toggle ) {
|
||||||
wait.fadeIn( 'fast' );
|
wait.fadeIn( 'fast' );
|
||||||
} else {
|
} else {
|
||||||
wait.fadeOut('fast');
|
wait.fadeOut( 'fast', function() {
|
||||||
|
if ( triggerUIReady ) {
|
||||||
|
$( document ).trigger( 'image-editor-ui-ready' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -402,10 +409,16 @@
|
||||||
|
|
||||||
t.toggleEditor(postid, 0);
|
t.toggleEditor(postid, 0);
|
||||||
})
|
})
|
||||||
.on('error', function() {
|
.on( 'error', function() {
|
||||||
$('#imgedit-crop-' + postid).empty().append('<div class="error"><p>' + imageEditL10n.error + '</p></div>');
|
var errorMessage = __( 'Could not load the preview image. Please reload the page and try again.' );
|
||||||
t.toggleEditor(postid, 0);
|
|
||||||
})
|
$( '#imgedit-crop-' + postid )
|
||||||
|
.empty()
|
||||||
|
.append( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' );
|
||||||
|
|
||||||
|
t.toggleEditor( postid, 0, true );
|
||||||
|
wp.a11y.speak( errorMessage, 'assertive' );
|
||||||
|
} )
|
||||||
.attr('src', ajaxurl + '?' + $.param(data));
|
.attr('src', ajaxurl + '?' + $.param(data));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -466,14 +479,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
t.toggleEditor(postid, 1);
|
t.toggleEditor(postid, 1);
|
||||||
$.post(ajaxurl, data, function(r) {
|
$.post( ajaxurl, data, function( response ) {
|
||||||
$('#image-editor-' + postid).empty().append(r);
|
$( '#image-editor-' + postid ).empty().append( response.data.html );
|
||||||
t.toggleEditor(postid, 0);
|
t.toggleEditor( postid, 0, true );
|
||||||
// Refresh the attachment model so that changes propagate.
|
// Refresh the attachment model so that changes propagate.
|
||||||
if ( t._view ) {
|
if ( t._view ) {
|
||||||
t._view.refresh();
|
t._view.refresh();
|
||||||
}
|
}
|
||||||
});
|
} ).done( function( response ) {
|
||||||
|
// Whether the executed action was `scale` or `restore`, the response does have a message.
|
||||||
|
if ( response && response.data.message.msg ) {
|
||||||
|
wp.a11y.speak( response.data.message.msg );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( response && response.data.message.error ) {
|
||||||
|
wp.a11y.speak( response.data.message.error );
|
||||||
|
}
|
||||||
|
} );
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -511,27 +534,30 @@
|
||||||
'do': 'save'
|
'do': 'save'
|
||||||
};
|
};
|
||||||
// Post the image edit data to the backend.
|
// Post the image edit data to the backend.
|
||||||
$.post(ajaxurl, data, function(r) {
|
$.post( ajaxurl, data, function( response ) {
|
||||||
// Read the response.
|
|
||||||
var ret = JSON.parse(r);
|
|
||||||
|
|
||||||
// If a response is returned, close the editor and show an error.
|
// If a response is returned, close the editor and show an error.
|
||||||
if ( ret.error ) {
|
if ( response.data.error ) {
|
||||||
$('#imgedit-response-' + postid).html('<div class="error"><p>' + ret.error + '</p></div>');
|
$( '#imgedit-response-' + postid )
|
||||||
|
.html( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + response.data.error + '</p></div>' );
|
||||||
|
|
||||||
imageEdit.close(postid);
|
imageEdit.close(postid);
|
||||||
|
wp.a11y.speak( response.data.error );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ret.fw && ret.fh ) {
|
if ( response.data.fw && response.data.fh ) {
|
||||||
$('#media-dims-' + postid).html( ret.fw + ' × ' + ret.fh );
|
$( '#media-dims-' + postid ).html( response.data.fw + ' × ' + response.data.fh );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ret.thumbnail ) {
|
if ( response.data.thumbnail ) {
|
||||||
$('.thumbnail', '#thumbnail-head-' + postid).attr('src', ''+ret.thumbnail);
|
$( '.thumbnail', '#thumbnail-head-' + postid ).attr( 'src', '' + response.data.thumbnail );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ret.msg ) {
|
if ( response.data.msg ) {
|
||||||
$('#imgedit-response-' + postid).html('<div class="updated"><p>' + ret.msg + '</p></div>');
|
$( '#imgedit-response-' + postid )
|
||||||
|
.html( '<div class="notice notice-success" tabindex="-1" role="alert"><p>' + response.data.msg + '</p></div>' );
|
||||||
|
|
||||||
|
wp.a11y.speak( response.data.msg );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( self._view ) {
|
if ( self._view ) {
|
||||||
|
@ -559,8 +585,11 @@
|
||||||
open : function( postid, nonce, view ) {
|
open : function( postid, nonce, view ) {
|
||||||
this._view = view;
|
this._view = view;
|
||||||
|
|
||||||
var dfd, data, elem = $('#image-editor-' + postid), head = $('#media-head-' + postid),
|
var dfd, data,
|
||||||
btn = $('#imgedit-open-btn-' + postid), spin = btn.siblings('.spinner');
|
elem = $( '#image-editor-' + postid ),
|
||||||
|
head = $( '#media-head-' + postid ),
|
||||||
|
btn = $( '#imgedit-open-btn-' + postid ),
|
||||||
|
spin = btn.siblings( '.spinner' );
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Instead of disabling the button, which causes a focus loss and makes screen
|
* Instead of disabling the button, which causes a focus loss and makes screen
|
||||||
|
@ -579,23 +608,37 @@
|
||||||
'do': 'open'
|
'do': 'open'
|
||||||
};
|
};
|
||||||
|
|
||||||
dfd = $.ajax({
|
dfd = $.ajax( {
|
||||||
url: ajaxurl,
|
url: ajaxurl,
|
||||||
type: 'post',
|
type: 'post',
|
||||||
data: data,
|
data: data,
|
||||||
beforeSend: function() {
|
beforeSend: function() {
|
||||||
btn.addClass( 'button-activated' );
|
btn.addClass( 'button-activated' );
|
||||||
}
|
}
|
||||||
}).done(function( html ) {
|
} ).done( function( response ) {
|
||||||
elem.html( html );
|
var errorMessage;
|
||||||
head.fadeOut('fast', function(){
|
|
||||||
elem.fadeIn('fast');
|
if ( '-1' === response ) {
|
||||||
|
errorMessage = __( 'Could not load the preview image.' );
|
||||||
|
elem.html( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( response.data && response.data.html ) {
|
||||||
|
elem.html( response.data.html );
|
||||||
|
}
|
||||||
|
|
||||||
|
head.fadeOut( 'fast', function() {
|
||||||
|
elem.fadeIn( 'fast', function() {
|
||||||
|
if ( errorMessage ) {
|
||||||
|
$( document ).trigger( 'image-editor-ui-ready' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
btn.removeClass( 'button-activated' );
|
btn.removeClass( 'button-activated' );
|
||||||
spin.removeClass( 'is-active' );
|
spin.removeClass( 'is-active' );
|
||||||
});
|
} );
|
||||||
// Initialise the Image Editor now that everything is ready.
|
// Initialise the Image Editor now that everything is ready.
|
||||||
imageEdit.init( postid );
|
imageEdit.init( postid );
|
||||||
});
|
} );
|
||||||
|
|
||||||
return dfd;
|
return dfd;
|
||||||
},
|
},
|
||||||
|
@ -622,9 +665,7 @@
|
||||||
this.initCrop(postid, img, parent);
|
this.initCrop(postid, img, parent);
|
||||||
this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0, 'width': img.innerWidth(), 'height': img.innerHeight() } );
|
this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0, 'width': img.innerWidth(), 'height': img.innerHeight() } );
|
||||||
|
|
||||||
this.toggleEditor(postid, 0);
|
this.toggleEditor( postid, 0, true );
|
||||||
|
|
||||||
$( document ).trigger( 'image-editor-image-loaded' );
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -636,12 +677,19 @@
|
||||||
*/
|
*/
|
||||||
focusManager: function() {
|
focusManager: function() {
|
||||||
/*
|
/*
|
||||||
* Editor is ready, move focus to the first focusable element. Since the
|
* Editor is ready. Move focus to one of the admin alert notices displayed
|
||||||
* DOM update is pretty large, the timeout helps browsers update their
|
* after a user action or to the first focusable element. Since the DOM
|
||||||
|
* update is pretty large, the timeout helps browsers update their
|
||||||
* accessibility tree to better support assistive technologies.
|
* accessibility tree to better support assistive technologies.
|
||||||
*/
|
*/
|
||||||
setTimeout( function() {
|
setTimeout( function() {
|
||||||
$( '.imgedit-wrap' ).find( ':tabbable:first' ).focus();
|
var elementToSetFocusTo = $( '.notice[role="alert"]' );
|
||||||
|
|
||||||
|
if ( ! elementToSetFocusTo.length ) {
|
||||||
|
elementToSetFocusTo = $( '.imgedit-wrap' ).find( ':tabbable:first' );
|
||||||
|
}
|
||||||
|
|
||||||
|
elementToSetFocusTo.focus();
|
||||||
}, 100 );
|
}, 100 );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1999,7 +1999,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal .imgedit-wrap div.updated {
|
.media-modal .imgedit-wrap div.updated, /* Back-compat for pre-5.5 */
|
||||||
|
.media-modal .imgedit-wrap .notice {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1998,7 +1998,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal .imgedit-wrap div.updated {
|
.media-modal .imgedit-wrap div.updated, /* Back-compat for pre-5.5 */
|
||||||
|
.media-modal .imgedit-wrap .notice {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1376,14 +1376,8 @@ function wp_default_scripts( $scripts ) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$scripts->add( 'image-edit', "/wp-admin/js/image-edit$suffix.js", array( 'jquery', 'jquery-ui-core', 'json2', 'imgareaselect' ), false, 1 );
|
$scripts->add( 'image-edit', "/wp-admin/js/image-edit$suffix.js", array( 'jquery', 'jquery-ui-core', 'json2', 'imgareaselect', 'wp-a11y' ), false, 1 );
|
||||||
did_action( 'init' ) && $scripts->localize(
|
$scripts->set_translations( 'image-edit' );
|
||||||
'image-edit',
|
|
||||||
'imageEditL10n',
|
|
||||||
array(
|
|
||||||
'error' => __( 'Could not load the preview image. Please reload the page and try again.' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$scripts->add( 'set-post-thumbnail', "/wp-admin/js/set-post-thumbnail$suffix.js", array( 'jquery' ), false, 1 );
|
$scripts->add( 'set-post-thumbnail', "/wp-admin/js/set-post-thumbnail$suffix.js", array( 'jquery' ), false, 1 );
|
||||||
did_action( 'init' ) && $scripts->localize(
|
did_action( 'init' ) && $scripts->localize(
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
*
|
*
|
||||||
* @global string $wp_version
|
* @global string $wp_version
|
||||||
*/
|
*/
|
||||||
$wp_version = '5.5-alpha-48374';
|
$wp_version = '5.5-alpha-48375';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
|
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
|
||||||
|
|
Loading…
Reference in New Issue