From f3fcc1110a1423ededb6a06c6a19d8e93bcb25d1 Mon Sep 17 00:00:00 2001 From: amy-corson-ibigroup <115499534+amy-corson-ibigroup@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:07:19 -0500 Subject: [PATCH] refactor: create get-entry-relative-to in utils --- lib/components/app/app-menu-item.tsx | 26 +++++----------- lib/components/map/point-popup.tsx | 27 ++++------------- lib/components/util/dropdown.tsx | 30 +++++-------------- lib/components/util/get-entry-relative-to.tsx | 28 +++++++++++++++++ 4 files changed, 50 insertions(+), 61 deletions(-) create mode 100644 lib/components/util/get-entry-relative-to.tsx diff --git a/lib/components/app/app-menu-item.tsx b/lib/components/app/app-menu-item.tsx index 236e3bc8e..ee3d360da 100644 --- a/lib/components/app/app-menu-item.tsx +++ b/lib/components/app/app-menu-item.tsx @@ -1,10 +1,12 @@ import { ChevronDown } from '@styled-icons/fa-solid/ChevronDown' import { ChevronUp } from '@styled-icons/fa-solid/ChevronUp' -import AnimateHeight from 'react-animate-height' -import React, { Component, HTMLAttributes, KeyboardEvent } from 'react' +import { getEntryRelativeTo } from '../util/get-entry-relative-to' +import AnimateHeight from 'react-animate-height' import Link from '../util/link' +import React, { Component, HTMLAttributes, KeyboardEvent } from 'react' + interface Props extends HTMLAttributes { href?: string icon?: JSX.Element @@ -17,20 +19,8 @@ interface Props extends HTMLAttributes { interface State { isExpanded: boolean } - -/** - * Helper method to find the element within the app menu at the given offset - * (e.g. previous or next) relative to the specified element. - * The query is limited to the app menu so that arrow navigation is contained within - * (tab navigation is not restricted). - */ -function getEntryRelativeTo(element: EventTarget, offset: 1 | -1): HTMLElement { - const entries = Array.from( - document.querySelectorAll('.app-menu a, .app-menu button') - ) - const elementIndex = entries.indexOf(element as HTMLElement) - return entries[elementIndex + offset] as HTMLElement -} +// Argument for document.querySelectorAll to target focusable elements. +const queryId = '.app-menu a, .app-menu button' /** * Renders a single entry from the hamburger menu. @@ -48,13 +38,13 @@ export default class AppMenuItem extends Component { subItems && this.setState({ isExpanded: false }) break case 'ArrowUp': - getEntryRelativeTo(element, -1)?.focus() + getEntryRelativeTo(queryId, element, -1)?.focus() break case 'ArrowRight': subItems && this.setState({ isExpanded: true }) break case 'ArrowDown': - getEntryRelativeTo(element, 1)?.focus() + getEntryRelativeTo(queryId, element, 1)?.focus() break case ' ': // For links (tagName "A" uppercase), trigger link on space for consistency with buttons. diff --git a/lib/components/map/point-popup.tsx b/lib/components/map/point-popup.tsx index 2b6d5b936..b13e3ce3a 100644 --- a/lib/components/map/point-popup.tsx +++ b/lib/components/map/point-popup.tsx @@ -9,6 +9,7 @@ import styled from 'styled-components' import type { Location } from '@opentripplanner/types' import * as mapActions from '../../actions/map' +import { getEntryRelativeTo } from '../util/get-entry-relative-to' import { Icon } from '../util/styledIcon' import { renderCoordinates } from '../../util/i18n' import { SetLocationHandler, ZoomToPlaceHandler } from '../util/types' @@ -54,25 +55,6 @@ function MapPopup({ * Creates a focus trap within map popup that can be dismissed with esc. * https://medium.com/cstech/achieving-focus-trapping-in-a-react-modal-component-3f28f596f35b */ - function getEntryRelativeTo( - element: EventTarget, - offset: 1 | -1 - ): HTMLElement { - const entries = Array.from( - document.querySelectorAll('button#zoom-btn, #from-to button') - ) - const firstElement = entries[0] - const lastElement = entries[entries.length - 1] - const elementIndex = entries.indexOf(element as HTMLButtonElement) - - if (element === firstElement && offset === -1) { - return lastElement as HTMLElement - } - if (element === lastElement && offset === 1) { - return firstElement as HTMLElement - } - return entries[elementIndex + offset] as HTMLElement - } /** * Check to see which keys are down by tracking keyUp and keyDown events @@ -80,6 +62,9 @@ function MapPopup({ */ let keysDown: string[] = useMemo(() => [], []) + // Argument for document.querySelectorAll to target focusable elements. + const queryId = 'button#zoom-btn, #from-to button' + const _handleKeyDown = useCallback( (e) => { keysDown.push(e.key) @@ -91,10 +76,10 @@ function MapPopup({ case 'Tab': if (keysDown.includes('Shift')) { e.preventDefault() - getEntryRelativeTo(element, -1)?.focus() + getEntryRelativeTo(queryId, element, -1)?.focus() } else { e.preventDefault() - getEntryRelativeTo(element, 1)?.focus() + getEntryRelativeTo(queryId, element, 1)?.focus() } break case ' ': diff --git a/lib/components/util/dropdown.tsx b/lib/components/util/dropdown.tsx index 93ded34a5..9a3e673f0 100644 --- a/lib/components/util/dropdown.tsx +++ b/lib/components/util/dropdown.tsx @@ -1,4 +1,3 @@ -import { NavbarButton } from '../app/nav-item' import React, { HTMLAttributes, KeyboardEvent, @@ -7,6 +6,9 @@ import React, { useRef, useState } from 'react' + +import { getEntryRelativeTo } from './get-entry-relative-to' +import { NavbarButton } from '../app/nav-item' import styled from 'styled-components' interface Props extends HTMLAttributes { @@ -61,25 +63,6 @@ const DropdownMenu = styled.ul` } ` -/** - * Helper method to find the element within dropdown menu at the given offset - * (e.g. previous or next) relative to the specified element. - * The query is limited to the dropdown so that arrow navigation is contained within - * (tab navigation is not restricted). - */ -function getEntryRelativeTo( - id: string, - element: EventTarget, - offset: 1 | -1 -): HTMLElement { - const entries = Array.from( - document.querySelectorAll(`#${id} button, #${id}-label`) - ) - const elementIndex = entries.indexOf(element as HTMLElement) - - return entries[elementIndex + offset] as HTMLElement -} - /** * Renders a dropdown menu. By default, only a passed "name" is rendered. If clicked, * a floating div is rendered below the "name" with list contents inside. Clicking anywhere @@ -100,6 +83,9 @@ export const Dropdown = ({ const toggleOpen = useCallback(() => setOpen(!open), [open, setOpen]) + // Argument for document.querySelectorAll to target focusable elements. + const queryId = `#${id} button, #${id}-label` + // Adding document event listeners allows us to close the dropdown // when the user interacts with any part of the page that isn't the dropdown useEffect(() => { @@ -124,11 +110,11 @@ export const Dropdown = ({ switch (e.key) { case 'ArrowUp': e.preventDefault() - getEntryRelativeTo(id, element, -1)?.focus() + getEntryRelativeTo(queryId, element, -1)?.focus() break case 'ArrowDown': e.preventDefault() - getEntryRelativeTo(id, element, 1)?.focus() + getEntryRelativeTo(queryId, element, 1)?.focus() break case 'Escape': setOpen(false) diff --git a/lib/components/util/get-entry-relative-to.tsx b/lib/components/util/get-entry-relative-to.tsx new file mode 100644 index 000000000..c53f853a5 --- /dev/null +++ b/lib/components/util/get-entry-relative-to.tsx @@ -0,0 +1,28 @@ +/** + * Helper method to find the element within the app menu at the given offset + * (e.g. previous or next) relative to the specified element. + * + * @param {string} query - Argument that gets passed to document.querySelectorAll + * @param {HTMLElement} element - Specified element (e.target) + * @param {1 | -1} offset - Determines direction to move within array of focusable elements (previous or next) + * @returns {HTMLElement} - element to be focused + */ + +export function getEntryRelativeTo( + query: string, + element: EventTarget, + offset: 1 | -1 +): HTMLElement { + const entries = Array.from(document.querySelectorAll(query)) + const firstElement = entries[0] + const lastElement = entries[entries.length - 1] + const elementIndex = entries.indexOf(element as HTMLButtonElement) + + if (element === firstElement && offset === -1) { + return lastElement as HTMLElement + } + if (element === lastElement && offset === 1) { + return firstElement as HTMLElement + } + return entries[elementIndex + offset] as HTMLElement +}