diff --git a/docs/content/Buttons.md b/docs/content/Buttons.md
index 87cc8141910..46697c6b9ff 100644
--- a/docs/content/Buttons.md
+++ b/docs/content/Buttons.md
@@ -22,6 +22,8 @@ To create a button group, wrap `Button` elements in the `ButtonGroup` element. `
+
+Button Table List
```
## System props
diff --git a/docs/content/SelectMenu.md b/docs/content/SelectMenu.md
new file mode 100644
index 00000000000..3a95eb0fe65
--- /dev/null
+++ b/docs/content/SelectMenu.md
@@ -0,0 +1,287 @@
+---
+title: SelectMenu
+---
+
+The `SelectMenu` components are a suite of components which can be combined together to make several different variations of our GitHub select menu. At it's most basic form, a select menu is comprised of a `SelectMenu` wrapper, which contains a `summary` component of your choice and a `Select.Modal` which contains the select menu content. Use `SelectMenu.List` to wrap items in the select menu, and `SelectMenu.Item` to wrap each item.
+
+Several additional components exist to provide even more functionality: `SelectMenu.Header`, `SelectMenu.Filter`, `SelectMenu.Tabs`, `SelectMenu.TabPanel` `SelectMenu.Footer` and `SelectMenu.Divider`.
+
+## Basic Example
+```jsx live
+
+
+
+ Projects
+
+ Primer Components bugs
+ Primer Components roadmap
+ Project 3
+ Project 4
+
+
+
+```
+
+## SelectMenu
+Main wrapper component for select menu.
+
+```jsx
+
+ {/* all other sub components are wrapped here*/}
+
+```
+
+### System props
+
+SelectMenu components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+| Name | Type | Default | Description |
+| :- | :- | :-: | :- |
+| initialTab | String | | If using the `SelectMenu.Tabs` component, you can use this prop to change the tab shown on open. By default, the first tab will be used.
+
+
+## SelectMenu.Modal
+Used to wrap the content in a `SelectMenu`.
+
+```jsx
+
+ {/* all menu content is wrapped in the modal*/}
+
+```
+
+### System Props
+
+SelectMenu.Modal components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+SelectMenu.Modal components do not get any additional props besides system props.
+
+
+## SelectMenu.List
+
+Used to wrap the select menu list content. All menu items **must** be wrapped in a SelectMenu.List in order for the accessbility keyboard handling to function properly. If you are using the `SelectMenu.TabPanel` you do not need to provide a `SelectMenu.List` as that component renders a `SelectMenu.List` as a wrapper.
+
+```jsx
+
+ {/* all menu list items are wrapped in the list*/}
+
+```
+
+### System Props
+
+SelectMenu.List components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+SelectMenu.List components do not get any additional props besides system props.
+
+
+## SelectMenu.Item
+
+Individual items in a select menu.
+
+```jsx
+
+ {/* wraps an individual list item*/}
+
+```
+
+### System Props
+
+SelectMenu.Item components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+| Name | Type | Default | Description |
+| :- | :- | :-: | :- |
+| selected | boolean | | Used to apply styles to the selected items in the list. |
+| onClick | function | | Function called when item is clicked. By default we also close the menu when items are clicked. If you would like the menu to stay open, pass an `e.preventDefault()` to your onClick handler. |
+
+## SelectMenu.Filter
+Use a `SelectMenu.Filter` to add a filter UI to your select menu. Users are expected to implement their own filtering and manage the state of the `value` prop on the input. This gives users more flexibility over the type of filtering and type of content passed into each select menu item.
+
+```jsx live
+
+
+
+ Filter by Project
+
+
+ Primer Components bugs
+ Primer Components roadmap
+ More Options
+ Project 3
+ Project 4
+
+
+
+```
+
+
+### System Props
+SelectMenu.Filter components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+SelectMenu.Filter components receive all the props that the [TextInput](/TextInput) component gets.
+
+| Name | Type | Default | Description |
+| :- | :- | :-: | :- |
+| value | String | | Users of this component must provide a value for the filter input that is managed in the consuming application |
+
+
+## SelectMenu.Tabs
+Use `SelectMenu.Tabs` to wrap the the tab navigation and `SelectMenu.Tab` for each tab in the navigation.
+
+`SelectMenu.TabPanel` should wrap each corresponding panel for each of the tabs. The `tabName` prop for each `SelectMenu.TabPanel` must match the name provided in the `tabName` prop on `SelectMenu.Tab`.
+
+To set one of the tabs to be open by default, use `initialTab` on the main `SelectMenu` component. Otherwise, the first tab will be shown by default.
+
+Each `Select.Menu` tab will need to have an `index` prop. The first tab should be at index `0`, the second at index `1` and so forth. The `index` prop is used to show the first tab by default.
+
+If you need access to the selected tab state, you can find it in the MenuContext object exported from `SelectMenu` as `MenuContext.selectedTab`.
+
+```jsx live
+
+
+
+ Projects
+
+
+
+
+
+ Primer Components bugs
+ Primer Components roadmap
+ Project 3
+ Project 4
+
+
+ Project 2
+
+ Showing 3 of 3
+
+
+```
+
+### System Props
+
+SelectMenu.Tabs components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+SelectMenu.Tabs components do not get any additional props besides system props.
+
+## SelectMenu.Tab
+Used for each individual tab inside of a `SelectMenu.Tabs`. Be sure to set the `index` prop to correspond to the order the tab is in. The `tabName` prop should correspond to the `tabName` set on the `SelectMenu.TabPanel`.
+
+The `onClick` prop is optional and can be used for any events or data fetching you might need to trigger on tab clicks.
+
+```jsx
+
+
+```
+
+### System Props
+SelectMenu.Tab components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+| Name | Type | Default | Description |
+| :- | :- | :-: | :- |
+| tabName | String | | Used to identify the corresponding tab. Must match the string used in the `tabs` array in the `SelectMenu.Tabs` component. |
+| index | Number | | The index at which the tab is in the list of tabs |
+| onClick | Function | | Function to be called when the tab is clicked. Optional. |
+
+## SelectMenu.TabPanel
+Wraps the content for each tab. Make sure to use the `tabName` prop to identify each tab panel with the correct tab in the tab navigation.
+
+**Note**: SelectMenu.TabPanel wraps content in a SelectMenu.List, so adding a SelectMenu.List manually is not necessary.
+
+```jsx
+
+ {/* Wraps content for each tab */}
+
+```
+
+### System Props
+SelectMenu.TabPanel components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+| Name | Type | Default | Description |
+| :- | :- | :-: | :- |
+| tabName | String | | Used to identify the corresponding tab. Must match the string used in the `tabs` array in the `SelectMenu.Tabs` component.
+
+## SelectMenu.Divider
+Use a `SelectMenu.Divider` to add information between items in a `SelectMenu.List`.
+
+```jsx live
+
+
+
+ Projects
+
+ Primer Components bugs
+ Primer Components roadmap
+ More Options
+ Project 3
+ Project 4
+
+
+
+```
+
+### System Props
+
+SelectMenu.Divder components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+SelectMenu.Divider components do not get any additional props besides system props.
+
+## SelectMenu.Footer
+Use a `SelectMenu.Footer` to add content to the bottom of the select menu.
+
+```jsx live
+
+
+
+ Projects
+
+ Primer Components bugs
+ Primer Components roadmap
+ Project 3
+ Project 4
+ Use ⌥ + click/return to exclude labels.
+
+
+
+```
+
+### System Props
+
+SelectMenu.Footer components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+SelectMenu.Footer components do not get any additional props besides system props.
+
+## SelectMenu.Header
+Use a `SelectMenu.Header` to add a header to the top of the select menu content.
+
+```jsx live
+
+
+
+ Projects
+
+ Primer Components bugs
+ Primer Components roadmap
+ Project 3
+ Project 4
+ Use ⌥ + click/return to exclude labels.
+
+
+
+```
+
+### System Props
+
+SelectMenu.Header components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+### Component Props
+SelectMenu.Header components do not get any additional props besides system props.
\ No newline at end of file
diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
index 432a3aadf78..29e1666c0d2 100644
--- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml
+++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
@@ -70,6 +70,8 @@
url: /Position
- title: ProgressBar
url: /ProgressBar
+ - title: SelectMenu
+ url: /SelectMenu
- title: SideNav
url: /SideNav
- title: StateLabel
diff --git a/index.d.ts b/index.d.ts
index 43c048de4b6..f8d555b5490 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -98,9 +98,16 @@ declare module '@primer/components' {
variant?: 'small' | 'medium' | 'large'
}
+ export interface ButtonTableListProps
+ extends CommonProps,
+ TypographyProps,
+ LayoutProps,
+ Omit, 'color'> {}
+
export const ButtonPrimary: React.FunctionComponent
export const ButtonOutline: React.FunctionComponent
export const ButtonDanger: React.FunctionComponent
+ export const ButtonTableList: React.FunctionComponent
export const ButtonGroup: React.FunctionComponent
export const Button: React.FunctionComponent
@@ -288,6 +295,58 @@ declare module '@primer/components' {
export const Sticky: React.FunctionComponent
export const Fixed: React.FunctionComponent
+ export interface SelectMenuProps extends Omit, Omit, 'color'> {
+ initialTab?: string
+ }
+
+ export interface SelectMenuModalProps extends CommonProps, Omit, 'color'> {}
+
+ export interface SelectMenuListProps extends CommonProps, Omit, 'color'> {}
+
+ export interface SelectMenuItemProps extends Omit,
+ Omit, 'color'> {
+ selected?: boolean
+ }
+
+ export interface SelectMenuFooterProps extends CommonProps, Omit, 'color'> {}
+
+ export interface SelectMenuDividerProps extends CommonProps, Omit, 'color'> {}
+
+ export interface SelectMenuFilterProps extends TextInputProps {
+ value: string
+ }
+
+ export interface SelectMenuTabsProps extends CommonProps,
+ Omit, 'color'> {}
+
+ export interface SelectMenuTabProps extends CommonProps, Omit, 'color'> {
+ index: number,
+ tabName: string
+ }
+
+ export interface SelectMenuTabPanelProps extends CommonProps, Omit, 'color'> {
+ tabName: string
+ }
+
+ export const SelectMenu: React.FunctionComponent & {
+ MenuContext: React.FunctionComponent<{
+ selectedTab: string | undefined
+ setSelectedTab: (selectedTab: string | undefined) => void,
+ open: boolean | undefined,
+ setOpen: (open: boolean | undefined) => void,
+ initialTab: string | undefined
+ }>
+ Divider: React.FunctionComponent
+ Filter: React.FunctionComponent
+ Footer: React.FunctionComponent
+ List: React.FunctionComponent
+ Item: React.FunctionComponent
+ Modal: React.FunctionComponent
+ Tabs: React.FunctionComponent
+ Tab: React.FunctionComponent
+ TabPanel: React.FunctionComponent
+ }
+
export interface SideNavProps extends CommonProps, BorderProps, Omit, 'color'> {
bordered?: boolean
variant?: 'normal' | 'lightweight'
@@ -447,7 +506,6 @@ declare module '@primer/components' {
export const ProgressBar: React.FunctionComponent
}
-
declare module '@primer/components/src/Box' {
import {Box} from '@primer/components'
export default Box
@@ -478,6 +536,11 @@ declare module '@primer/components/src/ButtonOutline' {
export default ButtonOutline
}
+declare module '@primer/components/src/ButtonTableList' {
+ import {ButtonTableList} from '@primer/components'
+ export default ButtonTableList
+}
+
declare module '@primer/components/src/ButtonGroup' {
import {ButtonGroup} from '@primer/components'
export default ButtonGroup
@@ -589,10 +652,17 @@ declare module '@primer/components/src/Fixed' {
import {Fixed} from '@primer/components'
export default Fixed
}
+
+declare module '@primer/components/src/SelectMenu' {
+ import {SelectMenu} from '@primer/components'
+ export default SelectMenu
+}
+
declare module '@primer/components/src/StateLabel' {
import {StateLabel} from '@primer/components'
export default StateLabel
}
+
declare module '@primer/components/src/TabNav' {
import {TabNav} from '@primer/components'
export default TabNav
diff --git a/package.json b/package.json
index 002d5618cab..5531ae268d2 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"@styled-system/prop-types": "5.1.2",
"@styled-system/props": "5.1.4",
"@styled-system/theme-get": "5.1.2",
+ "@types/styled-components": "^4.4.0",
"@testing-library/react": "9.4.0",
"@types/styled-system": "5.1.2",
"babel-plugin-macros": "2.6.1",
diff --git a/src/ButtonTableList.js b/src/ButtonTableList.js
new file mode 100644
index 00000000000..9a767522875
--- /dev/null
+++ b/src/ButtonTableList.js
@@ -0,0 +1,49 @@
+import styled from 'styled-components'
+import {COMMON, LAYOUT, TYPOGRAPHY, get} from './constants'
+import theme from './theme'
+
+const ButtonTableList = styled.summary`
+ display: inline-block;
+ padding: 0;
+ font-size: ${get('fontSizes.1')};
+ color: ${get('colors.gray.6')};
+ text-decoration: none;
+ white-space: nowrap;
+ cursor: pointer;
+ user-select: none;
+ background-color: transparent;
+ border: 0;
+ appearance: none; // Corrects inability to style clickable input types in iOS.
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ &:disabled {
+ &,
+ &:hover {
+ color: rgba(${get('colors.gray.6')}, 0.5);
+ cursor: default;
+ }
+ }
+
+ &:after {
+ display: inline-block;
+ margin-left: ${get('space.1')};
+ width: 0;
+ height: 0;
+ vertical-align: -2px;
+ content: "";
+ border: 4px solid transparent;
+ border-top-color: currentcolor;
+ }
+ ${COMMON}
+ ${TYPOGRAPHY}
+ ${LAYOUT}
+`
+
+ButtonTableList.defaultProps = {
+ theme
+}
+
+export default ButtonTableList
diff --git a/src/SelectMenu/SelectMenu.js b/src/SelectMenu/SelectMenu.js
new file mode 100644
index 00000000000..e6969f11570
--- /dev/null
+++ b/src/SelectMenu/SelectMenu.js
@@ -0,0 +1,101 @@
+import React, {useRef, useState} from 'react'
+import styled from 'styled-components'
+import PropTypes from 'prop-types'
+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 useKeyboardNav from './hooks/useKeyboardNav'
+
+const wrapperStyles = `
+ &[open] > summary::before {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 80;
+ display: block;
+ cursor: default;
+ content: ' ';
+ background: transparent;
+ }
+ // 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}
+`
+
+// 'as' is spread out because we don't want users to be able to change the tag.
+const SelectMenu = ({children, initialTab, as, ...rest}) => {
+ const ref = useRef(null)
+ const [selectedTab, setSelectedTab] = useState(initialTab)
+ const [open, setOpen] = useState(false)
+ const menuProviderValues = {
+ selectedTab,
+ setSelectedTab,
+ setOpen,
+ open,
+ initialTab
+ }
+
+ function toggle(event) {
+ setOpen(event.target.open)
+ }
+
+ useKeyboardNav(ref, open, setOpen)
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+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.defaultProps = {
+ theme
+}
+
+SelectMenu.propTypes = {
+ initialTab: PropTypes.string,
+ ...COMMON.propTypes
+}
+
+export default SelectMenu
diff --git a/src/SelectMenu/SelectMenuContext.js b/src/SelectMenu/SelectMenuContext.js
new file mode 100644
index 00000000000..a63f91b96fc
--- /dev/null
+++ b/src/SelectMenu/SelectMenuContext.js
@@ -0,0 +1,3 @@
+import {createContext} from 'react'
+
+export const MenuContext = createContext()
diff --git a/src/SelectMenu/SelectMenuDivider.js b/src/SelectMenu/SelectMenuDivider.js
new file mode 100644
index 00000000000..7ecebf1c9ea
--- /dev/null
+++ b/src/SelectMenu/SelectMenuDivider.js
@@ -0,0 +1,28 @@
+import styled, {css} from 'styled-components'
+import theme from '../theme'
+import {COMMON, get} from '../constants'
+
+const dividerStyles = css`
+ padding: ${get('space.1')} ${get('space.3')};
+ margin: 0;
+ font-size: ${get('fontSizes.0')};
+ font-weight: ${get('fontWeights.bold')};
+ color: ${get('colors.text.grayLight')};
+ background-color: ${get('colors.bg.gray')};
+ border-bottom: ${get('borders.1')} ${get('colors.border.grayLight')};
+`
+
+const SelectMenuDivider = styled.div`
+ ${dividerStyles}
+ ${COMMON}
+`
+
+SelectMenuDivider.defaultProps = {
+ theme
+}
+
+SelectMenuDivider.propTypes = {
+ ...COMMON.propTypes
+}
+
+export default SelectMenuDivider
diff --git a/src/SelectMenu/SelectMenuFilter.js b/src/SelectMenu/SelectMenuFilter.js
new file mode 100644
index 00000000000..085868b9127
--- /dev/null
+++ b/src/SelectMenu/SelectMenuFilter.js
@@ -0,0 +1,47 @@
+import React, {useRef, useContext, 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'
+
+const StyledForm = styled.form`
+ padding: ${get('space.3')};
+ margin: 0;
+ border-top: ${get('borders.1')} ${get('colors.border.gray')};
+ background-color: ${get('colors.white')};
+ ${COMMON};
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ padding: ${get('space.2')};
+ }
+`
+
+function SelectMenuFilter({theme, value, ...rest}) {
+ const inputRef = useRef(null)
+ 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,
+ value: PropTypes.string
+}
+
+export default SelectMenuFilter
diff --git a/src/SelectMenu/SelectMenuFooter.js b/src/SelectMenu/SelectMenuFooter.js
new file mode 100644
index 00000000000..4e107684879
--- /dev/null
+++ b/src/SelectMenu/SelectMenuFooter.js
@@ -0,0 +1,31 @@
+import styled, {css} from 'styled-components'
+import {COMMON, get} from '../constants'
+import theme from '../theme'
+
+const footerStyles = css`
+ margin-top: -1px;
+ padding: ${get('space.2')} ${get('space.3')};
+ font-size: ${get('fontSizes.0')};
+ color: ${get('colors.text.grayLight')};
+ text-align: center;
+ border-top: ${get('borders.1')} ${get('colors.border.gray')};
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ padding: ${get('space.1')} ${get('space.2')};
+ }
+`
+
+const SelectMenuFooter = styled.footer`
+ ${footerStyles}
+ ${COMMON}
+`
+
+SelectMenuFooter.defaultProps = {
+ theme
+}
+
+SelectMenuFooter.propTypes = {
+ ...COMMON.propTypes
+}
+
+export default SelectMenuFooter
diff --git a/src/SelectMenu/SelectMenuHeader.js b/src/SelectMenu/SelectMenuHeader.js
new file mode 100644
index 00000000000..35b42838526
--- /dev/null
+++ b/src/SelectMenu/SelectMenuHeader.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import styled from 'styled-components'
+import {get, COMMON, TYPOGRAPHY} from '../constants'
+import theme from '../theme'
+
+// SelectMenu.Header is intentionally not exported, it's an internal component used in
+// SelectMenu.Modal
+
+const SelectMenuTitle = styled.h3`
+ flex: auto;
+ font-size: ${get('fontSizes.1')};
+ font-weight: ${get('fontWeights.bold')};
+ margin: 0;
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ font-size: inherit;
+ }
+`
+
+const StyledHeader = styled.header`
+ display: flex;
+ flex: none; // fixes header from getting squeezed in Safari iOS
+ padding: ${get('space.3')};
+ ${COMMON}
+ ${TYPOGRAPHY}
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ padding-top: ${get('space.2')};
+ padding-bottom: ${get('space.2')};
+ }
+`
+const SelectMenuHeader = ({children, theme, ...rest}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+SelectMenuHeader.defaultProps = {
+ theme
+}
+
+export default SelectMenuHeader
diff --git a/src/SelectMenu/SelectMenuItem.js b/src/SelectMenu/SelectMenuItem.js
new file mode 100644
index 00000000000..cbd293bae71
--- /dev/null
+++ b/src/SelectMenu/SelectMenuItem.js
@@ -0,0 +1,130 @@
+import React, {useContext} from 'react'
+import PropTypes from 'prop-types'
+import styled, {css} from 'styled-components'
+import {Check} from '@primer/octicons-react'
+import {MenuContext} from './SelectMenuContext'
+import {COMMON, get} from '../constants'
+import StyledOcticon from '../StyledOcticon'
+import theme from '../theme'
+
+export const listItemStyles = css`
+ display: flex;
+ align-items: center;
+ padding: ${get('space.3')};
+ overflow: hidden;
+ text-align: left;
+ cursor: pointer;
+ background-color: ${get('colors.white')};
+ border: 0;
+ border-bottom: ${get('borders.1')} ${get('colors.border.grayLight')};
+ color: ${get('colors.text.gray')};
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: none;
+ }
+ &:focus {
+ outline: none;
+ }
+
+ &[hidden] {
+ display: none !important;
+ }
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ padding-top: ${get('space.2')};
+ padding-bottom: ${get('space.2')};
+ }
+
+ .SelectMenu-icon {
+ width: ${get('space.3')};
+ margin-right: ${get('space.2')};
+ flex-shrink: 0;
+ }
+
+ .SelectMenu-selected-icon {
+ visibility: hidden;
+ transition: transform 0.12s cubic-bezier(0.5, 0.1, 1, 0.5), visibility 0s 0.12s linear;
+ transform: scale(0);
+ }
+
+ // selected items
+ &[aria-checked='true'] {
+ font-weight: 500;
+ color: ${get('colors.gray.9')};
+
+ .SelectMenu-selected-icon {
+ visibility: visible;
+ transition: transform 0.12s cubic-bezier(0, 0, 0.2, 1), visibility 0s linear;
+ transform: scale(1);
+ }
+ }
+
+ // can hover states
+ @media (hover: hover) {
+ body:not(.intent-mouse) .SelectMenu-item:focus,
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: ${get('colors.bg.gray')};
+ }
+ }
+
+ // Can not hover states
+ //
+ // For touch input
+
+ @media (hover: none) {
+ // Android
+ &:focus,
+ &:active {
+ background-color: ${get('colors.bg.grayLight')};
+ }
+
+ // iOS Safari
+ // :active would work if ontouchstart is added to the button
+ // Instead this tweaks the "native" highlight color
+ -webkit-tap-highlight-color: rgba(${get('colors.gray.3')}, 0.5);
+ }
+`
+
+const StyledItem = styled.a.attrs(() => ({
+ role: 'menuitemcheckbox'
+}))`
+ ${listItemStyles}
+ ${COMMON}
+`
+
+// 'as' is spread out because we don't want users to be able to change the tag. using something
+// other than 'a' will break a11y.
+const SelectMenuItem = ({children, selected, theme, onClick, as, ...rest}) => {
+ const menuContext = useContext(MenuContext)
+
+ // 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)
+
+ if (!e.defaultPrevented) {
+ menuContext.setOpen(false)
+ }
+ }
+ return (
+
+
+ {children}
+
+ )
+}
+
+SelectMenuItem.defaultProps = {
+ theme,
+ selected: false
+}
+
+SelectMenuItem.propTypes = {
+ selected: PropTypes.bool,
+ ...COMMON.propTypes
+}
+
+export default SelectMenuItem
diff --git a/src/SelectMenu/SelectMenuList.js b/src/SelectMenu/SelectMenuList.js
new file mode 100644
index 00000000000..5e57b049e26
--- /dev/null
+++ b/src/SelectMenu/SelectMenuList.js
@@ -0,0 +1,45 @@
+import styled, {css} from 'styled-components'
+import theme from '../theme'
+import {COMMON, get} from '../constants'
+
+const listStyles = css`
+ position: relative;
+ padding: 0;
+ margin: 0;
+ flex: auto;
+ overflow-x: hidden;
+ overflow-y: auto;
+ background-color: ${get('colors.white')};
+ border-top: ${get('borders.1')} ${get('colors.border.gray')};
+ -webkit-overflow-scrolling: touch; // Adds momentum + bouncy scrolling
+
+ @media (hover: hover) {
+ .SelectMenuTab:focus {
+ background-color: ${get('colors.blue.1')};
+ }
+
+ .SelectMenuTab:not([aria-checked='true']):hover {
+ color: ${get('colors.gray.9')};
+ background-color: ${get('colors.gray.2')};
+ }
+
+ .SelectMenuTab:not([aria-checked='true']):active {
+ color: ${get('colors.gray.9')};
+ background-color: ${get('colors.gray.1')};
+ }
+ }
+`
+
+const SelectMenuList = styled.div`
+ ${listStyles}
+ ${COMMON}
+`
+SelectMenuList.defaultProps = {
+ theme
+}
+
+SelectMenuList.propTypes = {
+ ...COMMON.propTypes
+}
+
+export default SelectMenuList
diff --git a/src/SelectMenu/SelectMenuModal.js b/src/SelectMenu/SelectMenuModal.js
new file mode 100644
index 00000000000..fc172c2cecf
--- /dev/null
+++ b/src/SelectMenu/SelectMenuModal.js
@@ -0,0 +1,103 @@
+import React from 'react'
+import styled, {keyframes, css} from 'styled-components'
+import {COMMON, get} from '../constants'
+import theme from '../theme'
+
+const animateModal = keyframes`
+ 0% {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+`
+
+const modalStyles = css`
+ position: relative;
+ z-index: 99; // Needs to be higher than .details-overlay's z-index: 80.
+ display: flex;
+ ${props => (props.filter ? 'height: 80%' : '')};
+ max-height: ${props => (props.filter ? 'none' : '66%')};
+ margin: auto 0;
+ ${props => (props.filter ? 'margin-top: 0' : '')};
+ overflow: hidden; // Enables border radius on scrollable child elements
+ pointer-events: auto;
+ flex-direction: column;
+ background-color: ${get('colors.white')};
+ border-radius: ${get('radii.2')};
+ box-shadow: 0 1px 5px rgba(27, 31, 35, 0.15);
+ animation: ${animateModal} 0.12s cubic-bezier(0, 0.1, 0.1, 1) backwards;
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ width: '300px';
+ height: auto;
+ max-height: 350px;
+ margin: ${get('space.1')} 0 ${get('space.3')} 0;
+ font-size: ${get('fontSizes.0')};
+ border: ${get('borders.1')} ${get('colors.border.grayDark')};
+ border-radius: ${get('radii.2')};
+ box-shadow: 0 1px 5px ${get('colors.blackfade15')} !default;
+ }
+`
+
+const modalWrapperStyles = css`
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 99;
+ display: flex;
+ padding: ${get('space.3')};
+ pointer-events: none;
+ flex-direction: column;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ pointer-events: none;
+ content: '';
+ background-color: ${get('colors.blackfade50')};
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ display: none;
+ }
+ }
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ position: absolute;
+ top: auto;
+ right: auto;
+ bottom: auto;
+ left: auto;
+ padding: 0;
+ }
+`
+
+const Modal = styled.div`
+ ${modalStyles}
+`
+
+const ModalWrapper = styled.div`
+ ${modalWrapperStyles}
+ ${COMMON}
+`
+
+const SelectMenuModal = ({children, theme, ...rest}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+SelectMenuModal.defaultProps = {
+ theme
+}
+
+SelectMenuModal.propTypes = {
+ ...COMMON.propTypes
+}
+
+export default SelectMenuModal
diff --git a/src/SelectMenu/SelectMenuTab.js b/src/SelectMenu/SelectMenuTab.js
new file mode 100644
index 00000000000..160df1806da
--- /dev/null
+++ b/src/SelectMenu/SelectMenuTab.js
@@ -0,0 +1,94 @@
+import React, {useContext, useEffect} from 'react'
+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 theme from '../theme'
+
+const tabStyles = css`
+ flex: 1;
+ padding: ${get('space.2')} ${get('space.3')};
+ font-size: ${get('fontSizes.0')};
+ font-weight: 500;
+ color: ${get('colors.gray.5')};
+ text-align: center;
+ background-color: transparent;
+ border: 0;
+ box-shadow: inset 0 -1px 0 ${get('colors.border.gray')};
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ flex: none;
+ padding: ${get('space.1')} ${get('space.3')};
+ border: ${get('borders.1')} transparent;
+ border-bottom-width: 0;
+ border-top-left-radius: ${get('radii.2')};
+ border-top-right-radius: ${get('radii.2')};
+ }
+
+ &[aria-selected='true'] {
+ z-index: 1; // Keeps box-shadow visible when hovering
+ color: ${get('colors.gray.9')};
+ background-color: ${get('colors.white')};
+ box-shadow: 0 0 0 1px ${get('colors.border.gray')};
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ border-color: ${get('colors.border.gray')};
+ box-shadow: none;
+ }
+ }
+
+ &:focus {
+ background-color: #dbedff;
+ }
+`
+
+const StyledTab = styled.button`
+ ${tabStyles}
+ ${COMMON}
+`
+
+const SelectMenuTab = ({tabName, index, className, onClick, ...rest}) => {
+ const menuContext = useContext(MenuContext)
+ const handleClick = e => {
+ // if consumer has attached an onClick event, call it
+ onClick && onClick(e)
+ if (!e.defaultPrevented) {
+ 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)
+ }
+ }, [])
+
+ const isSelected = menuContext.selectedTab === tabName
+
+ return (
+
+ {tabName}
+
+ )
+}
+
+SelectMenuTab.defaultProps = {
+ theme
+}
+
+SelectMenuTab.propTypes = {
+ index: PropTypes.number,
+ onClick: PropTypes.func,
+ tabName: PropTypes.string,
+ ...COMMON.propTypes
+}
+
+export default SelectMenuTab
diff --git a/src/SelectMenu/SelectMenuTabPanel.js b/src/SelectMenu/SelectMenuTabPanel.js
new file mode 100644
index 00000000000..338598c9645
--- /dev/null
+++ b/src/SelectMenu/SelectMenuTabPanel.js
@@ -0,0 +1,31 @@
+import React, {useContext} from 'react'
+import PropTypes from 'prop-types'
+import styled from 'styled-components'
+import {MenuContext} from './SelectMenuContext'
+import SelectMenuList from './SelectMenuList'
+import theme from '../theme'
+import {COMMON} from '../constants'
+
+const TabPanelBase = ({tabName, className, children, ...rest}) => {
+ const menuContext = useContext(MenuContext)
+ return (
+
+ {children}
+
+ )
+}
+
+const TabPanel = styled(TabPanelBase)`
+ ${COMMON}
+`
+
+TabPanel.defaultProps = {
+ theme
+}
+
+TabPanel.propTypes = {
+ tabName: PropTypes.string,
+ ...COMMON.propTypes
+}
+
+export default TabPanel
diff --git a/src/SelectMenu/SelectMenuTabs.js b/src/SelectMenu/SelectMenuTabs.js
new file mode 100644
index 00000000000..72ae1007446
--- /dev/null
+++ b/src/SelectMenu/SelectMenuTabs.js
@@ -0,0 +1,45 @@
+import React from 'react'
+import styled, {css} from 'styled-components'
+import {COMMON, get} from '../constants'
+import theme from '../theme'
+
+const tabWrapperStyles = css`
+ display: flex;
+ flex-shrink: 0;
+ margin-bottom: -1px; // hide border of element below
+ border-top: ${get('borders.1')} ${get('colors.border.gray')};
+ -webkit-overflow-scrolling: touch;
+
+ // Hide scrollbar so it doesn't cover the text
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ @media (min-width: ${get('breakpoints.0')}) {
+ padding: 0 ${get('space.2')};
+ border-top: 0;
+ }
+`
+
+const Tabs = ({children, ...rest}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const SelectMenuTabs = styled(Tabs)`
+ ${tabWrapperStyles}
+ ${COMMON}
+`
+
+SelectMenuTabs.defaultProps = {
+ theme
+}
+
+SelectMenuTabs.propTypes = {
+ ...COMMON.propTypes
+}
+
+export default SelectMenuTabs
diff --git a/src/SelectMenu/hooks/useKeyboardNav.js b/src/SelectMenu/hooks/useKeyboardNav.js
new file mode 100644
index 00000000000..1c5dae1fb67
--- /dev/null
+++ b/src/SelectMenu/hooks/useKeyboardNav.js
@@ -0,0 +1,82 @@
+import {useEffect} from 'react'
+
+// adapted from details-menu web component https://github.com/github/details-menu-element
+function useKeyboardNav(details, open, setOpen) {
+ const handleKeyDown = event => {
+ const closeDetails = () => {
+ setOpen(false)
+ const summary = details.current.querySelector('summary')
+ if (summary) summary.focus()
+ }
+ const openDetails = () => {
+ setOpen(true)
+ }
+ const focusItem = next => {
+ const options = Array.from(
+ details.current.querySelectorAll('[role^="menuitem"]:not([hidden]):not([disabled]):not([aria-disabled="true"])')
+ )
+ const selected = document.activeElement
+ const index = options.indexOf(selected)
+ const found = next ? options[index + 1] : options[index - 1]
+ const def = next ? options[0] : options[options.length - 1]
+ return found || def
+ }
+
+ const isMenuItem = el => {
+ const role = el.getAttribute('role')
+ return role === 'menuitem' || role === 'menuitemcheckbox' || role === 'menuitemradio'
+ }
+ if (!(event instanceof KeyboardEvent)) return
+ const isSummaryFocused = event.target instanceof Element && event.target.tagName === 'SUMMARY'
+ switch (event.key) {
+ case 'Escape':
+ if (open) {
+ closeDetails(details)
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ break
+ case 'ArrowDown':
+ {
+ if (isSummaryFocused && !open) {
+ openDetails(details)
+ }
+ const target = focusItem(true)
+ if (target) target.focus()
+ event.preventDefault()
+ }
+ break
+ case 'ArrowUp':
+ {
+ if (isSummaryFocused && !open) {
+ openDetails()
+ }
+ const target = focusItem(false)
+ if (target) target.focus()
+ event.preventDefault()
+ }
+ break
+ case ' ':
+ case 'Enter':
+ {
+ const selected = document.activeElement
+ if (selected && isMenuItem(selected) && selected.closest('details') === details) {
+ event.preventDefault()
+ event.stopPropagation()
+ selected.click()
+ }
+ }
+ break
+ }
+ }
+ useEffect(() => {
+ if (!details.current) return
+
+ details.current.addEventListener('keydown', handleKeyDown)
+ return () => {
+ details.current.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [details.current])
+}
+
+export default useKeyboardNav
diff --git a/src/SelectMenu/index.js b/src/SelectMenu/index.js
new file mode 100644
index 00000000000..64cf8e036d8
--- /dev/null
+++ b/src/SelectMenu/index.js
@@ -0,0 +1,3 @@
+import SelectMenu from './SelectMenu'
+
+export default SelectMenu
diff --git a/src/TextInput.js b/src/TextInput.js
index 2f77cd7c9ac..d1742652519 100644
--- a/src/TextInput.js
+++ b/src/TextInput.js
@@ -26,7 +26,8 @@ const sizeVariants = variant({
}
})
-const TextInput = ({icon, className, block, disabled, ...rest}) => {
+// using forwardRef is important so that other components (ex. SelectMenu) can autofocus the input
+const TextInput = React.forwardRef(({icon, className, block, disabled, ...rest}, ref) => {
// this class is necessary to style FilterSearch, plz no touchy!
const wrapperClasses = classnames(className, 'TextInput-wrapper')
const wrapperProps = pick(rest)
@@ -41,14 +42,12 @@ const TextInput = ({icon, className, block, disabled, ...rest}) => {
{...wrapperProps}
>
{icon && }
-
+
)
-}
+})
-const Input = styled.input.attrs(props => ({
- type: props.type || 'text'
-}))`
+const Input = styled.input`
border: 0;
font-size: inherit;
background-color: transparent;
@@ -115,7 +114,7 @@ const Wrapper = styled.span`
`}
// Ensures inputs don't zoom on mobile but are body-font size on desktop
- @media (max-width: ${get('breakpoints.1')}) {
+ @media (min-width: ${get('breakpoints.1')}) {
font-size: ${get('fontSizes.1')};
}
${COMMON}
@@ -125,7 +124,10 @@ const Wrapper = styled.span`
${sizeVariants}
`
-TextInput.defaultProps = {theme}
+TextInput.defaultProps = {
+ theme,
+ type: 'text'
+}
TextInput.propTypes = {
block: PropTypes.bool,
diff --git a/src/__tests__/SelectMenu.js b/src/__tests__/SelectMenu.js
new file mode 100644
index 00000000000..250d0084e56
--- /dev/null
+++ b/src/__tests__/SelectMenu.js
@@ -0,0 +1,126 @@
+import React from 'react'
+import SelectMenu from '../SelectMenu'
+import Button from '../Button'
+import {mount, renderRoot} from '../utils/testing'
+import {COMMON} from '../constants'
+import {render as HTMLRender, cleanup} from '@testing-library/react'
+import {axe, toHaveNoViolations} from 'jest-axe'
+import 'babel-polyfill'
+expect.extend(toHaveNoViolations)
+
+const BasicSelectMenu = ({onClick, as}) => {
+ return (
+
+
+
+
+
+ Primer Components bugs
+
+
+ Primer Components roadmap
+
+ stuff
+ Project 3
+ Project 4
+ footer
+
+
+
+ )
+}
+
+const MenuWithTabs = ({onClick}) => {
+ return (
+
+
+
+
+
+
+
+
+ Primer Components bugs
+ Primer Components roadmap
+ Project 3
+ Project 4
+
+
+ Project 2
+
+ Showing 3 of 3
+
+
+ )
+}
+
+describe('SelectMenu', () => {
+ it('should have no axe violations', async () => {
+ const {container} = HTMLRender()
+ const results = await axe(container)
+ expect(results).toHaveNoViolations()
+ cleanup()
+ })
+ it('implements system props', () => {
+ expect(SelectMenu).toImplementSystemProps(COMMON)
+ })
+
+ it('has default theme', () => {
+ expect(SelectMenu).toSetDefaultTheme()
+ })
+
+ it('does not allow the "as" prop on SelectMenu', () => {
+ const component = mount()
+ expect(component.find('details').length).toEqual(1)
+ })
+
+ it('does not allow the "as" prop on SelectMenu.Item', () => {
+ const component = mount()
+ expect(
+ component
+ .find("[data-test='menu-item']")
+ .first()
+ .getDOMNode().tagName
+ ).toEqual('A')
+ })
+
+ it('shows correct initial tab', () => {
+ const testInstance = renderRoot()
+ expect(testInstance.findByProps({'aria-selected': true}).props.children).toBe('Organization')
+ })
+
+ it('clicking on a tab opens the tab', () => {
+ const component = mount()
+ const tab = component.find("[data-test='repo-tab']").first()
+ tab.simulate('click')
+ expect(tab.getDOMNode().attributes['aria-selected']).toBeTruthy()
+ })
+
+ it('selected items have aria-checked', () => {
+ const testInstance = renderRoot()
+ expect(testInstance.findByProps({'aria-checked': true}).props.children[1]).toBe('Primer Components bugs')
+ })
+
+ it('clicking on a list item calls user provided onClick handler', () => {
+ const mockClick = jest.fn()
+ const component = mount()
+ const item = component.find("[data-test='menu-item']").first()
+ item.simulate('click')
+ expect(mockClick.mock.calls.length).toEqual(1)
+ })
+
+ it('clicking on a tab calls user provided onClick handler', () => {
+ const mockClick = jest.fn()
+ const component = mount()
+ const item = component.find("[data-test='repo-tab']").first()
+ item.simulate('click')
+ expect(mockClick.mock.calls.length).toEqual(1)
+ })
+
+ it('clicking on an item closes the modal', () => {
+ const component = mount()
+ const item = component.find("[data-test='menu-item']").first()
+ item.simulate('click')
+ expect(component.getDOMNode().attributes.open).toBeFalsy()
+ })
+})
diff --git a/src/__tests__/__snapshots__/TextInput.js.snap b/src/__tests__/__snapshots__/TextInput.js.snap
index 14ac35370d0..5bd69d2daf3 100644
--- a/src/__tests__/__snapshots__/TextInput.js.snap
+++ b/src/__tests__/__snapshots__/TextInput.js.snap
@@ -53,7 +53,7 @@ exports[`TextInput renders 1`] = `
box-shadow: inset 0px 2px 0px rgba(225,228,232,0.2),rgba(3,102,214,0.3) 0px 0px 0px 0.2em;
}
-@media (max-width:768px) {
+@media (min-width:768px) {
.c0 {
font-size: 14px;
}
@@ -125,7 +125,7 @@ exports[`TextInput renders block 1`] = `
box-shadow: inset 0px 2px 0px rgba(225,228,232,0.2),rgba(3,102,214,0.3) 0px 0px 0px 0.2em;
}
-@media (max-width:768px) {
+@media (min-width:768px) {
.c0 {
font-size: 14px;
}
@@ -200,7 +200,7 @@ exports[`TextInput renders large 1`] = `
box-shadow: inset 0px 2px 0px rgba(225,228,232,0.2),rgba(3,102,214,0.3) 0px 0px 0px 0.2em;
}
-@media (max-width:768px) {
+@media (min-width:768px) {
.c0 {
font-size: 14px;
}
@@ -277,7 +277,7 @@ exports[`TextInput renders small 1`] = `
box-shadow: inset 0px 2px 0px rgba(225,228,232,0.2),rgba(3,102,214,0.3) 0px 0px 0px 0.2em;
}
-@media (max-width:768px) {
+@media (min-width:768px) {
.c0 {
font-size: 14px;
}
@@ -347,7 +347,7 @@ exports[`TextInput should render a password input 1`] = `
box-shadow: inset 0px 2px 0px rgba(225,228,232,0.2),rgba(3,102,214,0.3) 0px 0px 0px 0.2em;
}
-@media (max-width:768px) {
+@media (min-width:768px) {
.c0 {
font-size: 14px;
}
diff --git a/src/index.js b/src/index.js
index 1481979d276..8081a1fe0b2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,6 +19,7 @@ export {default as ButtonDanger} from './ButtonDanger'
export {default as ButtonGroup} from './ButtonGroup'
export {default as ButtonOutline} from './ButtonOutline'
export {default as ButtonPrimary} from './ButtonPrimary'
+export {default as ButtonTableList} from './ButtonTableList'
export {default as Button} from './Button'
export {default as Caret} from './Caret'
export {default as CircleBadge} from './CircleBadge'
@@ -38,6 +39,7 @@ export {default as Pagination} from './Pagination'
export {default as PointerBox} from './PointerBox'
export {default as Popover} from './Popover'
export {default as ProgressBar} from './ProgressBar'
+export {default as SelectMenu} from './SelectMenu'
export {default as SideNav} from './SideNav'
export {default as StateLabel} from './StateLabel'
export {default as StyledOcticon} from './StyledOcticon'
diff --git a/src/utils/testing.js b/src/utils/testing.js
index 0fc519a067e..c75252c531c 100644
--- a/src/utils/testing.js
+++ b/src/utils/testing.js
@@ -30,6 +30,15 @@ export function render(component) {
return renderer.create(component).toJSON()
}
+/**
+ * Render the component (a React.createElement() or JSX expression)
+ * using react-test-renderer and return the root node
+ * ```
+ */
+export function renderRoot(component) {
+ return renderer.create(component).root
+}
+
/**
* Get the HTML class names rendered by the component instance
* as an array.
diff --git a/yarn.lock b/yarn.lock
index 1b07f2339c0..08ac3cf2613 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2239,6 +2239,14 @@
"@types/minimatch" "*"
"@types/node" "*"
+"@types/hoist-non-react-statics@*":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -2318,6 +2326,13 @@
dependencies:
"@types/react" "*"
+"@types/react-native@*":
+ version "0.61.10"
+ resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.61.10.tgz#d010b9093322bc781151638f74124f58fc87cc90"
+ integrity sha512-z+RWEFfdwHnOLpq70DO//4mjyNqoZypdR3uBqpBB82t2HJg2YbY4j6XIow7sFqeO/r8XibgV9UFEVG2tYIRlSA==
+ dependencies:
+ "@types/react" "*"
+
"@types/react@*":
version "16.9.17"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.17.tgz#58f0cc0e9ec2425d1441dd7b623421a867aa253e"
@@ -2339,6 +2354,16 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+"@types/styled-components@^4.4.0":
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.2.tgz#709fa7afd7dc0963b8316a0159240f0fe19a026d"
+ integrity sha512-dngFx2PuGoy0MGE68eHayAmJvLSqWrnTe9w+DnQruu8PS+waWEsKmoBRhkzL2h2pK1OJhzJhVfuiz+oZa4etpA==
+ dependencies:
+ "@types/hoist-non-react-statics" "*"
+ "@types/react" "*"
+ "@types/react-native" "*"
+ csstype "^2.2.0"
+
"@types/styled-system@5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/styled-system/-/styled-system-5.1.2.tgz#d75c40bc4a3bb0d0022eb3dcae58854129e9dd32"
@@ -5749,6 +5774,13 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^3.3.0:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
hosted-git-info@^2.1.4:
version "2.8.5"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
@@ -8920,7 +8952,7 @@ react-is@16.10.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab"
integrity sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==
-react-is@^16.10.2, react-is@^16.6.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
+react-is@^16.10.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==