From 13d556ec72195ef96e98239b231c0b4fcea3ff54 Mon Sep 17 00:00:00 2001 From: Joe Heffernan Date: Mon, 21 Oct 2024 13:30:11 -0700 Subject: [PATCH] move dropdown focus concerns from hook into CustomDropdown --- src/components/CustomDropdown/index.tsx | 86 ++++++++++++++++--- .../CustomDropdown/useDropdownFocus.tsx | 83 ------------------ 2 files changed, 73 insertions(+), 96 deletions(-) delete mode 100644 src/components/CustomDropdown/useDropdownFocus.tsx diff --git a/src/components/CustomDropdown/index.tsx b/src/components/CustomDropdown/index.tsx index 92d23b0f..2166e36f 100644 --- a/src/components/CustomDropdown/index.tsx +++ b/src/components/CustomDropdown/index.tsx @@ -1,9 +1,15 @@ -import React, { ReactNode } from "react"; +import React, { + KeyboardEventHandler, + ReactNode, + useEffect, + useRef, + useState, +} from "react"; import classNames from "classnames"; import { Dropdown, DropDownProps, MenuProps } from "antd"; import { ButtonClass, DropdownState } from "../../constants/interfaces"; +import { DROPDOWN_HOVER_DELAY } from "../../constants"; import NavButton from "../NavButton"; -import { useDropdownFocus } from "./useDropdownFocus"; import styles from "./style.css"; @@ -26,19 +32,73 @@ const CustomDropdown: React.FC = ({ disabled, narrow, }) => { + const [dropdownState, setDropdownState] = useState( + DropdownState.CLOSED + ); + + const triggerRef = useRef(null); + const dropdownRef = useRef(null); + const closeTimeoutRef = useRef(null); + + /** + * Prevents the menu wrapper from capturing focus, + * this prevents losing focus to the body + * when "Escape" is pressed. + */ + useEffect(() => { + const element = dropdownRef.current; + if (element) { + const menuElement = element.querySelector( + ".ant-dropdown-menu" + ) as HTMLElement; + if (menuElement) { + if (dropdownState === DropdownState.FORCED_OPEN) { + menuElement.setAttribute("tabIndex", "-1"); + } else if (dropdownState === DropdownState.CLOSED) { + menuElement.setAttribute("tabIndex", "0"); + } + } + } + }, [dropdownRef, dropdownState]); + /** - * Calling this hook and exposing the necssary props - * to manage focus in the dropdown + * Manually handling keydown and hover behavior because + * our focus management overrides the defaults of the antd components. */ - const { - dropdownState, - setDropdownState, - triggerRef, - dropdownRef, - handleKeyDown, - handleMouseLeaveWithDelay, - handleMouseEnter, - } = useDropdownFocus(); + const handleKeyDown: KeyboardEventHandler = (event) => { + if (dropdownState === DropdownState.CLOSED) { + if ( + (event.key === "Enter" || + event.key === " " || + event.key === "ArrowDown") && + !event.defaultPrevented + ) { + event.preventDefault(); + setDropdownState(DropdownState.FORCED_OPEN); // Opened by keyboard + } + } else if (event.key === "Escape") { + event.preventDefault(); + setDropdownState(DropdownState.CLOSED); + triggerRef.current?.focus(); + } + }; + + const handleMouseEnter = () => { + if (dropdownState === DropdownState.CLOSED) { + setDropdownState(DropdownState.OPEN); + } + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + }; + + const handleMouseLeaveWithDelay = () => { + if (dropdownState !== DropdownState.FORCED_OPEN) { + closeTimeoutRef.current = setTimeout(() => { + setDropdownState(DropdownState.CLOSED); + }, DROPDOWN_HOVER_DELAY); + } + }; const menuClassNames = narrow ? classNames(styles.menu, styles.narrow) diff --git a/src/components/CustomDropdown/useDropdownFocus.tsx b/src/components/CustomDropdown/useDropdownFocus.tsx deleted file mode 100644 index 45a1164b..00000000 --- a/src/components/CustomDropdown/useDropdownFocus.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useState, useRef, KeyboardEventHandler, useEffect } from "react"; -import { DropdownState } from "../../constants/interfaces"; -import { DROPDOWN_HOVER_DELAY } from "../../constants"; - -export const useDropdownFocus = () => { - const [dropdownState, setDropdownState] = useState( - DropdownState.CLOSED - ); - - const triggerRef = useRef(null); - const dropdownRef = useRef(null); - const closeTimeoutRef = useRef(null); - - /** - * Prevents the menu wrapper from capturing focus, - * this prevents losing focus to the body - * when "Escape" is pressed. - */ - useEffect(() => { - const element = dropdownRef.current; - if (element) { - const menuElement = element.querySelector( - ".ant-dropdown-menu" - ) as HTMLElement; - if (menuElement) { - if (dropdownState === DropdownState.FORCED_OPEN) { - menuElement.setAttribute("tabIndex", "-1"); - } else if (dropdownState === DropdownState.CLOSED) { - menuElement.setAttribute("tabIndex", "0"); - } - } - } - }, [dropdownRef, dropdownState]); - - /** - * Manually handling keydown and hover behavior because - * our focus management overrides the defaults of the antd components. - */ - const handleKeyDown: KeyboardEventHandler = (event) => { - if (dropdownState === DropdownState.CLOSED) { - if ( - (event.key === "Enter" || - event.key === " " || - event.key === "ArrowDown") && - !event.defaultPrevented - ) { - event.preventDefault(); - setDropdownState(DropdownState.FORCED_OPEN); // Opened by keyboard - } - } else if (event.key === "Escape") { - event.preventDefault(); - setDropdownState(DropdownState.CLOSED); - triggerRef.current?.focus(); - } - }; - - const handleMouseEnter = () => { - if (dropdownState === DropdownState.CLOSED) { - setDropdownState(DropdownState.OPEN); - } - if (closeTimeoutRef.current) { - clearTimeout(closeTimeoutRef.current); - } - }; - - const handleMouseLeaveWithDelay = () => { - if (dropdownState !== DropdownState.FORCED_OPEN) { - closeTimeoutRef.current = setTimeout(() => { - setDropdownState(DropdownState.CLOSED); - }, DROPDOWN_HOVER_DELAY); - } - }; - - return { - dropdownState, - setDropdownState, - triggerRef, - dropdownRef, - handleKeyDown, - handleMouseLeaveWithDelay, - handleMouseEnter, - }; -};