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?",