mirror of https://github.com/vector-im/riot-web
Merge pull request #28452 from element-hq/midhun/fix-spotlight-1
commit
18ef975386
|
@ -10,7 +10,6 @@ import React, {
|
|||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useReducer,
|
||||
|
@ -18,11 +17,12 @@ import React, {
|
|||
Dispatch,
|
||||
RefObject,
|
||||
ReactNode,
|
||||
RefCallback,
|
||||
} from "react";
|
||||
|
||||
import { getKeyBindingsManager } from "../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "./KeyboardShortcuts";
|
||||
import { FocusHandler, Ref } from "./roving/types";
|
||||
import { FocusHandler } from "./roving/types";
|
||||
|
||||
/**
|
||||
* Module to simplify implementing the Roving TabIndex accessibility technique
|
||||
|
@ -49,8 +49,8 @@ export function checkInputableElement(el: HTMLElement): boolean {
|
|||
}
|
||||
|
||||
export interface IState {
|
||||
activeRef?: Ref;
|
||||
refs: Ref[];
|
||||
activeNode?: HTMLElement;
|
||||
nodes: HTMLElement[];
|
||||
}
|
||||
|
||||
export interface IContext {
|
||||
|
@ -60,7 +60,7 @@ export interface IContext {
|
|||
|
||||
export const RovingTabIndexContext = createContext<IContext>({
|
||||
state: {
|
||||
refs: [], // list of refs in DOM order
|
||||
nodes: [], // list of nodes in DOM order
|
||||
},
|
||||
dispatch: () => {},
|
||||
});
|
||||
|
@ -76,7 +76,7 @@ export enum Type {
|
|||
export interface IAction {
|
||||
type: Exclude<Type, Type.Update>;
|
||||
payload: {
|
||||
ref: Ref;
|
||||
node: HTMLElement;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -87,12 +87,12 @@ interface UpdateAction {
|
|||
|
||||
type Action = IAction | UpdateAction;
|
||||
|
||||
const refSorter = (a: Ref, b: Ref): number => {
|
||||
const nodeSorter = (a: HTMLElement, b: HTMLElement): number => {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const position = a.current!.compareDocumentPosition(b.current!);
|
||||
const position = a.compareDocumentPosition(b);
|
||||
|
||||
if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
||||
return -1;
|
||||
|
@ -106,54 +106,56 @@ const refSorter = (a: Ref, b: Ref): number => {
|
|||
export const reducer: Reducer<IState, Action> = (state: IState, action: Action) => {
|
||||
switch (action.type) {
|
||||
case Type.Register: {
|
||||
if (!state.activeRef) {
|
||||
// Our list of refs was empty, set activeRef to this first item
|
||||
state.activeRef = action.payload.ref;
|
||||
if (!state.activeNode) {
|
||||
// Our list of nodes was empty, set activeNode to this first item
|
||||
state.activeNode = action.payload.node;
|
||||
}
|
||||
|
||||
if (state.nodes.includes(action.payload.node)) return state;
|
||||
|
||||
// Sadly due to the potential of DOM elements swapping order we can't do anything fancy like a binary insert
|
||||
state.refs.push(action.payload.ref);
|
||||
state.refs.sort(refSorter);
|
||||
state.nodes.push(action.payload.node);
|
||||
state.nodes.sort(nodeSorter);
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
case Type.Unregister: {
|
||||
const oldIndex = state.refs.findIndex((r) => r === action.payload.ref);
|
||||
const oldIndex = state.nodes.findIndex((r) => r === action.payload.node);
|
||||
|
||||
if (oldIndex === -1) {
|
||||
return state; // already removed, this should not happen
|
||||
}
|
||||
|
||||
if (state.refs.splice(oldIndex, 1)[0] === state.activeRef) {
|
||||
// we just removed the active ref, need to replace it
|
||||
// pick the ref closest to the index the old ref was in
|
||||
if (oldIndex >= state.refs.length) {
|
||||
state.activeRef = findSiblingElement(state.refs, state.refs.length - 1, true);
|
||||
if (state.nodes.splice(oldIndex, 1)[0] === state.activeNode) {
|
||||
// we just removed the active node, need to replace it
|
||||
// pick the node closest to the index the old node was in
|
||||
if (oldIndex >= state.nodes.length) {
|
||||
state.activeNode = findSiblingElement(state.nodes, state.nodes.length - 1, true);
|
||||
} else {
|
||||
state.activeRef =
|
||||
findSiblingElement(state.refs, oldIndex) || findSiblingElement(state.refs, oldIndex, true);
|
||||
state.activeNode =
|
||||
findSiblingElement(state.nodes, oldIndex) || findSiblingElement(state.nodes, oldIndex, true);
|
||||
}
|
||||
if (document.activeElement === document.body) {
|
||||
// if the focus got reverted to the body then the user was likely focused on the unmounted element
|
||||
setTimeout(() => state.activeRef?.current?.focus(), 0);
|
||||
setTimeout(() => state.activeNode?.focus(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// update the refs list
|
||||
// update the nodes list
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
case Type.SetFocus: {
|
||||
// if the ref doesn't change just return the same object reference to skip a re-render
|
||||
if (state.activeRef === action.payload.ref) return state;
|
||||
// update active ref
|
||||
state.activeRef = action.payload.ref;
|
||||
// if the node doesn't change just return the same object reference to skip a re-render
|
||||
if (state.activeNode === action.payload.node) return state;
|
||||
// update active node
|
||||
state.activeNode = action.payload.node;
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
case Type.Update: {
|
||||
state.refs.sort(refSorter);
|
||||
state.nodes.sort(nodeSorter);
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
|
@ -174,28 +176,28 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const findSiblingElement = (
|
||||
refs: RefObject<HTMLElement>[],
|
||||
nodes: HTMLElement[],
|
||||
startIndex: number,
|
||||
backwards = false,
|
||||
loop = false,
|
||||
): RefObject<HTMLElement> | undefined => {
|
||||
): HTMLElement | undefined => {
|
||||
if (backwards) {
|
||||
for (let i = startIndex; i < refs.length && i >= 0; i--) {
|
||||
if (refs[i].current?.offsetParent !== null) {
|
||||
return refs[i];
|
||||
for (let i = startIndex; i < nodes.length && i >= 0; i--) {
|
||||
if (nodes[i]?.offsetParent !== null) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
if (loop) {
|
||||
return findSiblingElement(refs.slice(startIndex + 1), refs.length - 1, true, false);
|
||||
return findSiblingElement(nodes.slice(startIndex + 1), nodes.length - 1, true, false);
|
||||
}
|
||||
} else {
|
||||
for (let i = startIndex; i < refs.length && i >= 0; i++) {
|
||||
if (refs[i].current?.offsetParent !== null) {
|
||||
return refs[i];
|
||||
for (let i = startIndex; i < nodes.length && i >= 0; i++) {
|
||||
if (nodes[i]?.offsetParent !== null) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
if (loop) {
|
||||
return findSiblingElement(refs.slice(0, startIndex), 0, false, false);
|
||||
return findSiblingElement(nodes.slice(0, startIndex), 0, false, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -211,7 +213,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
onKeyDown,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer<Reducer<IState, Action>>(reducer, {
|
||||
refs: [],
|
||||
nodes: [],
|
||||
});
|
||||
|
||||
const context = useMemo<IContext>(() => ({ state, dispatch }), [state]);
|
||||
|
@ -227,17 +229,17 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
|
||||
let handled = false;
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
let focusRef: RefObject<HTMLElement> | undefined;
|
||||
let focusNode: HTMLElement | undefined;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
// but allow people to move focus from it with Tab.
|
||||
if (!handleInputFields && checkInputableElement(ev.target as HTMLElement)) {
|
||||
switch (action) {
|
||||
case KeyBindingAction.Tab:
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef!);
|
||||
focusRef = findSiblingElement(
|
||||
context.state.refs,
|
||||
if (context.state.nodes.length > 0) {
|
||||
const idx = context.state.nodes.indexOf(context.state.activeNode!);
|
||||
focusNode = findSiblingElement(
|
||||
context.state.nodes,
|
||||
idx + (ev.shiftKey ? -1 : 1),
|
||||
ev.shiftKey,
|
||||
);
|
||||
|
@ -251,7 +253,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
if (handleHomeEnd) {
|
||||
handled = true;
|
||||
// move focus to first (visible) item
|
||||
focusRef = findSiblingElement(context.state.refs, 0);
|
||||
focusNode = findSiblingElement(context.state.nodes, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -259,7 +261,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
if (handleHomeEnd) {
|
||||
handled = true;
|
||||
// move focus to last (visible) item
|
||||
focusRef = findSiblingElement(context.state.refs, context.state.refs.length - 1, true);
|
||||
focusNode = findSiblingElement(context.state.nodes, context.state.nodes.length - 1, true);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -270,9 +272,9 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
(action === KeyBindingAction.ArrowRight && handleLeftRight)
|
||||
) {
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef!);
|
||||
focusRef = findSiblingElement(context.state.refs, idx + 1, false, handleLoop);
|
||||
if (context.state.nodes.length > 0) {
|
||||
const idx = context.state.nodes.indexOf(context.state.activeNode!);
|
||||
focusNode = findSiblingElement(context.state.nodes, idx + 1, false, handleLoop);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -284,9 +286,9 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
(action === KeyBindingAction.ArrowLeft && handleLeftRight)
|
||||
) {
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef!);
|
||||
focusRef = findSiblingElement(context.state.refs, idx - 1, true, handleLoop);
|
||||
if (context.state.nodes.length > 0) {
|
||||
const idx = context.state.nodes.indexOf(context.state.activeNode!);
|
||||
focusNode = findSiblingElement(context.state.nodes, idx - 1, true, handleLoop);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -298,17 +300,17 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
if (focusRef) {
|
||||
focusRef.current?.focus();
|
||||
if (focusNode) {
|
||||
focusNode?.focus();
|
||||
// programmatic focus doesn't fire the onFocus handler, so we must do the do ourselves
|
||||
dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: {
|
||||
ref: focusRef,
|
||||
node: focusNode,
|
||||
},
|
||||
});
|
||||
if (scrollIntoView) {
|
||||
focusRef.current?.scrollIntoView(scrollIntoView);
|
||||
focusNode?.scrollIntoView(scrollIntoView);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -337,46 +339,61 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// Hook to register a roving tab index
|
||||
// inputRef parameter specifies the ref to use
|
||||
// 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
|
||||
/**
|
||||
* Hook to register a roving tab index.
|
||||
*
|
||||
* inputRef is an optional argument; when passed this ref points to the DOM element
|
||||
* to which the callback ref is attached.
|
||||
*
|
||||
* Returns:
|
||||
* 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 is a callback ref that should be passed to a DOM node which will be used for DOM compareDocumentPosition.
|
||||
* nodeRef is a ref that points to the DOM element to which the ref mentioned above is attached.
|
||||
*
|
||||
* nodeRef = inputRef when inputRef argument is provided.
|
||||
*/
|
||||
export const useRovingTabIndex = <T extends HTMLElement>(
|
||||
inputRef?: RefObject<T>,
|
||||
): [FocusHandler, boolean, RefObject<T>] => {
|
||||
): [FocusHandler, boolean, RefCallback<T>, RefObject<T | null>] => {
|
||||
const context = useContext(RovingTabIndexContext);
|
||||
let ref = useRef<T>(null);
|
||||
|
||||
let nodeRef = useRef<T | null>(null);
|
||||
|
||||
if (inputRef) {
|
||||
// if we are given a ref, use it instead of ours
|
||||
ref = inputRef;
|
||||
nodeRef = inputRef;
|
||||
}
|
||||
|
||||
// setup (after refs)
|
||||
useEffect(() => {
|
||||
const ref = useCallback((node: T | null) => {
|
||||
if (node) {
|
||||
nodeRef.current = node;
|
||||
context.dispatch({
|
||||
type: Type.Register,
|
||||
payload: { ref },
|
||||
payload: { node },
|
||||
});
|
||||
// teardown
|
||||
return () => {
|
||||
} else {
|
||||
context.dispatch({
|
||||
type: Type.Unregister,
|
||||
payload: { ref },
|
||||
payload: { node: nodeRef.current! },
|
||||
});
|
||||
};
|
||||
nodeRef.current = null;
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
if (!nodeRef.current) {
|
||||
console.warn("useRovingTabIndex.onFocus called but the react ref does not point to any DOM element!");
|
||||
return;
|
||||
}
|
||||
context.dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref },
|
||||
payload: { node: nodeRef.current },
|
||||
});
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const isActive = context.state.activeRef === ref;
|
||||
return [onFocus, isActive, ref];
|
||||
const isActive = context.state.activeNode === nodeRef.current;
|
||||
return [onFocus, isActive, ref, nodeRef];
|
||||
};
|
||||
|
||||
// re-export the semantic helper components for simplicity
|
||||
|
|
|
@ -6,14 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from "react";
|
||||
import React, { ReactElement, RefCallback } from "react";
|
||||
|
||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||
import { FocusHandler, Ref } from "./types";
|
||||
|
||||
interface IProps {
|
||||
inputRef?: Ref;
|
||||
children(renderProps: { onFocus: FocusHandler; isActive: boolean; ref: Ref }): ReactElement<any, any>;
|
||||
children(renderProps: {
|
||||
onFocus: FocusHandler;
|
||||
isActive: boolean;
|
||||
ref: RefCallback<HTMLElement>;
|
||||
}): ReactElement<any, any>;
|
||||
}
|
||||
|
||||
// Wrapper to allow use of useRovingTabIndex outside of React Functional Components.
|
||||
|
|
|
@ -114,7 +114,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
(room.room_type === RoomType.Space ? _t("common|unnamed_space") : _t("common|unnamed_room"));
|
||||
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const [onFocus, isActive, ref, nodeRef] = useRovingTabIndex();
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const onPreviewClick = (ev: ButtonEvent): void => {
|
||||
|
@ -288,7 +288,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
case KeyBindingAction.ArrowLeft:
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ref.current?.focus();
|
||||
nodeRef.current?.focus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -315,7 +315,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
case KeyBindingAction.ArrowRight:
|
||||
handled = true;
|
||||
if (showChildren) {
|
||||
const childSection = ref.current?.nextElementSibling;
|
||||
const childSection = nodeRef.current?.nextElementSibling;
|
||||
childSection?.querySelector<HTMLDivElement>(".mx_SpaceHierarchy_roomTile")?.focus();
|
||||
} else {
|
||||
toggleShowChildren();
|
||||
|
@ -790,7 +790,7 @@ const SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, a
|
|||
const onKeyDown = (ev: KeyboardEvent, state: IState): void => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
if (action === KeyBindingAction.ArrowDown && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) {
|
||||
state.refs[0]?.current?.focus();
|
||||
state.nodes[0]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -294,7 +294,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Enter: {
|
||||
state.activeRef?.current?.querySelector<HTMLButtonElement>(".mx_ForwardList_sendButton")?.click();
|
||||
state.activeNode?.querySelector<HTMLButtonElement>(".mx_ForwardList_sendButton")?.click();
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -347,13 +347,13 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
onSearch={(query: string): void => {
|
||||
setQuery(query);
|
||||
setTimeout(() => {
|
||||
const ref = context.state.refs[0];
|
||||
if (ref) {
|
||||
const node = context.state.nodes[0];
|
||||
if (node) {
|
||||
context.dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref },
|
||||
payload: { node },
|
||||
});
|
||||
ref.current?.scrollIntoView?.({
|
||||
node?.scrollIntoView?.({
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
|
@ -361,7 +361,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
}}
|
||||
autoFocus={true}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
aria-activedescendant={context.state.activeRef?.current?.id}
|
||||
aria-activedescendant={context.state.activeNode?.id}
|
||||
aria-owns="mx_ForwardDialog_resultsList"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -7,13 +7,12 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { ReactNode, RefObject } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
|
||||
|
||||
interface OptionProps {
|
||||
inputRef?: RefObject<HTMLLIElement>;
|
||||
endAdornment?: ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
|
@ -21,8 +20,8 @@ interface OptionProps {
|
|||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment, className, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLLIElement>(inputRef);
|
||||
export const Option: React.FC<OptionProps> = ({ children, endAdornment, className, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLLIElement>();
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
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 { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
|
||||
|
@ -90,8 +90,8 @@ interface IProps {
|
|||
onFinished(): void;
|
||||
}
|
||||
|
||||
function refIsForRecentlyViewed(ref?: RefObject<HTMLElement>): boolean {
|
||||
return ref?.current?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true;
|
||||
function nodeIsForRecentlyViewed(node?: HTMLElement): boolean {
|
||||
return node?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true;
|
||||
}
|
||||
|
||||
function getRoomTypes(filter: Filter | null): Set<RoomType | null> {
|
||||
|
@ -498,13 +498,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
};
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const ref = rovingContext.state.refs[0];
|
||||
if (ref) {
|
||||
const node = rovingContext.state.nodes[0];
|
||||
if (node) {
|
||||
rovingContext.dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref },
|
||||
payload: { node },
|
||||
});
|
||||
ref.current?.scrollIntoView?.({
|
||||
node?.scrollIntoView?.({
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
|
@ -1128,7 +1128,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
break;
|
||||
}
|
||||
|
||||
let ref: RefObject<HTMLElement> | undefined;
|
||||
let node: HTMLElement | undefined;
|
||||
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
switch (accessibilityAction) {
|
||||
case KeyBindingAction.Escape:
|
||||
|
@ -1141,20 +1141,20 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
if (rovingContext.state.activeRef && rovingContext.state.refs.length > 0) {
|
||||
let refs = rovingContext.state.refs;
|
||||
if (rovingContext.state.activeNode && rovingContext.state.nodes.length > 0) {
|
||||
let nodes = rovingContext.state.nodes;
|
||||
if (!query && !filter !== null) {
|
||||
// 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.
|
||||
const keptRecentlyViewedRef = refIsForRecentlyViewed(rovingContext.state.activeRef)
|
||||
? rovingContext.state.activeRef
|
||||
: refs.find(refIsForRecentlyViewed);
|
||||
const keptRecentlyViewedRef = nodeIsForRecentlyViewed(rovingContext.state.activeNode)
|
||||
? rovingContext.state.activeNode
|
||||
: nodes.find(nodeIsForRecentlyViewed);
|
||||
// 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);
|
||||
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1));
|
||||
const idx = nodes.indexOf(rovingContext.state.activeNode);
|
||||
node = findSiblingElement(nodes, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1));
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1164,27 +1164,30 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
if (
|
||||
!query &&
|
||||
!filter !== null &&
|
||||
rovingContext.state.activeRef &&
|
||||
rovingContext.state.refs.length > 0 &&
|
||||
refIsForRecentlyViewed(rovingContext.state.activeRef)
|
||||
rovingContext.state.activeNode &&
|
||||
rovingContext.state.nodes.length > 0 &&
|
||||
nodeIsForRecentlyViewed(rovingContext.state.activeNode)
|
||||
) {
|
||||
// we only intercept left/right arrows when the field is empty, and they'd do nothing anyway
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed);
|
||||
const idx = refs.indexOf(rovingContext.state.activeRef);
|
||||
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1));
|
||||
const nodes = rovingContext.state.nodes.filter(nodeIsForRecentlyViewed);
|
||||
const idx = nodes.indexOf(rovingContext.state.activeNode);
|
||||
node = findSiblingElement(
|
||||
nodes,
|
||||
idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ref) {
|
||||
if (node) {
|
||||
rovingContext.dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref },
|
||||
payload: { node },
|
||||
});
|
||||
ref.current?.scrollIntoView({
|
||||
node?.scrollIntoView({
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
|
@ -1204,12 +1207,12 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
case KeyBindingAction.Enter:
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
rovingContext.state.activeRef?.current?.click();
|
||||
rovingContext.state.activeNode?.click();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const activeDescendant = rovingContext.state.activeRef?.current?.id;
|
||||
const activeDescendant = rovingContext.state.activeNode?.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -12,6 +12,9 @@ import React, {
|
|||
RefObject,
|
||||
createRef,
|
||||
ComponentProps,
|
||||
MutableRefObject,
|
||||
RefCallback,
|
||||
Ref,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import { debounce } from "lodash";
|
||||
|
@ -75,7 +78,7 @@ interface IProps {
|
|||
|
||||
export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
// The ref pass through to the input
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
// The element to create. Defaults to "input".
|
||||
element: "input";
|
||||
// The input's value. This is a controlled component, so the value is required.
|
||||
|
@ -84,7 +87,7 @@ export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElemen
|
|||
|
||||
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
|
||||
// The ref pass through to the select
|
||||
inputRef?: RefObject<HTMLSelectElement>;
|
||||
inputRef?: Ref<HTMLSelectElement>;
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: "select";
|
||||
// The select's value. This is a controlled component, so the value is required.
|
||||
|
@ -93,7 +96,7 @@ interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
|
|||
|
||||
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
// The ref pass through to the textarea
|
||||
inputRef?: RefObject<HTMLTextAreaElement>;
|
||||
inputRef?: Ref<HTMLTextAreaElement>;
|
||||
element: "textarea";
|
||||
// The textarea's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
|
@ -101,7 +104,7 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElem
|
|||
|
||||
export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
// The ref pass through to the input
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
element: "input";
|
||||
// The input's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
|
@ -118,7 +121,17 @@ interface IState {
|
|||
|
||||
export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
private readonly id: string;
|
||||
private readonly _inputRef = createRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>();
|
||||
private readonly _inputRef: MutableRefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null> =
|
||||
createRef();
|
||||
|
||||
/**
|
||||
* When props.inputRef is a callback ref, we will pass callbackRef to the DOM element.
|
||||
* This is so that other methods here can still access the DOM element via this._inputRef.
|
||||
*/
|
||||
private readonly callbackRef: RefCallback<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> = (node) => {
|
||||
this._inputRef.current = node;
|
||||
(this.props.inputRef as RefCallback<unknown>)(node);
|
||||
};
|
||||
|
||||
public static readonly defaultProps = {
|
||||
element: "input",
|
||||
|
@ -230,7 +243,12 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
}
|
||||
|
||||
private get inputRef(): RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> {
|
||||
return this.props.inputRef ?? this._inputRef;
|
||||
const inputRef = this.props.inputRef;
|
||||
if (typeof inputRef === "function") {
|
||||
// This is a callback ref, so return _inputRef which will point to the actual DOM element.
|
||||
return this._inputRef;
|
||||
}
|
||||
return (inputRef ?? this._inputRef) as RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
private onTooltipOpenChange = (open: boolean): void => {
|
||||
|
@ -284,7 +302,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
const inputProps_: React.HTMLAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> &
|
||||
React.ClassAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> = {
|
||||
...inputProps,
|
||||
ref: this.inputRef,
|
||||
ref: typeof this.props.inputRef === "function" ? this.callbackRef : this.inputRef,
|
||||
};
|
||||
|
||||
const fieldInput = React.createElement(this.props.element, inputProps_, children);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { Ref } from "react";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import classnames from "classnames";
|
||||
|
||||
|
@ -16,7 +16,7 @@ export enum CheckboxStyle {
|
|||
}
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
kind?: CheckboxStyle;
|
||||
id?: string;
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { Ref } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
inputRef?: Ref<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
|
||||
|
|
|
@ -28,7 +28,6 @@ import {
|
|||
import { Key } from "../../../Keyboard";
|
||||
import { clamp } from "../../../utils/numbers";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { Ref } from "../../../accessibility/roving/types";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 20;
|
||||
export const EMOJI_HEIGHT = 35;
|
||||
|
@ -154,47 +153,47 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void {
|
||||
const node = state.activeRef?.current;
|
||||
const node = state.activeNode;
|
||||
const parent = node?.parentElement;
|
||||
if (!parent || !state.activeRef) return;
|
||||
if (!parent || !state.activeNode) return;
|
||||
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 focusNode: HTMLElement | undefined;
|
||||
let newParent: HTMLElement | undefined;
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
focusRef = state.refs[refIndex - 1];
|
||||
newParent = focusRef?.current?.parentElement ?? undefined;
|
||||
focusNode = state.nodes[refIndex - 1];
|
||||
newParent = focusNode?.parentElement ?? undefined;
|
||||
break;
|
||||
|
||||
case Key.ARROW_RIGHT:
|
||||
focusRef = state.refs[refIndex + 1];
|
||||
newParent = focusRef?.current?.parentElement ?? undefined;
|
||||
focusNode = state.nodes[refIndex + 1];
|
||||
newParent = focusNode?.parentElement ?? undefined;
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
case Key.ARROW_DOWN: {
|
||||
// For up/down we find the prev/next parent by inspecting the refs either side of our row
|
||||
const ref =
|
||||
const node =
|
||||
ev.key === Key.ARROW_UP
|
||||
? state.refs[refIndex - rowIndex - 1]
|
||||
: state.refs[refIndex - rowIndex + EMOJIS_PER_ROW];
|
||||
newParent = ref?.current?.parentElement ?? undefined;
|
||||
? state.nodes[refIndex - rowIndex - 1]
|
||||
: state.nodes[refIndex - rowIndex + EMOJIS_PER_ROW];
|
||||
newParent = node?.parentElement ?? undefined;
|
||||
const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)];
|
||||
focusRef = state.refs.find((r) => r.current === newTarget);
|
||||
focusNode = state.nodes.find((r) => r === newTarget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (focusRef) {
|
||||
if (focusNode) {
|
||||
dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { ref: focusRef },
|
||||
payload: { node: focusNode },
|
||||
});
|
||||
|
||||
if (parent !== newParent) {
|
||||
focusRef.current?.scrollIntoView({
|
||||
focusNode?.scrollIntoView({
|
||||
behavior: "auto",
|
||||
block: "center",
|
||||
inline: "center",
|
||||
|
@ -207,10 +206,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void => {
|
||||
if (
|
||||
state.activeRef?.current &&
|
||||
[Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key)
|
||||
) {
|
||||
if (state.activeNode && [Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key)) {
|
||||
this.keyboardNavigation(ev, state, dispatch);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -70,7 +70,7 @@ class Search extends React.PureComponent<IProps> {
|
|||
onChange={(ev) => this.props.onChange(ev.target.value)}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.inputRef}
|
||||
aria-activedescendant={this.context.state.activeRef?.current?.id}
|
||||
aria-activedescendant={this.context.state.activeNode?.id}
|
||||
aria-controls="mx_EmojiPicker_body"
|
||||
aria-haspopup="grid"
|
||||
aria-autocomplete="list"
|
||||
|
|
|
@ -23,7 +23,7 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
|
|||
const dateInputDefaultValue = formatDateForInput(date);
|
||||
|
||||
const [dateValue, setDateValue] = useState(dateInputDefaultValue);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||
const [onFocus, isActive, refCallback] = useRovingTabIndex<HTMLInputElement>();
|
||||
|
||||
const onDateValueInput = (ev: React.ChangeEvent<HTMLInputElement>): void => setDateValue(ev.target.value);
|
||||
const onJumpToDateSubmit = (ev: FormEvent): void => {
|
||||
|
@ -45,7 +45,7 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
|
|||
className="mx_JumpToDatePicker_datePicker"
|
||||
label={_t("room|jump_to_date_prompt")}
|
||||
onFocus={onFocus}
|
||||
inputRef={ref}
|
||||
inputRef={refCallback}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
/>
|
||||
<RovingAccessibleButton
|
||||
|
|
|
@ -73,7 +73,7 @@ export const SpaceButton = <T extends keyof JSX.IntrinsicElements>({
|
|||
...props
|
||||
}: ButtonProps<T>): JSX.Element => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(innerRef);
|
||||
const [onFocus, isActive] = useRovingTabIndex(handle);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(handle);
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
const spaceKey = _spaceKey ?? space?.roomId;
|
||||
|
@ -144,7 +144,7 @@ export const SpaceButton = <T extends keyof JSX.IntrinsicElements>({
|
|||
title={!isNarrow || menuDisplayed ? undefined : label}
|
||||
onClick={onClick}
|
||||
onContextMenu={openMenu}
|
||||
ref={handle}
|
||||
ref={ref}
|
||||
tabIndex={tabIndex}
|
||||
onFocus={onFocus}
|
||||
>
|
||||
|
|
|
@ -28,6 +28,12 @@ const checkTabIndexes = (buttons: NodeListOf<HTMLElement>, expectations: number[
|
|||
expect([...buttons].map((b) => b.tabIndex)).toStrictEqual(expectations);
|
||||
};
|
||||
|
||||
const createButtonElement = (text: string): HTMLButtonElement => {
|
||||
const button = document.createElement("button");
|
||||
button.textContent = text;
|
||||
return button;
|
||||
};
|
||||
|
||||
// give the buttons keys for the fibre reconciler to not treat them all as the same
|
||||
const button1 = <Button key={1}>a</Button>;
|
||||
const button2 = <Button key={2}>b</Button>;
|
||||
|
@ -114,6 +120,25 @@ describe("RovingTabIndex", () => {
|
|||
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
|
||||
});
|
||||
|
||||
it("RovingTabIndexProvider provides a ref to the dom element", () => {
|
||||
const nodeRef = React.createRef<HTMLButtonElement>();
|
||||
const MyButton = (props: HTMLAttributes<HTMLButtonElement>) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLButtonElement>(nodeRef);
|
||||
return <button {...props} onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref} />;
|
||||
};
|
||||
const { container } = render(
|
||||
<RovingTabIndexProvider>
|
||||
{() => (
|
||||
<React.Fragment>
|
||||
<MyButton />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</RovingTabIndexProvider>,
|
||||
);
|
||||
// nodeRef should point to button
|
||||
expect(nodeRef.current).toBe(container.querySelector("button"));
|
||||
});
|
||||
|
||||
it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => {
|
||||
const { container } = render(
|
||||
<RovingTabIndexProvider>
|
||||
|
@ -123,11 +148,7 @@ describe("RovingTabIndex", () => {
|
|||
{button2}
|
||||
<RovingTabIndexWrapper>
|
||||
{({ onFocus, isActive, ref }) => (
|
||||
<button
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
ref={ref as React.RefObject<HTMLButtonElement>}
|
||||
>
|
||||
<button onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref}>
|
||||
.
|
||||
</button>
|
||||
)}
|
||||
|
@ -147,75 +168,75 @@ describe("RovingTabIndex", () => {
|
|||
|
||||
describe("reducer functions as expected", () => {
|
||||
it("SetFocus works as expected", () => {
|
||||
const ref1 = React.createRef<HTMLElement>();
|
||||
const ref2 = React.createRef<HTMLElement>();
|
||||
const node1 = createButtonElement("Button 1");
|
||||
const node2 = createButtonElement("Button 2");
|
||||
expect(
|
||||
reducer(
|
||||
{
|
||||
activeRef: ref1,
|
||||
refs: [ref1, ref2],
|
||||
activeNode: node1,
|
||||
nodes: [node1, node2],
|
||||
},
|
||||
{
|
||||
type: Type.SetFocus,
|
||||
payload: {
|
||||
ref: ref2,
|
||||
node: node2,
|
||||
},
|
||||
},
|
||||
),
|
||||
).toStrictEqual({
|
||||
activeRef: ref2,
|
||||
refs: [ref1, ref2],
|
||||
activeNode: node2,
|
||||
nodes: [node1, node2],
|
||||
});
|
||||
});
|
||||
|
||||
it("Unregister works as expected", () => {
|
||||
const ref1 = React.createRef<HTMLElement>();
|
||||
const ref2 = React.createRef<HTMLElement>();
|
||||
const ref3 = React.createRef<HTMLElement>();
|
||||
const ref4 = React.createRef<HTMLElement>();
|
||||
const button1 = createButtonElement("Button 1");
|
||||
const button2 = createButtonElement("Button 2");
|
||||
const button3 = createButtonElement("Button 3");
|
||||
const button4 = createButtonElement("Button 4");
|
||||
|
||||
let state: IState = {
|
||||
refs: [ref1, ref2, ref3, ref4],
|
||||
nodes: [button1, button2, button3, button4],
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Unregister,
|
||||
payload: {
|
||||
ref: ref2,
|
||||
node: button2,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
refs: [ref1, ref3, ref4],
|
||||
nodes: [button1, button3, button4],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Unregister,
|
||||
payload: {
|
||||
ref: ref3,
|
||||
node: button3,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
refs: [ref1, ref4],
|
||||
nodes: [button1, button4],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Unregister,
|
||||
payload: {
|
||||
ref: ref4,
|
||||
node: button4,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
refs: [ref1],
|
||||
nodes: [button1],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Unregister,
|
||||
payload: {
|
||||
ref: ref1,
|
||||
node: button1,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
refs: [],
|
||||
nodes: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -235,122 +256,122 @@ describe("RovingTabIndex", () => {
|
|||
);
|
||||
|
||||
let state: IState = {
|
||||
refs: [],
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Register,
|
||||
payload: {
|
||||
ref: ref1,
|
||||
node: ref1.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref1,
|
||||
refs: [ref1],
|
||||
activeNode: ref1.current,
|
||||
nodes: [ref1.current],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Register,
|
||||
payload: {
|
||||
ref: ref2,
|
||||
node: ref2.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref1,
|
||||
refs: [ref1, ref2],
|
||||
activeNode: ref1.current,
|
||||
nodes: [ref1.current, ref2.current],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Register,
|
||||
payload: {
|
||||
ref: ref3,
|
||||
node: ref3.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref1,
|
||||
refs: [ref1, ref2, ref3],
|
||||
activeNode: ref1.current,
|
||||
nodes: [ref1.current, ref2.current, ref3.current],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Register,
|
||||
payload: {
|
||||
ref: ref4,
|
||||
node: ref4.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref1,
|
||||
refs: [ref1, ref2, ref3, ref4],
|
||||
activeNode: ref1.current,
|
||||
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
|
||||
});
|
||||
|
||||
// test that the automatic focus switch works for unmounting
|
||||
state = reducer(state, {
|
||||
type: Type.SetFocus,
|
||||
payload: {
|
||||
ref: ref2,
|
||||
node: ref2.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref2,
|
||||
refs: [ref1, ref2, ref3, ref4],
|
||||
activeNode: ref2.current,
|
||||
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Unregister,
|
||||
payload: {
|
||||
ref: ref2,
|
||||
node: ref2.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref3,
|
||||
refs: [ref1, ref3, ref4],
|
||||
activeNode: ref3.current,
|
||||
nodes: [ref1.current, ref3.current, ref4.current],
|
||||
});
|
||||
|
||||
// test that the insert into the middle works as expected
|
||||
state = reducer(state, {
|
||||
type: Type.Register,
|
||||
payload: {
|
||||
ref: ref2,
|
||||
node: ref2.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref3,
|
||||
refs: [ref1, ref2, ref3, ref4],
|
||||
activeNode: ref3.current,
|
||||
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
|
||||
});
|
||||
|
||||
// test that insertion at the edges works
|
||||
state = reducer(state, {
|
||||
type: Type.Unregister,
|
||||
payload: {
|
||||
ref: ref1,
|
||||
node: ref1.current!,
|
||||
},
|
||||
});
|
||||
state = reducer(state, {
|
||||
type: Type.Unregister,
|
||||
payload: {
|
||||
ref: ref4,
|
||||
node: ref4.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref3,
|
||||
refs: [ref2, ref3],
|
||||
activeNode: ref3.current,
|
||||
nodes: [ref2.current, ref3.current],
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Register,
|
||||
payload: {
|
||||
ref: ref1,
|
||||
node: ref1.current!,
|
||||
},
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: Type.Register,
|
||||
payload: {
|
||||
ref: ref4,
|
||||
node: ref4.current!,
|
||||
},
|
||||
});
|
||||
expect(state).toStrictEqual({
|
||||
activeRef: ref3,
|
||||
refs: [ref1, ref2, ref3, ref4],
|
||||
activeNode: ref3.current,
|
||||
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue