From ebd8f6ed0db64eb2ae60320918c75c951df29a1e Mon Sep 17 00:00:00 2001 From: Cole Bemis Date: Thu, 11 Feb 2021 09:25:28 -0800 Subject: [PATCH 1/6] Begin migrating SelectMenu --- src/SelectMenu/SelectMenuContext.js | 3 - src/SelectMenu/SelectMenuContext.tsx | 3 + ...ctMenuDivider.js => SelectMenuDivider.tsx} | 8 ++- src/SelectMenu/SelectMenuFilter.js | 55 ---------------- src/SelectMenu/SelectMenuFilter.tsx | 63 +++++++++++++++++++ ...lectMenuFooter.js => SelectMenuFooter.tsx} | 8 ++- ...lectMenuHeader.js => SelectMenuHeader.tsx} | 12 ++-- .../{SelectMenuItem.js => SelectMenuItem.tsx} | 0 .../{SelectMenuList.js => SelectMenuList.tsx} | 8 ++- ...tion.js => SelectMenuLoadingAnimation.tsx} | 9 ++- ...SelectMenuModal.js => SelectMenuModal.tsx} | 48 +++++++++----- .../{SelectMenuTab.js => SelectMenuTab.tsx} | 13 ++-- ...MenuTabPanel.js => SelectMenuTabPanel.tsx} | 18 +++--- .../{SelectMenuTabs.js => SelectMenuTabs.tsx} | 0 14 files changed, 144 insertions(+), 104 deletions(-) delete mode 100644 src/SelectMenu/SelectMenuContext.js create mode 100644 src/SelectMenu/SelectMenuContext.tsx rename src/SelectMenu/{SelectMenuDivider.js => SelectMenuDivider.tsx} (70%) delete mode 100644 src/SelectMenu/SelectMenuFilter.js create mode 100644 src/SelectMenu/SelectMenuFilter.tsx rename src/SelectMenu/{SelectMenuFooter.js => SelectMenuFooter.tsx} (71%) rename src/SelectMenu/{SelectMenuHeader.js => SelectMenuHeader.tsx} (74%) rename src/SelectMenu/{SelectMenuItem.js => SelectMenuItem.tsx} (100%) rename src/SelectMenu/{SelectMenuList.js => SelectMenuList.tsx} (77%) rename src/SelectMenu/{SelectMenuLoadingAnimation.js => SelectMenuLoadingAnimation.tsx} (71%) rename src/SelectMenu/{SelectMenuModal.js => SelectMenuModal.tsx} (68%) rename src/SelectMenu/{SelectMenuTab.js => SelectMenuTab.tsx} (85%) rename src/SelectMenu/{SelectMenuTabPanel.js => SelectMenuTabPanel.tsx} (75%) rename src/SelectMenu/{SelectMenuTabs.js => SelectMenuTabs.tsx} (100%) diff --git a/src/SelectMenu/SelectMenuContext.js b/src/SelectMenu/SelectMenuContext.js deleted file mode 100644 index a63f91b96fc..00000000000 --- a/src/SelectMenu/SelectMenuContext.js +++ /dev/null @@ -1,3 +0,0 @@ -import {createContext} from 'react' - -export const MenuContext = createContext() diff --git a/src/SelectMenu/SelectMenuContext.tsx b/src/SelectMenu/SelectMenuContext.tsx new file mode 100644 index 00000000000..c1e98789814 --- /dev/null +++ b/src/SelectMenu/SelectMenuContext.tsx @@ -0,0 +1,3 @@ +import {createContext} from 'react' + +export const MenuContext = createContext(null) diff --git a/src/SelectMenu/SelectMenuDivider.js b/src/SelectMenu/SelectMenuDivider.tsx similarity index 70% rename from src/SelectMenu/SelectMenuDivider.js rename to src/SelectMenu/SelectMenuDivider.tsx index 89826ddfca8..f6c3676a424 100644 --- a/src/SelectMenu/SelectMenuDivider.js +++ b/src/SelectMenu/SelectMenuDivider.tsx @@ -1,7 +1,8 @@ import styled, {css} from 'styled-components' import theme from '../theme' -import {COMMON, get} from '../constants' -import sx from '../sx' +import {COMMON, get, SystemCommonProps} from '../constants' +import sx, {SxProp} from '../sx' +import {ComponentProps} from '../utils/types' const dividerStyles = css` padding: ${get('space.1')} ${get('space.3')}; @@ -13,7 +14,7 @@ const dividerStyles = css` border-bottom: ${get('borderWidths.1')} solid ${get('colors.border.grayLight')}; ` -const SelectMenuDivider = styled.div` +const SelectMenuDivider = styled.div` ${dividerStyles} ${COMMON} ${sx}; @@ -30,4 +31,5 @@ SelectMenuDivider.propTypes = { SelectMenuDivider.displayName = 'SelectMenu.Divider' +export type SelectMenuDividerProps = ComponentProps export default SelectMenuDivider diff --git a/src/SelectMenu/SelectMenuFilter.js b/src/SelectMenu/SelectMenuFilter.js deleted file mode 100644 index e793bf1a4cb..00000000000 --- a/src/SelectMenu/SelectMenuFilter.js +++ /dev/null @@ -1,55 +0,0 @@ -import React, {useRef, useContext, forwardRef, useEffect} from 'react' -import styled from 'styled-components' -import PropTypes from 'prop-types' -import {COMMON, get} from '../constants' -import theme from '../theme' -import TextInput from '../TextInput' -import {MenuContext} from './SelectMenuContext' -import sx from '../sx' - -const StyledForm = styled.form` - padding: ${get('space.3')}; - margin: 0; - border-top: ${get('borderWidths.1')} solid ${get('colors.border.gray')}; - background-color: ${get('colors.white')}; - ${COMMON}; - - @media (min-width: ${get('breakpoints.0')}) { - padding: ${get('space.2')}; - } - - ${sx}; -` - -const SelectMenuFilter = forwardRef(({theme, value, sx, ...rest}, forwardedRef) => { - const inputRef = useRef(null) - const ref = forwardedRef ?? inputRef - const {open} = useContext(MenuContext) - - // puts focus on the filter input when the menu is opened - useEffect(() => { - if (open) { - inputRef.current.focus() - } - }, [open]) - - return ( - - - - ) -}) - -SelectMenuFilter.defaultProps = { - theme -} - -SelectMenuFilter.propTypes = { - ...COMMON.propTypes, - ...sx.propTypes, - value: PropTypes.string -} - -SelectMenuFilter.displayName = 'SelectMenu.Filter' - -export default SelectMenuFilter diff --git a/src/SelectMenu/SelectMenuFilter.tsx b/src/SelectMenu/SelectMenuFilter.tsx new file mode 100644 index 00000000000..b40cc27f941 --- /dev/null +++ b/src/SelectMenu/SelectMenuFilter.tsx @@ -0,0 +1,63 @@ +import React, {useRef, useContext, forwardRef, useEffect} from 'react' +import styled from 'styled-components' +import PropTypes from 'prop-types' +import {COMMON, get, SystemCommonProps} from '../constants' +import theme from '../theme' +import TextInput, {TextInputProps} from '../TextInput' +import {MenuContext} from './SelectMenuContext' +import sx from '../sx' +import {ComponentProps} from '../utils/types' + +const StyledForm = styled.form` + padding: ${get('space.3')}; + margin: 0; + border-top: ${get('borderWidths.1')} solid ${get('colors.border.gray')}; + background-color: ${get('colors.white')}; + ${COMMON}; + + @media (min-width: ${get('breakpoints.0')}) { + padding: ${get('space.2')}; + } + + ${sx}; +` + +type SelectMenuFilterInternalProps = { + value?: string +} & TextInputProps + +const SelectMenuFilter = forwardRef( + ({theme, value, sx, ...rest}, forwardedRef) => { + const inputRef = useRef(null) + const ref = forwardedRef ?? inputRef + const {open} = useContext(MenuContext) + + // puts focus on the filter input when the menu is opened + useEffect(() => { + if (open) { + inputRef.current.focus() + } + }, [open]) + + return ( + + + + ) + } +) + +SelectMenuFilter.defaultProps = { + theme +} + +SelectMenuFilter.propTypes = { + ...COMMON.propTypes, + ...sx.propTypes, + value: PropTypes.string +} + +SelectMenuFilter.displayName = 'SelectMenu.Filter' + +export type SelectMenuFilterProps = ComponentProps +export default SelectMenuFilter diff --git a/src/SelectMenu/SelectMenuFooter.js b/src/SelectMenu/SelectMenuFooter.tsx similarity index 71% rename from src/SelectMenu/SelectMenuFooter.js rename to src/SelectMenu/SelectMenuFooter.tsx index a3313ebce2b..b01d737bda6 100644 --- a/src/SelectMenu/SelectMenuFooter.js +++ b/src/SelectMenu/SelectMenuFooter.tsx @@ -1,7 +1,8 @@ import styled, {css} from 'styled-components' -import {COMMON, get} from '../constants' +import {COMMON, get, SystemCommonProps} from '../constants' import theme from '../theme' -import sx from '../sx' +import sx, {SxProp} from '../sx' +import {ComponentProps} from '../utils/types' const footerStyles = css` margin-top: -1px; @@ -16,7 +17,7 @@ const footerStyles = css` } ` -const SelectMenuFooter = styled.footer` +const SelectMenuFooter = styled.footer` ${footerStyles} ${COMMON} ${sx}; @@ -33,4 +34,5 @@ SelectMenuFooter.propTypes = { SelectMenuFooter.displayName = 'SelectMenu.Footer' +export type SelectMenuFooterProps = ComponentProps export default SelectMenuFooter diff --git a/src/SelectMenu/SelectMenuHeader.js b/src/SelectMenu/SelectMenuHeader.tsx similarity index 74% rename from src/SelectMenu/SelectMenuHeader.js rename to src/SelectMenu/SelectMenuHeader.tsx index b05b521641d..06ad93f9673 100644 --- a/src/SelectMenu/SelectMenuHeader.js +++ b/src/SelectMenu/SelectMenuHeader.tsx @@ -1,9 +1,10 @@ import React from 'react' import styled from 'styled-components' import PropTypes from 'prop-types' -import {get, COMMON, TYPOGRAPHY} from '../constants' +import {get, COMMON, TYPOGRAPHY, SystemCommonProps, SystemTypographyProps} from '../constants' import theme from '../theme' -import sx from '../sx' +import sx, {SxProp} from '../sx' +import {ComponentProps} from '../utils/types' // SelectMenu.Header is intentionally not exported, it's an internal component used in // SelectMenu.Modal @@ -19,7 +20,7 @@ const SelectMenuTitle = styled.h3` } ` -const StyledHeader = styled.header` +const StyledHeader = styled.header` display: flex; flex: none; // fixes header from getting squeezed in Safari iOS padding: ${get('space.3')}; @@ -34,7 +35,10 @@ const StyledHeader = styled.header` ${sx}; ` -const SelectMenuHeader = ({children, theme, ...rest}) => { + +export type SelectMenuHeaderProps = ComponentProps + +const SelectMenuHeader = ({children, theme, ...rest}: SelectMenuHeaderProps) => { return ( {children} diff --git a/src/SelectMenu/SelectMenuItem.js b/src/SelectMenu/SelectMenuItem.tsx similarity index 100% rename from src/SelectMenu/SelectMenuItem.js rename to src/SelectMenu/SelectMenuItem.tsx diff --git a/src/SelectMenu/SelectMenuList.js b/src/SelectMenu/SelectMenuList.tsx similarity index 77% rename from src/SelectMenu/SelectMenuList.js rename to src/SelectMenu/SelectMenuList.tsx index b16afdab4ff..73ca318a598 100644 --- a/src/SelectMenu/SelectMenuList.js +++ b/src/SelectMenu/SelectMenuList.tsx @@ -1,7 +1,8 @@ import styled, {css} from 'styled-components' +import {COMMON, get, SystemCommonProps} from '../constants' +import sx, {SxProp} from '../sx' import theme from '../theme' -import {COMMON, get} from '../constants' -import sx from '../sx' +import {ComponentProps} from '../utils/types' const listStyles = css` position: relative; @@ -30,7 +31,7 @@ const listStyles = css` } ` -const SelectMenuList = styled.div` +const SelectMenuList = styled.div` ${listStyles} ${COMMON} ${sx}; @@ -46,4 +47,5 @@ SelectMenuList.propTypes = { SelectMenuList.displayName = 'SelectMenu.List' +export type SelectMenuListProps = ComponentProps export default SelectMenuList diff --git a/src/SelectMenu/SelectMenuLoadingAnimation.js b/src/SelectMenu/SelectMenuLoadingAnimation.tsx similarity index 71% rename from src/SelectMenu/SelectMenuLoadingAnimation.js rename to src/SelectMenu/SelectMenuLoadingAnimation.tsx index 3a1843c47ce..374790fd103 100644 --- a/src/SelectMenu/SelectMenuLoadingAnimation.js +++ b/src/SelectMenu/SelectMenuLoadingAnimation.tsx @@ -2,7 +2,8 @@ import React from 'react' import styled, {keyframes} from 'styled-components' import StyledOcticon from '../StyledOcticon' import {OctofaceIcon} from '@primer/octicons-react' -import {get, COMMON} from '../constants' +import {get, COMMON, SystemCommonProps} from '../constants' +import {ComponentProps} from '../utils/types' const pulseKeyframes = keyframes` 0% { @@ -16,7 +17,7 @@ const pulseKeyframes = keyframes` } ` -const Animation = styled.div` +const Animation = styled.div` padding: ${get('space.6')} ${get('space.4')}; text-align: center; background-color: ${get('colors.white')}; @@ -27,7 +28,9 @@ const Animation = styled.div` ${COMMON} ` -const SelectMenuLoadingAnimation = props => { +export type SelectMenuLoadingAnimationProps = ComponentProps + +const SelectMenuLoadingAnimation = (props: SelectMenuLoadingAnimationProps) => { return ( diff --git a/src/SelectMenu/SelectMenuModal.js b/src/SelectMenu/SelectMenuModal.tsx similarity index 68% rename from src/SelectMenu/SelectMenuModal.js rename to src/SelectMenu/SelectMenuModal.tsx index 38f61c1b8e0..1460476d87f 100644 --- a/src/SelectMenu/SelectMenuModal.js +++ b/src/SelectMenu/SelectMenuModal.tsx @@ -1,10 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' import styled, {keyframes, css} from 'styled-components' -import {COMMON, get} from '../constants' -import {width} from 'styled-system' +import {COMMON, get, SystemCommonProps} from '../constants' +import {width, WidthProps} from 'styled-system' import theme from '../theme' -import sx from '../sx' +import sx, {SxProp} from '../sx' +import {ComponentProps} from '../utils/types' + +type StyledModalProps = { + filter?: boolean +} & WidthProps + +type StyledModalWrapperProps = { + align?: 'left' | 'right' +} & SystemCommonProps & + SxProp const animateModal = keyframes` 0% { @@ -13,7 +23,7 @@ const animateModal = keyframes` } ` -const modalStyles = css` +const modalStyles = css` position: relative; z-index: 99; // Needs to be higher than .details-overlay's z-index: 80. display: flex; @@ -40,7 +50,7 @@ const modalStyles = css` } ` -const modalWrapperStyles = css` +const modalWrapperStyles = css` position: fixed; top: 0; right: 0; @@ -77,25 +87,30 @@ const modalWrapperStyles = css` } ` -const Modal = styled.div` +const Modal = styled.div` ${modalStyles} ${width} ` -const ModalWrapper = styled.div` +const ModalWrapper = styled.div` ${modalWrapperStyles} ${COMMON} ${sx}; ` -const SelectMenuModal = React.forwardRef(({children, theme, width, ...rest}, forwardedRef) => { - return ( - - - {children} - - - ) -}) + +type SelectMenuModalInternalProps = Pick & ComponentProps + +const SelectMenuModal = React.forwardRef( + ({children, theme, width, ...rest}, forwardedRef) => { + return ( + + + {children} + + + ) + } +) SelectMenuModal.defaultProps = { align: 'left', @@ -113,4 +128,5 @@ SelectMenuModal.propTypes = { SelectMenuModal.displayName = 'SelectMenu.Modal' +export type SelectMenuModalProps = ComponentProps export default SelectMenuModal diff --git a/src/SelectMenu/SelectMenuTab.js b/src/SelectMenu/SelectMenuTab.tsx similarity index 85% rename from src/SelectMenu/SelectMenuTab.js rename to src/SelectMenu/SelectMenuTab.tsx index 5e668c0a71a..8ed76f00bb6 100644 --- a/src/SelectMenu/SelectMenuTab.js +++ b/src/SelectMenu/SelectMenuTab.tsx @@ -3,9 +3,10 @@ import classnames from 'classnames' import PropTypes from 'prop-types' import styled, {css} from 'styled-components' import {MenuContext} from './SelectMenuContext' -import {get, COMMON} from '../constants' +import {get, COMMON, SystemCommonProps} from '../constants' import theme from '../theme' -import sx from '../sx' +import sx, {SxProp} from '../sx' +import {ComponentProps} from '../utils/types' const tabStyles = css` flex: 1; @@ -45,15 +46,17 @@ const tabStyles = css` } ` -const StyledTab = styled.button` +const StyledTab = styled.button` ${tabStyles} ${COMMON} ${sx}; ` -const SelectMenuTab = ({tabName, index, className, onClick, ...rest}) => { +export type SelectMenuTabProps = {tabName?: string; index?: number} & ComponentProps + +const SelectMenuTab = ({tabName, index, className, onClick, ...rest}: SelectMenuTabProps) => { const menuContext = useContext(MenuContext) - const handleClick = e => { + const handleClick = (e: React.MouseEvent) => { // if consumer has attached an onClick event, call it onClick && onClick(e) if (!e.defaultPrevented) { diff --git a/src/SelectMenu/SelectMenuTabPanel.js b/src/SelectMenu/SelectMenuTabPanel.tsx similarity index 75% rename from src/SelectMenu/SelectMenuTabPanel.js rename to src/SelectMenu/SelectMenuTabPanel.tsx index 0eddb9d3cb2..0a6047e1faf 100644 --- a/src/SelectMenu/SelectMenuTabPanel.js +++ b/src/SelectMenu/SelectMenuTabPanel.tsx @@ -7,21 +7,21 @@ import theme from '../theme' import {COMMON, get} from '../constants' import sx from '../sx' -const TabPanelBase = ({tabName, className, children, ...rest}) => { +const TabPanelBase = styled.div` + border-top: ${get('borderWidths.1')} solid ${get('colors.border.gray')}; + ${COMMON} + ${sx}; +` + +const TabPanel = ({tabName, className, children, ...rest}) => { const menuContext = useContext(MenuContext) return ( - + ) } -const TabPanel = styled(TabPanelBase)` - border-top: ${get('borderWidths.1')} solid ${get('colors.border.gray')}; - ${COMMON} - ${sx}; -` - TabPanel.defaultProps = { theme } diff --git a/src/SelectMenu/SelectMenuTabs.js b/src/SelectMenu/SelectMenuTabs.tsx similarity index 100% rename from src/SelectMenu/SelectMenuTabs.js rename to src/SelectMenu/SelectMenuTabs.tsx From 15394bbbc1ef7cff6a89d2ff72b9f7da59be4626 Mon Sep 17 00:00:00 2001 From: Cole Bemis Date: Fri, 12 Feb 2021 12:08:07 -0800 Subject: [PATCH 2/6] Update the rest of select menu --- babel.config.js | 9 +- package.json | 1 + src/SelectMenu/SelectMenu.js | 117 ---------------------- src/SelectMenu/SelectMenu.tsx | 137 ++++++++++++++++++++++++++ src/SelectMenu/SelectMenuContext.tsx | 8 +- src/SelectMenu/SelectMenuFilter.tsx | 6 +- src/SelectMenu/SelectMenuItem.tsx | 48 +++++---- src/SelectMenu/SelectMenuTab.tsx | 6 +- src/SelectMenu/SelectMenuTabPanel.tsx | 13 ++- src/SelectMenu/SelectMenuTabs.tsx | 21 ++-- src/SelectMenu/index.js | 3 - src/SelectMenu/index.ts | 15 +++ yarn.lock | 9 ++ 13 files changed, 232 insertions(+), 161 deletions(-) delete mode 100644 src/SelectMenu/SelectMenu.js create mode 100644 src/SelectMenu/SelectMenu.tsx delete mode 100644 src/SelectMenu/index.js create mode 100644 src/SelectMenu/index.ts diff --git a/babel.config.js b/babel.config.js index 20b403b8c70..d93d727e779 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,7 +4,14 @@ function replacementPlugin(env) { return ['babel-plugin-transform-replace-expressions', {replace: defines[env]}] } -const sharedPlugins = ['macros', 'preval', 'add-react-displayname', 'babel-plugin-styled-components', '@babel/plugin-proposal-nullish-coalescing-operator'] +const sharedPlugins = [ + 'macros', + 'preval', + 'add-react-displayname', + 'babel-plugin-styled-components', + '@babel/plugin-proposal-nullish-coalescing-operator', + '@babel/plugin-proposal-optional-chaining' +] function makePresets(moduleValue) { return ['@babel/preset-typescript', ['@babel/preset-react', {modules: moduleValue}]] diff --git a/package.json b/package.json index 2f97df640da..01dcd16fe80 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@babel/core": "7.9.0", "@babel/eslint-parser": "7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator": "7.12.1", + "@babel/plugin-proposal-optional-chaining": "7.12.16", "@babel/plugin-transform-modules-commonjs": "7.12.1", "@babel/preset-react": "7.9.4", "@babel/preset-typescript": "7.12.7", diff --git a/src/SelectMenu/SelectMenu.js b/src/SelectMenu/SelectMenu.js deleted file mode 100644 index 31585e82d06..00000000000 --- a/src/SelectMenu/SelectMenu.js +++ /dev/null @@ -1,117 +0,0 @@ -import React, {useRef, useState, useCallback, useEffect} from 'react' -import styled from 'styled-components' -import PropTypes from 'prop-types' -import sx from '../sx' -import {COMMON} from '../constants' -import theme from '../theme' -import {MenuContext} from './SelectMenuContext' -import SelectMenuDivider from './SelectMenuDivider' -import SelectMenuFilter from './SelectMenuFilter' -import SelectMenuFooter from './SelectMenuFooter' -import SelectMenuItem from './SelectMenuItem' -import SelectMenuList from './SelectMenuList' -import SelectMenuModal from './SelectMenuModal' -import SelectMenuTabs from './SelectMenuTabs' -import SelectMenuHeader from './SelectMenuHeader' -import SelectMenuTab from './SelectMenuTab' -import SelectMenuTabPanel from './SelectMenuTabPanel' -import SelectMenuLoadingAnimation from './SelectMenuLoadingAnimation' -import useKeyboardNav from './hooks/useKeyboardNav' - -const wrapperStyles = ` - // Remove marker added by the display: list-item browser default - > summary { - list-style: none; - } - // Remove marker added by details polyfill - > summary::before { - display: none; - } - // Remove marker added by Chrome - > summary::-webkit-details-marker { - display: none; - } -` - -const StyledSelectMenu = styled.details` - ${wrapperStyles} - ${COMMON} - ${sx}; -` - -// 'as' is spread out because we don't want users to be able to change the tag. -const SelectMenu = React.forwardRef(({children, initialTab, as, ...rest}, forwardedRef) => { - const backupRef = useRef() - const ref = forwardedRef ?? backupRef - const [selectedTab, setSelectedTab] = useState(initialTab) - const [open, setOpen] = useState(false) - const menuProviderValues = { - selectedTab, - setSelectedTab, - setOpen, - open, - initialTab - } - - const onClickOutside = useCallback( - event => { - if (ref.current && !ref.current.contains(event.target)) { - if (!event.defaultPrevented) { - setOpen(false) - } - } - }, - [ref, setOpen] - ) - - // handles the overlay behavior - closing the menu when clicking outside of it - useEffect(() => { - if (open) { - document.addEventListener('click', onClickOutside) - return () => { - document.removeEventListener('click', onClickOutside) - } - } - }, [open, onClickOutside]) - - function toggle(event) { - setOpen(event.target.open) - } - - useKeyboardNav(ref, open, setOpen) - - return ( - - - {children} - - - ) -}) - -SelectMenu.displayName = 'SelectMenu' -SelectMenu.MenuContext = MenuContext -SelectMenu.List = SelectMenuList -SelectMenu.Divider = SelectMenuDivider -SelectMenu.Filter = SelectMenuFilter -SelectMenu.Footer = SelectMenuFooter -SelectMenu.Item = SelectMenuItem -SelectMenu.List = SelectMenuList -SelectMenu.Modal = SelectMenuModal -SelectMenu.Tabs = SelectMenuTabs -SelectMenu.Tab = SelectMenuTab -SelectMenu.TabPanel = SelectMenuTabPanel -SelectMenu.Header = SelectMenuHeader -SelectMenu.LoadingAnimation = SelectMenuLoadingAnimation - -SelectMenu.defaultProps = { - theme -} - -SelectMenu.propTypes = { - initialTab: PropTypes.string, - ...COMMON.propTypes, - ...sx.propTypes -} - -export default SelectMenu diff --git a/src/SelectMenu/SelectMenu.tsx b/src/SelectMenu/SelectMenu.tsx new file mode 100644 index 00000000000..ac2b847761f --- /dev/null +++ b/src/SelectMenu/SelectMenu.tsx @@ -0,0 +1,137 @@ +import React, {useRef, useState, useCallback, useEffect} from 'react' +import styled from 'styled-components' +import PropTypes from 'prop-types' +import sx, {SxProp} from '../sx' +import {COMMON, SystemCommonProps} from '../constants' +import theme from '../theme' +import {MenuContext} from './SelectMenuContext' +import SelectMenuDivider from './SelectMenuDivider' +import SelectMenuFilter from './SelectMenuFilter' +import SelectMenuFooter from './SelectMenuFooter' +import SelectMenuItem from './SelectMenuItem' +import SelectMenuList from './SelectMenuList' +import SelectMenuModal from './SelectMenuModal' +import SelectMenuTabs from './SelectMenuTabs' +import SelectMenuHeader from './SelectMenuHeader' +import SelectMenuTab from './SelectMenuTab' +import SelectMenuTabPanel from './SelectMenuTabPanel' +import SelectMenuLoadingAnimation from './SelectMenuLoadingAnimation' +import useKeyboardNav from './hooks/useKeyboardNav' +import {ComponentProps} from '../utils/types' + +const wrapperStyles = ` + // Remove marker added by the display: list-item browser default + > summary { + list-style: none; + } + // Remove marker added by details polyfill + > summary::before { + display: none; + } + // Remove marker added by Chrome + > summary::-webkit-details-marker { + display: none; + } +` + +const StyledSelectMenu = styled.details` + ${wrapperStyles} + ${COMMON} + ${sx}; +` + +type SelectMenuInternalProps = { + initialTab?: string + as?: React.ReactElement +} & ComponentProps + +// 'as' is spread out because we don't want users to be able to change the tag. +const SelectMenu = React.forwardRef( + ({children, initialTab = '', as, ...rest}, forwardedRef) => { + const backupRef = useRef(null) + const ref = forwardedRef ?? backupRef + const [selectedTab, setSelectedTab] = useState(initialTab) + const [open, setOpen] = useState(false) + const menuProviderValues = { + selectedTab, + setSelectedTab, + setOpen, + open, + initialTab + } + + const onClickOutside = useCallback( + event => { + if ('current' in ref && ref.current && !ref.current.contains(event.target)) { + if (!event.defaultPrevented) { + setOpen(false) + } + } + }, + [ref, setOpen] + ) + + // handles the overlay behavior - closing the menu when clicking outside of it + useEffect(() => { + if (open) { + document.addEventListener('click', onClickOutside) + return () => { + document.removeEventListener('click', onClickOutside) + } + } + }, [open, onClickOutside]) + + function toggle(event: React.SyntheticEvent) { + setOpen((event.target as HTMLDetailsElement).open) + } + + useKeyboardNav(ref, open, setOpen) + + return ( + + + {children} + + + ) + } +) + +SelectMenu.displayName = 'SelectMenu' + +SelectMenu.defaultProps = { + theme +} + +SelectMenu.propTypes = { + initialTab: PropTypes.string, + ...COMMON.propTypes, + ...sx.propTypes +} + +export type SelectMenuProps = ComponentProps +export type {SelectMenuDividerProps} from './SelectMenuDivider' +export type {SelectMenuFilterProps} from './SelectMenuFilter' +export type {SelectMenuFooterProps} from './SelectMenuFooter' +export type {SelectMenuItemProps} from './SelectMenuItem' +export type {SelectMenuListProps} from './SelectMenuList' +export type {SelectMenuModalProps} from './SelectMenuModal' +export type {SelectMenuTabsProps} from './SelectMenuTabs' +export type {SelectMenuHeaderProps} from './SelectMenuHeader' +export type {SelectMenuTabProps} from './SelectMenuTab' +export type {SelectMenuTabPanelProps} from './SelectMenuTabPanel' +export type {SelectMenuLoadingAnimationProps} from './SelectMenuLoadingAnimation' +export default Object.assign(SelectMenu, { + MenuContext: MenuContext, + List: SelectMenuList, + Divider: SelectMenuDivider, + Filter: SelectMenuFilter, + Footer: SelectMenuFooter, + Item: SelectMenuItem, + Modal: SelectMenuModal, + Tabs: SelectMenuTabs, + Tab: SelectMenuTab, + TabPanel: SelectMenuTabPanel, + Header: SelectMenuHeader, + LoadingAnimation: SelectMenuLoadingAnimation +}) diff --git a/src/SelectMenu/SelectMenuContext.tsx b/src/SelectMenu/SelectMenuContext.tsx index c1e98789814..53803162eb2 100644 --- a/src/SelectMenu/SelectMenuContext.tsx +++ b/src/SelectMenu/SelectMenuContext.tsx @@ -1,3 +1,9 @@ import {createContext} from 'react' -export const MenuContext = createContext(null) +export const MenuContext = createContext<{ + selectedTab?: string + setSelectedTab?: React.Dispatch> + setOpen?: React.Dispatch> + open?: boolean + initialTab?: string +}>({}) diff --git a/src/SelectMenu/SelectMenuFilter.tsx b/src/SelectMenu/SelectMenuFilter.tsx index b40cc27f941..f471577535e 100644 --- a/src/SelectMenu/SelectMenuFilter.tsx +++ b/src/SelectMenu/SelectMenuFilter.tsx @@ -5,7 +5,7 @@ import {COMMON, get, SystemCommonProps} from '../constants' import theme from '../theme' import TextInput, {TextInputProps} from '../TextInput' import {MenuContext} from './SelectMenuContext' -import sx from '../sx' +import sx, {SxProp} from '../sx' import {ComponentProps} from '../utils/types' const StyledForm = styled.form` @@ -28,14 +28,14 @@ type SelectMenuFilterInternalProps = { const SelectMenuFilter = forwardRef( ({theme, value, sx, ...rest}, forwardedRef) => { - const inputRef = useRef(null) + const inputRef = useRef(null) const ref = forwardedRef ?? inputRef const {open} = useContext(MenuContext) // puts focus on the filter input when the menu is opened useEffect(() => { if (open) { - inputRef.current.focus() + inputRef.current?.focus() } }, [open]) diff --git a/src/SelectMenu/SelectMenuItem.tsx b/src/SelectMenu/SelectMenuItem.tsx index 6cab7a81423..aac2b633fd0 100644 --- a/src/SelectMenu/SelectMenuItem.tsx +++ b/src/SelectMenu/SelectMenuItem.tsx @@ -3,10 +3,11 @@ import PropTypes from 'prop-types' import styled, {css} from 'styled-components' import {CheckIcon} from '@primer/octicons-react' import {MenuContext} from './SelectMenuContext' -import {COMMON, get} from '../constants' +import {COMMON, get, SystemCommonProps} from '../constants' import StyledOcticon from '../StyledOcticon' import theme from '../theme' -import sx from '../sx' +import sx, {SxProp} from '../sx' +import {ComponentProps} from '../utils/types' export const listItemStyles = css` display: flex; @@ -94,33 +95,39 @@ export const listItemStyles = css` const StyledItem = styled.a.attrs(() => ({ role: 'menuitemcheckbox' -}))` +}))` ${listItemStyles} ${COMMON} ${sx}; ` -const SelectMenuItem = forwardRef(({children, selected, theme, onClick, ...rest}, forwardedRef) => { - const menuContext = useContext(MenuContext) - const backupRef = useRef(null) - const itemRef = forwardedRef ?? backupRef +type SelectMenuItemInteralProps = { + selected?: boolean +} & ComponentProps - // close the menu when an item is clicked - // this can be overriden if the user provides a `onClick` prop and prevents default in it - const handleClick = e => { - onClick && onClick(e) +const SelectMenuItem = forwardRef( + ({children, selected, theme, onClick, ...rest}, forwardedRef) => { + const menuContext = useContext(MenuContext) + const backupRef = useRef(null) + const itemRef = forwardedRef ?? backupRef - if (!e.defaultPrevented) { - menuContext.setOpen(false) + // close the menu when an item is clicked + // this can be overriden if the user provides a `onClick` prop and prevents default in it + const handleClick = (e: React.MouseEvent) => { + onClick && onClick(e) + + if (!e.defaultPrevented) { + menuContext.setOpen?.(false) + } } + return ( + + + {children} + + ) } - return ( - - - {children} - - ) -}) +) SelectMenuItem.defaultProps = { theme, @@ -135,4 +142,5 @@ SelectMenuItem.propTypes = { SelectMenuItem.displayName = 'SelectMenu.Item' +export type SelectMenuItemProps = ComponentProps export default SelectMenuItem diff --git a/src/SelectMenu/SelectMenuTab.tsx b/src/SelectMenu/SelectMenuTab.tsx index 8ed76f00bb6..2f21fcb329b 100644 --- a/src/SelectMenu/SelectMenuTab.tsx +++ b/src/SelectMenu/SelectMenuTab.tsx @@ -54,20 +54,20 @@ const StyledTab = styled.button` export type SelectMenuTabProps = {tabName?: string; index?: number} & ComponentProps -const SelectMenuTab = ({tabName, index, className, onClick, ...rest}: SelectMenuTabProps) => { +const SelectMenuTab = ({tabName = '', index, className, onClick, ...rest}: SelectMenuTabProps) => { const menuContext = useContext(MenuContext) const handleClick = (e: React.MouseEvent) => { // if consumer has attached an onClick event, call it onClick && onClick(e) if (!e.defaultPrevented) { - menuContext.setSelectedTab(tabName) + menuContext.setSelectedTab?.(tabName) } } // if no tab is selected when the component renders, show the first tab useEffect(() => { if (!menuContext.selectedTab && index === 0) { - menuContext.setSelectedTab(tabName) + menuContext.setSelectedTab?.(tabName) } }, [index, menuContext, tabName]) diff --git a/src/SelectMenu/SelectMenuTabPanel.tsx b/src/SelectMenu/SelectMenuTabPanel.tsx index 0a6047e1faf..91132809e22 100644 --- a/src/SelectMenu/SelectMenuTabPanel.tsx +++ b/src/SelectMenu/SelectMenuTabPanel.tsx @@ -4,16 +4,21 @@ import styled from 'styled-components' import {MenuContext} from './SelectMenuContext' import SelectMenuList from './SelectMenuList' import theme from '../theme' -import {COMMON, get} from '../constants' -import sx from '../sx' +import {COMMON, get, SystemCommonProps} from '../constants' +import sx, {SxProp} from '../sx' +import {ComponentProps} from '../utils/types' -const TabPanelBase = styled.div` +const TabPanelBase = styled.div` border-top: ${get('borderWidths.1')} solid ${get('colors.border.gray')}; ${COMMON} ${sx}; ` -const TabPanel = ({tabName, className, children, ...rest}) => { +export type SelectMenuTabPanelProps = { + tabName?: string +} & ComponentProps + +const TabPanel = ({tabName, className, children, ...rest}: SelectMenuTabPanelProps) => { const menuContext = useContext(MenuContext) return (