Merge pull request #681 from ThibG/glitch-soc/fixes/accessibility
Port various accessibility improvements from upstream
This commit is contained in:
		
						commit
						c065717b67
					
				
					 16 changed files with 56 additions and 18 deletions
				
			
		|  | @ -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> | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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)} | ||||||
|  |  | ||||||
|  | @ -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)} /> | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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' | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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)} | ||||||
|  |  | ||||||
|  | @ -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)} | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue