From f30998aeb6d674c659fdee622a2ceb71f5b98e9c Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Mon, 17 Jun 2024 13:14:43 -0700 Subject: [PATCH] feat: add typings for hooks --- ...sx => useIndexOfLastVisibleChild.test.tsx} | 2 +- ...{useToggle.test.jsx => useToggle.test.tsx} | 3 +- ...owSize.test.jsx => useWindowSize.test.tsx} | 0 ...vigation.jsx => useArrowKeyNavigation.tsx} | 37 ++++++++++++------ ...ild.jsx => useIndexOfLastVisibleChild.tsx} | 8 ++-- .../{useIsVisible.jsx => useIsVisible.tsx} | 9 +++-- src/hooks/useToggle.jsx | 37 ------------------ src/hooks/useToggle.tsx | 38 +++++++++++++++++++ .../{useWindowSize.jsx => useWindowSize.tsx} | 9 ++++- src/index.d.ts | 10 ++--- src/index.js | 10 ++--- 11 files changed, 93 insertions(+), 70 deletions(-) rename src/hooks/tests/{useIndexOfLastVisibleChild.test.jsx => useIndexOfLastVisibleChild.test.tsx} (97%) rename src/hooks/tests/{useToggle.test.jsx => useToggle.test.tsx} (96%) rename src/hooks/tests/{useWindowSize.test.jsx => useWindowSize.test.tsx} (100%) rename src/hooks/{useArrowKeyNavigation.jsx => useArrowKeyNavigation.tsx} (68%) rename src/hooks/{useIndexOfLastVisibleChild.jsx => useIndexOfLastVisibleChild.tsx} (90%) rename src/hooks/{useIsVisible.jsx => useIsVisible.tsx} (74%) delete mode 100644 src/hooks/useToggle.jsx create mode 100644 src/hooks/useToggle.tsx rename src/hooks/{useWindowSize.jsx => useWindowSize.tsx} (81%) diff --git a/src/hooks/tests/useIndexOfLastVisibleChild.test.jsx b/src/hooks/tests/useIndexOfLastVisibleChild.test.tsx similarity index 97% rename from src/hooks/tests/useIndexOfLastVisibleChild.test.jsx rename to src/hooks/tests/useIndexOfLastVisibleChild.test.tsx index 9a104f1589..d47377072d 100644 --- a/src/hooks/tests/useIndexOfLastVisibleChild.test.jsx +++ b/src/hooks/tests/useIndexOfLastVisibleChild.test.tsx @@ -12,7 +12,7 @@ window.ResizeObserver = window.ResizeObserver })); function TestComponent() { - const [containerElementRef, setContainerElementRef] = React.useState(null); + const [containerElementRef, setContainerElementRef] = React.useState(null); const overflowElementRef = React.useRef(null); const indexOfLastVisibleChild = useIndexOfLastVisibleChild(containerElementRef, overflowElementRef.current); diff --git a/src/hooks/tests/useToggle.test.jsx b/src/hooks/tests/useToggle.test.tsx similarity index 96% rename from src/hooks/tests/useToggle.test.jsx rename to src/hooks/tests/useToggle.test.tsx index c1fd08fdc5..0c457617f1 100644 --- a/src/hooks/tests/useToggle.test.jsx +++ b/src/hooks/tests/useToggle.test.tsx @@ -4,6 +4,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useToggle } from '../..'; +import { ToggleHandlers } from '../useToggle'; const TOGGLE_IS_ON = 'on'; const TOGGLE_IS_OFF = 'off'; @@ -19,7 +20,7 @@ const resetHandlerMocks = () => { }; // eslint-disable-next-line react/prop-types -function FakeComponent({ defaultIsOn, handlers }) { +function FakeComponent({ defaultIsOn, handlers }: { defaultIsOn: boolean, handlers: ToggleHandlers }) { const [isOn, setOn, setOff, toggle] = useToggle(defaultIsOn, handlers); return ( diff --git a/src/hooks/tests/useWindowSize.test.jsx b/src/hooks/tests/useWindowSize.test.tsx similarity index 100% rename from src/hooks/tests/useWindowSize.test.jsx rename to src/hooks/tests/useWindowSize.test.tsx diff --git a/src/hooks/useArrowKeyNavigation.jsx b/src/hooks/useArrowKeyNavigation.tsx similarity index 68% rename from src/hooks/useArrowKeyNavigation.jsx rename to src/hooks/useArrowKeyNavigation.tsx index 69b3bb6cbc..862d3f8d14 100644 --- a/src/hooks/useArrowKeyNavigation.jsx +++ b/src/hooks/useArrowKeyNavigation.tsx @@ -4,13 +4,21 @@ import { useRef, useEffect } from 'react'; * A React hook to enable arrow key navigation on a component. */ -function handleEnter({ event, currentIndex, activeElement }) { +function handleEnter( + { event, currentIndex, activeElement }: { event: KeyboardEvent, currentIndex: number, activeElement: HTMLElement }, +) { if (currentIndex === -1) { return; } activeElement.click(); event.preventDefault(); } -function handleArrowKey({ event, currentIndex, availableElements }) { +function handleArrowKey( + { event, currentIndex, availableElements }: { + event: KeyboardEvent, + currentIndex: number, + availableElements: NodeListOf, + }, +) { // If the focus isn't in the container, focus on the first thing if (currentIndex === -1) { availableElements[0].focus(); } @@ -32,8 +40,7 @@ function handleArrowKey({ event, currentIndex, availableElements }) { [nextElement] = availableElements; } - // eslint-disable-next-line no-unused-expressions - nextElement && nextElement.focus(); + nextElement?.focus(); event.preventDefault(); } @@ -45,7 +52,7 @@ function handleEvents({ ignoredKeys = [], parentNode, selectors = 'a,button,input', -}) { +}: { event: KeyboardEvent, ignoredKeys?: string[], parentNode: HTMLElement | undefined, selectors?: string }) { if (!parentNode) { return; } const { key } = event; @@ -61,7 +68,7 @@ function handleEvents({ if (!parentNode.contains(activeElement)) { return; } // Get the list of elements we're allowed to scroll through - const availableElements = parentNode.querySelectorAll(selectors); + const availableElements = parentNode.querySelectorAll(selectors); // No elements are available to loop through. if (!availableElements.length) { return; } @@ -71,18 +78,24 @@ function handleEvents({ (availableElement) => availableElement === activeElement, ); - if (key === 'Enter') { - handleEnter({ event, currentIndex, activeElement }); + if (key === 'Enter' && activeElement) { + handleEnter({ event, currentIndex, activeElement: activeElement as HTMLElement }); } handleArrowKey({ event, currentIndex, availableElements }); } -export default function useArrowKeyNavigation(props) { - const { selectors, ignoredKeys } = props || {}; - const parentNode = useRef(); +export interface ArrowKeyNavProps { + /** e.g. 'a,button,input' */ + selectors?: string; + ignoredKeys?: string[]; +} + +export default function useArrowKeyNavigation(props: ArrowKeyNavProps = {}) { + const { selectors, ignoredKeys } = props; + const parentNode = useRef(); useEffect(() => { - const eventHandler = (event) => { + const eventHandler = (event: KeyboardEvent) => { handleEvents({ event, ignoredKeys, parentNode: parentNode.current, selectors, }); diff --git a/src/hooks/useIndexOfLastVisibleChild.jsx b/src/hooks/useIndexOfLastVisibleChild.tsx similarity index 90% rename from src/hooks/useIndexOfLastVisibleChild.jsx rename to src/hooks/useIndexOfLastVisibleChild.tsx index 786375cdbf..44aed66ea0 100644 --- a/src/hooks/useIndexOfLastVisibleChild.jsx +++ b/src/hooks/useIndexOfLastVisibleChild.tsx @@ -5,12 +5,12 @@ import { useLayoutEffect, useState } from 'react'; * that fits within its bounding rectangle. This is done by summing the widths * of the children until they exceed the width of the container. * - * @param {Element} containerElementRef - container element - * @param {Element} overflowElementRef - overflow element - * * The hook returns the index of the last visible child. + * + * @param containerElementRef - container element + * @param overflowElementRef - overflow element */ -const useIndexOfLastVisibleChild = (containerElementRef, overflowElementRef) => { +const useIndexOfLastVisibleChild = (containerElementRef: Element, overflowElementRef: Element): number => { const [indexOfLastVisibleChild, setIndexOfLastVisibleChild] = useState(-1); useLayoutEffect(() => { diff --git a/src/hooks/useIsVisible.jsx b/src/hooks/useIsVisible.tsx similarity index 74% rename from src/hooks/useIsVisible.jsx rename to src/hooks/useIsVisible.tsx index c9f661af86..0ac26a9f86 100644 --- a/src/hooks/useIsVisible.jsx +++ b/src/hooks/useIsVisible.tsx @@ -1,7 +1,10 @@ -import { useRef, useState, useEffect } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; -const useIsVisible = (defaultIsVisible = true) => { - const sentinelRef = useRef(); +const useIsVisible = (defaultIsVisible = true): [ + isVisible: boolean, + sentinelRef: React.MutableRefObject, +] => { + const sentinelRef = useRef(null); const [isVisible, setIsVisible] = useState(defaultIsVisible); useEffect(() => { diff --git a/src/hooks/useToggle.jsx b/src/hooks/useToggle.jsx deleted file mode 100644 index 731d50e07d..0000000000 --- a/src/hooks/useToggle.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useState, useCallback } from 'react'; - -export default function useToggle(defaultIsOn, handlers = {}) { - const { handleToggleOn, handleToggleOff, handleToggle } = handlers; - const [isOn, setIsOn] = useState(defaultIsOn || false); - - const setOn = useCallback(() => { - setIsOn(true); - // istanbul ignore else - if (handleToggleOn) { - handleToggleOn(); - } - // istanbul ignore else - if (handleToggle) { - handleToggle(true); - } - }, [handleToggleOn, handleToggle]); - - const setOff = useCallback(() => { - setIsOn(false); - // istanbul ignore else - if (handleToggleOff) { - handleToggleOff(); - } - // istanbul ignore else - if (handleToggle) { - handleToggle(false); - } - }, [handleToggleOff, handleToggle]); - - const toggle = useCallback(() => { - const doToggle = isOn ? setOff : setOn; - doToggle(); - }, [isOn, setOn, setOff]); - - return [isOn, setOn, setOff, toggle]; -} diff --git a/src/hooks/useToggle.tsx b/src/hooks/useToggle.tsx new file mode 100644 index 0000000000..20614dcf44 --- /dev/null +++ b/src/hooks/useToggle.tsx @@ -0,0 +1,38 @@ +import { useState, useCallback } from 'react'; + +export type Toggler = [ + isOn: boolean, + setOn: () => void, + setOff: () => void, + toggle: () => void, +]; + +export interface ToggleHandlers { + handleToggleOn?: () => void; + handleToggleOff?: () => void; + handleToggle?: (newStatus: boolean) => void; +} + +export default function useToggle(defaultIsOn = false, handlers: ToggleHandlers = {}): Toggler { + const { handleToggleOn, handleToggleOff, handleToggle } = handlers; + const [isOn, setIsOn] = useState(defaultIsOn); + + const setOn = useCallback(() => { + setIsOn(true); + handleToggleOn?.(); + handleToggle?.(true); + }, [handleToggleOn, handleToggle]); + + const setOff = useCallback(() => { + setIsOn(false); + handleToggleOff?.(); + handleToggle?.(false); + }, [handleToggleOff, handleToggle]); + + const toggle = useCallback(() => { + const doToggle = isOn ? setOff : setOn; + doToggle(); + }, [isOn, setOn, setOff]); + + return [isOn, setOn, setOff, toggle]; +} diff --git a/src/hooks/useWindowSize.jsx b/src/hooks/useWindowSize.tsx similarity index 81% rename from src/hooks/useWindowSize.jsx rename to src/hooks/useWindowSize.tsx index 68229c7b01..6053563e0d 100644 --- a/src/hooks/useWindowSize.jsx +++ b/src/hooks/useWindowSize.tsx @@ -1,9 +1,14 @@ import { useState, useLayoutEffect } from 'react'; -function useWindowSize() { +export interface WindowSizeData { + width: number | undefined; + height: number | undefined; +} + +function useWindowSize(): WindowSizeData { // Initialize state with undefined width/height so server and client renders match // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ - const [windowSize, setWindowSize] = useState({ + const [windowSize, setWindowSize] = useState({ width: undefined, height: undefined, }); diff --git a/src/index.d.ts b/src/index.d.ts index a680a1ac86..0e8ce5d401 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -13,6 +13,11 @@ export { default as Icon } from './Icon'; export { default as IconButton, IconButtonWithTooltip } from './IconButton'; export { default as Overlay, OverlayTrigger } from './Overlay'; export { default as Tooltip } from './Tooltip'; +export { default as useWindowSize, type WindowSizeData } from './hooks/useWindowSize'; +export { default as useToggle, type Toggler, type ToggleHandlers } from './hooks/useToggle'; +export { default as useArrowKeyNavigation, type ArrowKeyNavProps } from './hooks/useArrowKeyNavigation'; +export { default as useIndexOfLastVisibleChild } from './hooks/useIndexOfLastVisibleChild'; +export { default as useIsVisible } from './hooks/useIsVisible'; // // // // // // // // // // // // // // // // // // // // // // // // // // // // Things that don't have types @@ -173,11 +178,6 @@ export const Sticky: any; // from './Sticky'; export const SelectableBox: any; // from './SelectableBox'; export const breakpoints: any; // from './utils/breakpoints'; export const Variant: any; // from './utils/constants'; -export const useWindowSize: any; // from './hooks/useWindowSize'; -export const useToggle: any; // from './hooks/useToggle'; -export const useArrowKeyNavigation: any; // from './hooks/useArrowKeyNavigation'; -export const useIndexOfLastVisibleChild: any; // from './hooks/useIndexOfLastVisibleChild'; -export const useIsVisible: any; // from './hooks/useIsVisible'; export const OverflowScrollContext: any, OverflowScroll: any, diff --git a/src/index.js b/src/index.js index 258b03af3f..d545c54171 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,11 @@ export { default as Icon } from './Icon'; export { default as IconButton, IconButtonWithTooltip } from './IconButton'; export { default as Overlay, OverlayTrigger } from './Overlay'; export { default as Tooltip } from './Tooltip'; +export { default as useWindowSize } from './hooks/useWindowSize'; +export { default as useToggle } from './hooks/useToggle'; +export { default as useArrowKeyNavigation } from './hooks/useArrowKeyNavigation'; +export { default as useIndexOfLastVisibleChild } from './hooks/useIndexOfLastVisibleChild'; +export { default as useIsVisible } from './hooks/useIsVisible'; // // // // // // // // // // // // // // // // // // // // // // // // // // // // Things that don't have types @@ -173,11 +178,6 @@ export { default as Sticky } from './Sticky'; export { default as SelectableBox } from './SelectableBox'; export { default as breakpoints } from './utils/breakpoints'; export { default as Variant } from './utils/constants'; -export { default as useWindowSize } from './hooks/useWindowSize'; -export { default as useToggle } from './hooks/useToggle'; -export { default as useArrowKeyNavigation } from './hooks/useArrowKeyNavigation'; -export { default as useIndexOfLastVisibleChild } from './hooks/useIndexOfLastVisibleChild'; -export { default as useIsVisible } from './hooks/useIsVisible'; export { OverflowScrollContext, OverflowScroll,