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:
Zac Anger 2017-04-15 05:27:27 -06:00 committed by Eugen
parent df4ff9a8e1
commit f4045ba3d9
32 changed files with 155 additions and 57 deletions

View file

@ -6,3 +6,6 @@ node_modules
storybook storybook
neo4j neo4j
vendor/bundle vendor/bundle
.DS_Store
*.swp
*~

View file

@ -8,7 +8,8 @@
"parser": "babel-eslint", "parser": "babel-eslint",
"plugins": [ "plugins": [
"react" "react",
"jsx-a11y"
], ],
"parserOptions": { "parserOptions": {
@ -43,9 +44,36 @@
"no-mixed-spaces-and-tabs": 1, "no-mixed-spaces-and-tabs": 1,
"no-nested-ternary": 1, "no-nested-ternary": 1,
"no-trailing-spaces": 1, "no-trailing-spaces": 1,
"react/wrap-multilines": 2,
"react/jsx-wrap-multilines": 2,
"react/self-closing-comp": 2, "react/self-closing-comp": 2,
"react/prop-types": 2, "react/prop-types": 2,
"react/no-multi-comp": 0 "react/no-multi-comp": 0,
"jsx-a11y/accessible-emoji": 1,
"jsx-a11y/anchor-has-content": 1,
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
"jsx-a11y/aria-props": 1,
"jsx-a11y/aria-proptypes": 1,
"jsx-a11y/aria-role": 1,
"jsx-a11y/aria-unsupported-elements": 1,
"jsx-a11y/heading-has-content": 1,
"jsx-a11y/href-no-hash": 1,
"jsx-a11y/html-has-lang": 1,
"jsx-a11y/iframe-has-title": 1,
"jsx-a11y/img-has-alt": 1,
"jsx-a11y/img-redundant-alt": 1,
"jsx-a11y/label-has-for": 1,
"jsx-a11y/mouse-events-have-key-events": 1,
"jsx-a11y/no-access-key": 1,
"jsx-a11y/no-distracting-elements": 1,
"jsx-a11y/no-onchange": 1,
"jsx-a11y/no-redundant-roles": 1,
"jsx-a11y/onclick-has-focus": 1,
"jsx-a11y/onclick-has-role": 1,
"jsx-a11y/role-has-required-aria-props": 1,
"jsx-a11y/role-supports-aria-props": 1,
"jsx-a11y/scope": 1,
"jsx-a11y/tabindex-no-positive": 1
} }
} }

8
.gitignore vendored
View file

@ -29,10 +29,16 @@ neo4j/
# Ignore Capistrano customizations # Ignore Capistrano customizations
config/deploy/* config/deploy/*
# Ignore IDE files # Ignore IDE files
.vscode/ .vscode/
# Ignore postgres + redis volume optionally created by docker-compose # Ignore postgres + redis volume optionally created by docker-compose
postgres postgres
redis redis
# Ignore Apple files
.DS_Store
# Ignore vim files
*~
*.swp

View file

@ -28,7 +28,7 @@ RUN BUILD_DEPS=" \
imagemagick \ imagemagick \
&& npm install -g npm@3 && npm install -g yarn \ && npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \ && bundle install --deployment --without test development \
&& yarn \ && yarn --ignore-optional \
&& yarn cache clean \ && yarn cache clean \
&& npm -g cache clean \ && npm -g cache clean \
&& apk del $BUILD_DEPS \ && apk del $BUILD_DEPS \

View file

@ -178,7 +178,12 @@ const AutosuggestTextarea = React.createClass({
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'> <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
{suggestions.map((suggestion, i) => ( {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} /> <AutosuggestAccountContainer id={suggestion} />
</div> </div>
))} ))}

View file

@ -9,6 +9,7 @@ const Button = React.createClass({
block: React.PropTypes.bool, block: React.PropTypes.bool,
secondary: React.PropTypes.bool, secondary: React.PropTypes.bool,
size: React.PropTypes.number, size: React.PropTypes.number,
style: React.PropTypes.object,
children: React.PropTypes.node children: React.PropTypes.node
}, },

View file

@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
handleClick () { 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(); else this.context.router.goBack();
}, },
render () { render () {
return ( 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} /> <i className='fa fa-fw fa-chevron-left' style={iconStyle} />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div> </div>

View file

@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
render () { render () {
return ( return (
<div style={{ position: 'relative' }}> <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} /> <i className='fa fa-fw fa-chevron-left' style={iconStyle} />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div> </div>

View file

@ -46,7 +46,9 @@ const ColumnCollapsable = React.createClass({
return ( return (
<div style={{ position: 'relative' }}> <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 }) }}> <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 }) => {({ opacity, height }) =>

View file

@ -1,7 +1,7 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
const LoadMore = ({ onClick }) => ( 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' /> <FormattedMessage id='status.load_more' defaultMessage='Load more' />
</a> </a>
); );

View file

@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
} }
children = ( 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={spoilerSpanStyle}>{warning}</span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div> </div>

View file

@ -6,7 +6,8 @@ const Permalink = React.createClass({
propTypes: { propTypes: {
href: React.PropTypes.string.isRequired, href: React.PropTypes.string.isRequired,
to: React.PropTypes.string.isRequired to: React.PropTypes.string.isRequired,
children: React.PropTypes.node.isRequired
}, },
handleClick (e) { handleClick (e) {

View file

@ -119,7 +119,7 @@ const StatusContent = React.createClass({
return ( return (
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} > <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> </p>
{mentionsPlaceholder} {mentionsPlaceholder}

View file

@ -194,7 +194,7 @@ const VideoPlayer = React.createClass({
if (!this.state.visible) { if (!this.state.visible) {
if (sensitive) { if (sensitive) {
return ( 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} {spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> <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> <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -202,7 +202,7 @@ const VideoPlayer = React.createClass({
); );
} else { } else {
return ( 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} {spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> <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> <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) { if (this.state.preview && !autoplay) {
return ( 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} {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 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> </div>
@ -225,7 +225,7 @@ const VideoPlayer = React.createClass({
{spoilerButton} {spoilerButton}
{muteButton} {muteButton}
{expandButton} {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> </div>
); );
} }

View file

@ -43,7 +43,16 @@ const Avatar = React.createClass({
return ( return (
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
{({ radius }) => {({ 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' }} /> <img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
</a> </a>
} }

View file

@ -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__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'> <div className='privacy-dropdown__dropdown'>
{options.map(item => {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__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
<div className='privacy-dropdown__option__content'> <div className='privacy-dropdown__option__content'>
<strong>{item.shortText}</strong> <strong>{item.shortText}</strong>

View file

@ -36,6 +36,10 @@ const Search = React.createClass({
} }
}, },
noop () {
},
handleFocus () { handleFocus () {
this.props.onShow(); this.props.onShow();
}, },
@ -56,9 +60,9 @@ const Search = React.createClass({
onFocus={this.handleFocus} 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-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>
</div> </div>
); );

View file

@ -15,7 +15,7 @@ const ClearColumnButton = React.createClass({
const { intl } = this.props; const { intl } = this.props;
return ( 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' /> <i className='fa fa-eraser' />
</div> </div>
); );

View file

@ -27,9 +27,11 @@ const ColumnSettings = React.createClass({
propTypes: { propTypes: {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
intl: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired, onChange: React.PropTypes.func.isRequired,
onSave: React.PropTypes.func.isRequired, onSave: React.PropTypes.func.isRequired,
intl: React.PropTypes.shape({
formatMessage: React.PropTypes.func.isRequired
}).isRequired
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],

View file

@ -71,7 +71,7 @@ const Notification = React.createClass({
); );
}, },
render () { render () { // eslint-disable-line consistent-return
const { notification } = this.props; const { notification } = this.props;
const account = notification.get('account'); const account = notification.get('account');
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');

View file

@ -14,8 +14,8 @@ const labelSpanStyle = {
marginLeft: '8px' marginLeft: '8px'
}; };
const SettingToggle = ({ settings, settingKey, label, onChange }) => ( const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
<label style={labelStyle}> <label htmlFor={htmlFor} style={labelStyle}>
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} /> <Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
<span className='setting-toggle' style={labelSpanStyle}>{label}</span> <span className='setting-toggle' style={labelSpanStyle}>{label}</span>
</label> </label>
@ -25,7 +25,8 @@ SettingToggle.propTypes = {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
settingKey: React.PropTypes.array.isRequired, settingKey: React.PropTypes.array.isRequired,
label: React.PropTypes.node.isRequired, label: React.PropTypes.node.isRequired,
onChange: React.PropTypes.func.isRequired onChange: React.PropTypes.func.isRequired,
htmlFor: React.PropTypes.string
}; };
export default SettingToggle; export default SettingToggle;

View file

@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
} }
return ( 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} {icon}
{type} {type}
</div> </div>

View file

@ -34,7 +34,8 @@ ColumnLink.propTypes = {
icon: React.PropTypes.string.isRequired, icon: React.PropTypes.string.isRequired,
text: React.PropTypes.string.isRequired, text: React.PropTypes.string.isRequired,
to: React.PropTypes.string, to: React.PropTypes.string,
href: React.PropTypes.string href: React.PropTypes.string,
method: React.PropTypes.string
}; };
export default ColumnLink; export default ColumnLink;

View file

@ -104,8 +104,8 @@ const MediaModal = React.createClass({
leftNav = rightNav = content = ''; leftNav = rightNav = content = '';
if (media.size > 1) { if (media.size > 1) {
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></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 style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></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') { if (attachment.get('type') === 'image') {

View file

@ -66,7 +66,7 @@ const ModalRoot = React.createClass({
return ( return (
<div key={key}> <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})` }}> <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
<SpecificComponent {...props} onClose={onClose} /> <SpecificComponent {...props} onClose={onClose} />
</div> </div>

View file

@ -14,7 +14,7 @@ Link.parseAttrs = (link, parts) => {
link = Link.parseParams(link, uriAttrs[1]) 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() attr = match[1].toLowerCase()
value = match[4] || match[3] || match[2] value = match[4] || match[3] || match[2]

View file

@ -73,7 +73,7 @@ const es = {
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.reblog": "Retoots:",
"emoji_button.label": "Insertar emoji", "emoji_button.label": "Insertar emoji",
"privacy.public.short": "Público", "privacy.public.short": "Público",
"privacy.public.long": "Mostrar en la historia federada", "privacy.public.long": "Mostrar en la historia federada",
"privacy.unlisted.short": "Sin federar", "privacy.unlisted.short": "Sin federar",
"privacy.unlisted.long": "No mostrar en la historia federada", "privacy.unlisted.long": "No mostrar en la historia federada",

View file

@ -16,7 +16,7 @@ const ru = {
"status.show_less": "Свернуть", "status.show_less": "Свернуть",
"status.open": "Развернуть статус", "status.open": "Развернуть статус",
"status.report": "Пожаловаться", "status.report": "Пожаловаться",
"status.load_more": "Показать еще", "status.load_more": "Показать еще",
"video_player.toggle_sound": "Вкл./выкл. звук", "video_player.toggle_sound": "Вкл./выкл. звук",
"video_player.toggle_visible": "Показать/скрыть", "video_player.toggle_visible": "Показать/скрыть",
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.", "account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",

View file

@ -22,7 +22,7 @@ export default function errorsMiddleware() {
dispatch(showAlert(title, message)); dispatch(showAlert(title, message));
} else { } else {
console.error(action.error); console.error(action.error); // eslint-disable-line no-console
dispatch(showAlert('Oops!', 'An unexpected error occurred.')); dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
} }
} }

View file

@ -9,17 +9,17 @@ const initialState = Immutable.List([]);
export default function alerts(state = initialState, action) { export default function alerts(state = initialState, action) {
switch(action.type) { switch(action.type) {
case ALERT_SHOW: case ALERT_SHOW:
return state.push(Immutable.Map({ return state.push(Immutable.Map({
key: state.size > 0 ? state.last().get('key') + 1 : 0, key: state.size > 0 ? state.last().get('key') + 1 : 0,
title: action.title, title: action.title,
message: action.message message: action.message
})); }));
case ALERT_DISMISS: case ALERT_DISMISS:
return state.filterNot(item => item.get('key') === action.alert.key); return state.filterNot(item => item.get('key') === action.alert.key);
case ALERT_CLEAR: case ALERT_CLEAR:
return state.clear(); return state.clear();
default: default:
return state; return state;
} }
}; };

View file

@ -1,9 +1,11 @@
{ {
"name": "mastodon", "name": "mastodon",
"scripts": { "scripts": {
"test": "mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.jsx", "start": "babel-node ./streaming/index.js --presets es2015,stage-2",
"storybook": "start-storybook -p 9001 -c storybook", "storybook": "start-storybook -p 9001 -c storybook",
"start": "babel-node ./streaming/index.js --presets es2015,stage-2" "test": "npm run test:lint && npm run test:mocha",
"test:lint": "eslint -c .eslintrc --ext=js --ext=jsx app/assets/javascripts/",
"test:mocha": "mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.jsx"
}, },
"dependencies": { "dependencies": {
"@kadira/storybook": "^2.35.3", "@kadira/storybook": "^2.35.3",
@ -74,8 +76,12 @@
"ws": "^2.1.0" "ws": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^7.2.1", "babel-eslint": "^7.2.2",
"eslint": "^3.19.0", "eslint": "^3.19.0",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^6.10.3" "eslint-plugin-react": "^6.10.3"
},
"optionalDependencies": {
"fsevents": "*"
} }
} }

View file

@ -250,6 +250,12 @@ argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" sprintf-js "~1.0.2"
aria-query@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.3.0.tgz#cb8a9984e2862711c83c80ade5b8f5ca0de2b467"
dependencies:
ast-types-flow "0.0.7"
arr-diff@^2.0.0: arr-diff@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
@ -357,6 +363,10 @@ assertion-error@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
ast-types-flow@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
ast-types@0.9.5: ast-types@0.9.5:
version "0.9.5" version "0.9.5"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.5.tgz#1a660a09945dbceb1f9c9cbb715002617424e04a" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.5.tgz#1a660a09945dbceb1f9c9cbb715002617424e04a"
@ -511,9 +521,9 @@ babel-core@^6.11.4:
slash "^1.0.0" slash "^1.0.0"
source-map "^0.5.0" source-map "^0.5.0"
babel-eslint@^7.2.1: babel-eslint@^7.2.2:
version "7.2.1" version "7.2.2"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.2.tgz#0da2cbe6554fd0fb069f19674f2db2f9c59270ff"
dependencies: dependencies:
babel-code-frame "^6.22.0" babel-code-frame "^6.22.0"
babel-traverse "^6.23.1" babel-traverse "^6.23.1"
@ -2128,6 +2138,10 @@ d@1:
dependencies: dependencies:
es5-ext "^0.10.9" es5-ext "^0.10.9"
damerau-levenshtein@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
dashdash@^1.12.0: dashdash@^1.12.0:
version "1.14.0" version "1.14.0"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.0.tgz#29e486c5418bf0f356034a993d51686a33e84141" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.0.tgz#29e486c5418bf0f356034a993d51686a33e84141"
@ -2343,6 +2357,10 @@ elliptic@^6.0.0:
hash.js "^1.0.0" hash.js "^1.0.0"
inherits "^2.0.1" inherits "^2.0.1"
emoji-regex@^6.1.0:
version "6.4.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.2.tgz#a30b6fee353d406d96cfb9fa765bdc82897eff6e"
emojione-picker@^2.0.1: emojione-picker@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/emojione-picker/-/emojione-picker-2.0.1.tgz#62e58db67d37a400a883c82d39abb1cc1c8ed65a" resolved "https://registry.yarnpkg.com/emojione-picker/-/emojione-picker-2.0.1.tgz#62e58db67d37a400a883c82d39abb1cc1c8ed65a"
@ -2542,6 +2560,17 @@ escope@^3.6.0:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-plugin-jsx-a11y@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-4.0.0.tgz#779bb0fe7b08da564a422624911de10061e048ee"
dependencies:
aria-query "^0.3.0"
ast-types-flow "0.0.7"
damerau-levenshtein "^1.0.0"
emoji-regex "^6.1.0"
jsx-ast-utils "^1.0.0"
object-assign "^4.0.1"
eslint-plugin-react@^6.10.3: eslint-plugin-react@^6.10.3:
version "6.10.3" version "6.10.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78"
@ -2886,7 +2915,7 @@ fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
fsevents@^1.0.0: fsevents@*, fsevents@^1.0.0:
version "1.0.14" version "1.0.14"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.14.tgz#558e8cc38643d8ef40fe45158486d0d25758eee4" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.14.tgz#558e8cc38643d8ef40fe45158486d0d25758eee4"
dependencies: dependencies:
@ -3672,7 +3701,7 @@ jsprim@^1.2.2:
json-schema "0.2.3" json-schema "0.2.3"
verror "1.3.6" verror "1.3.6"
jsx-ast-utils@^1.3.4: jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.4:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.0.tgz#5afe38868f56bc8cc7aeaef0100ba8c75bd12591" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.0.tgz#5afe38868f56bc8cc7aeaef0100ba8c75bd12591"
dependencies: dependencies: