Skip to content

Commit c0c9494

Browse files
author
Erin Doyle
committed
Added a MovieToolbar component containing a toolbar widget for the Movie actions buttons
1 parent d9cd674 commit c0c9494

File tree

3 files changed

+216
-10
lines changed

3 files changed

+216
-10
lines changed

src/primitives/MovieToolbar.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import MovieToolbarButton from './MovieToolbarButton';
5+
6+
7+
class MovieToolbar extends Component {
8+
constructor(props) {
9+
super(props);
10+
11+
const { buttonList } = this.props;
12+
13+
this.state = {
14+
selectedButton: buttonList[0]
15+
};
16+
17+
this.selectedButtonRef = null;
18+
19+
this.setSelectedButtonRef = this.setSelectedButtonRef.bind(this);
20+
this.selectButton = this.selectButton.bind(this);
21+
this.gotoFirstButton = this.gotoFirstButton.bind(this);
22+
this.gotoLastButton = this.gotoLastButton.bind(this);
23+
this.gotoPreviousButton = this.gotoPreviousButton.bind(this);
24+
this.gotoNextButton = this.gotoNextButton.bind(this);
25+
this.handleClick = this.handleClick.bind(this);
26+
this.handleKeydown = this.handleKeydown.bind(this);
27+
}
28+
29+
componentDidUpdate() {
30+
if (!this.selectedButtonRef) return;
31+
32+
this.selectedButtonRef.focus();
33+
}
34+
35+
setSelectedButtonRef(element) {
36+
this.selectedButtonRef = element;
37+
}
38+
39+
selectButton (button) {
40+
this.setState({selectedButton: button});
41+
}
42+
43+
gotoFirstButton () {
44+
const { buttonList } = this.props;
45+
this.selectButton(buttonList[0]);
46+
}
47+
48+
gotoLastButton () {
49+
const { buttonList } = this.props;
50+
this.selectButton(buttonList[buttonList.length - 1]);
51+
}
52+
53+
gotoPreviousButton (currentButton) {
54+
const { buttonList } = this.props;
55+
const index = buttonList.findIndex((button) => button === currentButton);
56+
57+
// If the current button is already the first button, circle round to the last button
58+
if (index === 0) {
59+
this.gotoLastButton();
60+
} else {
61+
// Else go to the previous button
62+
this.selectButton(buttonList[index - 1]);
63+
}
64+
}
65+
66+
gotoNextButton (currentButton) {
67+
const { buttonList } = this.props;
68+
const index = buttonList.findIndex((button) => button === currentButton);
69+
70+
// If the current button is already the last button, circle round to the first button
71+
if (index === buttonList.length - 1) {
72+
this.gotoFirstButton();
73+
} else {
74+
// Else go to the next button
75+
this.selectButton(buttonList[index + 1]);
76+
}
77+
}
78+
79+
handleClick (e, button) {
80+
e.preventDefault();
81+
this.selectButton(button);
82+
83+
// Fire the button's action
84+
button.action();
85+
}
86+
87+
/**
88+
* Per the WAI ARIA Button List Design Pattern the following interaction is supported:
89+
*
90+
* When focus is on a button element in a horizontal button list:
91+
* Left Arrow: moves focus to the previous button. If focus is on the first button, moves focus to the last button.
92+
* Right Arrow: Moves focus to the next button. If focus is on the last button element, moves focus to the first button.
93+
*
94+
* When focus is on a button in a buttonlist with either horizontal or vertical orientation:
95+
* Space or Enter: Activates the button if it was not activated automatically on focus.
96+
* Home (Optional): Moves focus to the first button.
97+
* End (Optional): Moves focus to the last button.
98+
*
99+
* WAI ARIA recommendation is that when a button receives focus it "automatically activates" the newly focused button.
100+
*/
101+
handleKeydown (e, button) {
102+
switch (e.key) {
103+
case 'ArrowLeft':
104+
e.preventDefault();
105+
this.gotoPreviousButton(button);
106+
break;
107+
108+
case 'ArrowRight':
109+
e.preventDefault();
110+
this.gotoNextButton(button);
111+
break;
112+
113+
case 'Home':
114+
e.preventDefault();
115+
this.gotoFirstButton();
116+
break;
117+
118+
case 'End':
119+
e.preventDefault();
120+
this.gotoLastButton();
121+
break;
122+
123+
case 'Enter':
124+
case ' ':
125+
case 'Spacebar': // for older browsers
126+
e.preventDefault();
127+
this.selectButton(button);
128+
129+
// Fire the button's action
130+
button.action();
131+
break;
132+
133+
default:
134+
break;
135+
}
136+
}
137+
138+
render() {
139+
const { ariaLabel, movieTitle, buttonList } = this.props;
140+
const { selectedButton } = this.state;
141+
142+
const buttonItems = buttonList.map((buttonItem) => {
143+
const { title } = buttonItem;
144+
const isSelectedButton = buttonItem.title === selectedButton.title;
145+
146+
return (
147+
<MovieToolbarButton
148+
key={`${title}-button`}
149+
id={`${title}-button`}
150+
movieTitle={movieTitle}
151+
buttonText={title}
152+
153+
tabIndex={isSelectedButton ? 0 : -1}
154+
155+
clickHandler={e => this.handleClick(e, buttonItem)}
156+
keyDownHandler={e => this.handleKeydown(e, buttonItem)}
157+
158+
innerRef={ref => { if (isSelectedButton) this.setSelectedButtonRef(ref); }}
159+
/>
160+
);
161+
});
162+
163+
return (
164+
<div className="btn-group" role="toolbar" aria-label={ariaLabel}>
165+
{buttonItems}
166+
</div>
167+
);
168+
}
169+
}
170+
171+
MovieToolbar.propTypes = {
172+
ariaLabel: PropTypes.string.isRequired,
173+
movieTitle: PropTypes.string.isRequired,
174+
buttonList: PropTypes.arrayOf(PropTypes.shape({
175+
title: PropTypes.string,
176+
action: PropTypes.func
177+
})).isRequired
178+
};
179+
180+
181+
export default MovieToolbar;

src/primitives/MovieToolbarButton.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,48 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33

44

5-
const MovieToolbarButton = ({ movieTitle, buttonText, buttonLabel, clickHandler }) => {
5+
const MovieToolbarButton = ({
6+
movieTitle,
7+
buttonText,
8+
buttonLabel,
9+
clickHandler,
10+
keyDownHandler,
11+
tabIndex,
12+
innerRef
13+
}) => {
614
const ariaLabel = buttonLabel || `${buttonText} ${movieTitle}`;
715

816
return (
9-
<button className="btn btn-secondary" aria-label={ariaLabel} onClick={clickHandler}>{buttonText}</button>
17+
<button
18+
className="btn btn-secondary"
19+
aria-label={ariaLabel}
20+
onClick={clickHandler}
21+
onKeyDown={keyDownHandler}
22+
tabIndex={tabIndex}
23+
ref={innerRef}
24+
>
25+
{buttonText}
26+
</button>
1027
);
1128
};
1229

1330
MovieToolbarButton.defaultProps = {
1431
buttonText: '',
1532
buttonLabel: null,
16-
clickHandler: () => {}
33+
clickHandler: () => {},
34+
keyDownHandler: () => {},
35+
tabIndex: 0,
36+
innerRef: () => {}
1737
};
1838

1939
MovieToolbarButton.propTypes = {
2040
movieTitle: PropTypes.string.isRequired,
2141
buttonText: PropTypes.string,
2242
buttonLabel: PropTypes.string,
23-
clickHandler: PropTypes.func
43+
clickHandler: PropTypes.func,
44+
keyDownHandler: PropTypes.func,
45+
tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
46+
innerRef: PropTypes.func
2447
};
2548

2649

src/wishlist/getWishlistActions.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import MovieToolbarButton from '../primitives/MovieToolbarButton';
2+
import MovieToolbar from '../primitives/MovieToolbar';
33

44

55
const getMovieActions = (showEditor, setAsWatched, setAsUnwatched, handleRemove) =>
@@ -9,12 +9,14 @@ const getMovieActions = (showEditor, setAsWatched, setAsUnwatched, handleRemove)
99
const editClickHandler = () => showEditor(movieId);
1010
const removeClickHandler = () => handleRemove(movieId);
1111

12+
const movieButtonList = [
13+
{ title: watchButtonText, action: watchClickHandler },
14+
{ title: 'Edit', action: editClickHandler },
15+
{ title: 'Remove', action: removeClickHandler }
16+
];
17+
1218
return (
13-
<div className="btn-group">
14-
<MovieToolbarButton movieTitle={movieTitle} buttonText={watchButtonText} clickHandler={watchClickHandler} />
15-
<MovieToolbarButton movieTitle={movieTitle} buttonText="Edit" clickHandler={editClickHandler} />
16-
<MovieToolbarButton movieTitle={movieTitle} buttonText="Remove" clickHandler={removeClickHandler} />
17-
</div>
19+
<MovieToolbar ariaLabel={`${movieTitle} Actions`} movieTitle={movieTitle} buttonList={movieButtonList}/>
1820
);
1921
};
2022

0 commit comments

Comments
 (0)