diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx
index fd284752..76a66b5b 100644
--- a/src/components/ColorPicker.tsx
+++ b/src/components/ColorPicker.tsx
@@ -129,19 +129,7 @@ const Picker = ({
const handleKeyDown = (event: React.KeyboardEvent) => {
let handled = false;
- if (event.key === KEYS.TAB) {
- 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)) {
+ if (isArrowKey(event.key)) {
handled = true;
const { activeElement } = document;
const isRTL = getLanguage().rtl;
@@ -272,7 +260,8 @@ const Picker = ({
gallery.current = el;
}
}}
- tabIndex={0}
+ // to allow focusing by clicking but not by tabbing
+ tabIndex={-1}
>
{renderColors(colors)}
diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx
index 06615101..f406a8bb 100644
--- a/src/components/Dialog.tsx
+++ b/src/components/Dialog.tsx
@@ -9,6 +9,7 @@ import { back, close } from "./icons";
import { Island } from "./Island";
import { Modal } from "./Modal";
import { AppState } from "../types";
+import { queryFocusableElements } from "../utils";
export interface DialogProps {
children: React.ReactNode;
@@ -64,14 +65,6 @@ export const Dialog = (props: DialogProps) => {
return () => islandNode.removeEventListener("keydown", handleKeyDown);
}, [islandNode, props.autofocus]);
- const queryFocusableElements = (node: HTMLElement) => {
- const focusableElements = node.querySelectorAll(
- "button, a, input, select, textarea, div[tabindex]",
- );
-
- return focusableElements ? Array.from(focusableElements) : [];
- };
-
const onClose = () => {
(lastActiveElement as HTMLElement).focus();
props.onCloseRequest();
diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx
index ca47d728..ba424f3a 100644
--- a/src/components/Popover.tsx
+++ b/src/components/Popover.tsx
@@ -1,6 +1,8 @@
import React, { useLayoutEffect, useRef, useEffect } from "react";
import "./Popover.scss";
import { unstable_batchedUpdates } from "react-dom";
+import { queryFocusableElements } from "../utils";
+import { KEYS } from "../keys";
type Props = {
top?: number;
@@ -27,6 +29,41 @@ export const Popover = ({
}: Props) => {
const popoverRef = useRef(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
useLayoutEffect(() => {
if (fitInViewport && popoverRef.current) {
diff --git a/src/utils.ts b/src/utils.ts
index d81ddfec..2e651ef8 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -668,3 +668,16 @@ export const isPromiseLike = (
"finally" in value
);
};
+
+export const queryFocusableElements = (container: HTMLElement | null) => {
+ const focusableElements = container?.querySelectorAll(
+ "button, a, input, select, textarea, div[tabindex], label[tabindex]",
+ );
+
+ return focusableElements
+ ? Array.from(focusableElements).filter(
+ (element) =>
+ element.tabIndex > -1 && !(element as HTMLInputElement).disabled,
+ )
+ : [];
+};