diff --git a/ui/src/clockface/components/context_menu/Context.tsx b/ui/src/clockface/components/context_menu/Context.tsx new file mode 100644 index 0000000000..3d46f48945 --- /dev/null +++ b/ui/src/clockface/components/context_menu/Context.tsx @@ -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 { + public static defaultProps: Partial = { + align: Alignment.Right, + } + + public static Menu = ContextMenu + public static Item = ContextMenuItem + + public render() { + const {children} = this.props + + return
{children}
+ } + + 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 diff --git a/ui/src/clockface/components/context_menu/ContextMenu.scss b/ui/src/clockface/components/context_menu/ContextMenu.scss new file mode 100644 index 0000000000..0e0e2f98a2 --- /dev/null +++ b/ui/src/clockface/components/context_menu/ContextMenu.scss @@ -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; + } +} diff --git a/ui/src/clockface/components/context_menu/ContextMenu.tsx b/ui/src/clockface/components/context_menu/ContextMenu.tsx new file mode 100644 index 0000000000..5b85a1f7be --- /dev/null +++ b/ui/src/clockface/components/context_menu/ContextMenu.tsx @@ -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 { + constructor(props: Props) { + super(props) + + this.state = { + isExpanded: false, + } + } + + public render() { + const {icon} = this.props + + return ( + +
+ + {this.menu} +
+
+ ) + } + + private handleExpandMenu = (): void => { + this.setState({isExpanded: true}) + } + + private handleCollapseMenu = (): void => { + this.setState({isExpanded: false}) + } + + private get menu(): JSX.Element { + const {children} = this.props + + return ( +
+
+ {React.Children.map(children, (child: JSX.Element) => { + if (child.type === ContextMenuItem) { + return ( + + ) + } else { + throw new Error('Expected children of type ') + } + })} +
+
+ ) + } + + 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 diff --git a/ui/src/clockface/components/context_menu/ContextMenuItem.tsx b/ui/src/clockface/components/context_menu/ContextMenuItem.tsx new file mode 100644 index 0000000000..a16e108056 --- /dev/null +++ b/ui/src/clockface/components/context_menu/ContextMenuItem.tsx @@ -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 { + public render() { + const {label, disabled} = this.props + + return ( + + ) + } + + 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 diff --git a/ui/src/clockface/index.ts b/ui/src/clockface/index.ts index 29fe640882..6f712aa516 100644 --- a/ui/src/clockface/index.ts +++ b/ui/src/clockface/index.ts @@ -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,