mirror of https://github.com/vector-im/riot-web
Refactor `ContextMenu` to use `RovingTabIndex` (more consistent keyboard navigation accessibility) (#7353)
Split off from https://github.com/matrix-org/matrix-react-sdk/pull/7339pull/21833/head
parent
6761ef9540
commit
9289c0c90f
|
@ -208,14 +208,51 @@ limitations under the License.
|
|||
.mx_UserMenu_CustomStatusSection {
|
||||
margin: 0 12px 8px;
|
||||
|
||||
.mx_UserMenu_CustomStatusSection_input {
|
||||
.mx_UserMenu_CustomStatusSection_field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
> input {
|
||||
&.mx_UserMenu_CustomStatusSection_field_hasQuery {
|
||||
.mx_UserMenu_CustomStatusSection_clear {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> .mx_UserMenu_CustomStatusSection_input {
|
||||
border: 1px solid $accent;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
|
||||
&:focus + .mx_UserMenu_CustomStatusSection_clear {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> .mx_UserMenu_CustomStatusSection_clear {
|
||||
display: none;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
background-color: $quinary-content;
|
||||
border-radius: 50%;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
mask-image: url('$(res)/img/feather-customised/x.svg');
|
||||
mask-position: center;
|
||||
mask-size: 12px;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $secondary-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,17 @@ import { FocusHandler, Ref } from "./roving/types";
|
|||
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Technique_1_Roving_tabindex
|
||||
*/
|
||||
|
||||
// Check for form elements which utilize the arrow keys for native functions
|
||||
// like many of the text input varieties.
|
||||
//
|
||||
// i.e. it's ok to press the down arrow on a radio button to move to the next
|
||||
// radio. But it's not ok to press the down arrow on a <input type="text"> to
|
||||
// move away because the down arrow should move the cursor to the end of the
|
||||
// input.
|
||||
export function checkInputableElement(el: HTMLElement): boolean {
|
||||
return el.matches('input:not([type="radio"]):not([type="checkbox"]), textarea, select, [contenteditable=true]');
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
activeRef: Ref;
|
||||
refs: Ref[];
|
||||
|
@ -187,7 +198,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
|
||||
const context = useMemo<IContext>(() => ({ state, dispatch }), [state]);
|
||||
|
||||
const onKeyDownHandler = useCallback((ev) => {
|
||||
const onKeyDownHandler = useCallback((ev: React.KeyboardEvent) => {
|
||||
if (onKeyDown) {
|
||||
onKeyDown(ev, context.state);
|
||||
if (ev.defaultPrevented) {
|
||||
|
@ -198,7 +209,18 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
let handled = false;
|
||||
let focusRef: RefObject<HTMLElement>;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||
// but allow people to move focus from it with Tab.
|
||||
if (checkInputableElement(ev.target as HTMLElement)) {
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||
focusRef = findSiblingElement(context.state.refs, idx + (ev.shiftKey ? -1 : 1), ev.shiftKey);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// check if we actually have any items
|
||||
switch (ev.key) {
|
||||
case Key.HOME:
|
||||
|
@ -270,9 +292,11 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
// onFocus should be called when the index gained focus in any manner
|
||||
// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}`
|
||||
// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition
|
||||
export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => {
|
||||
export const useRovingTabIndex = <T extends HTMLElement>(
|
||||
inputRef?: RefObject<T>,
|
||||
): [FocusHandler, boolean, RefObject<T>] => {
|
||||
const context = useContext(RovingTabIndexContext);
|
||||
let ref = useRef<HTMLElement>(null);
|
||||
let ref = useRef<T>(null);
|
||||
|
||||
if (inputRef) {
|
||||
// if we are given a ref, use it instead of ours
|
||||
|
|
|
@ -18,10 +18,9 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
|
||||
import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../RovingTabIndex";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
interface IProps extends React.ComponentProps<typeof RovingAccessibleButton> {
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
@ -31,15 +30,14 @@ export const MenuItem: React.FC<IProps> = ({ children, label, tooltip, ...props
|
|||
const ariaLabel = props["aria-label"] || label;
|
||||
|
||||
if (tooltip) {
|
||||
return <AccessibleTooltipButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel} title={tooltip}>
|
||||
return <RovingAccessibleTooltipButton {...props} role="menuitem" aria-label={ariaLabel} title={tooltip}>
|
||||
{ children }
|
||||
</AccessibleTooltipButton>;
|
||||
</RovingAccessibleTooltipButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
|
||||
<RovingAccessibleButton {...props} role="menuitem" aria-label={ariaLabel}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||
import { RovingAccessibleButton } from "../RovingTabIndex";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
interface IProps extends React.ComponentProps<typeof RovingAccessibleButton> {
|
||||
label?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
@ -28,16 +28,15 @@ interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
|||
// Semantic component for representing a role=menuitemcheckbox
|
||||
export const MenuItemCheckbox: React.FC<IProps> = ({ children, label, active, disabled, ...props }) => {
|
||||
return (
|
||||
<AccessibleButton
|
||||
<RovingAccessibleButton
|
||||
{...props}
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={active}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
tabIndex={-1}
|
||||
aria-label={label}
|
||||
>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,9 +18,9 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||
import { RovingAccessibleButton } from "../RovingTabIndex";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
interface IProps extends React.ComponentProps<typeof RovingAccessibleButton> {
|
||||
label?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
@ -28,16 +28,15 @@ interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
|||
// Semantic component for representing a role=menuitemradio
|
||||
export const MenuItemRadio: React.FC<IProps> = ({ children, label, active, disabled, ...props }) => {
|
||||
return (
|
||||
<AccessibleButton
|
||||
<RovingAccessibleButton
|
||||
{...props}
|
||||
role="menuitemradio"
|
||||
aria-checked={active}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
tabIndex={-1}
|
||||
aria-label={label}
|
||||
>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import { Key } from "../../Keyboard";
|
||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||
import StyledCheckbox from "../../components/views/elements/StyledCheckbox";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
||||
|
@ -29,6 +30,8 @@ interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
|||
|
||||
// Semantic component for representing a styled role=menuitemcheckbox
|
||||
export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onChange, onClose, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||
e.stopPropagation();
|
||||
|
@ -52,11 +55,13 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onCh
|
|||
<StyledCheckbox
|
||||
{...props}
|
||||
role="menuitemcheckbox"
|
||||
tabIndex={-1}
|
||||
aria-label={label}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={onFocus}
|
||||
inputRef={ref}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ children }
|
||||
</StyledCheckbox>
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import { Key } from "../../Keyboard";
|
||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||
import StyledRadioButton from "../../components/views/elements/StyledRadioButton";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof StyledRadioButton> {
|
||||
|
@ -29,6 +30,8 @@ interface IProps extends React.ComponentProps<typeof StyledRadioButton> {
|
|||
|
||||
// Semantic component for representing a styled role=menuitemradio
|
||||
export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChange, onClose, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||
e.stopPropagation();
|
||||
|
@ -52,11 +55,13 @@ export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChang
|
|||
<StyledRadioButton
|
||||
{...props}
|
||||
role="menuitemradio"
|
||||
tabIndex={-1}
|
||||
aria-label={label}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={onFocus}
|
||||
inputRef={ref}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ children }
|
||||
</StyledRadioButton>
|
||||
|
|
|
@ -25,7 +25,7 @@ import { Key } from "../../Keyboard";
|
|||
import { Writeable } from "../../@types/common";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import { getInputableElement } from "./LoggedInView";
|
||||
import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -180,108 +180,39 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
if (this.props.onFinished) this.props.onFinished();
|
||||
};
|
||||
|
||||
private onMoveFocus = (element: Element, up: boolean) => {
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
|
||||
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) {
|
||||
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
}
|
||||
} while (element && !element.getAttribute("role")?.startsWith("menuitem"));
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
private onMoveFocusHomeEnd = (element: Element, up: boolean) => {
|
||||
let results = element.querySelectorAll('[role^="menuitem"]');
|
||||
if (!results) {
|
||||
results = element.querySelectorAll('[tab-index]');
|
||||
}
|
||||
if (results && results.length) {
|
||||
if (up) {
|
||||
(results[0] as HTMLElement).focus();
|
||||
} else {
|
||||
(results[results.length - 1] as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
// Don't allow clicks to escape the context menu wrapper
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
// We now only handle closing the ContextMenu in this keyDown handler.
|
||||
// All of the item/option navigation is delegated to RovingTabIndex.
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
// don't let keyboard handling escape the context menu
|
||||
ev.stopPropagation();
|
||||
|
||||
// If someone is managing their own focus, we will only exit for them with Escape.
|
||||
// They are probably using props.focusLock along with this option as well.
|
||||
if (!this.props.managed) {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
this.props.onFinished();
|
||||
ev.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// only handle escape when in an input field
|
||||
if (ev.key !== Key.ESCAPE && getInputableElement(ev.target as HTMLElement)) return;
|
||||
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
// XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils
|
||||
// to inherit proper handling of unmount edge cases
|
||||
case Key.TAB:
|
||||
case Key.ESCAPE:
|
||||
case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a <Toolbar />
|
||||
case Key.ARROW_RIGHT:
|
||||
this.props.onFinished();
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this.onMoveFocus(ev.target as Element, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
this.onMoveFocus(ev.target as Element, false);
|
||||
break;
|
||||
case Key.HOME:
|
||||
this.onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
||||
break;
|
||||
case Key.END:
|
||||
this.onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
// When an <input> is focused, only handle the Escape key
|
||||
if (checkInputableElement(ev.target as HTMLElement) && ev.key !== Key.ESCAPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
// consume all other keys in context menu
|
||||
ev.preventDefault();
|
||||
if (
|
||||
ev.key === Key.ESCAPE ||
|
||||
// You can only navigate the ContextMenu by arrow keys and Home/End (see RovingTabIndex).
|
||||
// Tabbing to the next section of the page, will close the ContextMenu.
|
||||
ev.key === Key.TAB ||
|
||||
// When someone moves left or right along a <Toolbar /> (like the
|
||||
// MessageActionBar), we should close any ContextMenu that is open.
|
||||
ev.key === Key.ARROW_LEFT ||
|
||||
ev.key === Key.ARROW_RIGHT
|
||||
) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -408,23 +339,27 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
style={{ ...position, ...wrapperStyle }}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
{ background }
|
||||
<div
|
||||
className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContextMenuRect}
|
||||
role={this.props.managed ? "menu" : undefined}
|
||||
>
|
||||
{ body }
|
||||
</div>
|
||||
</div>
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.onKeyDown}>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
<div
|
||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
style={{ ...position, ...wrapperStyle }}
|
||||
onClick={this.onClick}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
{ background }
|
||||
<div
|
||||
className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContextMenuRect}
|
||||
role={this.props.managed ? "menu" : undefined}
|
||||
>
|
||||
{ body }
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,10 @@ import LegacyCommunityPreview from "./LegacyCommunityPreview";
|
|||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
const MAX_PINNED_NOTICES_PER_ROOM = 2;
|
||||
|
||||
export function getInputableElement(el: HTMLElement): HTMLElement | null {
|
||||
// Used to find the closest inputable thing. Because of how our composer works,
|
||||
// your caret might be within a paragraph/font/div/whatever within the
|
||||
// contenteditable rather than directly in something inputable.
|
||||
function getInputableElement(el: HTMLElement): HTMLElement | null {
|
||||
return el.closest("input, textarea, select, [contenteditable=true]");
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, useContext, useState } from "react";
|
||||
import React, { createRef, useContext, useRef, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import classNames from "classnames";
|
||||
|
@ -33,13 +33,17 @@ import Modal from "../../Modal";
|
|||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme";
|
||||
import {
|
||||
RovingAccessibleButton,
|
||||
RovingAccessibleTooltipButton,
|
||||
useRovingTabIndex,
|
||||
} from "../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { 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 BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuCheckbox,
|
||||
|
@ -61,30 +65,43 @@ const CustomStatusSection = () => {
|
|||
const setStatus = cli.getUser(cli.getUserId()).unstable_statusMessage || "";
|
||||
const [value, setValue] = useState(setStatus);
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [onFocus, isActive] = useRovingTabIndex(ref);
|
||||
|
||||
const classes = classNames({
|
||||
'mx_UserMenu_CustomStatusSection_field': true,
|
||||
'mx_UserMenu_CustomStatusSection_field_hasQuery': value,
|
||||
});
|
||||
|
||||
let details: JSX.Element;
|
||||
if (value !== setStatus) {
|
||||
details = <>
|
||||
<p>{ _t("Your status will be shown to people you have a DM with.") }</p>
|
||||
|
||||
<AccessibleButton
|
||||
<RovingAccessibleButton
|
||||
onClick={() => cli._unstable_setStatusMessage(value)}
|
||||
kind="primary_outline"
|
||||
>
|
||||
{ value ? _t("Set status") : _t("Clear status") }
|
||||
</AccessibleButton>
|
||||
</RovingAccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div className="mx_UserMenu_CustomStatusSection">
|
||||
<div className="mx_UserMenu_CustomStatusSection_input">
|
||||
return <form className="mx_UserMenu_CustomStatusSection">
|
||||
<div className={classes}>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
className="mx_UserMenu_CustomStatusSection_input"
|
||||
onChange={e => setValue(e.target.value)}
|
||||
placeholder={_t("Set a new status")}
|
||||
autoComplete="off"
|
||||
onFocus={onFocus}
|
||||
ref={ref}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
/>
|
||||
<AccessibleButton
|
||||
// The clear button is only for mouse users
|
||||
tabIndex={-1}
|
||||
title={_t("Clear")}
|
||||
className="mx_UserMenu_CustomStatusSection_clear"
|
||||
|
@ -93,7 +110,7 @@ const CustomStatusSection = () => {
|
|||
</div>
|
||||
|
||||
{ details }
|
||||
</div>;
|
||||
</form>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
|
@ -486,7 +503,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<AccessibleTooltipButton
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
|
||||
|
@ -496,7 +513,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
alt={_t("Switch theme")}
|
||||
width={16}
|
||||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</RovingAccessibleTooltipButton>
|
||||
</div>
|
||||
{ customStatusSection }
|
||||
{ topSection }
|
||||
|
|
|
@ -26,6 +26,7 @@ export enum CheckboxStyle {
|
|||
}
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
kind?: CheckboxStyle;
|
||||
}
|
||||
|
||||
|
@ -48,7 +49,8 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
|
|||
|
||||
public render() {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { children, className, kind = CheckboxStyle.Solid, ...otherProps } = this.props;
|
||||
const { children, className, kind = CheckboxStyle.Solid, inputRef, ...otherProps } = this.props;
|
||||
|
||||
const newClassName = classnames(
|
||||
"mx_Checkbox",
|
||||
className,
|
||||
|
@ -58,7 +60,13 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
|
|||
},
|
||||
);
|
||||
return <span className={newClassName}>
|
||||
<input id={this.id} {...otherProps} type="checkbox" />
|
||||
<input
|
||||
// Pass through the ref - used for keyboard shortcut access to some buttons
|
||||
ref={inputRef}
|
||||
id={this.id}
|
||||
{...otherProps}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label htmlFor={this.id}>
|
||||
{ /* Using the div to center the image */ }
|
||||
<div className="mx_Checkbox_background">
|
||||
|
|
|
@ -20,6 +20,7 @@ import classnames from 'classnames';
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
outlined?: boolean;
|
||||
// If true (default), the children will be contained within a <label> element
|
||||
// If false, they'll be in a div. Putting interactive components that have labels
|
||||
|
@ -38,7 +39,7 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
|||
};
|
||||
|
||||
public render() {
|
||||
const { children, className, disabled, outlined, childrenInLabel, ...otherProps } = this.props;
|
||||
const { children, className, disabled, outlined, childrenInLabel, inputRef, ...otherProps } = this.props;
|
||||
const _className = classnames(
|
||||
'mx_StyledRadioButton',
|
||||
className,
|
||||
|
@ -50,7 +51,13 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
|||
});
|
||||
|
||||
const radioButton = <React.Fragment>
|
||||
<input type='radio' disabled={disabled} {...otherProps} />
|
||||
<input
|
||||
// Pass through the ref - used for keyboard shortcut access to some buttons
|
||||
ref={inputRef}
|
||||
type='radio'
|
||||
disabled={disabled}
|
||||
{...otherProps}
|
||||
/>
|
||||
{ /* Used to render the radio button circle */ }
|
||||
<div><div /></div>
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -402,6 +402,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
|
||||
private onTagSortChanged = async (sort: SortAlgorithm) => {
|
||||
await RoomListStore.instance.setTagSorting(this.props.tagId, sort);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onMessagePreviewChanged = () => {
|
||||
|
|
|
@ -45,29 +45,55 @@ exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly
|
|||
`;
|
||||
|
||||
exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option selected in the context menu 1`] = `
|
||||
<AccessibleButton
|
||||
<RovingAccessibleButton
|
||||
aria-checked={true}
|
||||
className="mx_ThreadPanel_Header_FilterOptionItem"
|
||||
element="div"
|
||||
onClick={[Function]}
|
||||
role="menuitemradio"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div
|
||||
<AccessibleButton
|
||||
aria-checked={true}
|
||||
className="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem"
|
||||
className="mx_ThreadPanel_Header_FilterOptionItem"
|
||||
element="div"
|
||||
inputRef={
|
||||
Object {
|
||||
"current": <div
|
||||
aria-checked="true"
|
||||
class="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem"
|
||||
role="menuitemradio"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span>
|
||||
All threads
|
||||
</span>
|
||||
<span>
|
||||
Shows all threads from current room
|
||||
</span>
|
||||
</div>,
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onFocus={[Function]}
|
||||
role="menuitemradio"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span>
|
||||
All threads
|
||||
</span>
|
||||
<span>
|
||||
Shows all threads from current room
|
||||
</span>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
<div
|
||||
aria-checked={true}
|
||||
className="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem"
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="menuitemradio"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span>
|
||||
All threads
|
||||
</span>
|
||||
<span>
|
||||
Shows all threads from current room
|
||||
</span>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</RovingAccessibleButton>
|
||||
`;
|
||||
|
|
Loading…
Reference in New Issue