Add eslint-plugin-jsx-a11y (#1651)
* Add eslint-plugin-jsx-a11y. * Fix npm script. * Adjust npm scripts so test also runs lint. * Fix existing lint errors. * Don't break on a11y issues. * Add role and tabIndex. * Add vim and Mac files to .gitignore and .dockerignore. * Handle htmlFor (partially), a that's actually a button. * Fix missing tabIndex. * Add cursor:pointer to load-more * Revert change to load_more. * Fixes based on review. * Update yarn.lock. * Don't try to install fsevents on Linux (hides warning noise).
This commit is contained in:
@ -178,7 +178,12 @@ const AutosuggestTextarea = React.createClass({
|
||||
|
||||
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
key={suggestion}
|
||||
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
|
||||
onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<AutosuggestAccountContainer id={suggestion} />
|
||||
</div>
|
||||
))}
|
||||
|
@ -9,6 +9,7 @@ const Button = React.createClass({
|
||||
block: React.PropTypes.bool,
|
||||
secondary: React.PropTypes.bool,
|
||||
size: React.PropTypes.number,
|
||||
style: React.PropTypes.object,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
|
@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleClick () {
|
||||
if (window.history && window.history.length == 1) this.context.router.push("/");
|
||||
if (window.history && window.history.length === 1) this.context.router.push("/");
|
||||
else this.context.router.goBack();
|
||||
},
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div onClick={this.handleClick} className='column-back-button'>
|
||||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
|
||||
render () {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
@ -46,7 +46,9 @@ const ColumnCollapsable = React.createClass({
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
||||
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||
<i className={`fa fa-${icon}`} />
|
||||
</div>
|
||||
|
||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||
{({ opacity, height }) =>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const LoadMore = ({ onClick }) => (
|
||||
<a href='#' className='load-more' onClick={onClick}>
|
||||
<a href="#" className='load-more' role='button' onClick={onClick}>
|
||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||
</a>
|
||||
);
|
||||
|
@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
|
||||
}
|
||||
|
||||
children = (
|
||||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<span style={spoilerSpanStyle}>{warning}</span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</div>
|
||||
|
@ -6,7 +6,8 @@ const Permalink = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
href: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string.isRequired
|
||||
to: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.node.isRequired
|
||||
},
|
||||
|
||||
handleClick (e) {
|
||||
|
@ -119,7 +119,7 @@ const StatusContent = React.createClass({
|
||||
return (
|
||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
</p>
|
||||
|
||||
{mentionsPlaceholder}
|
||||
|
@ -194,7 +194,7 @@ const VideoPlayer = React.createClass({
|
||||
if (!this.state.visible) {
|
||||
if (sensitive) {
|
||||
return (
|
||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div role='button' tabIndex='0' style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
@ -202,7 +202,7 @@ const VideoPlayer = React.createClass({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div role='button' tabIndex='0' style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
@ -213,7 +213,7 @@ const VideoPlayer = React.createClass({
|
||||
|
||||
if (this.state.preview && !autoplay) {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
{spoilerButton}
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
|
||||
</div>
|
||||
@ -225,7 +225,7 @@ const VideoPlayer = React.createClass({
|
||||
{spoilerButton}
|
||||
{muteButton}
|
||||
{expandButton}
|
||||
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -43,7 +43,16 @@ const Avatar = React.createClass({
|
||||
return (
|
||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ radius }) =>
|
||||
<a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<a
|
||||
href={account.get('url')}
|
||||
className='account__header__avatar'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
onFocus={this.handleMouseOver}
|
||||
onBlur={this.handleMouseOut}>
|
||||
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
||||
</a>
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ const PrivacyDropdown = React.createClass({
|
||||
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||
<div className='privacy-dropdown__dropdown'>
|
||||
{options.map(item =>
|
||||
<div key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.shortText}</strong>
|
||||
|
@ -36,6 +36,10 @@ const Search = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
noop () {
|
||||
|
||||
},
|
||||
|
||||
handleFocus () {
|
||||
this.props.onShow();
|
||||
},
|
||||
@ -56,9 +60,9 @@ const Search = React.createClass({
|
||||
onFocus={this.handleFocus}
|
||||
/>
|
||||
|
||||
<div className='search__icon'>
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
|
||||
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
||||
<i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
|
||||
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ const ClearColumnButton = React.createClass({
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<i className='fa fa-eraser' />
|
||||
</div>
|
||||
);
|
||||
|
@ -27,9 +27,11 @@ const ColumnSettings = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onSave: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.shape({
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
@ -71,7 +71,7 @@ const Notification = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
render () {
|
||||
render () { // eslint-disable-line consistent-return
|
||||
const { notification } = this.props;
|
||||
const account = notification.get('account');
|
||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||
|
@ -14,8 +14,8 @@ const labelSpanStyle = {
|
||||
marginLeft: '8px'
|
||||
};
|
||||
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
|
||||
<label style={labelStyle}>
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
|
||||
<label htmlFor={htmlFor} style={labelStyle}>
|
||||
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
||||
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
||||
</label>
|
||||
@ -25,7 +25,8 @@ SettingToggle.propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingKey: React.PropTypes.array.isRequired,
|
||||
label: React.PropTypes.node.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
htmlFor: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default SettingToggle;
|
||||
|
@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
<div role='button' tabIndex='0' aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
{icon}
|
||||
{type}
|
||||
</div>
|
||||
|
@ -34,7 +34,8 @@ ColumnLink.propTypes = {
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
text: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string,
|
||||
href: React.PropTypes.string
|
||||
href: React.PropTypes.string,
|
||||
method: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default ColumnLink;
|
||||
|
@ -104,8 +104,8 @@ const MediaModal = React.createClass({
|
||||
leftNav = rightNav = content = '';
|
||||
|
||||
if (media.size > 1) {
|
||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
leftNav = <div role='button' tabIndex='0' style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div role='button' tabIndex='0' style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
}
|
||||
|
||||
if (attachment.get('type') === 'image') {
|
||||
|
@ -66,7 +66,7 @@ const ModalRoot = React.createClass({
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||
<SpecificComponent {...props} onClose={onClose} />
|
||||
</div>
|
||||
|
@ -14,7 +14,7 @@ Link.parseAttrs = (link, parts) => {
|
||||
link = Link.parseParams(link, uriAttrs[1])
|
||||
}
|
||||
|
||||
while(match = Link.attrPattern.exec(attrs)) {
|
||||
while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign
|
||||
attr = match[1].toLowerCase()
|
||||
value = match[4] || match[3] || match[2]
|
||||
|
||||
|
@ -73,7 +73,7 @@ const es = {
|
||||
"notifications.column_settings.mention": "Menciones:",
|
||||
"notifications.column_settings.reblog": "Retoots:",
|
||||
"emoji_button.label": "Insertar emoji",
|
||||
"privacy.public.short": "Público",
|
||||
"privacy.public.short": "Público",
|
||||
"privacy.public.long": "Mostrar en la historia federada",
|
||||
"privacy.unlisted.short": "Sin federar",
|
||||
"privacy.unlisted.long": "No mostrar en la historia federada",
|
||||
|
@ -16,7 +16,7 @@ const ru = {
|
||||
"status.show_less": "Свернуть",
|
||||
"status.open": "Развернуть статус",
|
||||
"status.report": "Пожаловаться",
|
||||
"status.load_more": "Показать еще",
|
||||
"status.load_more": "Показать еще",
|
||||
"video_player.toggle_sound": "Вкл./выкл. звук",
|
||||
"video_player.toggle_visible": "Показать/скрыть",
|
||||
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
|
||||
|
@ -22,7 +22,7 @@ export default function errorsMiddleware() {
|
||||
|
||||
dispatch(showAlert(title, message));
|
||||
} else {
|
||||
console.error(action.error);
|
||||
console.error(action.error); // eslint-disable-line no-console
|
||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
||||
}
|
||||
}
|
||||
|
@ -9,17 +9,17 @@ const initialState = Immutable.List([]);
|
||||
|
||||
export default function alerts(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user