Browse Source

Accessibility stuff

Phyks (Lucas Verney) 5 years ago
parent
commit
ef4dfd1176

+ 4
- 1
.eslintrc.js View File

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

+ 9
- 5
TODO View File

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

+ 12
- 10
app/components/Login.jsx View File

@@ -50,7 +50,9 @@ export class LoginForm extends Component {
50 50
                     this.props.error ?
51 51
                         <div className="row">
52 52
                             <div className="alert alert-danger">
53
-                                <span className="glyphicon glyphicon-exclamation-sign"></span> { this.props.error }
53
+                                <p id="loginFormError">
54
+                                    <span className="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> { this.props.error }
55
+                                </p>
54 56
                             </div>
55 57
                         </div>
56 58
                         : null
@@ -58,40 +60,40 @@ export class LoginForm extends Component {
58 60
                 {
59 61
                     this.props.info ?
60 62
                         <div className="row">
61
-                            <div className="alert alert-info">
62
-                                { this.props.info }
63
+                            <div className="alert alert-info" id="loginFormInfo">
64
+                                <p>{ this.props.info }</p>
63 65
                             </div>
64 66
                         </div>
65 67
                         : null
66 68
                 }
67 69
                 <div className="row">
68
-                    <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">
70
+                    <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">
69 71
                         <div className="row">
70 72
                             <div className="form-group" ref="usernameFormGroup">
71 73
                                 <div className="col-xs-12">
72
-                                    <input type="text" className="form-control" ref="username" placeholder="Username" autoFocus defaultValue={this.props.username} />
74
+                                    <input type="text" className="form-control" ref="username" aria-label="Username" placeholder="Username" autoFocus defaultValue={this.props.username} />
73 75
                                 </div>
74 76
                             </div>
75 77
                             <div className="form-group" ref="passwordFormGroup">
76 78
                                 <div className="col-xs-12">
77
-                                    <input type="password" className="form-control" ref="password" placeholder="Password" />
79
+                                    <input type="password" className="form-control" ref="password" aria-label="Password" placeholder="Password" />
78 80
                                 </div>
79 81
                             </div>
80 82
                             <div className="form-group" ref="endpointFormGroup">
81 83
                                 <div className="col-xs-12">
82
-                                    <input type="text" className="form-control" ref="endpoint" placeholder="http://ampache.example.com" defaultValue={this.props.endpoint} />
84
+                                    <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} />
83 85
                                 </div>
84 86
                             </div>
85 87
                             <div className="form-group">
86 88
                                 <div className="col-xs-12">
87 89
                                     <div className="row">
88 90
                                         <div className="col-sm-6 col-xs-12 checkbox">
89
-                                            <label>
90
-                                                <input type="checkbox" ref="rememberMe" defaultChecked={this.props.rememberMe} /> Remember me
91
+                                            <label id="rememberMeLabel">
92
+                                                <input type="checkbox" ref="rememberMe" defaultChecked={this.props.rememberMe} aria-labelledby="rememberMeLabel" /> Remember me
91 93
                                             </label>
92 94
                                         </div>
93 95
                                         <div className="col-sm-6 col-sm-12 submit text-right">
94
-                                            <input type="submit" className="btn btn-default" defaultValue="Sign in" disabled={this.props.isAuthenticating} />
96
+                                            <input type="submit" className="btn btn-default" aria-label="Sign in" defaultValue="Sign in" disabled={this.props.isAuthenticating} />
95 97
                                         </div>
96 98
                                     </div>
97 99
                                 </div>

+ 2
- 2
app/components/elements/FilterBar.jsx View File

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

+ 2
- 1
app/components/elements/Grid.jsx View File

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

+ 36
- 14
app/components/elements/Pagination.jsx View File

@@ -38,8 +38,26 @@ export class Pagination extends Component {
38 38
 
39 39
     goToPage() {
40 40
         const pageNumber = parseInt(this.refs.pageInput.value);
41
-        $("#paginationModal").modal("hide");
42
-        this.props.router.push(this.buildLinkTo(pageNumber));
41
+        $(this.refs.paginationModal).modal("hide");
42
+        if (pageNumber) {
43
+            this.props.router.push(this.buildLinkTo(pageNumber));
44
+        }
45
+    }
46
+
47
+    dotsOnClick() {
48
+        $(this.refs.paginationModal).modal();
49
+    }
50
+
51
+    dotsOnKeyDown(ev) {
52
+        ev.preventDefault;
53
+        const code = ev.keyCode || ev.which;
54
+        if (code == 13 || code == 32) {  // Enter or Space key
55
+            this.dotsOnClick();  // Fire same event as onClick
56
+        }
57
+    }
58
+
59
+    cancelModalBox() {
60
+        $(this.refs.paginationModal).modal("hide");
43 61
     }
44 62
 
45 63
     render () {
@@ -50,7 +68,7 @@ export class Pagination extends Component {
50 68
             // Push first page
51 69
             pagesButton.push(
52 70
                 <li className="page-item" key={key}>
53
-                    <Link className="page-link" to={this.buildLinkTo(1)}>1</Link>
71
+                    <Link className="page-link" title="Go to page 1" to={this.buildLinkTo(1)}><span className="sr-only">Go to page </span>1</Link>
54 72
                 </li>
55 73
             );
56 74
             key++;
@@ -58,7 +76,7 @@ export class Pagination extends Component {
58 76
                 // Eventually push "…"
59 77
                 pagesButton.push(
60 78
                     <li className="page-item" key={key}>
61
-                        <span onClick={() => $("#paginationModal").modal() }>…</span>
79
+                        <span tabIndex="0" role="button" onKeyDown={this.dotsOnKeyDown.bind(this)} onClick={this.dotsOnClick.bind(this)}>&hellip;</span>
62 80
                     </li>
63 81
                 );
64 82
                 key++;
@@ -67,12 +85,15 @@ export class Pagination extends Component {
67 85
         var i = 0;
68 86
         for (i = lowerLimit; i < upperLimit; i++) {
69 87
             var className = "page-item";
88
+            var currentSpan = null;
70 89
             if (this.props.currentPage == i) {
71 90
                 className += " active";
91
+                currentSpan = <span className="sr-only">(current)</span>;
72 92
             }
93
+            const title = "Go to page " + i;
73 94
             pagesButton.push(
74 95
                 <li className={className} key={key}>
75
-                    <Link className="page-link" to={this.buildLinkTo(i)}>{i}</Link>
96
+                    <Link className="page-link" title={title} to={this.buildLinkTo(i)}><span className="sr-only">Go to page </span>{i} {currentSpan}</Link>
76 97
                 </li>
77 98
             );
78 99
             key++;
@@ -82,40 +103,41 @@ export class Pagination extends Component {
82 103
                 // Eventually push "…"
83 104
                 pagesButton.push(
84 105
                     <li className="page-item" key={key}>
85
-                        <span onClick={() => $("#paginationModal").modal() }>…</span>
106
+                        <span tabIndex="0" role="button" onKeyDown={this.dotsOnKeyDown.bind(this)} onClick={this.dotsOnClick.bind(this)}>&hellip;</span>
86 107
                     </li>
87 108
                 );
88 109
                 key++;
89 110
             }
111
+            const title = "Go to page " + this.props.nPages;
90 112
             // Push last page
91 113
             pagesButton.push(
92 114
                 <li className="page-item" key={key}>
93
-                    <Link className="page-link" to={this.buildLinkTo(this.props.nPages)}>{this.props.nPages}</Link>
115
+                    <Link className="page-link" title={title} to={this.buildLinkTo(this.props.nPages)}><span className="sr-only">Go to page </span>{this.props.nPages}</Link>
94 116
                 </li>
95 117
             );
96 118
         }
97 119
         if (pagesButton.length > 1) {
98 120
             return (
99 121
                 <div>
100
-                    <nav className="pagination-nav">
122
+                    <nav className="pagination-nav" aria-label="Page navigation">
101 123
                         <ul className="pagination">
102 124
                             { pagesButton }
103 125
                         </ul>
104 126
                     </nav>
105
-                    <div className="modal fade" id="paginationModal" tabIndex="-1" role="dialog" aria-hidden="false">
106
-                        <div className="modal-dialog">
127
+                    <div className="modal fade" ref="paginationModal" tabIndex="-1" role="dialog" aria-labelledby="paginationModalLabel">
128
+                        <div className="modal-dialog" role="document">
107 129
                             <div className="modal-content">
108 130
                                 <div className="modal-header">
109
-                                    <button type="button" className="close" data-dismiss="modal" aria-hidden="true">×</button>
110
-                                    <h4 className="modal-title">Page to go to?</h4>
131
+                                    <button type="button" className="close" data-dismiss="modal" aria-label="Close">&times;</button>
132
+                                    <h4 className="modal-title" id="paginationModalLabel">Page to go to?</h4>
111 133
                                 </div>
112 134
                                 <div className="modal-body">
113 135
                                     <form>
114
-                                        <input className="form-control" autoComplete="off" type="number" ref="pageInput" />
136
+                                        <input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label="Page number to go to" />
115 137
                                     </form>
116 138
                                 </div>
117 139
                                 <div className="modal-footer">
118
-                                    <button type="button" className="btn btn-default" onClick={ () => $("#paginationModal").modal("hide") }>Cancel</button>
140
+                                    <button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>Cancel</button>
119 141
                                     <button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>OK</button>
120 142
                                 </div>
121 143
                             </div>

+ 13
- 13
app/components/layouts/Sidebar.jsx View File

@@ -7,42 +7,42 @@ export default class SidebarLayout extends Component {
7 7
             <div>
8 8
                 <div className="col-sm-3 col-md-2 sidebar hidden-xs">
9 9
                     <h1 className="text-center"><IndexLink to="/"><img alt="A" src="./app/assets/img/ampache-blue.png"/>mpache</IndexLink></h1>
10
-                    <nav>
10
+                    <nav aria-label="Main navigation menu">
11 11
                         <div className="navbar text-center icon-navbar">
12 12
                             <div className="container-fluid">
13 13
                                 <ul className="nav navbar-nav icon-navbar-nav">
14
-                                    <li aria-hidden="true">
15
-                                        <Link to="/" className="glyphicon glyphicon-home"></Link>
14
+                                    <li>
15
+                                        <Link to="/" title="Home"><span className="glyphicon glyphicon-home" aria-hidden="true"></span> <span className="sr-only">Home</span></Link>
16 16
                                     </li>
17
-                                    <li aria-hidden="true">
18
-                                        <Link to="/settings" className="glyphicon glyphicon-wrench"></Link>
17
+                                    <li>
18
+                                        <Link to="/settings" title="Settings"><span className="glyphicon glyphicon-wrench" aria-hidden="true"></span> <span className="sr-only">Settings</span></Link>
19 19
                                     </li>
20
-                                    <li aria-hidden="true">
21
-                                        <Link to="/logout" className="glyphicon glyphicon-off"></Link>
20
+                                    <li>
21
+                                        <Link to="/logout" title="Logout"><span className="glyphicon glyphicon-off" aria-hidden="true"></span> <span className="sr-only">Logout</span></Link>
22 22
                                     </li>
23 23
                                 </ul>
24 24
                             </div>
25 25
                         </div>
26 26
                         <ul className="nav nav-sidebar">
27 27
                             <li>
28
-                                <Link to="/discover">
28
+                                <Link to="/discover" title="Discover">
29 29
                                     <span className="glyphicon glyphicon-globe" aria-hidden="true"></span>
30 30
                                     <span className="hidden-sm"> Discover</span>
31 31
                                 </Link>
32 32
                             </li>
33 33
                             <li>
34
-                                <Link to="/browse">
34
+                                <Link to="/browse" title="Browse">
35 35
                                     <span className="glyphicon glyphicon-headphones" aria-hidden="true"></span>
36 36
                                     <span className="hidden-sm"> Browse</span>
37 37
                                 </Link>
38 38
                                 <ul className="nav nav-sidebar text-center">
39
-                                    <li><Link to="/artists"><span className="glyphicon glyphicon-user"></span> Artists</Link></li>
40
-                                    <li><Link to="/albums"><span className="glyphicon glyphicon-cd" aria-hidden="true"></span> Albums</Link></li>
41
-                                    <li><Link to="/songs"><span className="glyphicon glyphicon-music"></span> Songs</Link></li>
39
+                                    <li><Link to="/artists" title="Browse artists"><span className="glyphicon glyphicon-user" aria-hidden="true"></span> Artists</Link></li>
40
+                                    <li><Link to="/albums" title="Browse albums"><span className="glyphicon glyphicon-cd" aria-hidden="true"></span> Albums</Link></li>
41
+                                    <li><Link to="/songs" title="Browse songs"><span className="glyphicon glyphicon-music" aria-hidden="true"></span> Songs</Link></li>
42 42
                                 </ul>
43 43
                             </li>
44 44
                             <li>
45
-                                <Link to="/search">
45
+                                <Link to="/search" title="Search">
46 46
                                     <span className="glyphicon glyphicon-search" aria-hidden="true"></span>
47 47
                                     <span className="hidden-sm"> Search</span>
48 48
                                 </Link>

+ 879
- 777
app/dist/index.js
File diff suppressed because it is too large
View File


+ 0
- 1
app/dist/style.css View File

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

+ 1
- 0
app/middleware/api.js View File

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

+ 1
- 1
app/reducers/paginate.js View File

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

+ 0
- 1
app/styles/ampache.css View File

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

+ 21
- 0
index.all.js View File

@@ -0,0 +1,21 @@
1
+// Export
2
+import "bootstrap";
3
+import "bootstrap/dist/css/bootstrap.css";
4
+import "./app/styles/ampache.css";
5
+
6
+// Handle app init
7
+import React from "react";
8
+import { render } from "react-dom";
9
+import { hashHistory } from "react-router";
10
+import { syncHistoryWithStore } from "react-router-redux";
11
+
12
+import Root from "./app/containers/Root";
13
+import configureStore from "./app/store/configureStore";
14
+
15
+const store = configureStore();
16
+const history = syncHistoryWithStore(hashHistory, store);
17
+
18
+render(
19
+    <Root store={store} history={history} />,
20
+    document.getElementById("root")
21
+);

+ 7
- 0
index.development.js View File

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

+ 5
- 21
index.js View File

@@ -1,21 +1,5 @@
1
-// Export
2
-import "bootstrap";
3
-import "bootstrap/dist/css/bootstrap.css";
4
-import "./app/styles/ampache.css";
5
-
6
-// Handle app init
7
-import React from "react";
8
-import { render } from "react-dom";
9
-import { hashHistory } from "react-router";
10
-import { syncHistoryWithStore } from "react-router-redux";
11
-
12
-import Root from "./app/containers/Root";
13
-import configureStore from "./app/store/configureStore";
14
-
15
-const store = configureStore();
16
-const history = syncHistoryWithStore(hashHistory, store);
17
-
18
-render(
19
-    <Root store={store} history={history} />,
20
-    document.getElementById("root")
21
-);
1
+if (process.env.NODE_ENV === "production") {
2
+    module.exports = require("./index.production.js");
3
+} else {
4
+    module.exports = require("./index.development.js");
5
+}

+ 1
- 0
index.production.js View File

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

+ 1
- 0
package.json View File

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