mirror of https://github.com/vector-im/riot-web
				
				
				
			Create a generic ARIA toolbar component which works with existing roving tab index context
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/21833/head
							parent
							
								
									87069a9856
								
							
						
					
					
						commit
						8703bc1abc
					
				|  | @ -47,7 +47,7 @@ const DOCUMENT_POSITION_PRECEDING = 2; | |||
| 
 | ||||
| type Ref = RefObject<HTMLElement>; | ||||
| 
 | ||||
| interface IState { | ||||
| export interface IState { | ||||
|     activeRef: Ref; | ||||
|     refs: Ref[]; | ||||
| } | ||||
|  | @ -156,7 +156,7 @@ interface IProps { | |||
|     children(renderProps: { | ||||
|         onKeyDownHandler(ev: React.KeyboardEvent); | ||||
|     }); | ||||
|     onKeyDown?(ev: React.KeyboardEvent); | ||||
|     onKeyDown?(ev: React.KeyboardEvent, state: IState); | ||||
| } | ||||
| 
 | ||||
| export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEnd, onKeyDown}) => { | ||||
|  | @ -193,7 +193,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn | |||
|             ev.preventDefault(); | ||||
|             ev.stopPropagation(); | ||||
|         } else if (onKeyDown) { | ||||
|             return onKeyDown(ev); | ||||
|             return onKeyDown(ev, state); | ||||
|         } | ||||
|     }, [context.state, onKeyDown, handleHomeEnd]); | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,69 @@ | |||
| /* | ||||
| Copyright 2020 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React from "react"; | ||||
| 
 | ||||
| import {IState, RovingTabIndexProvider} from "./RovingTabIndex"; | ||||
| import {Key} from "../Keyboard"; | ||||
| 
 | ||||
| interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> { | ||||
| } | ||||
| 
 | ||||
| // 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
 | ||||
| // All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref`
 | ||||
| const Toolbar: React.FC<IProps> = ({children, ...props}) => { | ||||
|     const onKeyDown = (ev: React.KeyboardEvent, state: IState) => { | ||||
|         const target = ev.target as HTMLElement; | ||||
|         let handled = true; | ||||
| 
 | ||||
|         switch (ev.key) { | ||||
|             case Key.ARROW_UP: | ||||
|             case Key.ARROW_DOWN: | ||||
|                 if (target.hasAttribute('aria-haspopup')) { | ||||
|                     target.click(); | ||||
|                 } | ||||
|                 break; | ||||
| 
 | ||||
|             case Key.ARROW_LEFT: | ||||
|             case Key.ARROW_RIGHT: | ||||
|                 if (state.refs.length > 0) { | ||||
|                     const i = state.refs.findIndex(r => r === state.activeRef); | ||||
|                     const delta = ev.key === Key.ARROW_RIGHT ? 1 : -1; | ||||
|                     state.refs.slice((i + delta) % state.refs.length)[0].current.focus(); | ||||
|                 } | ||||
|                 break; | ||||
| 
 | ||||
|             // HOME and END are handled by RovingTabIndexProvider
 | ||||
| 
 | ||||
|             default: | ||||
|                 handled = false; | ||||
|         } | ||||
| 
 | ||||
|         if (handled) { | ||||
|             ev.preventDefault(); | ||||
|             ev.stopPropagation(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}> | ||||
|         {({onKeyDownHandler}) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar"> | ||||
|             { children } | ||||
|         </div>} | ||||
|     </RovingTabIndexProvider>; | ||||
| }; | ||||
| 
 | ||||
| export default Toolbar; | ||||
|  | @ -233,6 +233,9 @@ export class ContextMenu extends React.PureComponent<IProps, IState> { | |||
|         switch (ev.key) { | ||||
|             case Key.TAB: | ||||
|             case Key.ESCAPE: | ||||
|             // close on left and right arrows too for when it is a context menu on a <Toolbar />
 | ||||
|             case Key.ARROW_LEFT: | ||||
|             case Key.ARROW_RIGHT: | ||||
|                 this.props.onFinished(); | ||||
|                 break; | ||||
|             case Key.ARROW_UP: | ||||
|  |  | |||
|  | @ -25,9 +25,12 @@ 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 Toolbar from "../../../accessibility/Toolbar"; | ||||
| import {RovingAccessibleButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex"; | ||||
| 
 | ||||
| const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { | ||||
|     const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); | ||||
|     const [onFocus, isActive, ref] = useRovingTabIndex(button); | ||||
|     useEffect(() => { | ||||
|         onFocusChange(menuDisplayed); | ||||
|     }, [onFocusChange, menuDisplayed]); | ||||
|  | @ -57,7 +60,9 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo | |||
|             label={_t("Options")} | ||||
|             onClick={openMenu} | ||||
|             isExpanded={menuDisplayed} | ||||
|             inputRef={button} | ||||
|             inputRef={ref} | ||||
|             onFocus={onFocus} | ||||
|             tabIndex={isActive ? 0 : -1} | ||||
|         /> | ||||
| 
 | ||||
|         { contextMenu } | ||||
|  | @ -66,6 +71,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo | |||
| 
 | ||||
| const ReactButton = ({mxEvent, reactions, onFocusChange}) => { | ||||
|     const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); | ||||
|     const [onFocus, isActive, ref] = useRovingTabIndex(button); | ||||
|     useEffect(() => { | ||||
|         onFocusChange(menuDisplayed); | ||||
|     }, [onFocusChange, menuDisplayed]); | ||||
|  | @ -85,7 +91,9 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => { | |||
|             label={_t("React")} | ||||
|             onClick={openMenu} | ||||
|             isExpanded={menuDisplayed} | ||||
|             inputRef={button} | ||||
|             inputRef={ref} | ||||
|             onFocus={onFocus} | ||||
|             tabIndex={isActive ? 0 : -1} | ||||
|         /> | ||||
| 
 | ||||
|         { contextMenu } | ||||
|  | @ -148,8 +156,6 @@ export default class MessageActionBar extends React.PureComponent { | |||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|         const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); | ||||
| 
 | ||||
|         let reactButton; | ||||
|         let replyButton; | ||||
|         let editButton; | ||||
|  | @ -161,7 +167,7 @@ export default class MessageActionBar extends React.PureComponent { | |||
|                 ); | ||||
|             } | ||||
|             if (this.context.canReply) { | ||||
|                 replyButton = <AccessibleButton | ||||
|                 replyButton = <RovingAccessibleButton | ||||
|                     className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" | ||||
|                     title={_t("Reply")} | ||||
|                     onClick={this.onReplyClick} | ||||
|  | @ -169,7 +175,7 @@ export default class MessageActionBar extends React.PureComponent { | |||
|             } | ||||
|         } | ||||
|         if (canEditContent(this.props.mxEvent)) { | ||||
|             editButton = <AccessibleButton | ||||
|             editButton = <RovingAccessibleButton | ||||
|                 className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton" | ||||
|                 title={_t("Edit")} | ||||
|                 onClick={this.onEditClick} | ||||
|  | @ -177,7 +183,7 @@ export default class MessageActionBar extends React.PureComponent { | |||
|         } | ||||
| 
 | ||||
|         // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
 | ||||
|         return <div className="mx_MessageActionBar" role="toolbar" aria-label={_t("Message Actions")} aria-live="off"> | ||||
|         return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off"> | ||||
|             {reactButton} | ||||
|             {replyButton} | ||||
|             {editButton} | ||||
|  | @ -188,6 +194,6 @@ export default class MessageActionBar extends React.PureComponent { | |||
|                 permalinkCreator={this.props.permalinkCreator} | ||||
|                 onFocusChange={this.onFocusChange} | ||||
|             /> | ||||
|         </div>; | ||||
|         </Toolbar>; | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski