Skip to content

Commit

Permalink
[components] Add MenuButton for consistent dropdown menu behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
mariuslundgard authored and rexxars committed Oct 6, 2020
1 parent 1c25dd1 commit 8c78720
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 129 deletions.
8 changes: 8 additions & 0 deletions packages/@sanity/components/sanity.json
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,14 @@
{
"implements": "part:@sanity/base/component",
"path": "clickOutside/story"
},
{
"implements": "part:@sanity/components/menu-button",
"path": "menuButton"
},
{
"implements": "part:@sanity/base/component",
"path": "menuButton/story"
}
]
}
11 changes: 11 additions & 0 deletions packages/@sanity/components/src/@types/parts.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ declare module 'part:@sanity/components/click-outside' {
export const ClickOutside: any
}

declare module 'part:@sanity/components/menu-button' {
export const MenuButton: ComponentType<{
// @todo: typings
buttonProps?: any
menu?: React.ReactNode
placement?: string
open?: boolean
setOpen?: (val: boolean) => void
}>
}

declare module 'part:@sanity/components/popover' {
// export const Popover: ComponentType<{}>
export const Popover: any
Expand Down
1 change: 1 addition & 0 deletions packages/@sanity/components/src/menuButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './menuButton'
42 changes: 42 additions & 0 deletions packages/@sanity/components/src/menuButton/menuButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Button from 'part:@sanity/components/buttons/default'
import {ClickOutside} from 'part:@sanity/components/click-outside'
import {Popover} from 'part:@sanity/components/popover'
import React, {useCallback} from 'react'

interface MenuButtonProps {
// @todo: typings
buttonProps?: any
menu?: React.ReactNode
placement?: string
open?: boolean
setOpen?: (val: boolean) => void
}

export function MenuButton(props: MenuButtonProps & React.HTMLProps<HTMLDivElement>) {
const {buttonProps, children, menu, open, placement, setOpen, ...restProps} = props
const handleClickOutside = useCallback(() => {
if (!setOpen) return
setOpen(false)
}, [setOpen])

const handleButtonClick = useCallback(() => {
if (!setOpen) return
setOpen(!open)
}, [open, setOpen])

return (
<ClickOutside onClickOutside={handleClickOutside}>
{ref => (
<div {...restProps} ref={ref}>
<Popover content={menu} open={open} placement={placement}>
<div>
<Button {...buttonProps} onClick={handleButtonClick}>
{children}
</Button>
</div>
</Popover>
</div>
)}
</ClickOutside>
)
}
44 changes: 44 additions & 0 deletions packages/@sanity/components/src/menuButton/stories/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {MenuButton} from 'part:@sanity/components/menu-button'
import {action} from 'part:@sanity/storybook/addons/actions'
import {boolean, select} from 'part:@sanity/storybook/addons/knobs'
import Sanity from 'part:@sanity/storybook/addons/sanity'
import {CenteredContainer} from 'part:@sanity/storybook/components'
import React from 'react'

export function DefaultStory() {
const open = boolean('Open', false, 'Props')
const placement = select(
'Placement',
{
'top-start': 'Top start',
top: 'Top',
'top-end': 'Top end',
'right-start': 'Right start',
right: 'Right',
'right-end': 'Right end',
'bottom-start': 'Bottom start',
bottom: 'Bottom',
'bottom-end': 'Bottom end',
'left-start': 'Left start',
left: 'Left',
'left-end': 'Left end'
},
'bottom',
'Props'
)

return (
<CenteredContainer>
<Sanity part="part:@sanity/components/dialogs/default" propTables={[MenuButton]}>
<MenuButton
menu={<div style={{padding: 20}}>This is the menu</div>}
open={open}
placement={placement}
setOpen={action('setOpen')}
>
Open menu
</MenuButton>
</Sanity>
</CenteredContainer>
)
}
7 changes: 7 additions & 0 deletions packages/@sanity/components/src/menuButton/story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {storiesOf} from 'part:@sanity/storybook'
import {withKnobs} from 'part:@sanity/storybook/addons/knobs'
import {DefaultStory} from './stories/default'

storiesOf('@sanity/components/menu-button', module)
.addDecorator(withKnobs)
.add('Default', DefaultStory)
117 changes: 66 additions & 51 deletions packages/@sanity/components/src/panes/DefaultPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import PropTypes from 'prop-types'
import React from 'react'
import {negate} from 'lodash'
import classNames from 'classnames'
import {MenuButton} from 'part:@sanity/components/menu-button'
import Menu from 'part:@sanity/components/menus/default'
import IconMoreVert from 'part:@sanity/base/more-vert-icon'
import {IntentLink} from 'part:@sanity/base/router'
import Button from 'part:@sanity/components/buttons/default'
import IntentButton from 'part:@sanity/components/buttons/intent'
import {Popover} from 'part:@sanity/components/popover'
import TabPanel from 'part:@sanity/components/tabs/tab-panel'
import ScrollContainer from 'part:@sanity/components/utilities/scroll-container'
import S from '@sanity/base/structure-builder'
Expand Down Expand Up @@ -123,17 +123,30 @@ class Pane extends React.PureComponent {
}

// Triggered by clicking "outside" of the menu when open, or after triggering action
handleCloseMenu = () => {
handleCloseContextMenu = () => {
this.setState({isMenuOpen: false})
}

// Triggered by pane menu button
handleToggleMenu = () => {
this.setState(prevState => ({isMenuOpen: !prevState.isMenuOpen}))
// handleToggleMenu = () => {
// this.setState(prevState => ({isMenuOpen: !prevState.isMenuOpen}))
// }

setContextMenuOpen = val => {
this.setState({isMenuOpen: val})
}

handleToggleInitialValueTemplateMenu = () => {
this.setState(prevState => ({isInitialValueMenuOpen: !prevState.isInitialValueMenuOpen}))
setInitialValueMenuOpen = val => {
this.setState({isInitialValueMenuOpen: val})
// this.setState(prevState => ({isInitialValueMenuOpen: !prevState.isInitialValueMenuOpen}))
}

// handleToggleInitialValueTemplateMenu = () => {
// this.setState(prevState => ({isInitialValueMenuOpen: !prevState.isInitialValueMenuOpen}))
// }

handleCloseTemplateMenu = () => {
this.setState({isInitialValueMenuOpen: false})
}

handleRootClick = event => {
Expand All @@ -151,7 +164,9 @@ class Pane extends React.PureComponent {
}

handleMenuAction = item => {
this.handleCloseMenu()
this.setContextMenuOpen(false)

// this.handleCloseContextMenu()
if (typeof item.action === 'function') {
item.action(item.params)
return
Expand Down Expand Up @@ -190,7 +205,12 @@ class Pane extends React.PureComponent {
const params = item.intent.params
const Icon = item.icon
return (
<IntentLink className={styles.initialValueTemplateMenuItem} intent="create" params={params}>
<IntentLink
className={styles.initialValueTemplateMenuItem}
intent="create"
onClick={this.handleCloseTemplateMenu}
params={params}
>
<div>
<div>{item.title}</div>
<div className={styles.initialValueTemplateSubtitle}>{params.type}</div>
Expand Down Expand Up @@ -223,31 +243,30 @@ class Pane extends React.PureComponent {
onClick={this.handleMenuAction.bind(this, action)}
/>
)}

{action.action === 'toggleTemplateSelectionMenu' && (
<Popover
placement="bottom"
open={this.state.isInitialValueMenuOpen}
content={
<MenuButton
buttonProps={{
'aria-label': 'Menu',
'aria-haspopup': 'menu',
'aria-expanded': this.state.isInitialValueMenuOpen,
'aria-controls': this.templateMenuId,
icon: Icon,
kind: 'simple',
// onClick: this.handleToggleInitialValueTemplateMenu,
padding: 'small',
selected: this.state.isInitialValueMenuOpen,
title: 'Create new document'
}}
menu={
<div className={styles.initialValueTemplateMenu}>
{items.map(item => this.renderActionMenuItem(item))}
</div>
}
>
<div>
<Button
aria-label="Menu"
aria-haspopup="menu"
aria-expanded={this.state.isInitialValueMenuOpen}
aria-controls={this.templateMenuId}
icon={Icon}
kind="simple"
onClick={this.handleToggleInitialValueTemplateMenu}
padding="small"
selected={this.state.isInitialValueMenuOpen}
title="Create new document"
/>
</div>
</Popover>
open={this.state.isInitialValueMenuOpen}
placement="bottom-end"
setOpen={this.setInitialValueMenuOpen}
/>
)}
</div>
)
Expand Down Expand Up @@ -277,37 +296,33 @@ class Pane extends React.PureComponent {

return (
<div className={styles.headerToolContainer}>
<Popover
placement="bottom-end"
open={isMenuOpen}
content={
<MenuButton
buttonProps={{
'aria-label': 'Menu',
'aria-haspopup': 'menu',
'aria-expanded': isMenuOpen,
'aria-controls': this.paneMenuId,
className: styles.menuOverflowButton,
icon: IconMoreVert,
kind: 'simple',
padding: 'small',
selected: isMenuOpen,
title: 'Show menu'
}}
menu={
<Menu
id={this.paneMenuId}
items={items}
groups={menuItemGroups}
origin={isCollapsed ? 'top-left' : 'top-right'}
onAction={this.handleMenuAction}
onClose={this.handleCloseMenu}
onClickOutside={this.handleCloseMenu}
onClose={this.handleCloseContextMenu}
/>
}
>
<div>
<Button
aria-label="Menu"
aria-haspopup="menu"
aria-expanded={isMenuOpen}
aria-controls={this.paneMenuId}
className={styles.menuOverflowButton}
icon={IconMoreVert}
kind="simple"
onClick={this.handleToggleMenu}
padding="small"
selected={isMenuOpen}
title="Show menu"
/>
</div>
</Popover>
open={isMenuOpen}
placement="bottom-end"
setOpen={this.setContextMenuOpen}
/>
</div>
)
}
Expand Down
11 changes: 11 additions & 0 deletions packages/@sanity/desk-tool/src/@types/parts.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
declare module 'part:*'
declare module 'all:part:*'

declare module 'part:@sanity/components/menu-button' {
export const MenuButton: ComponentType<{
// @todo: typings
buttonProps?: any
menu?: React.ReactNode
placement?: string
open?: boolean
setOpen?: (val: boolean) => void
}>
}

declare module 'all:part:@sanity/base/diff-resolver' {
import {ComponentType} from 'react'

Expand Down

0 comments on commit 8c78720

Please sign in to comment.