Improve accessibility (part 2) (#4377)
* fix(column_header): Invalid ARIA role * fix(column): Remove hidden nodes from the DOM * refactor(column_link): Remove unused property hideOnMobile * fix(column_header): Use aria-pressed * fix(column_header): Make collapsed content not focusable, add focusable property * fix(column_loading): Make header non-focusable * fix(column_settings): Use role to group the toggles
This commit is contained in:
		
				
					committed by
					
						
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							aa8fa71df6
						
					
				
				
					commit
					6a6a62f13f
				
			@@ -21,6 +21,7 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
    icon: PropTypes.string.isRequired,
 | 
					    icon: PropTypes.string.isRequired,
 | 
				
			||||||
    active: PropTypes.bool,
 | 
					    active: PropTypes.bool,
 | 
				
			||||||
    multiColumn: PropTypes.bool,
 | 
					    multiColumn: PropTypes.bool,
 | 
				
			||||||
 | 
					    focusable: PropTypes.bool,
 | 
				
			||||||
    showBackButton: PropTypes.bool,
 | 
					    showBackButton: PropTypes.bool,
 | 
				
			||||||
    children: PropTypes.node,
 | 
					    children: PropTypes.node,
 | 
				
			||||||
    pinned: PropTypes.bool,
 | 
					    pinned: PropTypes.bool,
 | 
				
			||||||
@@ -29,6 +30,10 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
    onClick: PropTypes.func,
 | 
					    onClick: PropTypes.func,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static defaultProps = {
 | 
				
			||||||
 | 
					    focusable: true,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					  state = {
 | 
				
			||||||
    collapsed: true,
 | 
					    collapsed: true,
 | 
				
			||||||
    animating: false,
 | 
					    animating: false,
 | 
				
			||||||
@@ -61,7 +66,7 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton, intl: { formatMessage } } = this.props;
 | 
					    const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
 | 
				
			||||||
    const { collapsed, animating } = this.state;
 | 
					    const { collapsed, animating } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const wrapperClassName = classNames('column-header__wrapper', {
 | 
					    const wrapperClassName = classNames('column-header__wrapper', {
 | 
				
			||||||
@@ -123,12 +128,12 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (children || multiColumn) {
 | 
					    if (children || multiColumn) {
 | 
				
			||||||
      collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
 | 
					      collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className={wrapperClassName}>
 | 
					      <div className={wrapperClassName}>
 | 
				
			||||||
        <div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}>
 | 
					        <div role='heading' tabIndex={focusable && '0'} className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
 | 
				
			||||||
          <i className={`fa fa-fw fa-${icon} column-header__icon`} />
 | 
					          <i className={`fa fa-fw fa-${icon} column-header__icon`} />
 | 
				
			||||||
          {title}
 | 
					          {title}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -138,7 +143,7 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
 | 
					        <div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}>
 | 
				
			||||||
          <div className='column-header__collapsible-inner'>
 | 
					          <div className='column-header__collapsible-inner'>
 | 
				
			||||||
            {(!collapsed || animating) && collapsedContent}
 | 
					            {(!collapsed || animating) && collapsedContent}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,40 +36,48 @@ export default class ColumnSettings extends React.PureComponent {
 | 
				
			|||||||
          <ClearColumnButton onClick={onClear} />
 | 
					          <ClearColumnButton onClick={onClear} />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 | 
					        <div role='group' aria-labelledby='notifications-follow'>
 | 
				
			||||||
 | 
					          <span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className='column-settings__row'>
 | 
					          <div className='column-settings__row'>
 | 
				
			||||||
          <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
 | 
					            <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
          {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
					            {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 | 
					        <div role='group' aria-labelledby='notifications-favourite'>
 | 
				
			||||||
 | 
					          <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className='column-settings__row'>
 | 
					          <div className='column-settings__row'>
 | 
				
			||||||
          <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
 | 
					            <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
          {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
					            {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 | 
					        <div role='group' aria-labelledby='notifications-mention'>
 | 
				
			||||||
 | 
					          <span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className='column-settings__row'>
 | 
					          <div className='column-settings__row'>
 | 
				
			||||||
          <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
 | 
					            <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
          {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
					            {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 | 
					        <div role='group' aria-labelledby='notifications-reblog'>
 | 
				
			||||||
 | 
					          <span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className='column-settings__row'>
 | 
					          <div className='column-settings__row'>
 | 
				
			||||||
          <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
 | 
					            <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
          {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
					            {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
 | 
				
			||||||
          <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
 | 
					            <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import ColumnHeader from './column_header';
 | 
				
			|||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import { debounce } from 'lodash';
 | 
					import { debounce } from 'lodash';
 | 
				
			||||||
import scrollTop from '../../../scroll';
 | 
					import scrollTop from '../../../scroll';
 | 
				
			||||||
 | 
					import { isMobile } from '../../../is_mobile';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Column extends React.PureComponent {
 | 
					export default class Column extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -37,13 +38,12 @@ export default class Column extends React.PureComponent {
 | 
				
			|||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { heading, icon, children, active, hideHeadingOnMobile } = this.props;
 | 
					    const { heading, icon, children, active, hideHeadingOnMobile } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let columnHeaderId = null;
 | 
					    const showHeading = !hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth));
 | 
				
			||||||
    let header = '';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (heading) {
 | 
					    const columnHeaderId = showHeading && heading.replace(/ /g, '-');
 | 
				
			||||||
      columnHeaderId = heading.replace(/ /g, '-');
 | 
					    const header = showHeading && (
 | 
				
			||||||
      header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} hideOnMobile={hideHeadingOnMobile} columnHeaderId={columnHeaderId} />;
 | 
					      <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} />
 | 
				
			||||||
    }
 | 
					    );
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        ref={this.setRef}
 | 
					        ref={this.setRef}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
    type: PropTypes.string,
 | 
					    type: PropTypes.string,
 | 
				
			||||||
    active: PropTypes.bool,
 | 
					    active: PropTypes.bool,
 | 
				
			||||||
    onClick: PropTypes.func,
 | 
					    onClick: PropTypes.func,
 | 
				
			||||||
    hideOnMobile: PropTypes.bool,
 | 
					 | 
				
			||||||
    columnHeaderId: PropTypes.string,
 | 
					    columnHeaderId: PropTypes.string,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -17,7 +16,7 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { type, active, hideOnMobile, columnHeaderId } = this.props;
 | 
					    const { type, active, columnHeaderId } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let icon = '';
 | 
					    let icon = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,7 +25,7 @@ export default class ColumnHeader extends React.PureComponent {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div role='button heading' tabIndex='0' className={`column-header ${active ? 'active' : ''} ${hideOnMobile ? 'hidden-on-mobile' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}>
 | 
					      <div role='heading' tabIndex='0' className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}>
 | 
				
			||||||
        {icon}
 | 
					        {icon}
 | 
				
			||||||
        {type}
 | 
					        {type}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,17 +2,17 @@ import React from 'react';
 | 
				
			|||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import Link from 'react-router-dom/Link';
 | 
					import Link from 'react-router-dom/Link';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => {
 | 
					const ColumnLink = ({ icon, text, to, href, method }) => {
 | 
				
			||||||
  if (href) {
 | 
					  if (href) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}>
 | 
					      <a href={href} className='column-link' data-method={method}>
 | 
				
			||||||
        <i className={`fa fa-fw fa-${icon} column-link__icon`} />
 | 
					        <i className={`fa fa-fw fa-${icon} column-link__icon`} />
 | 
				
			||||||
        {text}
 | 
					        {text}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}>
 | 
					      <Link to={to} className='column-link'>
 | 
				
			||||||
        <i className={`fa fa-fw fa-${icon} column-link__icon`} />
 | 
					        <i className={`fa fa-fw fa-${icon} column-link__icon`} />
 | 
				
			||||||
        {text}
 | 
					        {text}
 | 
				
			||||||
      </Link>
 | 
					      </Link>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import ColumnHeader from '../../../components/column_header';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const ColumnLoading = ({ title = '', icon = ' ' }) => (
 | 
					const ColumnLoading = ({ title = '', icon = ' ' }) => (
 | 
				
			||||||
  <Column>
 | 
					  <Column>
 | 
				
			||||||
    <ColumnHeader icon={icon} title={title} multiColumn={false} />
 | 
					    <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} />
 | 
				
			||||||
    <div className='scrollable' />
 | 
					    <div className='scrollable' />
 | 
				
			||||||
  </Column>
 | 
					  </Column>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1743,12 +1743,6 @@
 | 
				
			|||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: lighten($ui-base-color, 11%);
 | 
					    background: lighten($ui-base-color, 11%);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.hidden-on-mobile {
 | 
					 | 
				
			||||||
    @media screen and (max-width: 1024px) {
 | 
					 | 
				
			||||||
      display: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.column-link__icon {
 | 
					.column-link__icon {
 | 
				
			||||||
@@ -2132,12 +2126,6 @@ button.icon-button.active i.fa-retweet {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.hidden-on-mobile {
 | 
					 | 
				
			||||||
    @media screen and (max-width: 1024px) {
 | 
					 | 
				
			||||||
      display: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:focus,
 | 
					  &:focus,
 | 
				
			||||||
  &:active {
 | 
					  &:active {
 | 
				
			||||||
    outline: 0;
 | 
					    outline: 0;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user