1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-11-13 08:02:38 +03:00

Merge branches 'develop' and 't3chguy/room-list/3' of github.com:matrix-org/matrix-react-sdk into t3chguy/room-list/3

 Conflicts:
	src/components/structures/ContextMenu.tsx
	src/components/structures/UserMenu.tsx
	src/components/views/rooms/RoomSublist2.tsx
	src/components/views/rooms/RoomTile2.tsx
This commit is contained in:
Michael Telatynski
2020-07-07 15:01:27 +01:00
42 changed files with 1084 additions and 248 deletions

View File

@@ -23,6 +23,8 @@ import classNames from "classnames";
import {Key} from "../../Keyboard";
import AccessibleButton, { IProps as IAccessibleButtonProps, ButtonEvent } from "../views/elements/AccessibleButton";
import {Writeable} from "../../@types/common";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import StyledRadioButton from "../views/elements/StyledRadioButton";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@@ -455,6 +457,54 @@ export const MenuItemCheckbox: React.FC<IMenuItemCheckboxProps> = ({children, la
);
};
interface IStyledMenuItemCheckboxProps extends IAccessibleButtonProps {
label?: string;
active: boolean;
disabled?: boolean;
className?: string;
onChange();
onClose(): void; // gets called after onChange on Key.ENTER
}
// Semantic component for representing a styled role=menuitemcheckbox
export const StyledMenuItemCheckbox: React.FC<IStyledMenuItemCheckboxProps> = ({children, label, onChange, onClose, checked, disabled=false, ...props}) => {
const onKeyDown = (e) => {
if (e.key === Key.ENTER || e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
onChange();
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
if (e.key === Key.ENTER) {
onClose();
}
}
};
const onKeyUp = (e) => {
// prevent the input default handler as we handle it on keydown to match
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
if (e.key === Key.SPACE || e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
}
};
return (
<StyledCheckbox
{...props}
role="menuitemcheckbox"
aria-checked={checked}
checked={checked}
aria-disabled={disabled}
tabIndex={-1}
aria-label={label}
onChange={onChange}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
>
{ children }
</StyledCheckbox>
);
};
interface IMenuItemRadioProps extends IAccessibleButtonProps {
label?: string;
active: boolean;
@@ -472,6 +522,55 @@ export const MenuItemRadio: React.FC<IMenuItemRadioProps> = ({children, label, a
);
};
interface IStyledMenuItemRadioProps extends IAccessibleButtonProps {
label?: string;
active: boolean;
disabled?: boolean;
className?: string;
onChange();
onClose(): void; // gets called after onChange on Key.ENTER
}
// Semantic component for representing a styled role=menuitemradio
export const StyledMenuItemRadio: React.FC<IStyledMenuItemRadioProps> = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
const onKeyDown = (e) => {
if (e.key === Key.ENTER || e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
onChange();
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
if (e.key === Key.ENTER) {
onClose();
}
}
};
const onKeyUp = (e) => {
// prevent the input default handler as we handle it on keydown to match
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
if (e.key === Key.SPACE || e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
}
};
return (
<StyledRadioButton
{...props}
role="menuitemradio"
aria-checked={checked}
checked={checked}
aria-disabled={disabled}
tabIndex={-1}
aria-label={label}
onChange={onChange}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
>
{ children }
</StyledRadioButton>
);
};
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
const left = elementRect.right + window.pageXOffset + 3;

View File

@@ -30,7 +30,9 @@ import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import ResizeNotifier from "../../utils/ResizeNotifier";
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore, { RoomListStore2, LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
import {Key} from "../../Keyboard";
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@@ -54,9 +56,19 @@ interface IState {
showTagPanel: boolean;
}
// List of CSS classes which should be included in keyboard navigation within the room list
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSublist2_headerText",
"mx_RoomTile2",
"mx_RoomSublist2_showNButton",
];
export default class LeftPanel2 extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string;
private focusedElement = null;
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
@@ -113,6 +125,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
const headerStickyWidth = rlRect.width - headerRightMargin;
let gotBottom = false;
let lastTopHeader;
for (const sublist of sublists) {
const slRect = sublist.getBoundingClientRect();
@@ -122,19 +135,25 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `${headerStickyWidth}px`;
header.style.top = `unset`;
header.style.removeProperty("top");
gotBottom = true;
} else if (slRect.top < top) {
} else if ((slRect.top - (headerHeight / 3)) < top) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
header.style.width = `${headerStickyWidth}px`;
header.style.top = `${rlRect.top}px`;
if (lastTopHeader) {
lastTopHeader.style.display = "none";
}
// first unset it, if set in last iteration
header.style.removeProperty("display");
lastTopHeader = header;
} else {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `unset`;
header.style.top = `unset`;
header.style.removeProperty("width");
header.style.removeProperty("top");
}
}
}
@@ -150,13 +169,76 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
this.handleStickyHeaders(this.listContainerRef.current);
};
private onFocus = (ev: React.FocusEvent) => {
this.focusedElement = ev.target;
};
private onBlur = () => {
this.focusedElement = null;
};
private onKeyDown = (ev: React.KeyboardEvent) => {
if (!this.focusedElement) return;
switch (ev.key) {
case Key.ARROW_UP:
case Key.ARROW_DOWN:
ev.stopPropagation();
ev.preventDefault();
this.onMoveFocus(ev.key === Key.ARROW_UP);
break;
}
};
private onMoveFocus = (up: boolean) => {
let element = this.focusedElement;
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes: DOMTokenList;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
classes = element.classList;
}
} while (element && !cssClasses.some(c => classes.contains(c)));
if (element) {
element.focus();
this.focusedElement = element;
}
};
private renderHeader(): React.ReactNode {
let breadcrumbs;
if (this.state.showBreadcrumbs) {
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
breadcrumbs = (
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar">
{this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
</div>
<IndicatorScrollbar
className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar"
verticalScrollsHorizontally={true}
>
<RoomBreadcrumbs2 />
</IndicatorScrollbar>
);
}
@@ -170,13 +252,22 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private renderSearchExplore(): React.ReactNode {
return (
<div className="mx_LeftPanel2_filterContainer">
<RoomSearch onQueryUpdate={this.onSearch} isMinimized={this.props.isMinimized} />
<div
className="mx_LeftPanel2_filterContainer"
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
>
<RoomSearch
onQueryUpdate={this.onSearch}
isMinimized={this.props.isMinimized}
onVerticalArrow={this.onKeyDown}
/>
<AccessibleButton
tabIndex={-1}
className='mx_LeftPanel2_exploreButton'
// TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
className="mx_LeftPanel2_exploreButton"
onClick={this.onExplore}
alt={_t("Explore rooms")}
title={_t("Explore rooms")}
/>
</div>
);
@@ -189,15 +280,15 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
</div>
);
// TODO: Determine what these onWhatever handlers do: https://github.com/vector-im/riot-web/issues/14180
const roomList = <RoomList2
onKeyDown={() => {/*TODO*/}}
onKeyDown={this.onKeyDown}
resizeNotifier={null}
collapsed={false}
searchFilter={this.state.searchFilter}
onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}}
onFocus={this.onFocus}
onBlur={this.onBlur}
isMinimized={this.props.isMinimized}
onResize={this.onResize}
/>;
// TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
@@ -223,7 +314,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
className={roomListClasses}
onScroll={this.onScroll}
ref={this.listContainerRef}
>{roomList}</div>
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>
{roomList}
</div>
</aside>
</div>
);

View File

@@ -146,6 +146,7 @@ class LoggedInView extends React.Component<IProps, IState> {
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _sessionStore: sessionStore;
protected readonly _sessionStoreToken: { remove: () => void };
protected readonly _compactLayoutWatcherRef: string;
protected resizer: Resizer;
constructor(props, context) {
@@ -177,6 +178,10 @@ class LoggedInView extends React.Component<IProps, IState> {
this._matrixClient.on("sync", this.onSync);
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
this._compactLayoutWatcherRef = SettingsStore.watchSetting(
"useCompactLayout", null, this.onCompactLayoutChanged,
);
fixupColorFonts();
this._roomView = React.createRef();
@@ -194,6 +199,7 @@ class LoggedInView extends React.Component<IProps, IState> {
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
@@ -263,16 +269,17 @@ class LoggedInView extends React.Component<IProps, IState> {
}
onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout,
});
}
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"});
}
};
onCompactLayoutChanged = (setting, roomId, level, valueAtLevel, newValue) => {
this.setState({
useCompactLayout: valueAtLevel,
});
};
onSync = (syncState, oldSyncState, data) => {
const oldErrCode = (
this.state.syncErrorData &&

View File

@@ -38,6 +38,7 @@ import { Action } from "../../dispatcher/actions";
interface IProps {
onQueryUpdate: (newQuery: string) => void;
isMinimized: boolean;
onVerticalArrow(ev: React.KeyboardEvent);
}
interface IState {
@@ -111,6 +112,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
if (ev.key === Key.ESCAPE) {
this.clearInput();
defaultDispatcher.fire(Action.FocusComposer);
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
this.props.onVerticalArrow(ev);
}
};
@@ -146,7 +149,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
let clearButton = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_clearButton'
title={_t("Clear filter")}
className="mx_RoomSearch_clearButton"
onClick={this.clearInput}
/>
);
@@ -154,8 +158,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
if (this.props.isMinimized) {
icon = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_icon'
title={_t("Search rooms")}
className="mx_RoomSearch_icon"
onClick={this.openSearch}
/>
);

View File

@@ -14,27 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import {createRef} from "react";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import React, { createRef } from "react";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
import {ActionPayload} from "../../dispatcher/payloads";
import {Action} from "../../dispatcher/actions";
import {_t} from "../../languageHandler";
import {ChevronFace, ContextMenu, ContextMenuButton} from "./ContextMenu";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu";
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
import {OpenToTabPayload} from "../../dispatcher/payloads/OpenToTabPayload";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import {getCustomTheme} from "../../theme";
import {getHostingLink} from "../../utils/HostingLink";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
import {ButtonEvent} from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import {getHomePageUrl} from "../../utils/pages";
import {OwnProfileStore} from "../../stores/OwnProfileStore";
import {UPDATE_EVENT} from "../../stores/AsyncStore";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from '../views/avatars/BaseAvatar';
import classNames from "classnames";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
@@ -50,6 +49,19 @@ interface IState {
isDarkTheme: boolean;
}
interface IMenuButtonProps {
iconClassName: string;
label: string;
onClick(ev: ButtonEvent);
}
const MenuButton: React.FC<IMenuButtonProps> = ({iconClassName, label, onClick}) => {
return <MenuItem label={label} onClick={onClick}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{label}</span>
</MenuItem>;
};
export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;
@@ -102,8 +114,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
private onAction = (ev: ActionPayload) => {
if (ev.action !== Action.ToggleUserMenu) return; // not interested
// For accessibility
if (this.buttonRef.current) this.buttonRef.current.click();
if (this.state.contextMenuPosition) {
this.setState({contextMenuPosition: null});
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
}
};
private onOpenMenuClick = (ev: React.MouseEvent) => {
@@ -130,7 +145,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({contextMenuPosition: null});
};
private onSwitchThemeClick = () => {
private onSwitchThemeClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
// Disable system theme matching if the user hits this button
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
@@ -206,10 +224,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
let homeButton = null;
if (this.hasHomePage) {
homeButton = (
<AccessibleButton onClick={this.onHomeClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
<span>{_t("Home")}</span>
</AccessibleButton>
<MenuButton
iconClassName="mx_UserMenu_iconHome"
label={_t("Home")}
onClick={this.onHomeClick}
/>
);
}
@@ -246,32 +265,38 @@ export default class UserMenu extends React.Component<IProps, IState> {
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
{homeButton}
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
<span className="mx_IconizedContextMenu_label">{_t("Notification settings")}</span>
</AccessibleButton>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
<span className="mx_IconizedContextMenu_label">{_t("Security & privacy")}</span>
</AccessibleButton>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("All settings")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onShowArchived}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
<span className="mx_IconizedContextMenu_label">{_t("Archived rooms")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onProvideFeedback}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
<span className="mx_IconizedContextMenu_label">{_t("Feedback")}</span>
</AccessibleButton>
<MenuButton
iconClassName="mx_UserMenu_iconBell"
label={_t("Notification settings")}
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
/>
<MenuButton
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
/>
<MenuButton
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
<MenuButton
iconClassName="mx_UserMenu_iconArchive"
label={_t("Archived rooms")}
onClick={this.onShowArchived}
/>
<MenuButton
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>
</div>
<div className="mx_IconizedContextMenu_optionList mx_UserMenu_contextMenu_redRow">
<AccessibleButton onClick={this.onSignOutClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Sign out")}</span>
</AccessibleButton>
<MenuButton
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</div>
</div>
</ContextMenu>
@@ -303,7 +328,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
className={classes}
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("Account settings")}
label={_t("User menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
>

View File

@@ -132,7 +132,7 @@ const BaseAvatar = (props) => {
);
} else {
return (
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
{ textNode }
{ imgNode }
</span>

View File

@@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler.js';
import {_t} from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";

View File

@@ -18,6 +18,7 @@ import React from 'react';
import classnames from 'classnames';
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
outlined?: boolean;
}
interface IState {
@@ -29,7 +30,7 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
};
public render() {
const { children, className, disabled, ...otherProps } = this.props;
const { children, className, disabled, outlined, ...otherProps } = this.props;
const _className = classnames(
'mx_RadioButton',
className,
@@ -37,11 +38,12 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
"mx_RadioButton_disabled": disabled,
"mx_RadioButton_enabled": !disabled,
"mx_RadioButton_checked": this.props.checked,
"mx_RadioButton_outlined": outlined,
});
return <label className={_className}>
<input type='radio' disabled={disabled} {...otherProps} />
{/* Used to render the radio button circle */}
<div><div></div></div>
<div><div /></div>
<div className="mx_RadioButton_content">{children}</div>
<div className="mx_RadioButton_spacer" />
</label>;

View File

@@ -32,10 +32,11 @@ interface IProps<T extends string> {
className?: string;
definitions: IDefinition<T>[];
value?: T; // if not provided no options will be selected
outlined?: boolean;
onChange(newValue: T);
}
function StyledRadioGroup<T extends string>({name, definitions, value, className, onChange}: IProps<T>) {
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
const _onChange = e => {
onChange(e.target.value);
};
@@ -49,6 +50,7 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
name={name}
value={d.value}
disabled={d.disabled}
outlined={outlined}
>
{d.label}
</StyledRadioButton>

View File

@@ -51,6 +51,7 @@ interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
onFocus: (ev: React.FocusEvent) => void;
onBlur: (ev: React.FocusEvent) => void;
onResize: () => void;
resizeNotifier: ResizeNotifier;
collapsed: boolean;
searchFilter: string;
@@ -63,8 +64,6 @@ interface IState {
}
const TAG_ORDER: TagID[] = [
// -- Community Invites Placeholder --
DefaultTagID.Invite,
DefaultTagID.Favourite,
DefaultTagID.DM,
@@ -76,7 +75,6 @@ const TAG_ORDER: TagID[] = [
DefaultTagID.ServerNotice,
DefaultTagID.Archived,
];
const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite;
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
const ALWAYS_VISIBLE_TAGS: TagID[] = [
DefaultTagID.DM,
@@ -183,14 +181,16 @@ export default class RoomList2 extends React.Component<IProps, IState> {
layoutMap.set(tagId, new ListLayout(tagId));
}
this.setState({sublists: newLists, layouts: layoutMap});
this.setState({sublists: newLists, layouts: layoutMap}, () => {
this.props.onResize();
});
};
private renderCommunityInvites(): React.ReactElement[] {
// TODO: Put community invites in a more sensible place (not in the room list)
return MatrixClientPeg.get().getGroups().filter(g => {
if (g.myMembership !== 'invite') return false;
return !this.searchFilter || this.searchFilter.matches(g.name);
return !this.searchFilter || this.searchFilter.matches(g.name || "");
}).map(g => {
const avatar = (
<GroupAvatar
@@ -224,17 +224,15 @@ export default class RoomList2 extends React.Component<IProps, IState> {
const components: React.ReactElement[] = [];
for (const orderedTagId of TAG_ORDER) {
if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) {
// Populate community invites if we have the chance
// TODO: Community invites: https://github.com/vector-im/riot-web/issues/14179
}
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
// Populate custom tags if needed
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091
}
const orderedRooms = this.state.sublists[orderedTagId] || [];
if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
continue; // skip tag - not needed
}
@@ -242,7 +240,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
components.push(
<RoomSublist2
key={`sublist-${orderedTagId}`}
@@ -256,6 +253,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}
/>
);
@@ -276,9 +274,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
className="mx_RoomList2"
role="tree"
aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>{sublists}</div>
)}
</RovingTabIndexProvider>

View File

@@ -26,17 +26,22 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import StyledCheckbox from "../elements/StyledCheckbox";
import StyledRadioButton from "../elements/StyledRadioButton";
import {
ChevronFace,
ContextMenu,
ContextMenuButton,
StyledMenuItemCheckbox,
StyledMenuItemRadio,
} from "../../structures/ContextMenu";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher";
import NotificationBadge from "./NotificationBadge";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import Tooltip from "../elements/Tooltip";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Key } from "../../../Keyboard";
import StyledCheckbox from "../elements/StyledCheckbox";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@@ -62,9 +67,10 @@ interface IProps {
onAddRoom?: () => void;
addRoomLabel: string;
isInvite: boolean;
layout: ListLayout;
layout?: ListLayout;
isMinimized: boolean;
tagId: TagID;
onResize: () => void;
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
// You should feel bad if you use this.
@@ -82,6 +88,9 @@ interface IState {
}
export default class RoomSublist2 extends React.Component<IProps, IState> {
private headerButton = createRef<HTMLDivElement>();
private sublistRef = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
@@ -132,11 +141,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private onShowAllClick = () => {
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onShowLessClick = () => {
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@@ -199,6 +210,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
});
}
};
@@ -217,8 +229,53 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
sublist.scrollIntoView({behavior: 'smooth'});
} else {
// on screen - toggle collapse
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
this.toggleCollapsed();
}
};
private toggleCollapsed = () => {
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
};
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
const isCollapsed = this.props.layout && this.props.layout.isCollapsed;
switch (ev.key) {
case Key.ARROW_LEFT:
ev.stopPropagation();
if (!isCollapsed) {
// On ARROW_LEFT collapse the room sublist if it isn't already
this.toggleCollapsed();
}
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
if (isCollapsed) {
// On ARROW_RIGHT expand the room sublist if it isn't already
this.toggleCollapsed();
} else if (this.sublistRef.current) {
// otherwise focus the first room
const element = this.sublistRef.current.querySelector(".mx_RoomTile2") as HTMLDivElement;
if (element) {
element.focus();
}
}
break;
}
}
};
private onKeyDown = (ev: React.KeyboardEvent) => {
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();
}
};
@@ -230,10 +287,6 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const tiles: React.ReactElement[] = [];
if (this.props.extraBadTilesThatShouldntExist) {
tiles.push(...this.props.extraBadTilesThatShouldntExist);
}
if (this.props.rooms) {
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
for (const room of visibleRooms) {
@@ -249,6 +302,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
}
if (this.props.extraBadTilesThatShouldntExist) {
tiles.push(...this.props.extraBadTilesThatShouldntExist);
}
// We only have to do this because of the extra tiles. We do it conditionally
// to avoid spending cycles on slicing. It's generally fine to do this though
// as users are unlikely to have more than a handful of tiles when the extra
@@ -261,15 +318,42 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
private renderMenu(): React.ReactElement {
// TODO: Get a proper invite context menu, or take invites out of the room list.
if (this.props.tagId === DefaultTagID.Invite) {
return null;
}
let contextMenu = null;
if (this.state.contextMenuPosition) {
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
// Invites don't get some nonsense options, so only add them if we have to.
let otherSections = null;
if (this.props.tagId !== DefaultTagID.Invite) {
otherSections = (
<React.Fragment>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onUnreadFirstChanged}
checked={isUnreadFirst}
>
{_t("Always show first")}
</StyledMenuItemCheckbox>
</div>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onMessagePreviewChanged}
checked={this.props.layout.showPreviews}
>
{_t("Message preview")}
</StyledMenuItemCheckbox>
</div>
</React.Fragment>
);
}
contextMenu = (
<ContextMenu
chevronFace={ChevronFace.None}
@@ -280,41 +364,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<div className="mx_RoomSublist2_contextMenu">
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
<StyledRadioButton
<StyledMenuItemRadio
onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
checked={!isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{_t("Activity")}
</StyledRadioButton>
<StyledRadioButton
</StyledMenuItemRadio>
<StyledMenuItemRadio
onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
checked={isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{_t("A-Z")}
</StyledRadioButton>
</div>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
<StyledCheckbox
onChange={this.onUnreadFirstChanged}
checked={isUnreadFirst}
>
{_t("Always show first")}
</StyledCheckbox>
</div>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
<StyledCheckbox
onChange={this.onMessagePreviewChanged}
checked={this.props.layout.showPreviews}
>
{_t("Message preview")}
</StyledCheckbox>
</StyledMenuItemRadio>
</div>
{otherSections}
</div>
</ContextMenu>
);
@@ -335,17 +402,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private renderHeader(): React.ReactElement {
return (
<RovingTabIndexWrapper>
<RovingTabIndexWrapper inputRef={this.headerButton}>
{({onFocus, isActive, ref}) => {
// TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180
const tabIndex = isActive ? 0 : -1;
let ariaLabel = _t("Jump to first unread room.");
if (this.props.tagId === DefaultTagID.Invite) {
ariaLabel = _t("Jump to first invite.");
}
const badge = (
<NotificationBadge
forceCount={true}
notification={this.state.notificationState}
onClick={this.onBadgeClick}
tabIndex={tabIndex}
aria-label={ariaLabel}
/>
);
@@ -386,14 +458,15 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// doesn't become sticky.
// The same applies to the notification badge.
return (
<div className={classes}>
<div className='mx_RoomSublist2_stickable'>
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
<div className="mx_RoomSublist2_stickable">
<AccessibleButton
onFocus={onFocus}
inputRef={ref}
tabIndex={tabIndex}
className={"mx_RoomSublist2_headerText"}
className="mx_RoomSublist2_headerText"
role="treeitem"
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
aria-level={1}
onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu}
@@ -449,12 +522,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) showMoreText = null;
showNButton = (
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
<AccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses} tabIndex={-1}>
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showMoreText}
</div>
</AccessibleButton>
);
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
@@ -464,13 +537,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
</span>
);
if (this.props.isMinimized) showLessText = null;
// TODO Roving tab index / treeitem?: https://github.com/vector-im/riot-web/issues/14180
showNButton = (
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
<AccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses} tabIndex={-1}>
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showLessText}
</div>
</AccessibleButton>
);
}
@@ -520,12 +594,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
);
}
// TODO: onKeyDown support: https://github.com/vector-im/riot-web/issues/14180
return (
<div
ref={this.sublistRef}
className={classes}
role="group"
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>
{this.renderHeader()}
{content}

View File

@@ -26,17 +26,31 @@ import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import {ChevronFace, ContextMenu, ContextMenuButton, MenuItemRadio} from "../../structures/ContextMenu";
import {
ChevronFace,
ContextMenu,
ContextMenuButton,
MenuItemRadio,
MenuItemCheckbox,
MenuItem,
} from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import {
getRoomNotifsState,
setRoomNotifsState,
ALL_MESSAGES,
ALL_MESSAGES_LOUD,
MENTIONS_ONLY,
MUTE,
} from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { setRoomNotifsState } from "../../../RoomNotifs";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
import { INotificationState } from "../../../stores/notifications/INotificationState";
import NotificationBadge from "./NotificationBadge";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { Volume } from "../../../RoomNotifsTypes";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@@ -68,6 +82,8 @@ interface IState {
generalMenuPosition: PartialDOMRect;
}
const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
const contextMenuBelow = (elementRect: PartialDOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset - 9;
@@ -123,6 +139,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
}
private get showMessagePreview(): boolean {
return !this.props.isMinimized && this.props.showMessagePreview;
}
public componentWillUnmount() {
if (this.props.room) {
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
@@ -195,6 +215,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({generalMenuPosition: null}); // hide the menu
}
};
private onLeaveRoomClick = (ev: ButtonEvent) => {
@@ -219,11 +244,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.setState({generalMenuPosition: null}); // hide the menu
};
private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
ev.preventDefault();
ev.stopPropagation();
if (MatrixClientPeg.get().isGuest()) return;
// get key before we go async and React discards the nativeEvent
const key = (ev as React.KeyboardEvent).key;
try {
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
await setRoomNotifsState(this.props.room.roomId, newState);
@@ -233,7 +260,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
console.error(error);
}
this.setState({notificationsMenuPosition: null}); // Close the context menu
if (key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({notificationsMenuPosition: null}); // hide the menu
}
}
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
@@ -241,7 +271,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
private onClickMute = ev => this.saveNotifState(ev, MUTE);
private renderNotificationsMenu(): React.ReactElement {
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
if (MatrixClientPeg.get().isGuest() || !this.showContextMenu) {
// the menu makes no sense in these cases so do not show one
return null;
@@ -287,7 +317,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const classes = classNames("mx_RoomTile2_notificationsButton", {
// Show bell icon for the default case too.
mx_RoomTile2_iconBell: state === state === ALL_MESSAGES,
mx_RoomTile2_iconBell: state === ALL_MESSAGES,
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile2_iconBellCrossed: state === MUTE,
@@ -304,6 +334,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
onClick={this.onNotificationsMenuOpenClick}
label={_t("Notification options")}
isExpanded={!!this.state.notificationsMenuPosition}
tabIndex={isActive ? 0 : -1}
/>
{contextMenu}
</React.Fragment>
@@ -321,20 +352,24 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<MenuItemCheckbox
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
label={_t("Favourite")}
>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onOpenRoomSettings}>
</MenuItemCheckbox>
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
</AccessibleButton>
</MenuItem>
</div>
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
</AccessibleButton>
</MenuItem>
</div>
</div>
</ContextMenu>
@@ -374,8 +409,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
let badge: React.ReactNode;
if (!this.props.isMinimized) {
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
badge = (
<div className="mx_RoomTile2_badgeContainer">
<div className="mx_RoomTile2_badgeContainer" aria-hidden="true">
<NotificationBadge
notification={this.state.notificationState}
forceCount={false}
@@ -391,24 +427,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let messagePreview = null;
if (this.props.showMessagePreview && !this.props.isMinimized) {
if (this.showMessagePreview) {
// The preview store heavily caches this info, so should be safe to hammer.
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
// Only show the preview if there is one to show.
if (text) {
messagePreview = (
<div className="mx_RoomTile2_messagePreview">
<div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
{text}
</div>
);
}
}
const notificationColor = this.state.notificationState.color;
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameWithPreview": !!messagePreview,
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold,
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
});
let nameContainer = (
@@ -421,6 +458,27 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) nameContainer = null;
let ariaLabel = name;
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
if (this.props.tag === DefaultTagID.Invite) {
// append nothing
} else if (notificationColor >= NotificationColor.Red) {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: this.state.notificationState.count,
});
} else if (notificationColor >= NotificationColor.Grey) {
ariaLabel += " " + _t("%(count)s unread messages.", {
count: this.state.notificationState.count,
});
} else if (notificationColor >= NotificationColor.Bold) {
ariaLabel += " " + _t("Unread messages.");
}
let ariaDescribedBy: string;
if (this.showMessagePreview) {
ariaDescribedBy = messagePreviewId(this.props.room.roomId);
}
return (
<React.Fragment>
<RovingTabIndexWrapper>
@@ -433,14 +491,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.onTileClick}
role="treeitem"
onContextMenu={this.onContextMenu}
role="treeitem"
aria-label={ariaLabel}
aria-selected={this.state.selected}
aria-describedby={ariaDescribedBy}
>
{roomAvatar}
{nameContainer}
{badge}
{this.renderNotificationsMenu()}
{this.renderGeneralMenu()}
{this.renderNotificationsMenu(isActive)}
</AccessibleButton>
}
</RovingTabIndexWrapper>

View File

@@ -34,6 +34,7 @@ import SettingsFlag from '../../../elements/SettingsFlag';
import Field from '../../../elements/Field';
import EventTilePreview from '../../../elements/EventTilePreview';
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import classNames from 'classnames';
interface IProps {
}
@@ -288,10 +289,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
}))}
onChange={this.onThemeChange}
value={this.state.useSystemTheme ? undefined : this.state.theme}
outlined
/>
</div>
{customThemeForm}
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} useCheckbox={true} />
</div>
);
}
@@ -343,8 +344,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons" >
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButton">
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.useIRCLayout,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
@@ -360,7 +363,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
</StyledRadioButton>
</div>
<div className="mx_AppearanceUserSettingsTab_spacer" />
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButton">
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: !this.state.useIRCLayout,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}

View File

@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ReactChild} from "react";
import React, {ReactNode} from "react";
import FormButton from "../elements/FormButton";
import {XOR} from "../../../@types/common";
export interface IProps {
description: ReactChild;
description: ReactNode;
acceptLabel: string;
onAccept();