Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Refactor ContextMenu to use RovingTabIndex (more consistent keybo…
Browse files Browse the repository at this point in the history
…ard navigation accessibility) (#7353)

Split off from #7339
  • Loading branch information
MadLittleMods authored Dec 17, 2021
1 parent 6761ef9 commit 9289c0c
Show file tree
Hide file tree
Showing 14 changed files with 224 additions and 160 deletions.
41 changes: 39 additions & 2 deletions res/css/structures/_UserMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down
32 changes: 28 additions & 4 deletions src/accessibility/RovingTabIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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) {
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
14 changes: 6 additions & 8 deletions src/accessibility/context_menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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>
);
};

9 changes: 4 additions & 5 deletions src/accessibility/context_menu/MenuItemCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,25 @@ 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;
}

// 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>
);
};
9 changes: 4 additions & 5 deletions src/accessibility/context_menu/MenuItemRadio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,25 @@ 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;
}

// 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>
);
};
7 changes: 6 additions & 1 deletion src/accessibility/context_menu/StyledMenuItemCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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();
Expand All @@ -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>
Expand Down
7 changes: 6 additions & 1 deletion src/accessibility/context_menu/StyledMenuItemRadio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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();
Expand All @@ -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>
Expand Down
Loading

0 comments on commit 9289c0c

Please sign in to comment.