Merge pull request #681 from ThibG/glitch-soc/fixes/accessibility

Port various accessibility improvements from upstream
This commit is contained in:
David Yip 2018-09-29 19:01:19 -05:00 committed by GitHub
commit c065717b67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 56 additions and 18 deletions

View file

@ -9,6 +9,7 @@ export default class Column extends React.PureComponent {
children: PropTypes.node, children: PropTypes.node,
extraClasses: PropTypes.string, extraClasses: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
label: PropTypes.string,
}; };
scrollTop () { scrollTop () {
@ -42,10 +43,10 @@ export default class Column extends React.PureComponent {
} }
render () { render () {
const { children, extraClasses, name } = this.props; const { children, extraClasses, name, label } = this.props;
return ( return (
<div role='region' data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}> <div role='region' aria-label={label} data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
{children} {children}
</div> </div>
); );

View file

@ -107,7 +107,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
return ( return (
<article <article
ref={this.handleRef} ref={this.handleRef}
aria-posinset={index} aria-posinset={index + 1}
aria-setsize={listLength} aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }} style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id} data-id={id}
@ -119,7 +119,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
return ( return (
<article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'> <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
{children && React.cloneElement(children, { hidden: false })} {children && React.cloneElement(children, { hidden: false })}
</article> </article>
); );

View file

@ -7,7 +7,7 @@ import StatusIcons from './status_icons';
import StatusContent from './status_content'; import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
import { FormattedMessage } from 'react-intl'; import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from 'flavours/glitch/util/async-components'; import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
@ -19,6 +19,24 @@ import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
const displayName = status.getIn(['account', 'display_name']);
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
if (rebloggedByText) {
values.push(rebloggedByText);
}
return values.join(', ');
};
@injectIntl
export default class Status extends ImmutablePureComponent { export default class Status extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
@ -52,6 +70,7 @@ export default class Status extends ImmutablePureComponent {
getScrollPosition: PropTypes.func, getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func, updateScrollBottom: PropTypes.func,
expanded: PropTypes.bool, expanded: PropTypes.bool,
intl: PropTypes.object.isRequired,
}; };
state = { state = {
@ -337,6 +356,7 @@ export default class Status extends ImmutablePureComponent {
} = this; } = this;
const { router } = this.context; const { router } = this.context;
const { const {
intl,
status, status,
account, account,
settings, settings,
@ -474,6 +494,12 @@ export default class Status extends ImmutablePureComponent {
selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`; selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`;
} }
let rebloggedByText;
if (prepend === 'reblog') {
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
}
const handlers = { const handlers = {
reply: this.handleHotkeyReply, reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite, favourite: this.handleHotkeyFavourite,
@ -502,6 +528,7 @@ export default class Status extends ImmutablePureComponent {
ref={handleRef} ref={handleRef}
tabIndex='0' tabIndex='0'
data-featured={featured ? 'true' : null} data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
> >
<header className='status__info'> <header className='status__info'>
<span> <span>

View file

@ -76,7 +76,7 @@ export default class CommunityTimeline extends React.PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
<Column ref={this.setRef} name='local'> <Column ref={this.setRef} name='local' label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='users' icon='users'
active={hasUnread} active={hasUnread}

View file

@ -76,7 +76,7 @@ export default class DirectTimeline extends React.PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='envelope' icon='envelope'
active={hasUnread} active={hasUnread}

View file

@ -2,6 +2,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
// Actions. // Actions.
@ -25,6 +26,11 @@ import DrawerSearch from './search';
import { me } from 'flavours/glitch/util/initial_state'; import { me } from 'flavours/glitch/util/initial_state';
import { wrap } from 'flavours/glitch/util/redux_helpers'; import { wrap } from 'flavours/glitch/util/redux_helpers';
// Messages.
const messages = defineMessages({
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
});
// State mapping. // State mapping.
const mapStateToProps = state => ({ const mapStateToProps = state => ({
account: state.getIn(['accounts', me]), account: state.getIn(['accounts', me]),
@ -96,7 +102,7 @@ class Drawer extends React.Component {
// The result. // The result.
return ( return (
<div className={computedClass}> <div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}>
{multiColumn ? ( {multiColumn ? (
<DrawerHeader <DrawerHeader
columns={columns} columns={columns}

View file

@ -71,7 +71,7 @@ export default class Favourites extends ImmutablePureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
<Column ref={this.setRef} name='favourites'> <Column ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}>
<ColumnHeader <ColumnHeader
icon='star' icon='star'
title={intl.formatMessage(messages.heading)} title={intl.formatMessage(messages.heading)}

View file

@ -33,6 +33,7 @@ const messages = defineMessages({
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' }, lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' }, misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' },
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -148,7 +149,7 @@ export default class GettingStarted extends ImmutablePureComponent {
]); ]);
return ( return (
<Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile> <Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
<div className='scrollable optionally-scrollable'> <div className='scrollable optionally-scrollable'>
<div className='getting-started__wrapper'> <div className='getting-started__wrapper'>
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} /> <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />

View file

@ -88,7 +88,7 @@ export default class HashtagTimeline extends React.PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
<Column ref={this.setRef} name='hashtag'> <Column ref={this.setRef} name='hashtag' label={`#${id}`}>
<ColumnHeader <ColumnHeader
icon='hashtag' icon='hashtag'
active={hasUnread} active={hasUnread}

View file

@ -97,7 +97,7 @@ export default class HomeTimeline extends React.PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
<Column ref={this.setRef} name='home'> <Column ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='home' icon='home'
active={hasUnread} active={hasUnread}

View file

@ -136,7 +136,7 @@ export default class ListTimeline extends React.PureComponent {
} }
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef} label={title}>
<ColumnHeader <ColumnHeader
icon='list-ul' icon='list-ul'
active={hasUnread} active={hasUnread}

View file

@ -203,6 +203,7 @@ export default class Notifications extends React.PureComponent {
ref={this.setColumnRef} ref={this.setColumnRef}
name='notifications' name='notifications'
extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null} extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
label={intl.formatMessage(messages.title)}
> >
<ColumnHeader <ColumnHeader
icon='bell' icon='bell'

View file

@ -76,7 +76,7 @@ export default class PublicTimeline extends React.PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
<Column ref={this.setRef} name='federated'> <Column ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='globe' icon='globe'
active={hasUnread} active={hasUnread}

View file

@ -51,7 +51,7 @@ export default class CommunityTimeline extends React.PureComponent {
const { intl } = this.props; const { intl } = this.props;
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='users' icon='users'
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}

View file

@ -51,7 +51,7 @@ export default class PublicTimeline extends React.PureComponent {
const { intl } = this.props; const { intl } = this.props;
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='globe' icon='globe'
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}

View file

@ -39,6 +39,7 @@ import { HotKeys } from 'react-hotkeys';
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning'; import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
import { textForScreenReader } from 'flavours/glitch/components/status';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -48,6 +49,7 @@ const messages = defineMessages({
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -387,7 +389,7 @@ export default class Status extends ImmutablePureComponent {
}; };
return ( return (
<Column> <Column label={intl.formatMessage(messages.detailedStatus)}>
<ColumnHeader <ColumnHeader
showBackButton showBackButton
extraButton={( extraButton={(
@ -400,7 +402,7 @@ export default class Status extends ImmutablePureComponent {
{ancestors} {ancestors}
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className='focusable' tabIndex='0'> <div className='focusable' tabIndex='0' aria-label={textForScreenReader(intl, status, false, !status.get('hidden'))}>
<DetailedStatus <DetailedStatus
status={status} status={status}
settings={settings} settings={settings}