Introduce clockface context menu component

pull/10616/head
Alex P 2018-11-19 17:45:47 -08:00
parent 10d6c3e6b6
commit 0e1ea2bf4c
5 changed files with 342 additions and 0 deletions

View File

@ -0,0 +1,47 @@
// Libraries
import React, {PureComponent} from 'react'
import classnames from 'classnames'
// Components
import ContextMenu from 'src/clockface/components/context_menu/ContextMenu'
import ContextMenuItem from 'src/clockface/components/context_menu/ContextMenuItem'
// Types
import {Alignment} from 'src/clockface/types'
// Styles
import './ContextMenu.scss'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
align?: Alignment
children: JSX.Element | JSX.Element[]
}
@ErrorHandling
class CellContext extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
align: Alignment.Right,
}
public static Menu = ContextMenu
public static Item = ContextMenuItem
public render() {
const {children} = this.props
return <div className={this.className}>{children}</div>
}
private get className(): string {
const {align} = this.props
return classnames('context-menu', {
'context-menu--align-left': align === Alignment.Left,
'context-menu--align-right': align === Alignment.Right,
})
}
}
export default CellContext

View File

@ -0,0 +1,151 @@
/*
Context Menu Styles
------------------------------------------------------------------------------
*/
@import 'src/style/modules';
$context-menu--toggle-size: 24px;
$context-menu--arrow-size: 8px;
.context-menu {
display: flex;
align-items: center;
}
.context-menu--align-left {
justify-content: flex-start;
& > .context-menu--container {
margin-left: 2px;
}
& > .context-menu--container:first-child {
margin-left: 0;
}
}
.context-menu--align-right {
justify-content: flex-end;
& > .context-menu--container {
margin-right: 2px;
}
& > .context-menu--container:last-child {
margin-right: 0;
}
}
.context-menu--container {
position: relative;
width: $context-menu--toggle-size;
height: $context-menu--toggle-size;
}
.context-menu--toggle {
position: relative;
width: $context-menu--toggle-size;
height: $context-menu--toggle-size;
border: 0;
border-radius: $radius-small;
background-color: $g2-kevlar;
color: $g13-mist;
transition: color 0.25s ease, background-color 0.25s ease;
outline: none;
&:hover {
cursor: pointer;
background-color: $c-pool;
color: $g20-white;
}
&.active {
background-color: $c-laser;
color: $g20-white;
}
}
.context-menu--icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 13px;
}
.context-menu--list {
@extend %drop-shadow;
transition: opacity 0.25s ease;
opacity: 0;
display: flex;
flex-direction: column;
align-items: stretch;
background-color: $c-pool;
border-radius: $ix-radius;
position: relative;
&:before {
content: '';
border: $context-menu--arrow-size solid transparent;
border-bottom-color: $c-pool;
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -100%);
}
}
.context-menu--list-container {
z-index: 2;
position: absolute;
left: 50%;
top: $context-menu--toggle-size;
padding-top: $context-menu--arrow-size;
transform: translateX(-50%);
transition: all;
visibility: hidden;
&.open {
visibility: visible;
.context-menu--list {
opacity: 1;
}
}
}
.context-menu--item {
outline: none;
border: 0;
background-color: transparent;
font-weight: 700;
font-size: 13px;
line-height: 13px;
white-space: nowrap;
padding: 8px;
transition: background-color 0.25s ease;
&:first-child {
border-top-left-radius: $ix-radius;
border-top-right-radius: $ix-radius;
}
&:last-child {
border-bottom-left-radius: $ix-radius;
border-bottom-right-radius: $ix-radius;
}
&:hover {
cursor: pointer;
background-color: $c-laser;
}
&.context-menu--item__disabled,
&.context-menu--item__disabled:hover {
cursor: default;
color: rgba($g20-white, 0.6);
font-style: italic;
background-color: transparent;
}
}

View File

@ -0,0 +1,95 @@
// Libraries
import React, {Component} from 'react'
import classnames from 'classnames'
// Components
import ContextMenuItem from 'src/clockface/components/context_menu/ContextMenuItem'
import {ClickOutside} from 'src/shared/components/ClickOutside'
// Types
import {IconFont} from 'src/clockface/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
children: JSX.Element | JSX.Element[]
icon: IconFont
}
interface State {
isExpanded: boolean
}
@ErrorHandling
class CellMenu extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isExpanded: false,
}
}
public render() {
const {icon} = this.props
return (
<ClickOutside onClickOutside={this.handleCollapseMenu}>
<div className="context-menu--container">
<button
className={this.toggleClassName}
onClick={this.handleExpandMenu}
>
<span className={`context-menu--icon icon ${icon}`} />
</button>
{this.menu}
</div>
</ClickOutside>
)
}
private handleExpandMenu = (): void => {
this.setState({isExpanded: true})
}
private handleCollapseMenu = (): void => {
this.setState({isExpanded: false})
}
private get menu(): JSX.Element {
const {children} = this.props
return (
<div className={this.menuClassName}>
<div className="context-menu--list">
{React.Children.map(children, (child: JSX.Element) => {
if (child.type === ContextMenuItem) {
return (
<ContextMenuItem
{...child.props}
onCollapseMenu={this.handleCollapseMenu}
/>
)
} else {
throw new Error('Expected children of type <ContextMenu.Item />')
}
})}
</div>
</div>
)
}
private get menuClassName(): string {
const {isExpanded} = this.state
return classnames('context-menu--list-container', {open: isExpanded})
}
private get toggleClassName(): string {
const {isExpanded} = this.state
return classnames('context-menu--toggle', {active: isExpanded})
}
}
export default CellMenu

View File

@ -0,0 +1,47 @@
// Libraries
import React, {Component} from 'react'
import classnames from 'classnames'
interface Props {
label: string
action: () => void
onCollapseMenu?: () => void
disabled?: boolean
}
class CellContextMenuItem extends Component<Props> {
public render() {
const {label, disabled} = this.props
return (
<button
className={this.className}
onClick={this.handleClick}
disabled={disabled}
>
{label}
</button>
)
}
private get className(): string {
const {disabled} = this.props
return classnames('context-menu--item', {
'context-menu--item__disabled': disabled,
})
}
private handleClick = (): void => {
const {action, onCollapseMenu} = this.props
if (!onCollapseMenu) {
return
}
onCollapseMenu()
action()
}
}
export default CellContextMenuItem

View File

@ -18,6 +18,7 @@ import ComponentSpacer from './components/component_spacer/ComponentSpacer'
import EmptyState from './components/empty_state/EmptyState'
import Spinner from './components/spinners/Spinner'
import IndexList from './components/index_views/IndexList'
import Context from './components/context_menu/Context'
// Import Types
import {
@ -41,6 +42,7 @@ export {
Button,
ButtonType,
ComponentSpacer,
Context,
Dropdown,
DropdownMode,
MultiSelectDropdown,