Add icon on media that lacks alt description (#1261)

A warning icon now appears on media that lacks an alt description.

Also, for admins who want to add custom CSS rules to media that is
missing descriptions, there is now a `.media-missing-description` rule
that can be added to the custom CSS settings so you can do stuff like
this if you want:

Fixes #1165
This commit is contained in:
Darius Kazemi 2022-12-28 23:35:49 -08:00 committed by GitHub
parent 5de8a60c1a
commit f295633a6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 56 additions and 16 deletions

View File

@ -12,6 +12,7 @@ import Blurhash from 'mastodon/components/blurhash';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' }, toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },
no_descriptive_text: { id: 'media.no_descriptive_text', defaultMessage: 'No descriptive text was provided for this media.' },
}); });
class Item extends React.PureComponent { class Item extends React.PureComponent {
@ -25,6 +26,7 @@ class Item extends React.PureComponent {
displayWidth: PropTypes.number, displayWidth: PropTypes.number,
visible: PropTypes.bool.isRequired, visible: PropTypes.bool.isRequired,
autoplay: PropTypes.bool, autoplay: PropTypes.bool,
noDescriptionTitle: PropTypes.object,
}; };
static defaultProps = { static defaultProps = {
@ -135,7 +137,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> <a className={`media-gallery__item-thumbnail ${!attachment.get('description') && 'media-missing-description'}`} href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
<Blurhash <Blurhash
hash={attachment.get('blurhash')} hash={attachment.get('blurhash')}
className='media-gallery__preview' className='media-gallery__preview'
@ -163,12 +165,13 @@ class Item extends React.PureComponent {
thumbnail = ( thumbnail = (
<a <a
className='media-gallery__item-thumbnail' className={`media-gallery__item-thumbnail ${!attachment.get('description') && 'media-missing-description'}`}
href={attachment.get('remote_url') || originalUrl} href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick} onClick={this.handleClick}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
> >
{ !attachment.get('description') && <IconButton className='media-gallery__item-no-alt' title={this.props.noDescriptionTitle} icon='exclamation-triangle' overlay /> }
<img <img
src={previewUrl} src={previewUrl}
srcSet={srcSet} srcSet={srcSet}
@ -186,7 +189,7 @@ class Item extends React.PureComponent {
thumbnail = ( thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<video <video
className='media-gallery__item-gifv-thumbnail' className={`media-gallery__item-gifv-thumbnail ${!attachment.get('description') && 'media-missing-description'}`}
aria-label={attachment.get('description')} aria-label={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
role='application' role='application'
@ -199,7 +202,10 @@ class Item extends React.PureComponent {
muted muted
/> />
<span className='media-gallery__gifv__label'>GIF</span> <div className='media-gallery__gifv__label__container'>
<span className='media-gallery__gifv__label'>GIF</span>
{ !attachment.get('description') && <span className='media-gallery__gifv__label__no-description'><IconButton title={this.props.noDescriptionTitle} icon='exclamation-triangle' overlay /></span> }
</div>
</div> </div>
); );
} }
@ -329,13 +335,14 @@ class MediaGallery extends React.PureComponent {
style.height = height; style.height = height;
} }
const size = media.take(4).size; const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown'); const uncached = media.every(attachment => attachment.get('type') === 'unknown');
const noDescriptionTitle = intl.formatMessage(messages.no_descriptive_text);
if (standalone && this.isFullSizeEligible()) { if (standalone && this.isFullSizeEligible()) {
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} noDescriptionTitle={noDescriptionTitle} />;
} else { } else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} noDescriptionTitle={noDescriptionTitle} />);
} }
if (uncached) { if (uncached) {

View File

@ -19,6 +19,7 @@ const messages = defineMessages({
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
download: { id: 'video.download', defaultMessage: 'Download file' }, download: { id: 'video.download', defaultMessage: 'Download file' },
hide: { id: 'audio.hide', defaultMessage: 'Hide audio' }, hide: { id: 'audio.hide', defaultMessage: 'Hide audio' },
no_descriptive_text: { id: 'media.no_descriptive_text', defaultMessage: 'No descriptive text was provided for this media.' },
}); });
const TICK_SIZE = 10; const TICK_SIZE = 10;
@ -470,7 +471,7 @@ class Audio extends React.PureComponent {
} }
return ( return (
<div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex='0' onKeyDown={this.handleKeyDown}> <div className={classNames('audio-player', { editable, inactive: !revealed, 'media-missing-description': !alt })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex='0' onKeyDown={this.handleKeyDown}>
<Blurhash <Blurhash
hash={blurhash} hash={blurhash}
@ -555,6 +556,7 @@ class Audio extends React.PureComponent {
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>
{!alt && <button type='button' title={intl.formatMessage(messages.no_descriptive_text)} aria-label={intl.formatMessage(messages.no_descriptive_text)} className='player-button no-action' ><Icon id='exclamation-triangle' fixedWidth /></button>}
{!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} {!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download> <a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
<Icon id={'download'} fixedWidth /> <Icon id={'download'} fixedWidth />

View File

@ -19,6 +19,7 @@ const messages = defineMessages({
close: { id: 'video.close', defaultMessage: 'Close video' }, close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
no_descriptive_text: { id: 'media.no_descriptive_text', defaultMessage: 'No descriptive text was provided for this media.' },
}); });
export const formatTime = secondsNum => { export const formatTime = secondsNum => {
@ -558,7 +559,7 @@ class Video extends React.PureComponent {
return ( return (
<div <div
role='menuitem' role='menuitem'
className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable })} className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, 'media-missing-description': !alt })}
style={playerStyle} style={playerStyle}
ref={this.setPlayerRef} ref={this.setPlayerRef}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
@ -640,6 +641,7 @@ class Video extends React.PureComponent {
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>
{!alt && <button type='button' title={intl.formatMessage(messages.no_descriptive_text)} aria-label={intl.formatMessage(messages.no_descriptive_text)} className='player-button no-action' ><Icon id='exclamation-triangle' fixedWidth /></button>}
{(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}

View File

@ -363,6 +363,7 @@
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}", "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
"media.no_descriptive_text": "No descriptive text was provided for this media.",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",

View File

@ -5885,22 +5885,40 @@ a.status-card.compact:hover {
z-index: 9999; z-index: 9999;
} }
.media-gallery__gifv__label { .media-gallery__gifv__label__container {
display: block; display: block;
position: absolute; position: absolute;
bottom: 4px;
left: 4px;
z-index: 1;
cursor: default;
}
.media-gallery__gifv__label {
color: $primary-text-color; color: $primary-text-color;
background: rgba($base-overlay-background, 0.5); background: rgba($base-overlay-background, 0.5);
bottom: 6px;
left: 6px;
padding: 2px 6px;
border-radius: 2px; border-radius: 2px;
padding: 3px 6px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
z-index: 1;
pointer-events: none; pointer-events: none;
opacity: 0.9; opacity: 0.9;
transition: opacity 0.1s ease; transition: opacity 0.1s ease;
line-height: 18px; }
.media-gallery__gifv__label__no-description {
color: $primary-text-color;
border-radius: 2px;
padding: 2px 6px;
font-size: 11px;
font-weight: 600;
z-index: 1;
opacity: 0.9;
transition: opacity 0.1s ease;
button {
cursor: default;
}
} }
.media-gallery__gifv { .media-gallery__gifv {
@ -6002,6 +6020,12 @@ a.status-card.compact:hover {
} }
} }
.media-gallery__item-no-alt {
position: absolute;
bottom: 4px;
left: 4px;
}
.media-gallery__item-thumbnail { .media-gallery__item-thumbnail {
cursor: zoom-in; cursor: zoom-in;
display: block; display: block;
@ -6289,6 +6313,10 @@ a.status-card.compact:hover {
color: $white; color: $white;
} }
} }
.no-action {
cursor: default;
}
} }
&__time { &__time {