Skip to content

Commit 3a1aa75

Browse files
committed
Refactor ActionBarView to use the RovingTabIndexProvider
1 parent 13bcca6 commit 3a1aa75

File tree

2 files changed

+34
-89
lines changed

2 files changed

+34
-89
lines changed

packages/shared-components/src/room/timeline/event-tile/actions/ActionBarView/ActionBarButton.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import React, { type JSX } from "react";
8+
import React, { type JSX, useLayoutEffect, useRef } from "react";
9+
import { useMergeRefs } from "react-merge-refs";
910
import { Button, Tooltip } from "@vector-im/compound-web";
1011

12+
import { useRovingTabIndex } from "../../../../../core/roving";
1113
import styles from "./ActionBarView.module.css";
1214

1315
interface ActionBarButtonProps {
@@ -36,6 +38,15 @@ export function ActionBarButton({
3638
tooltipCaption,
3739
}: Readonly<ActionBarButtonProps>): JSX.Element {
3840
const iconOnly = presentation === "icon";
41+
const [onFocus, isActive, rovingRef] = useRovingTabIndex<HTMLButtonElement>();
42+
const localRef = useRef<HTMLButtonElement | null>(null);
43+
const ref = useMergeRefs([buttonRef, localRef, disabled ? null : rovingRef]);
44+
45+
useLayoutEffect(() => {
46+
if (!localRef.current) return;
47+
48+
localRef.current.tabIndex = disabled ? -1 : isActive ? 0 : -1;
49+
}, [disabled, isActive]);
3950

4051
const handleContextMenu = (event: React.MouseEvent<HTMLButtonElement>): void => {
4152
event.preventDefault();
@@ -47,7 +58,7 @@ export function ActionBarButton({
4758
<Tooltip description={tooltipDescription ?? label} caption={tooltipCaption} placement="top">
4859
<Button
4960
data-presentation={presentation}
50-
ref={buttonRef}
61+
ref={ref}
5162
kind="tertiary"
5263
size="sm"
5364
iconOnly={iconOnly}
@@ -57,6 +68,7 @@ export function ActionBarButton({
5768
disabled={disabled}
5869
onClick={(event) => onActivate?.(event.currentTarget)}
5970
onContextMenu={handleContextMenu}
71+
onFocus={disabled ? undefined : onFocus}
6072
className={styles.toolbar_item}
6173
Icon={iconOnly ? icon : undefined}
6274
>

packages/shared-components/src/room/timeline/event-tile/actions/ActionBarView/ActionBarView.tsx

Lines changed: 20 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import React, { type JSX, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
8+
import React, { type JSX, useCallback, useMemo, useRef } from "react";
99
import classNames from "classnames";
1010
import {
1111
CollapseIcon,
@@ -27,6 +27,7 @@ import {
2727
} from "@vector-im/compound-design-tokens/assets/web/icons";
2828
import { InlineSpinner } from "@vector-im/compound-web";
2929

30+
import { RovingTabIndexProvider } from "../../../../../core/roving";
3031
import { useI18n } from "../../../../../core/i18n/i18nContext";
3132
import { Flex } from "../../../../../core/utils/Flex";
3233
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
@@ -131,7 +132,6 @@ interface ActionBarViewProps {
131132
*/
132133
export function ActionBarView({ vm, className }: Readonly<ActionBarViewProps>): JSX.Element | null {
133134
const { translate: _t } = useI18n();
134-
const [activeIndex, setActiveIndex] = useState(0);
135135
const {
136136
actions,
137137
presentation = "icon",
@@ -364,97 +364,30 @@ export function ActionBarView({ vm, className }: Readonly<ActionBarViewProps>):
364364
disabled: isActionDisabled(action),
365365
}));
366366
}, [actions, isActionDisabled]);
367-
368-
// Handle RovingIndex for toolbar
369-
const enabledIndices = toolbarButtons
370-
.map((item, index) => (item.disabled ? -1 : index))
371-
.filter((index) => index >= 0);
372-
const fallbackIndex = enabledIndices[0] ?? 0;
373-
const currentIndex =
374-
toolbarButtons[activeIndex] && !toolbarButtons[activeIndex].disabled ? activeIndex : fallbackIndex;
375-
376-
useLayoutEffect(() => {
377-
setActiveIndex(currentIndex);
378-
379-
toolbarButtons.forEach(({ action }, index) => {
380-
const button = actionButtonRefs.current[action] ?? null;
381-
if (button) {
382-
button.tabIndex = index === currentIndex ? 0 : -1;
383-
}
384-
});
385-
}, [currentIndex, toolbarButtons]);
386-
387-
const focusButtonAtIndex = (index: number): void => {
388-
const action = toolbarButtons[index]?.action;
389-
const button = action ? (actionButtonRefs.current[action] ?? null) : null;
390-
if (!button) {
391-
return;
392-
}
393-
394-
setActiveIndex(index);
395-
button.focus();
396-
};
397-
398-
const handleToolbarKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
399-
if (enabledIndices.length === 0) {
400-
return;
401-
}
402-
403-
const focusedIndex = toolbarButtons.findIndex(
404-
({ action }) => actionButtonRefs.current[action] === document.activeElement,
405-
);
406-
const startIndex = focusedIndex >= 0 ? focusedIndex : currentIndex;
407-
408-
switch (event.key) {
409-
case "ArrowRight": {
410-
event.preventDefault();
411-
const nextIndex = enabledIndices.find((index) => index > startIndex) ?? enabledIndices[0];
412-
focusButtonAtIndex(nextIndex);
413-
break;
414-
}
415-
case "ArrowLeft": {
416-
event.preventDefault();
417-
const previousIndex = [...enabledIndices].reverse().find((index) => index < startIndex);
418-
focusButtonAtIndex(previousIndex ?? enabledIndices[enabledIndices.length - 1]);
419-
break;
420-
}
421-
case "Home":
422-
event.preventDefault();
423-
focusButtonAtIndex(enabledIndices[0]);
424-
break;
425-
case "End":
426-
event.preventDefault();
427-
focusButtonAtIndex(enabledIndices[enabledIndices.length - 1]);
428-
break;
429-
}
430-
};
431-
432-
const handleToolbarFocusCapture = (): void => {
433-
const focusedIndex = toolbarButtons.findIndex(
434-
({ action }) => actionButtonRefs.current[action] === document.activeElement,
435-
);
436-
if (focusedIndex >= 0 && focusedIndex !== activeIndex) {
437-
setActiveIndex(focusedIndex);
438-
}
439-
};
367+
const rovingProviderKey = toolbarButtons
368+
.map(({ action, disabled }) => `${action}:${disabled ? "1" : "0"}`)
369+
.join("|");
440370

441371
if (toolbarButtons.length === 0) {
442372
return null;
443373
}
444374

445375
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
446376
return (
447-
<Flex
448-
display="inline-flex"
449-
direction="row"
450-
role="toolbar"
451-
aria-label={_t("timeline|mab|label")}
452-
aria-live="off"
453-
onKeyDown={handleToolbarKeyDown}
454-
onFocusCapture={handleToolbarFocusCapture}
455-
className={classNames(className, styles.toolbar)}
456-
>
457-
{toolbarButtons.map((meta) => actionButtons[meta.action])}
458-
</Flex>
377+
<RovingTabIndexProvider key={rovingProviderKey} handleLeftRight handleHomeEnd handleLoop>
378+
{({ onKeyDownHandler }) => (
379+
<Flex
380+
display="inline-flex"
381+
direction="row"
382+
role="toolbar"
383+
aria-label={_t("timeline|mab|label")}
384+
aria-live="off"
385+
onKeyDown={onKeyDownHandler}
386+
className={classNames(className, styles.toolbar)}
387+
>
388+
{toolbarButtons.map((meta) => actionButtons[meta.action])}
389+
</Flex>
390+
)}
391+
</RovingTabIndexProvider>
459392
);
460393
}

0 commit comments

Comments
 (0)