From b2d1daff5c3936e502dc6e0aef31050a77aea31c Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Wed, 24 Jun 2020 22:46:26 -0400 Subject: [PATCH 1/3] Refactor MessageActionBar w/ARIA Toolbar pattern Signed-off-by: Mike Pennisi --- .../views/messages/MessageActionBar.js | 112 ++++++++++++++---- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 95eb37b588f..4768f00e520 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -25,8 +25,9 @@ import dis from '../../../dispatcher/dispatcher'; import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import RoomContext from "../../../contexts/RoomContext"; +import {Key} from '../../../Keyboard'; -const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { +const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange, tabIndex}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); useEffect(() => { onFocusChange(menuDisplayed); @@ -58,13 +59,14 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo onClick={openMenu} isExpanded={menuDisplayed} inputRef={button} + tabIndex={tabIndex} /> { contextMenu } ; }; -const ReactButton = ({mxEvent, reactions, onFocusChange}) => { +const ReactButton = ({mxEvent, reactions, onFocusChange, tabIndex}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); useEffect(() => { onFocusChange(menuDisplayed); @@ -86,12 +88,17 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => { onClick={openMenu} isExpanded={menuDisplayed} inputRef={button} + tabIndex={tabIndex} /> { contextMenu } ; }; +// This component implements the Toolbar design pattern from the WAI-ARIA +// Authoring Practices guidelines. +// +// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar export default class MessageActionBar extends React.PureComponent { static propTypes = { mxEvent: PropTypes.object.isRequired, @@ -105,14 +112,24 @@ export default class MessageActionBar extends React.PureComponent { static contextType = RoomContext; + constructor(props) { + super(props); + this.elRef = React.createRef(); + this.state = { focused: "options", buttonNames: [] }; + } + componentDidMount() { this.props.mxEvent.on("Event.decrypted", this.onDecrypted); this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction); + + this.onKeyDown = this.onKeyDown.bind(this); + this.elRef.current.addEventListener("keydown", this.onKeyDown, true); } componentWillUnmount() { this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted); this.props.mxEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction); + this.elRef.current.removeEventListener("keydown", this.onKeyDown, true); } onDecrypted = () => { @@ -133,6 +150,37 @@ export default class MessageActionBar extends React.PureComponent { this.props.onFocusChange(focused); }; + onKeyDown = (ev) => { + const {key} = ev; + const buttonNames = this.state.buttonNames; + const current = buttonNames.indexOf(this.state.focused); + let newIndex = null; + + if (key === Key.ARROW_UP || key === Key.ARROW_DOWN) { + ev.target.click(); + } else if (key === Key.ARROW_RIGHT) { + newIndex = (current + 1) % buttonNames.length; + } else if (key === Key.ARROW_LEFT) { + newIndex = current ? current - 1 : buttonNames.length - 1; + } else if (key === Key.HOME) { + newIndex = 0; + } else if (key === Key.END) { + newIndex = buttonNames.length - 1; + } else { + return; + } + + if (newIndex !== null) { + this.setState({ focused: buttonNames[newIndex] }); + this.elRef.current.children[newIndex].focus(); + } + + ev.stopPropagation(); + ev.preventDefault(); + }; + + tabIndex = (target) => target === this.state.focused ? 0 : -1; + onReplyClick = (ev) => { dis.dispatch({ action: 'reply_to_event', @@ -149,45 +197,61 @@ export default class MessageActionBar extends React.PureComponent { render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - let reactButton; - let replyButton; - let editButton; + const buttons = []; if (isContentActionable(this.props.mxEvent)) { if (this.context.canReact) { - reactButton = ( - + buttons.push( + , ); } if (this.context.canReply) { - replyButton = ; + buttons.push( + , + ); } } if (canEditContent(this.props.mxEvent)) { - editButton = ; + buttons.push( + , + ); } - // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. - return
- {reactButton} - {replyButton} - {editButton} + buttons.push( + tabIndex={this.tabIndex("options")} + key="options" + />, + ); + + const newButtonNames = buttons.map((button) => button.key); + if (this.state.buttonNames.join() !== newButtonNames.join()) { + this.setState({buttonNames: newButtonNames}); + } + + // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. + return
+ {buttons}
; } } From f6d081940b9afcd492823caedc045027596f7ea6 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Wed, 24 Jun 2020 23:13:39 -0400 Subject: [PATCH 2/3] fixup! Refactor MessageActionBar w/ARIA Toolbar pattern --- .../views/messages/MessageActionBar.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 4768f00e520..5db7f779a4c 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -121,8 +121,6 @@ export default class MessageActionBar extends React.PureComponent { componentDidMount() { this.props.mxEvent.on("Event.decrypted", this.onDecrypted); this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction); - - this.onKeyDown = this.onKeyDown.bind(this); this.elRef.current.addEventListener("keydown", this.onKeyDown, true); } @@ -202,9 +200,13 @@ export default class MessageActionBar extends React.PureComponent { if (isContentActionable(this.props.mxEvent)) { if (this.context.canReact) { buttons.push( - , + , ); } if (this.context.canReply) { @@ -249,8 +251,13 @@ export default class MessageActionBar extends React.PureComponent { } // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. - return
+ return
{buttons}
; } From 9fb873215835607eed4981e40c320537dfb595b6 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Mon, 6 Jul 2020 23:45:09 -0400 Subject: [PATCH 3/3] fixup! Refactor MessageActionBar w/ARIA Toolbar pattern --- src/components/views/messages/MessageActionBar.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 5db7f779a4c..2582f4374bd 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -155,7 +155,9 @@ export default class MessageActionBar extends React.PureComponent { let newIndex = null; if (key === Key.ARROW_UP || key === Key.ARROW_DOWN) { - ev.target.click(); + if (ev.target.hasAttribute('aria-haspopup')) { + ev.target.click(); + } } else if (key === Key.ARROW_RIGHT) { newIndex = (current + 1) % buttonNames.length; } else if (key === Key.ARROW_LEFT) {