Introduce clockface context menu component
parent
10d6c3e6b6
commit
0e1ea2bf4c
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue