Skip to content

Commit

Permalink
Adds open, onOpenChange, and anchorRef props to DropdownMenu (#1372)
Browse files Browse the repository at this point in the history
* Adds open, onOpenChange, and anchorRef props to DropdownMenu

* Changeset

* Update src/DropdownMenu/DropdownMenu.tsx

Co-authored-by: Cole Bemis <colebemis@github.com>

* comment update

Co-authored-by: Cole Bemis <colebemis@github.com>
  • Loading branch information
jfuchs and colebemis committed Aug 10, 2021
1 parent ad1d426 commit 23be0ed
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-hotels-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/components': patch
---

Extends DropdownMenu to allow anchorRef, open, and onOpenChange props.
54 changes: 35 additions & 19 deletions src/DropdownMenu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import React, {useCallback, useMemo, useState} from 'react'
import React, {useCallback, useMemo} from 'react'
import {List, GroupedListProps, ListPropsBase, ItemInput} from '../ActionList/List'
import {DropdownButton, DropdownButtonProps} from './DropdownButton'
import {ItemProps} from '../ActionList/Item'
import {AnchoredOverlay} from '../AnchoredOverlay'
import {OverlayProps} from '../Overlay'
import {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay'
import {useProvidedRefOrCreate} from '../hooks/useProvidedRefOrCreate'
import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'

export interface DropdownMenuProps extends Partial<Omit<GroupedListProps, keyof ListPropsBase>>, ListPropsBase {
/**
* A custom function component used to render the anchor element.
* Will receive the selected text as `children` prop when an item is activated.
* Uses a `DropdownButton` by default.
*/
renderAnchor?: <T extends React.HTMLAttributes<HTMLElement>>(props: T) => JSX.Element

interface DropdownMenuBaseProps extends Partial<Omit<GroupedListProps, keyof ListPropsBase>>, ListPropsBase {
/**
* A placeholder value to display on the trigger button when no selection has been made.
*/
Expand All @@ -33,35 +29,54 @@ export interface DropdownMenuProps extends Partial<Omit<GroupedListProps, keyof
* Props to be spread on the internal `Overlay` component.
*/
overlayProps?: Partial<OverlayProps>

/**
* If defined, will control the open/closed state of the overlay. If not defined, the overlay will manage its own state (in other words, an
* uncontrolled component). Must be used in conjuction with `onOpenChange`.
*/
open?: boolean

/**
* If defined, will control the open/closed state of the overlay. Must be used in conjuction with `open`.
*/
onOpenChange?: (open: boolean) => void
}

export type DropdownMenuProps = DropdownMenuBaseProps & AnchoredOverlayWrapperAnchorProps

/**
* A `DropdownMenu` provides an anchor (button by default) that will open a floating menu of selectable items. The menu can be
* opened and navigated using keyboard or mouse. When an item is selected, the menu will close and the `onChange` callback will be called.
* If the default anchor button is used, the anchor contents will be updated with the selection.
*/
export function DropdownMenu({
renderAnchor = <T extends DropdownButtonProps>(props: T) => <DropdownButton {...props} />,
anchorRef: externalAnchorRef,
placeholder,
selectedItem,
onChange,
overlayProps,
items,
open,
onOpenChange,
...listProps
}: DropdownMenuProps): JSX.Element {
const [open, setOpen] = useState(false)
const onOpen = useCallback(() => setOpen(true), [])
const onClose = useCallback(() => setOpen(false), [])
const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false)
const onOpen = useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
const onClose = useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])

const anchorRef = useProvidedRefOrCreate(externalAnchorRef)

const renderMenuAnchor = useCallback(
<T extends React.HTMLAttributes<HTMLElement>>(props: T) => {
return renderAnchor({
const renderMenuAnchor = useMemo(() => {
if (renderAnchor === null) {
return null
}
return <T extends React.HTMLAttributes<HTMLElement>>(props: T) =>
renderAnchor({
...props,
children: selectedItem?.text ?? placeholder
})
},
[placeholder, renderAnchor, selectedItem?.text]
)
}, [placeholder, renderAnchor, selectedItem?.text])

const itemsToRender = useMemo(() => {
return items.map(item => {
Expand All @@ -86,7 +101,8 @@ export function DropdownMenu({
return (
<AnchoredOverlay
renderAnchor={renderMenuAnchor}
open={open}
anchorRef={anchorRef}
open={combinedOpenState}
onOpen={onOpen}
onClose={onClose}
overlayProps={overlayProps}
Expand Down
Empty file added src/hooks/useAnchoredOverlay
Empty file.
30 changes: 30 additions & 0 deletions src/stories/DropdownMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react'
import {theme, ThemeProvider} from '..'
import {ItemInput} from '../ActionList/List'
import BaseStyles from '../BaseStyles'
import Box from '../Box'
import {DropdownMenu, DropdownButton} from '../DropdownMenu'
import TextInput from '../TextInput'

Expand Down Expand Up @@ -52,3 +53,32 @@ export function FavoriteColorStory(): JSX.Element {
)
}
FavoriteColorStory.storyName = 'Favorite Color'

export function ExternalAnchorStory(): JSX.Element {
const items = React.useMemo(() => [{text: '🔵 Cyan'}, {text: '🔴 Magenta'}, {text: '🟡 Yellow'}], [])
const [selectedItem, setSelectedItem] = React.useState<ItemInput | undefined>()
const anchorRef = React.useRef<HTMLDivElement>(null)
const [open, setOpen] = React.useState(false)

return (
<Box display="flex" flexDirection="column" alignItems="flex-start">
<Box display="flex" flexDirection="row">
<DropdownButton onClick={() => setOpen(true)}>Click me to open the dropdown</DropdownButton>
<Box ref={anchorRef} bg="papayawhip" p={4} ml={40} borderRadius={2} display="inline-block">
Anchored on me!
</Box>
</Box>
<DropdownMenu
renderAnchor={null}
anchorRef={anchorRef}
open={open}
onOpenChange={setOpen}
placeholder="🎨"
items={items}
selectedItem={selectedItem}
onChange={setSelectedItem}
/>
</Box>
)
}
ExternalAnchorStory.storyName = 'DropdownMenu with External Anchor'

0 comments on commit 23be0ed

Please sign in to comment.