From bb55fc2e322bdc48427bf4e00d7f4ff3c8f8f6a2 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Wed, 23 Jun 2021 09:09:46 +0100 Subject: [PATCH] Add useDropdown hook (#4228) GitOrigin-RevId: a16762139049aed1e309b1330602c3b291d41f81 --- .../shared/components/controlled-dropdown.js | 24 +++------ .../frontend/js/shared/hooks/use-dropdown.js | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 services/web/frontend/js/shared/hooks/use-dropdown.js diff --git a/services/web/frontend/js/shared/components/controlled-dropdown.js b/services/web/frontend/js/shared/components/controlled-dropdown.js index f94d70fb84..9e577537dd 100644 --- a/services/web/frontend/js/shared/components/controlled-dropdown.js +++ b/services/web/frontend/js/shared/components/controlled-dropdown.js @@ -1,25 +1,13 @@ -import React, { useCallback, useState } from 'react' +import React from 'react' import { Dropdown } from 'react-bootstrap' import PropTypes from 'prop-types' +import useDropdown from '../hooks/use-dropdown' export default function ControlledDropdown(props) { - const [open, setOpen] = useState(Boolean(props.defaultOpen)) - - const handleClick = useCallback(event => { - event.stopPropagation() - }, []) - - const handleToggle = useCallback(value => { - setOpen(value) - }, []) + const dropdownProps = useDropdown(Boolean(props.defaultOpen)) return ( - + {React.Children.map(props.children, child => { if (!React.isValidElement(child)) { return child @@ -27,12 +15,12 @@ export default function ControlledDropdown(props) { // Dropdown.Menu if ('open' in child.props) { - return React.cloneElement(child, { open }) + return React.cloneElement(child, { open: dropdownProps.open }) } // Overlay if ('show' in child.props) { - return React.cloneElement(child, { show: open }) + return React.cloneElement(child, { show: dropdownProps.open }) } // anything else diff --git a/services/web/frontend/js/shared/hooks/use-dropdown.js b/services/web/frontend/js/shared/hooks/use-dropdown.js new file mode 100644 index 0000000000..b58013e2b4 --- /dev/null +++ b/services/web/frontend/js/shared/hooks/use-dropdown.js @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { findDOMNode } from 'react-dom' + +export default function useDropdown(defaultOpen = false) { + const [open, setOpen] = useState(defaultOpen) + + // store the dropdown node for use in the "click outside" event listener + const ref = useRef(null) + + // react-bootstrap v0.x passes `component` instead of `node` to the ref callback + const handleRef = useCallback( + component => { + if (component) { + // eslint-disable-next-line react/no-find-dom-node + ref.current = findDOMNode(component) + } + }, + [ref] + ) + + // prevent a click on the dropdown toggle propagating to the original handler + const handleClick = useCallback(event => { + event.stopPropagation() + }, []) + + // handle dropdown toggle + const handleToggle = useCallback(value => { + setOpen(value) + }, []) + + // close the dropdown on click outside the dropdown + const handleDocumentClick = useCallback( + event => { + if (ref.current && !ref.current.contains(event.target)) { + setOpen(false) + } + }, + [ref] + ) + + // add/remove listener for click anywhere in document + useEffect(() => { + if (open) { + document.addEventListener('mousedown', handleDocumentClick) + } + + return () => { + document.removeEventListener('mousedown', handleDocumentClick) + } + }, [open, handleDocumentClick]) + + // return props for the Dropdown component + return { ref: handleRef, onClick: handleClick, onToggle: handleToggle, open } +}