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:
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;
|
||||||
|
|
Loading…
Reference in New Issue