diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 2acddc233c..1814919b61 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -143,6 +143,8 @@ limitations under the License. // toggle menuButton and badge on hover/menu displayed .mx_RoomTile_menuDisplayed, +// or on keyboard focus of room tile +.mx_RoomTile.focus-visible:focus-within, .mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover { .mx_RoomTile_menuButton { display: block; diff --git a/src/Keyboard.js b/src/Keyboard.js index 738da478e4..f63956777f 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.js @@ -69,6 +69,8 @@ export const Key = { BACKSPACE: "Backspace", ARROW_UP: "ArrowUp", ARROW_DOWN: "ArrowDown", + ARROW_LEFT: "ArrowLeft", + ARROW_RIGHT: "ArrowRight", TAB: "Tab", ESCAPE: "Escape", ENTER: "Enter", diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 36dd3a7a61..d1d3bb1b63 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -186,6 +186,7 @@ const LeftPanel = createReactClass({ } } while (element && !( classes.contains("mx_RoomTile") || + classes.contains("mx_RoomSubList_label") || classes.contains("mx_textinput_search"))); if (element) { diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 60be1d7b34..92b9d91e0e 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector Ltd +Copyright 2019 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. @@ -16,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; import sdk from '../../index'; @@ -25,7 +26,7 @@ import Unread from '../../Unread'; import * as RoomNotifs from '../../RoomNotifs'; import * as FormattingUtils from '../../utils/FormattingUtils'; import IndicatorScrollbar from './IndicatorScrollbar'; -import { KeyCode } from '../../Keyboard'; +import {Key, KeyCode} from '../../Keyboard'; import { Group } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; import RoomTile from "../views/rooms/RoomTile"; @@ -56,7 +57,6 @@ const RoomSubList = createReactClass({ collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed? onHeaderClick: PropTypes.func, incomingCall: PropTypes.object, - isFiltered: PropTypes.bool, extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles forceExpand: PropTypes.bool, }, @@ -80,6 +80,7 @@ const RoomSubList = createReactClass({ }, componentWillMount: function() { + this._headerButton = createRef(); this.dispatcherRef = dis.register(this.onAction); }, @@ -87,9 +88,9 @@ const RoomSubList = createReactClass({ dis.unregister(this.dispatcherRef); }, - // The header is collapsable if it is hidden or not stuck + // The header is collapsible if it is hidden or not stuck // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method - isCollapsableOnClick: function() { + isCollapsibleOnClick: function() { const stuck = this.refs.header.dataset.stuck; if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) { return true; @@ -114,8 +115,8 @@ const RoomSubList = createReactClass({ }, onClick: function(ev) { - if (this.isCollapsableOnClick()) { - // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic + if (this.isCollapsibleOnClick()) { + // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic const isHidden = !this.state.hidden; this.setState({hidden: isHidden}, () => { this.props.onHeaderClick(isHidden); @@ -124,6 +125,30 @@ const RoomSubList = createReactClass({ // The header is stuck, so the click is to be interpreted as a scroll to the header this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); } + this._headerButton.current.focus(); + }, + + onKeyDown: function(ev) { + switch (ev.key) { + case Key.TAB: + // Prevent LeftPanel handling Tab if focus is on the sublist header itself + ev.stopPropagation(); + break; + case Key.ARROW_LEFT: + ev.stopPropagation(); + if (!this.state.hidden && !this.props.forceExpand) { + this.onClick(); + } + break; + case Key.ARROW_RIGHT: + ev.stopPropagation(); + if (this.state.hidden && !this.props.forceExpand) { + this.onClick(); + } else { + // TODO go to first element in subtree + } + break; + } }, onRoomTileClick(roomId, ev) { @@ -193,6 +218,11 @@ const RoomSubList = createReactClass({ } }, + onAddRoom: function(e) { + e.stopPropagation(); + if (this.props.onAddRoom) this.props.onAddRoom(); + }, + _getHeaderJsx: function(isCollapsed) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); @@ -209,12 +239,18 @@ const RoomSubList = createReactClass({ 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, }); if (subListNotifCount > 0) { - badge =