fix: focus traps inside popovers (#5317)
This commit is contained in:
parent
50bc7e099a
commit
af31e9dcc2
@ -129,19 +129,7 @@ const Picker = ({
|
|||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
if (event.key === KEYS.TAB) {
|
if (isArrowKey(event.key)) {
|
||||||
handled = true;
|
|
||||||
const { activeElement } = document;
|
|
||||||
if (event.shiftKey) {
|
|
||||||
if (activeElement === firstItem.current) {
|
|
||||||
colorInput.current?.focus();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
} else if (activeElement === colorInput.current) {
|
|
||||||
firstItem.current?.focus();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
} else if (isArrowKey(event.key)) {
|
|
||||||
handled = true;
|
handled = true;
|
||||||
const { activeElement } = document;
|
const { activeElement } = document;
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
@ -272,7 +260,8 @@ const Picker = ({
|
|||||||
gallery.current = el;
|
gallery.current = el;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex={0}
|
// to allow focusing by clicking but not by tabbing
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className="color-picker-content--default">
|
<div className="color-picker-content--default">
|
||||||
{renderColors(colors)}
|
{renderColors(colors)}
|
||||||
|
@ -9,6 +9,7 @@ import { back, close } from "./icons";
|
|||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { Modal } from "./Modal";
|
import { Modal } from "./Modal";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { queryFocusableElements } from "../utils";
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -64,14 +65,6 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [islandNode, props.autofocus]);
|
}, [islandNode, props.autofocus]);
|
||||||
|
|
||||||
const queryFocusableElements = (node: HTMLElement) => {
|
|
||||||
const focusableElements = node.querySelectorAll<HTMLElement>(
|
|
||||||
"button, a, input, select, textarea, div[tabindex]",
|
|
||||||
);
|
|
||||||
|
|
||||||
return focusableElements ? Array.from(focusableElements) : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
(lastActiveElement as HTMLElement).focus();
|
(lastActiveElement as HTMLElement).focus();
|
||||||
props.onCloseRequest();
|
props.onCloseRequest();
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React, { useLayoutEffect, useRef, useEffect } from "react";
|
import React, { useLayoutEffect, useRef, useEffect } from "react";
|
||||||
import "./Popover.scss";
|
import "./Popover.scss";
|
||||||
import { unstable_batchedUpdates } from "react-dom";
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
import { queryFocusableElements } from "../utils";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
top?: number;
|
top?: number;
|
||||||
@ -27,6 +29,41 @@ export const Popover = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const container = popoverRef.current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === KEYS.TAB) {
|
||||||
|
const focusableElements = queryFocusableElements(container);
|
||||||
|
const { activeElement } = document;
|
||||||
|
const currentIndex = focusableElements.findIndex(
|
||||||
|
(element) => element === activeElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentIndex === 0 && event.shiftKey) {
|
||||||
|
focusableElements[focusableElements.length - 1].focus();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
} else if (
|
||||||
|
currentIndex === focusableElements.length - 1 &&
|
||||||
|
!event.shiftKey
|
||||||
|
) {
|
||||||
|
focusableElements[0].focus();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => container.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [container]);
|
||||||
|
|
||||||
// ensure the popover doesn't overflow the viewport
|
// ensure the popover doesn't overflow the viewport
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (fitInViewport && popoverRef.current) {
|
if (fitInViewport && popoverRef.current) {
|
||||||
|
13
src/utils.ts
13
src/utils.ts
@ -668,3 +668,16 @@ export const isPromiseLike = (
|
|||||||
"finally" in value
|
"finally" in value
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const queryFocusableElements = (container: HTMLElement | null) => {
|
||||||
|
const focusableElements = container?.querySelectorAll<HTMLElement>(
|
||||||
|
"button, a, input, select, textarea, div[tabindex], label[tabindex]",
|
||||||
|
);
|
||||||
|
|
||||||
|
return focusableElements
|
||||||
|
? Array.from(focusableElements).filter(
|
||||||
|
(element) =>
|
||||||
|
element.tabIndex > -1 && !(element as HTMLInputElement).disabled,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user