Accessibility stuff

This commit is contained in:
Lucas Verney 2016-07-26 13:21:37 +02:00
parent 136fb59ed5
commit ef4dfd1176
17 changed files with 994 additions and 847 deletions

View File

@ -43,6 +43,9 @@ module.exports = {
"error", "error",
], ],
"react/jsx-uses-react": "error", "react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error" "react/jsx-uses-vars": "error",
// Disable no-console rule in production
"no-console": process.env.NODE_ENV !== "production" ? "off" : ["error"]
} }
}; };

14
TODO
View File

@ -11,6 +11,14 @@
* /artist/:id and /album/:id arts in responsive view * /artist/:id and /album/:id arts in responsive view
* Scroll horizontal sidebar * Scroll horizontal sidebar
* Move CSS in modules * Move CSS in modules
=> https://github.com/gajus/react-css-modules
# API middleware
* https://github.com/reactjs/redux/issues/1824#issuecomment-228609501
* https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f#.eezt3dano
* https://github.com/reactjs/redux/issues/644
* https://github.com/peterpme/redux-crud-api-middleware/blob/master/README.md
* https://github.com/madou/armory-front/tree/master/src/app/reducers
## Global UI ## Global UI
@ -20,9 +28,5 @@
## Miscellaneous ## Miscellaneous
* See TODOs in the code * See TODOs in the code
* https://facebook.github.io/immutable-js/ ?
* Web workers?
* Accessibility and semantics
* Uncaught TypeError: this.props.tracks.forEach is not a function * Uncaught TypeError: this.props.tracks.forEach is not a function
=> Be more robust, after, getHostNode is null => Be more robust, else, getHostNode is null after

View File

@ -50,7 +50,9 @@ export class LoginForm extends Component {
this.props.error ? this.props.error ?
<div className="row"> <div className="row">
<div className="alert alert-danger"> <div className="alert alert-danger">
<span className="glyphicon glyphicon-exclamation-sign"></span> { this.props.error } <p id="loginFormError">
<span className="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> { this.props.error }
</p>
</div> </div>
</div> </div>
: null : null
@ -58,40 +60,40 @@ export class LoginForm extends Component {
{ {
this.props.info ? this.props.info ?
<div className="row"> <div className="row">
<div className="alert alert-info"> <div className="alert alert-info" id="loginFormInfo">
{ this.props.info } <p>{ this.props.info }</p>
</div> </div>
</div> </div>
: null : null
} }
<div className="row"> <div className="row">
<form className="col-sm-9 col-sm-offset-1 col-md-6 col-md-offset-3 text-left form-horizontal login" onSubmit={this.handleSubmit} ref="loginForm"> <form className="col-sm-9 col-sm-offset-1 col-md-6 col-md-offset-3 text-left form-horizontal login" onSubmit={this.handleSubmit} ref="loginForm" aria-describedby="loginFormInfo loginFormError">
<div className="row"> <div className="row">
<div className="form-group" ref="usernameFormGroup"> <div className="form-group" ref="usernameFormGroup">
<div className="col-xs-12"> <div className="col-xs-12">
<input type="text" className="form-control" ref="username" placeholder="Username" autoFocus defaultValue={this.props.username} /> <input type="text" className="form-control" ref="username" aria-label="Username" placeholder="Username" autoFocus defaultValue={this.props.username} />
</div> </div>
</div> </div>
<div className="form-group" ref="passwordFormGroup"> <div className="form-group" ref="passwordFormGroup">
<div className="col-xs-12"> <div className="col-xs-12">
<input type="password" className="form-control" ref="password" placeholder="Password" /> <input type="password" className="form-control" ref="password" aria-label="Password" placeholder="Password" />
</div> </div>
</div> </div>
<div className="form-group" ref="endpointFormGroup"> <div className="form-group" ref="endpointFormGroup">
<div className="col-xs-12"> <div className="col-xs-12">
<input type="text" className="form-control" ref="endpoint" placeholder="http://ampache.example.com" defaultValue={this.props.endpoint} /> <input type="text" className="form-control" ref="endpoint" aria-label="URL of your Ampache instance (e.g. http://ampache.example.com)" placeholder="http://ampache.example.com" defaultValue={this.props.endpoint} />
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
<div className="col-xs-12"> <div className="col-xs-12">
<div className="row"> <div className="row">
<div className="col-sm-6 col-xs-12 checkbox"> <div className="col-sm-6 col-xs-12 checkbox">
<label> <label id="rememberMeLabel">
<input type="checkbox" ref="rememberMe" defaultChecked={this.props.rememberMe} /> Remember me <input type="checkbox" ref="rememberMe" defaultChecked={this.props.rememberMe} aria-labelledby="rememberMeLabel" /> Remember me
</label> </label>
</div> </div>
<div className="col-sm-6 col-sm-12 submit text-right"> <div className="col-sm-6 col-sm-12 submit text-right">
<input type="submit" className="btn btn-default" defaultValue="Sign in" disabled={this.props.isAuthenticating} /> <input type="submit" className="btn btn-default" aria-label="Sign in" defaultValue="Sign in" disabled={this.props.isAuthenticating} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,9 +15,9 @@ export default class FilterBar extends Component {
render () { render () {
return ( return (
<div className="filter"> <div className="filter">
<p className="col-xs-12 col-sm-6 col-md-4 col-md-offset-1 filter-legend">What are we listening to today?</p> <p className="col-xs-12 col-sm-6 col-md-4 col-md-offset-1 filter-legend" id="filterInputDescription">What are we listening to today?</p>
<div className="col-xs-12 col-sm-6 col-md-4 input-group"> <div className="col-xs-12 col-sm-6 col-md-4 input-group">
<form className="form-inline" onSubmit={this.handleChange}> <form className="form-inline" onSubmit={this.handleChange} aria-describedby="filterInputDescription">
<div className="form-group"> <div className="form-group">
<input type="text" className="form-control filter-input" placeholder="Filter…" aria-label="Filter…" value={this.props.filterText} onChange={this.handleChange} ref="filterTextInput" /> <input type="text" className="form-control filter-input" placeholder="Filter…" aria-label="Filter…" value={this.props.filterText} onChange={this.handleChange} ref="filterTextInput" />
</div> </div>

View File

@ -20,10 +20,11 @@ export class GridItem extends Component {
} }
const to = "/" + this.props.itemsType.rstrip("s") + "/" + this.props.item.id; const to = "/" + this.props.itemsType.rstrip("s") + "/" + this.props.item.id;
const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id; const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id;
const title = "Go to " + this.props.itemsType.rstrip("s") + " page";
return ( return (
<div className="grid-item col-xs-6 col-sm-3 placeholders" id={id}> <div className="grid-item col-xs-6 col-sm-3 placeholders" id={id}>
<div className="grid-item-content placeholder text-center"> <div className="grid-item-content placeholder text-center">
<Link to={to}><img src={this.props.item.art} width="200" height="200" className="img-responsive art" alt={this.props.item.name}/></Link> <Link title={title} to={to}><img src={this.props.item.art} width="200" height="200" className="img-responsive img-circle art" alt={this.props.item.name}/></Link>
<h4 className="name">{this.props.item.name}</h4> <h4 className="name">{this.props.item.name}</h4>
<span className="sub-items text-muted"><span className="n-sub-items">{nSubItems}</span> <span className="sub-items-type">{subItemsLabel}</span></span> <span className="sub-items text-muted"><span className="n-sub-items">{nSubItems}</span> <span className="sub-items-type">{subItemsLabel}</span></span>
</div> </div>

View File

@ -38,9 +38,27 @@ export class Pagination extends Component {
goToPage() { goToPage() {
const pageNumber = parseInt(this.refs.pageInput.value); const pageNumber = parseInt(this.refs.pageInput.value);
$("#paginationModal").modal("hide"); $(this.refs.paginationModal).modal("hide");
if (pageNumber) {
this.props.router.push(this.buildLinkTo(pageNumber)); this.props.router.push(this.buildLinkTo(pageNumber));
} }
}
dotsOnClick() {
$(this.refs.paginationModal).modal();
}
dotsOnKeyDown(ev) {
ev.preventDefault;
const code = ev.keyCode || ev.which;
if (code == 13 || code == 32) { // Enter or Space key
this.dotsOnClick(); // Fire same event as onClick
}
}
cancelModalBox() {
$(this.refs.paginationModal).modal("hide");
}
render () { render () {
const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages); const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages);
@ -50,7 +68,7 @@ export class Pagination extends Component {
// Push first page // Push first page
pagesButton.push( pagesButton.push(
<li className="page-item" key={key}> <li className="page-item" key={key}>
<Link className="page-link" to={this.buildLinkTo(1)}>1</Link> <Link className="page-link" title="Go to page 1" to={this.buildLinkTo(1)}><span className="sr-only">Go to page </span>1</Link>
</li> </li>
); );
key++; key++;
@ -58,7 +76,7 @@ export class Pagination extends Component {
// Eventually push "" // Eventually push ""
pagesButton.push( pagesButton.push(
<li className="page-item" key={key}> <li className="page-item" key={key}>
<span onClick={() => $("#paginationModal").modal() }></span> <span tabIndex="0" role="button" onKeyDown={this.dotsOnKeyDown.bind(this)} onClick={this.dotsOnClick.bind(this)}>&hellip;</span>
</li> </li>
); );
key++; key++;
@ -67,12 +85,15 @@ export class Pagination extends Component {
var i = 0; var i = 0;
for (i = lowerLimit; i < upperLimit; i++) { for (i = lowerLimit; i < upperLimit; i++) {
var className = "page-item"; var className = "page-item";
var currentSpan = null;
if (this.props.currentPage == i) { if (this.props.currentPage == i) {
className += " active"; className += " active";
currentSpan = <span className="sr-only">(current)</span>;
} }
const title = "Go to page " + i;
pagesButton.push( pagesButton.push(
<li className={className} key={key}> <li className={className} key={key}>
<Link className="page-link" to={this.buildLinkTo(i)}>{i}</Link> <Link className="page-link" title={title} to={this.buildLinkTo(i)}><span className="sr-only">Go to page </span>{i} {currentSpan}</Link>
</li> </li>
); );
key++; key++;
@ -82,40 +103,41 @@ export class Pagination extends Component {
// Eventually push "" // Eventually push ""
pagesButton.push( pagesButton.push(
<li className="page-item" key={key}> <li className="page-item" key={key}>
<span onClick={() => $("#paginationModal").modal() }></span> <span tabIndex="0" role="button" onKeyDown={this.dotsOnKeyDown.bind(this)} onClick={this.dotsOnClick.bind(this)}>&hellip;</span>
</li> </li>
); );
key++; key++;
} }
const title = "Go to page " + this.props.nPages;
// Push last page // Push last page
pagesButton.push( pagesButton.push(
<li className="page-item" key={key}> <li className="page-item" key={key}>
<Link className="page-link" to={this.buildLinkTo(this.props.nPages)}>{this.props.nPages}</Link> <Link className="page-link" title={title} to={this.buildLinkTo(this.props.nPages)}><span className="sr-only">Go to page </span>{this.props.nPages}</Link>
</li> </li>
); );
} }
if (pagesButton.length > 1) { if (pagesButton.length > 1) {
return ( return (
<div> <div>
<nav className="pagination-nav"> <nav className="pagination-nav" aria-label="Page navigation">
<ul className="pagination"> <ul className="pagination">
{ pagesButton } { pagesButton }
</ul> </ul>
</nav> </nav>
<div className="modal fade" id="paginationModal" tabIndex="-1" role="dialog" aria-hidden="false"> <div className="modal fade" ref="paginationModal" tabIndex="-1" role="dialog" aria-labelledby="paginationModalLabel">
<div className="modal-dialog"> <div className="modal-dialog" role="document">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-hidden="true">×</button> <button type="button" className="close" data-dismiss="modal" aria-label="Close">&times;</button>
<h4 className="modal-title">Page to go to?</h4> <h4 className="modal-title" id="paginationModalLabel">Page to go to?</h4>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<form> <form>
<input className="form-control" autoComplete="off" type="number" ref="pageInput" /> <input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label="Page number to go to" />
</form> </form>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-default" onClick={ () => $("#paginationModal").modal("hide") }>Cancel</button> <button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>Cancel</button>
<button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>OK</button> <button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>OK</button>
</div> </div>
</div> </div>

View File

@ -7,42 +7,42 @@ export default class SidebarLayout extends Component {
<div> <div>
<div className="col-sm-3 col-md-2 sidebar hidden-xs"> <div className="col-sm-3 col-md-2 sidebar hidden-xs">
<h1 className="text-center"><IndexLink to="/"><img alt="A" src="./app/assets/img/ampache-blue.png"/>mpache</IndexLink></h1> <h1 className="text-center"><IndexLink to="/"><img alt="A" src="./app/assets/img/ampache-blue.png"/>mpache</IndexLink></h1>
<nav> <nav aria-label="Main navigation menu">
<div className="navbar text-center icon-navbar"> <div className="navbar text-center icon-navbar">
<div className="container-fluid"> <div className="container-fluid">
<ul className="nav navbar-nav icon-navbar-nav"> <ul className="nav navbar-nav icon-navbar-nav">
<li aria-hidden="true"> <li>
<Link to="/" className="glyphicon glyphicon-home"></Link> <Link to="/" title="Home"><span className="glyphicon glyphicon-home" aria-hidden="true"></span> <span className="sr-only">Home</span></Link>
</li> </li>
<li aria-hidden="true"> <li>
<Link to="/settings" className="glyphicon glyphicon-wrench"></Link> <Link to="/settings" title="Settings"><span className="glyphicon glyphicon-wrench" aria-hidden="true"></span> <span className="sr-only">Settings</span></Link>
</li> </li>
<li aria-hidden="true"> <li>
<Link to="/logout" className="glyphicon glyphicon-off"></Link> <Link to="/logout" title="Logout"><span className="glyphicon glyphicon-off" aria-hidden="true"></span> <span className="sr-only">Logout</span></Link>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<ul className="nav nav-sidebar"> <ul className="nav nav-sidebar">
<li> <li>
<Link to="/discover"> <Link to="/discover" title="Discover">
<span className="glyphicon glyphicon-globe" aria-hidden="true"></span> <span className="glyphicon glyphicon-globe" aria-hidden="true"></span>
<span className="hidden-sm"> Discover</span> <span className="hidden-sm"> Discover</span>
</Link> </Link>
</li> </li>
<li> <li>
<Link to="/browse"> <Link to="/browse" title="Browse">
<span className="glyphicon glyphicon-headphones" aria-hidden="true"></span> <span className="glyphicon glyphicon-headphones" aria-hidden="true"></span>
<span className="hidden-sm"> Browse</span> <span className="hidden-sm"> Browse</span>
</Link> </Link>
<ul className="nav nav-sidebar text-center"> <ul className="nav nav-sidebar text-center">
<li><Link to="/artists"><span className="glyphicon glyphicon-user"></span> Artists</Link></li> <li><Link to="/artists" title="Browse artists"><span className="glyphicon glyphicon-user" aria-hidden="true"></span> Artists</Link></li>
<li><Link to="/albums"><span className="glyphicon glyphicon-cd" aria-hidden="true"></span> Albums</Link></li> <li><Link to="/albums" title="Browse albums"><span className="glyphicon glyphicon-cd" aria-hidden="true"></span> Albums</Link></li>
<li><Link to="/songs"><span className="glyphicon glyphicon-music"></span> Songs</Link></li> <li><Link to="/songs" title="Browse songs"><span className="glyphicon glyphicon-music" aria-hidden="true"></span> Songs</Link></li>
</ul> </ul>
</li> </li>
<li> <li>
<Link to="/search"> <Link to="/search" title="Search">
<span className="glyphicon glyphicon-search" aria-hidden="true"></span> <span className="glyphicon glyphicon-search" aria-hidden="true"></span>
<span className="hidden-sm"> Search</span> <span className="hidden-sm"> Search</span>
</Link> </Link>

1656
app/dist/index.js vendored

File diff suppressed because one or more lines are too long

1
app/dist/style.css vendored
View File

@ -6914,7 +6914,6 @@ div.filter {
.art { .art {
display: inline-block; display: inline-block;
border-radius: 50%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
width: 75%; width: 75%;
height: auto; height: auto;

View File

@ -1,4 +1,5 @@
// TODO: Refactor using normalizr // TODO: Refactor using normalizr
// TODO: https://facebook.github.io/immutable-js/ ?
import "babel-polyfill"; import "babel-polyfill";
import fetch from "isomorphic-fetch"; import fetch from "isomorphic-fetch";
import humps from "humps"; import humps from "humps";

View File

@ -1,6 +1,6 @@
import { createReducer } from "../utils"; import { createReducer } from "../utils";
export const DEFAULT_LIMIT = 30; /** Default max number of elements to retrieve. */ export const DEFAULT_LIMIT = 1; /** Default max number of elements to retrieve. */
const initialState = { const initialState = {
isFetching: false, isFetching: false,

View File

@ -156,7 +156,6 @@ div.filter {
.art { .art {
display: inline-block; display: inline-block;
border-radius: 50%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
width: 75%; width: 75%;
height: auto; height: auto;

21
index.all.js Normal file
View File

@ -0,0 +1,21 @@
// Export
import "bootstrap";
import "bootstrap/dist/css/bootstrap.css";
import "./app/styles/ampache.css";
// Handle app init
import React from "react";
import { render } from "react-dom";
import { hashHistory } from "react-router";
import { syncHistoryWithStore } from "react-router-redux";
import Root from "./app/containers/Root";
import configureStore from "./app/store/configureStore";
const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store);
render(
<Root store={store} history={history} />,
document.getElementById("root")
);

7
index.development.js Normal file
View File

@ -0,0 +1,7 @@
import React from "react";
import ReactDOM from "react-dom";
var a11y = require("react-a11y");
a11y(React, { ReactDOM: ReactDOM, includeSrcNode: true });
require("./index.all.js");

View File

@ -1,21 +1,5 @@
// Export if (process.env.NODE_ENV === "production") {
import "bootstrap"; module.exports = require("./index.production.js");
import "bootstrap/dist/css/bootstrap.css"; } else {
import "./app/styles/ampache.css"; module.exports = require("./index.development.js");
}
// Handle app init
import React from "react";
import { render } from "react-dom";
import { hashHistory } from "react-router";
import { syncHistoryWithStore } from "react-router-redux";
import Root from "./app/containers/Root";
import configureStore from "./app/store/configureStore";
const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store);
render(
<Root store={store} history={history} />,
document.getElementById("root")
);

1
index.production.js Normal file
View File

@ -0,0 +1 @@
require("./index.all.js");

View File

@ -45,6 +45,7 @@
"postcss-loader": "^0.9.1", "postcss-loader": "^0.9.1",
"postcss-reporter": "^1.4.1", "postcss-reporter": "^1.4.1",
"precss": "^1.4.0", "precss": "^1.4.0",
"react-a11y": "^0.3.3",
"redux-logger": "^2.6.1", "redux-logger": "^2.6.1",
"style-loader": "^0.13.1", "style-loader": "^0.13.1",
"stylelint": "^7.0.3", "stylelint": "^7.0.3",