diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index 7f51284179..c04f961d75 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { remSize, prop } from '../theme'; import IconButton from '../common/IconButton'; -const DropdownWrapper = styled.ul` +export const DropdownWrapper = styled.ul` background-color: ${prop('Modal.background')}; border: 1px solid ${prop('Modal.border')}; box-shadow: 0 0 18px 0 ${prop('shadowColor')}; @@ -52,6 +52,7 @@ const DropdownWrapper = styled.ul` & button span, & a { padding: ${remSize(8)} ${remSize(16)}; + font-size: ${remSize(12)}; } * { diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx new file mode 100644 index 0000000000..da41b30101 --- /dev/null +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types'; +import React, { forwardRef, useCallback, useRef, useState } from 'react'; +import useModalClose from '../../common/useModalClose'; +import DownArrowIcon from '../../images/down-filled-triangle.svg'; +import { DropdownWrapper } from '../Dropdown'; + +// TODO: enable arrow keys to navigate options from list + +const DropdownMenu = forwardRef( + ( + { children, anchor, 'aria-label': ariaLabel, align, className, classes }, + ref + ) => { + // Note: need to use a ref instead of a state to avoid stale closures. + const focusedRef = useRef(false); + + const [isOpen, setIsOpen] = useState(false); + + const close = useCallback(() => setIsOpen(false), [setIsOpen]); + + const anchorRef = useModalClose(close, ref); + + const toggle = useCallback(() => { + setIsOpen((prevState) => !prevState); + }, [setIsOpen]); + + const handleFocus = () => { + focusedRef.current = true; + }; + + const handleBlur = () => { + focusedRef.current = false; + setTimeout(() => { + if (!focusedRef.current) { + close(); + } + }, 200); + }; + + return ( +
+ + {isOpen && ( + { + setTimeout(close, 0); + }} + onBlur={handleBlur} + onFocus={handleFocus} + > + {children} + + )} +
+ ); + } +); + +DropdownMenu.propTypes = { + /** + * Provide elements as children to control the contents of the menu. + */ + children: PropTypes.node.isRequired, + /** + * Can optionally override the contents of the button which opens the menu. + * Defaults to + */ + anchor: PropTypes.node, + 'aria-label': PropTypes.string.isRequired, + align: PropTypes.oneOf(['left', 'right']), + className: PropTypes.string, + classes: PropTypes.shape({ + button: PropTypes.string, + list: PropTypes.string + }) +}; + +DropdownMenu.defaultProps = { + anchor: null, + align: 'right', + className: '', + classes: {} +}; + +export default DropdownMenu; diff --git a/client/components/Dropdown/MenuItem.jsx b/client/components/Dropdown/MenuItem.jsx new file mode 100644 index 0000000000..8b6f6d7247 --- /dev/null +++ b/client/components/Dropdown/MenuItem.jsx @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ButtonOrLink from '../../common/ButtonOrLink'; + +// TODO: combine with NavMenuItem + +function MenuItem({ hideIf, ...rest }) { + if (hideIf) { + return null; + } + + return ( +
  • + +
  • + ); +} + +MenuItem.propTypes = { + ...ButtonOrLink.propTypes, + onClick: PropTypes.func, + value: PropTypes.string, + /** + * Provides a way to deal with optional items. + */ + hideIf: PropTypes.bool +}; + +MenuItem.defaultProps = { + onClick: null, + value: null, + hideIf: false +}; + +export default MenuItem; diff --git a/client/components/Dropdown/TableDropdown.jsx b/client/components/Dropdown/TableDropdown.jsx new file mode 100644 index 0000000000..d4db78f963 --- /dev/null +++ b/client/components/Dropdown/TableDropdown.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useMediaQuery } from 'react-responsive'; +import styled from 'styled-components'; +import { prop, remSize } from '../../theme'; +import DropdownMenu from './DropdownMenu'; + +import DownFilledTriangleIcon from '../../images/down-filled-triangle.svg'; +import MoreIconSvg from '../../images/more.svg'; + +const DotsHorizontal = styled(MoreIconSvg)` + transform: rotate(90deg); +`; + +const TableDropdownIcon = () => { + // TODO: centralize breakpoints + const isMobile = useMediaQuery({ maxWidth: 770 }); + + return isMobile ? ( +