Make corresponding changes in consumers

pull/28452/head
R Midhun Suresh 2024-11-13 00:36:08 +05:30
parent 0faf298e05
commit 86c6ba9dd7
No known key found for this signature in database
10 changed files with 70 additions and 57 deletions

View File

@ -114,7 +114,7 @@ const Tile: React.FC<ITileProps> = ({
(room.room_type === RoomType.Space ? _t("common|unnamed_space") : _t("common|unnamed_room")); (room.room_type === RoomType.Space ? _t("common|unnamed_space") : _t("common|unnamed_room"));
const [showChildren, toggleShowChildren] = useStateToggle(true); const [showChildren, toggleShowChildren] = useStateToggle(true);
const [onFocus, isActive, ref] = useRovingTabIndex(); const [onFocus, isActive, ref, nodeRef] = useRovingTabIndex();
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const onPreviewClick = (ev: ButtonEvent): void => { const onPreviewClick = (ev: ButtonEvent): void => {
@ -288,7 +288,7 @@ const Tile: React.FC<ITileProps> = ({
case KeyBindingAction.ArrowLeft: case KeyBindingAction.ArrowLeft:
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
ref.current?.focus(); nodeRef.current?.focus();
break; break;
} }
}; };
@ -315,7 +315,7 @@ const Tile: React.FC<ITileProps> = ({
case KeyBindingAction.ArrowRight: case KeyBindingAction.ArrowRight:
handled = true; handled = true;
if (showChildren) { if (showChildren) {
const childSection = ref.current?.nextElementSibling; const childSection = nodeRef.current?.nextElementSibling;
childSection?.querySelector<HTMLDivElement>(".mx_SpaceHierarchy_roomTile")?.focus(); childSection?.querySelector<HTMLDivElement>(".mx_SpaceHierarchy_roomTile")?.focus();
} else { } else {
toggleShowChildren(); toggleShowChildren();
@ -790,7 +790,7 @@ const SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, a
const onKeyDown = (ev: KeyboardEvent, state: IState): void => { const onKeyDown = (ev: KeyboardEvent, state: IState): void => {
const action = getKeyBindingsManager().getAccessibilityAction(ev); const action = getKeyBindingsManager().getAccessibilityAction(ev);
if (action === KeyBindingAction.ArrowDown && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) { if (action === KeyBindingAction.ArrowDown && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) {
state.refs[0]?.current?.focus(); state.nodes[0]?.focus();
} }
}; };

View File

@ -294,7 +294,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
const action = getKeyBindingsManager().getAccessibilityAction(ev); const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) { switch (action) {
case KeyBindingAction.Enter: { case KeyBindingAction.Enter: {
state.activeRef?.current?.querySelector<HTMLButtonElement>(".mx_ForwardList_sendButton")?.click(); state.activeNode?.querySelector<HTMLButtonElement>(".mx_ForwardList_sendButton")?.click();
break; break;
} }
@ -347,13 +347,13 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
onSearch={(query: string): void => { onSearch={(query: string): void => {
setQuery(query); setQuery(query);
setTimeout(() => { setTimeout(() => {
const ref = context.state.refs[0]; const node = context.state.nodes[0];
if (ref) { if (node) {
context.dispatch({ context.dispatch({
type: Type.SetFocus, type: Type.SetFocus,
payload: { ref }, payload: { node },
}); });
ref.current?.scrollIntoView?.({ node?.scrollIntoView?.({
block: "nearest", block: "nearest",
}); });
} }
@ -361,7 +361,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
}} }}
autoFocus={true} autoFocus={true}
onKeyDown={onKeyDownHandler} onKeyDown={onKeyDownHandler}
aria-activedescendant={context.state.activeRef?.current?.id} aria-activedescendant={context.state.activeNode?.id}
aria-owns="mx_ForwardDialog_resultsList" aria-owns="mx_ForwardDialog_resultsList"
/> />
)} )}

View File

@ -7,13 +7,12 @@ Please see LICENSE files in the repository root for full details.
*/ */
import classNames from "classnames"; import classNames from "classnames";
import React, { ReactNode, RefObject } from "react"; import React, { ReactNode } from "react";
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
interface OptionProps { interface OptionProps {
inputRef?: RefObject<HTMLLIElement>;
endAdornment?: ReactNode; endAdornment?: ReactNode;
id?: string; id?: string;
className?: string; className?: string;
@ -21,8 +20,8 @@ interface OptionProps {
children?: ReactNode; children?: ReactNode;
} }
export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment, className, ...props }) => { export const Option: React.FC<OptionProps> = ({ children, endAdornment, className, ...props }) => {
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLLIElement>(inputRef); const [onFocus, isActive, ref] = useRovingTabIndex<HTMLLIElement>();
return ( return (
<AccessibleButton <AccessibleButton
{...props} {...props}

View File

@ -20,7 +20,7 @@ import {
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { normalize } from "matrix-js-sdk/src/utils"; import { normalize } from "matrix-js-sdk/src/utils";
import React, { ChangeEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import React, { ChangeEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
@ -90,8 +90,8 @@ interface IProps {
onFinished(): void; onFinished(): void;
} }
function refIsForRecentlyViewed(ref?: RefObject<HTMLElement>): boolean { function nodeIsForRecentlyViewed(node?: HTMLElement): boolean {
return ref?.current?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true; return node?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true;
} }
function getRoomTypes(filter: Filter | null): Set<RoomType | null> { function getRoomTypes(filter: Filter | null): Set<RoomType | null> {
@ -498,13 +498,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
}; };
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
const ref = rovingContext.state.refs[0]; const node = rovingContext.state.nodes[0];
if (ref) { if (node) {
rovingContext.dispatch({ rovingContext.dispatch({
type: Type.SetFocus, type: Type.SetFocus,
payload: { ref }, payload: { node },
}); });
ref.current?.scrollIntoView?.({ node?.scrollIntoView?.({
block: "nearest", block: "nearest",
}); });
} }
@ -1128,7 +1128,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
break; break;
} }
let ref: RefObject<HTMLElement> | undefined; let node: HTMLElement | undefined;
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev); const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev);
switch (accessibilityAction) { switch (accessibilityAction) {
case KeyBindingAction.Escape: case KeyBindingAction.Escape:
@ -1141,20 +1141,20 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
if (rovingContext.state.activeRef && rovingContext.state.refs.length > 0) { if (rovingContext.state.activeNode && rovingContext.state.nodes.length > 0) {
let refs = rovingContext.state.refs; let nodes = rovingContext.state.nodes;
if (!query && !filter !== null) { if (!query && !filter !== null) {
// If the current selection is not in the recently viewed row then only include the // If the current selection is not in the recently viewed row then only include the
// first recently viewed so that is the target when the user is switching into recently viewed. // first recently viewed so that is the target when the user is switching into recently viewed.
const keptRecentlyViewedRef = refIsForRecentlyViewed(rovingContext.state.activeRef) const keptRecentlyViewedRef = nodeIsForRecentlyViewed(rovingContext.state.activeNode)
? rovingContext.state.activeRef ? rovingContext.state.activeNode
: refs.find(refIsForRecentlyViewed); : nodes.find(nodeIsForRecentlyViewed);
// exclude all other recently viewed items from the list so up/down arrows skip them // exclude all other recently viewed items from the list so up/down arrows skip them
refs = refs.filter((ref) => ref === keptRecentlyViewedRef || !refIsForRecentlyViewed(ref)); nodes = nodes.filter((ref) => ref === keptRecentlyViewedRef || !nodeIsForRecentlyViewed(ref));
} }
const idx = refs.indexOf(rovingContext.state.activeRef); const idx = nodes.indexOf(rovingContext.state.activeNode);
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1)); node = findSiblingElement(nodes, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1));
} }
break; break;
@ -1164,27 +1164,30 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
if ( if (
!query && !query &&
!filter !== null && !filter !== null &&
rovingContext.state.activeRef && rovingContext.state.activeNode &&
rovingContext.state.refs.length > 0 && rovingContext.state.nodes.length > 0 &&
refIsForRecentlyViewed(rovingContext.state.activeRef) nodeIsForRecentlyViewed(rovingContext.state.activeNode)
) { ) {
// we only intercept left/right arrows when the field is empty, and they'd do nothing anyway // we only intercept left/right arrows when the field is empty, and they'd do nothing anyway
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed); const nodes = rovingContext.state.nodes.filter(nodeIsForRecentlyViewed);
const idx = refs.indexOf(rovingContext.state.activeRef); const idx = nodes.indexOf(rovingContext.state.activeNode);
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1)); node = findSiblingElement(
nodes,
idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1),
);
} }
break; break;
} }
if (ref) { if (node) {
rovingContext.dispatch({ rovingContext.dispatch({
type: Type.SetFocus, type: Type.SetFocus,
payload: { ref }, payload: { node },
}); });
ref.current?.scrollIntoView({ node?.scrollIntoView({
block: "nearest", block: "nearest",
}); });
} }
@ -1204,12 +1207,12 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
case KeyBindingAction.Enter: case KeyBindingAction.Enter:
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
rovingContext.state.activeRef?.current?.click(); rovingContext.state.activeNode?.click();
break; break;
} }
}; };
const activeDescendant = rovingContext.state.activeRef?.current?.id; const activeDescendant = rovingContext.state.activeNode?.id;
return ( return (
<> <>

View File

@ -73,9 +73,13 @@ interface IProps {
// All other props pass through to the <input>. // All other props pass through to the <input>.
} }
type RefCallback<T extends HTMLElement> = (node: T | null) => void;
export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> { export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
// The ref pass through to the input // The ref pass through to the input
inputRef?: RefObject<HTMLInputElement>; inputRef?: RefObject<HTMLInputElement>;
// Ref callback that will be attached to the input. This takes precedence over inputRef.
refCallback?: RefCallback<HTMLInputElement>;
// The element to create. Defaults to "input". // The element to create. Defaults to "input".
element: "input"; element: "input";
// The input's value. This is a controlled component, so the value is required. // The input's value. This is a controlled component, so the value is required.
@ -85,6 +89,8 @@ export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElemen
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> { interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
// The ref pass through to the select // The ref pass through to the select
inputRef?: RefObject<HTMLSelectElement>; inputRef?: RefObject<HTMLSelectElement>;
// Ref callback that will be attached to the input. This takes precedence over inputRef.
refCallback?: RefCallback<HTMLSelectElement>;
// To define options for a select, use <Field><option ... /></Field> // To define options for a select, use <Field><option ... /></Field>
element: "select"; element: "select";
// The select's value. This is a controlled component, so the value is required. // The select's value. This is a controlled component, so the value is required.
@ -94,6 +100,8 @@ interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> { interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
// The ref pass through to the textarea // The ref pass through to the textarea
inputRef?: RefObject<HTMLTextAreaElement>; inputRef?: RefObject<HTMLTextAreaElement>;
// Ref callback that will be attached to the input. This takes precedence over inputRef.
refCallback?: RefCallback<HTMLTextAreaElement>;
element: "textarea"; element: "textarea";
// The textarea's value. This is a controlled component, so the value is required. // The textarea's value. This is a controlled component, so the value is required.
value: string; value: string;
@ -102,6 +110,8 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElem
export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> { export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
// The ref pass through to the input // The ref pass through to the input
inputRef?: RefObject<HTMLInputElement>; inputRef?: RefObject<HTMLInputElement>;
// Ref callback that will be attached to the input. This takes precedence over inputRef.
refCallback?: RefCallback<HTMLInputElement>;
element: "input"; element: "input";
// The input's value. This is a controlled component, so the value is required. // The input's value. This is a controlled component, so the value is required.
value: string; value: string;
@ -284,7 +294,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
const inputProps_: React.HTMLAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> & const inputProps_: React.HTMLAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> &
React.ClassAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> = { React.ClassAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> = {
...inputProps, ...inputProps,
ref: this.inputRef, ref: this.props.refCallback ?? this.inputRef,
}; };
const fieldInput = React.createElement(this.props.element, inputProps_, children); const fieldInput = React.createElement(this.props.element, inputProps_, children);

View File

@ -16,7 +16,7 @@ export enum CheckboxStyle {
} }
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> { interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
inputRef?: React.RefObject<HTMLInputElement>; inputRef?: (node: HTMLInputElement | null) => void;
kind?: CheckboxStyle; kind?: CheckboxStyle;
id?: string; id?: string;
} }

View File

@ -10,7 +10,7 @@ import React from "react";
import classnames from "classnames"; import classnames from "classnames";
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> { interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
inputRef?: React.RefObject<HTMLInputElement>; inputRef?: (node: HTMLInputElement | null) => void;
outlined?: boolean; outlined?: boolean;
// If true (default), the children will be contained within a <label> element // 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 // If false, they'll be in a div. Putting interactive components that have labels

View File

@ -154,22 +154,22 @@ class EmojiPicker extends React.Component<IProps, IState> {
}; };
private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void { private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void {
const node = state.activeRef?.current; const node = state.activeNode?.current;
const parent = node?.parentElement; const parent = node?.parentElement;
if (!parent || !state.activeRef) return; if (!parent || !state.activeNode) return;
const rowIndex = Array.from(parent.children).indexOf(node); const rowIndex = Array.from(parent.children).indexOf(node);
const refIndex = state.refs.indexOf(state.activeRef); const refIndex = state.nodes.indexOf(state.activeNode);
let focusRef: Ref | undefined; let focusRef: Ref | undefined;
let newParent: HTMLElement | undefined; let newParent: HTMLElement | undefined;
switch (ev.key) { switch (ev.key) {
case Key.ARROW_LEFT: case Key.ARROW_LEFT:
focusRef = state.refs[refIndex - 1]; focusRef = state.nodes[refIndex - 1];
newParent = focusRef?.current?.parentElement ?? undefined; newParent = focusRef?.current?.parentElement ?? undefined;
break; break;
case Key.ARROW_RIGHT: case Key.ARROW_RIGHT:
focusRef = state.refs[refIndex + 1]; focusRef = state.nodes[refIndex + 1];
newParent = focusRef?.current?.parentElement ?? undefined; newParent = focusRef?.current?.parentElement ?? undefined;
break; break;
@ -178,11 +178,11 @@ class EmojiPicker extends React.Component<IProps, IState> {
// For up/down we find the prev/next parent by inspecting the refs either side of our row // For up/down we find the prev/next parent by inspecting the refs either side of our row
const ref = const ref =
ev.key === Key.ARROW_UP ev.key === Key.ARROW_UP
? state.refs[refIndex - rowIndex - 1] ? state.nodes[refIndex - rowIndex - 1]
: state.refs[refIndex - rowIndex + EMOJIS_PER_ROW]; : state.nodes[refIndex - rowIndex + EMOJIS_PER_ROW];
newParent = ref?.current?.parentElement ?? undefined; newParent = ref?.current?.parentElement ?? undefined;
const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)]; const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)];
focusRef = state.refs.find((r) => r.current === newTarget); focusRef = state.nodes.find((r) => r.current === newTarget);
break; break;
} }
} }
@ -208,7 +208,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
private onKeyDown = (ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void => { private onKeyDown = (ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void => {
if ( if (
state.activeRef?.current && state.activeNode?.current &&
[Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key) [Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key)
) { ) {
this.keyboardNavigation(ev, state, dispatch); this.keyboardNavigation(ev, state, dispatch);

View File

@ -70,7 +70,7 @@ class Search extends React.PureComponent<IProps> {
onChange={(ev) => this.props.onChange(ev.target.value)} onChange={(ev) => this.props.onChange(ev.target.value)}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
ref={this.inputRef} ref={this.inputRef}
aria-activedescendant={this.context.state.activeRef?.current?.id} aria-activedescendant={this.context.state.activeNode?.id}
aria-controls="mx_EmojiPicker_body" aria-controls="mx_EmojiPicker_body"
aria-haspopup="grid" aria-haspopup="grid"
aria-autocomplete="list" aria-autocomplete="list"

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { useState, FormEvent } from "react"; import React, { useState, FormEvent, RefObject } from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Field from "../elements/Field"; import Field from "../elements/Field";
@ -23,7 +23,7 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
const dateInputDefaultValue = formatDateForInput(date); const dateInputDefaultValue = formatDateForInput(date);
const [dateValue, setDateValue] = useState(dateInputDefaultValue); const [dateValue, setDateValue] = useState(dateInputDefaultValue);
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>(); const [onFocus, isActive, refCallback, inputRef] = useRovingTabIndex<HTMLInputElement>();
const onDateValueInput = (ev: React.ChangeEvent<HTMLInputElement>): void => setDateValue(ev.target.value); const onDateValueInput = (ev: React.ChangeEvent<HTMLInputElement>): void => setDateValue(ev.target.value);
const onJumpToDateSubmit = (ev: FormEvent): void => { const onJumpToDateSubmit = (ev: FormEvent): void => {
@ -45,7 +45,8 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
className="mx_JumpToDatePicker_datePicker" className="mx_JumpToDatePicker_datePicker"
label={_t("room|jump_to_date_prompt")} label={_t("room|jump_to_date_prompt")}
onFocus={onFocus} onFocus={onFocus}
inputRef={ref} inputRef={inputRef as RefObject<HTMLInputElement>}
refCallback={refCallback}
tabIndex={isActive ? 0 : -1} tabIndex={isActive ? 0 : -1}
/> />
<RovingAccessibleButton <RovingAccessibleButton