diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index fc61395bf9..b39504593a 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -20,7 +20,7 @@ limitations under the License. so they ideally wouldn't affect each other. lowest category: .mx_RoomSubList flex-shrink: 10000000 - distribute size of items within the same categery by their size + distribute size of items within the same category by their size middle category: .mx_RoomSubList.resized-sized flex-shrink: 1000 applied when using the resizer, will have a max-height set to it, @@ -46,10 +46,15 @@ limitations under the License. flex-direction: row; align-items: center; flex: 0 0 auto; - margin: 0 16px; + margin: 0 8px; + padding: 0 8px; height: 36px; } +.mx_RoomSubList_labelContainer:focus-within { + background-color: $roomtile-focused-bg-color; +} + .mx_RoomSubList_label { flex: 1; cursor: pointer; @@ -67,7 +72,7 @@ limitations under the License. margin-left: 8px; } -.mx_RoomSubList_badge { +.mx_RoomSubList_badge > div { flex: 0 0 auto; border-radius: 8px; font-weight: 600; @@ -103,7 +108,7 @@ limitations under the License. } } -.mx_RoomSubList_badgeHighlight { +.mx_RoomSubList_badgeHighlight > div { color: $accent-fg-color; background-color: $warning-color; } 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 3d09c05c43..8054ef01be 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,9 +57,8 @@ const RoomSubList = createReactClass({ collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed? onHeaderClick: PropTypes.func, incomingCall: PropTypes.object, - isFiltered: PropTypes.bool, - headerItems: PropTypes.node, // content shown in the sublist header extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles + forceExpand: PropTypes.bool, }, getInitialState: function() { @@ -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); @@ -126,6 +127,49 @@ const RoomSubList = createReactClass({ } }, + onHeaderKeyDown: 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: + // On ARROW_LEFT collapse the room sublist + if (!this.state.hidden && !this.props.forceExpand) { + this.onClick(); + } + ev.stopPropagation(); + break; + case Key.ARROW_RIGHT: { + ev.stopPropagation(); + if (this.state.hidden && !this.props.forceExpand) { + // sublist is collapsed, expand it + this.onClick(); + } else if (!this.props.forceExpand) { + // sublist is expanded, go to first room + const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile"); + if (element) { + element.focus(); + } + } + break; + } + } + }, + + onKeyDown: function(ev) { + switch (ev.key) { + // On ARROW_LEFT go to the sublist header + case Key.ARROW_LEFT: + ev.stopPropagation(); + this._headerButton.current.focus(); + break; + // Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer + case Key.ARROW_RIGHT: + ev.stopPropagation(); + } + }, + onRoomTileClick(roomId, ev) { dis.dispatch({ action: 'view_room', @@ -193,6 +237,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'); @@ -208,13 +257,24 @@ const RoomSubList = createReactClass({ 'mx_RoomSubList_badge': true, 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, }); + // Wrap the contents in a div and apply styles to the child div so that the browser default outline works if (subListNotifCount > 0) { - badge =
- { FormattingUtils.formatCount(subListNotifCount) } -
; + badge = ( + +
+ { FormattingUtils.formatCount(subListNotifCount) } +
+
+ ); } else if (this.props.isInvite && this.props.list.length) { // no notifications but highlight anyway because this is an invite badge - badge =
{this.props.list.length}
; + badge = ( + +
+ { this.props.list.length } +
+
+ ); } } @@ -237,7 +297,7 @@ const RoomSubList = createReactClass({ if (this.props.onAddRoom) { addRoomButton = ( @@ -255,10 +315,16 @@ const RoomSubList = createReactClass({ chevron = (
); } - const tabindex = this.props.isFiltered ? "0" : "-1"; return ( -
- +
+ { chevron } {this.props.label} { incomingCall } @@ -299,21 +365,20 @@ const RoomSubList = createReactClass({ render: function() { const len = this.props.list.length + this.props.extraTiles.length; const isCollapsed = this.state.hidden && !this.props.forceExpand; - if (len) { - const subListClasses = classNames({ - "mx_RoomSubList": true, - "mx_RoomSubList_hidden": isCollapsed, - "mx_RoomSubList_nonEmpty": len && !isCollapsed, - }); + const subListClasses = classNames({ + "mx_RoomSubList": true, + "mx_RoomSubList_hidden": len && isCollapsed, + "mx_RoomSubList_nonEmpty": len && !isCollapsed, + }); + + let content; + if (len) { if (isCollapsed) { - return
- {this._getHeaderJsx(isCollapsed)} -
; + // no body } else if (this._canUseLazyListRendering()) { - return
- {this._getHeaderJsx(isCollapsed)} - + content = ( + -
; + ); } else { const roomTiles = this.props.list.map(r => this.makeRoomTile(r)); const tiles = roomTiles.concat(this.props.extraTiles); - return
- {this._getHeaderJsx(isCollapsed)} - + content = ( + { tiles } -
; + ); } } else { - const Loader = sdk.getComponent("elements.Spinner"); - let content; if (this.props.showSpinner && !isCollapsed) { + const Loader = sdk.getComponent("elements.Spinner"); content = ; } - - return ( -
- { this._getHeaderJsx(isCollapsed) } - { content } -
- ); } + + return ( +
+ { this._getHeaderJsx(isCollapsed) } + { content } +
+ ); }, }); diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index bfc3e45246..1ccb7d0796 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -67,8 +67,6 @@ export default function AccessibleButton(props) { restProps.ref = restProps.inputRef; delete restProps.inputRef; - restProps.tabIndex = restProps.tabIndex || "0"; - restProps.role = restProps.role || "button"; restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton"; if (kind) { @@ -93,19 +91,30 @@ export default function AccessibleButton(props) { */ AccessibleButton.propTypes = { children: PropTypes.node, - inputRef: PropTypes.func, + inputRef: PropTypes.oneOfType([ + // Either a function + PropTypes.func, + // Or the instance of a DOM native element + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), element: PropTypes.string, onClick: PropTypes.func.isRequired, // The kind of button, similar to how Bootstrap works. // See available classes for AccessibleButton for options. kind: PropTypes.string, + // The ARIA role + role: PropTypes.string, + // The tabIndex + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), disabled: PropTypes.bool, }; AccessibleButton.defaultProps = { element: 'div', + role: 'button', + tabIndex: "0", }; AccessibleButton.displayName = "AccessibleButton"; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 6c031563cd..036f50d899 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -49,21 +49,6 @@ function labelForTagName(tagName) { return tagName; } -function phraseForSection(section) { - switch (section) { - case 'm.favourite': - return _t('Drop here to favourite'); - case 'im.vector.fake.direct': - return _t('Drop here to tag direct chat'); - case 'im.vector.fake.recent': - return _t('Drop here to restore'); - case 'm.lowpriority': - return _t('Drop here to demote'); - default: - return _t('Drop here to tag %(section)s', {section: section}); - } -} - module.exports = createReactClass({ displayName: 'RoomList', @@ -203,7 +188,7 @@ module.exports = createReactClass({ this.resizer.setClassNames({ handle: "mx_ResizeHandle", vertical: "mx_ResizeHandle_vertical", - reverse: "mx_ResizeHandle_reverse" + reverse: "mx_ResizeHandle_reverse", }); this._layout.update( this._layoutSections, @@ -584,23 +569,6 @@ module.exports = createReactClass({ } }, - _getHeaderItems: function(section) { - const StartChatButton = sdk.getComponent('elements.StartChatButton'); - const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); - const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); - switch (section) { - case 'im.vector.fake.direct': - return - - ; - case 'im.vector.fake.recent': - return - - - ; - } - }, - _makeGroupInviteTiles(filter) { const ret = []; const lcFilter = filter && filter.toLowerCase(); @@ -676,7 +644,7 @@ module.exports = createReactClass({ props = Object.assign({}, defaultProps, props); const isLast = i === subListsProps.length - 1; const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0); - const {key, label, onHeaderClick, ... otherProps} = props; + const {key, label, onHeaderClick, ...otherProps} = props; const chosenKey = key || label; const onSubListHeaderClick = (collapsed) => { this._handleCollapsedState(chosenKey, collapsed); @@ -746,16 +714,14 @@ module.exports = createReactClass({ list: this.state.lists['im.vector.fake.direct'], label: _t('People'), tagName: "im.vector.fake.direct", - headerItems: this._getHeaderItems('im.vector.fake.direct'), order: "recent", incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'), - onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})}, + onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});}, addRoomLabel: _t("Start chat"), }, { list: this.state.lists['im.vector.fake.recent'], label: _t('Rooms'), - headerItems: this._getHeaderItems('im.vector.fake.recent'), order: "recent", incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), onAddRoom: () => {dis.dispatch({action: 'view_create_room'});}, @@ -805,7 +771,7 @@ module.exports = createReactClass({ const subListComponents = this._mapSubListProps(subLists); return ( -
{ subListComponents }
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index b727abd261..1398e03b10 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -398,7 +398,8 @@ module.exports = createReactClass({ onMouseLeave={this.onMouseLeave} onContextMenu={this.onContextMenu} aria-label={ariaLabel} - role="option" + aria-selected={this.state.selected} + role="treeitem" >
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0eeb518dde..e5c6043c7f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -907,11 +907,6 @@ "Forget room": "Forget room", "Search": "Search", "Share room": "Share room", - "Drop here to favourite": "Drop here to favourite", - "Drop here to tag direct chat": "Drop here to tag direct chat", - "Drop here to restore": "Drop here to restore", - "Drop here to demote": "Drop here to demote", - "Drop here to tag %(section)s": "Drop here to tag %(section)s", "Community Invites": "Community Invites", "Invites": "Invites", "Favourites": "Favourites", @@ -1664,6 +1659,8 @@ "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", + "Jump to first unread room.": "Jump to first unread room.", + "Jump to first invite.": "Jump to first invite.", "Add room": "Add room", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",