Introduce reusable Dropdown components
parent
42ef7bd8bc
commit
1d95a703c6
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
Dropdowns
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import 'src/style/modules/influx-colors';
|
||||
@import 'src/style/modules/variables';
|
||||
@import 'src/style/modules/mixins';
|
||||
|
||||
/* Dropdown Menu */
|
||||
.dropdown--menu-container {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 500;
|
||||
border-radius: $radius;
|
||||
box-shadow: 0 2px 5px 0.6px fade-out($g0-obsidian, 0.7);
|
||||
}
|
||||
|
||||
.dropdown--menu {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown--item {
|
||||
padding: 6px 11px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
font-weight: 600;
|
||||
color: fade-out($g20-white, 0.18);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
padding-left: 24px;
|
||||
color: $g20-white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $g20-white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-wrap & {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item--dot {
|
||||
display: none;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 11px;
|
||||
transform: translateY(-50%);
|
||||
background-color: $g20-white;
|
||||
|
||||
.active & {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown--divider {
|
||||
padding: 6px 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
&.line {
|
||||
padding: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown--item.multi-select--item {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.dropdown-item--checkbox {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border-radius: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
opacity: 0;
|
||||
transform: translate(-50%,-50%) scale(1.5,1.5);
|
||||
}
|
||||
|
||||
.active &:after {
|
||||
opacity: 1;
|
||||
transform: translate(-50%,-50%) scale(1,1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
/* Currently only a single theme, I plan on adding more later */
|
||||
|
||||
@mixin dropdownMenuColor($backgroundA, $backgroundB, $hoverA, $hoverB, $dividerA, $dividerB, $dividerText, $scrollA, $scrollB) {
|
||||
@include gradient-h($backgroundA, $backgroundB);
|
||||
|
||||
.dropdown--item:hover,
|
||||
.dropdown--item.active {
|
||||
@include gradient-h($hoverA, $hoverB);
|
||||
}
|
||||
.dropdown--divider {
|
||||
color: $dividerText;
|
||||
@include gradient-h($dividerA, $dividerB);
|
||||
}
|
||||
.dropdown-item--checkbox {
|
||||
background-color: $dividerA;
|
||||
}
|
||||
.dropdown-item--checkbox:after {
|
||||
background-color: $scrollA;
|
||||
}
|
||||
.fancy-scroll--thumb-h {
|
||||
@include gradient-h($scrollA, $scrollB);
|
||||
}
|
||||
.fancy-scroll--thumb-v {
|
||||
@include gradient-v($scrollA, $scrollB);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown--amethyst {
|
||||
@include dropdownMenuColor($c-star, $c-pool, $c-comet, $c-laser, $c-amethyst, $c-ocean, $c-potassium, $c-neutrino, $c-hydrogen);
|
||||
}
|
||||
|
||||
.dropdown--sapphire {
|
||||
@include dropdownMenuColor($c-ocean, $c-pool, $c-pool, $c-laser, $c-sapphire, $c-ocean, $c-laser, $c-neutrino, $c-hydrogen);
|
||||
}
|
||||
|
||||
.dropdown--malachite {
|
||||
@include dropdownMenuColor($c-pool, $c-rainforest, $c-laser, $c-honeydew, $c-ocean, $c-viridian, $c-krypton, $c-neutrino, $c-krypton);
|
||||
}
|
||||
|
||||
.dropdown--onyx {
|
||||
@include dropdownMenuColor($g2-kevlar, $g4-onyx, $g4-onyx, $g6-smoke, $g0-obsidian, $g2-kevlar, $g11-sidewalk, $c-pool, $c-comet);
|
||||
}
|
||||
|
||||
/* TODO: Make fancyscroll more customizable */
|
||||
.dropdown--menu-container .fancy-scroll--track-h {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
import React, {Component, CSSProperties, Fragment} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import {
|
||||
ComponentColor,
|
||||
ComponentSize,
|
||||
IconFont,
|
||||
DropdownMenuColors,
|
||||
} from 'src/reusable_ui/types'
|
||||
import DropdownDivider from 'src/reusable_ui/components/dropdowns/DropdownDivider'
|
||||
import DropdownItem from 'src/reusable_ui/components/dropdowns/DropdownItem'
|
||||
import DropdownButton from 'src/reusable_ui/components/dropdowns/DropdownButton'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import './Dropdown.scss'
|
||||
|
||||
interface Props {
|
||||
children: Array<JSX.Element | JSX.Element[]>
|
||||
onChange: (value: any) => void
|
||||
selectedItem: string
|
||||
color?: ComponentColor
|
||||
menuColor?: DropdownMenuColors
|
||||
size?: ComponentSize
|
||||
disabled?: boolean
|
||||
width?: number
|
||||
icon?: IconFont
|
||||
wrapText?: boolean
|
||||
customClass?: string
|
||||
maxMenuHeight?: number
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class Dropdown extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
color: ComponentColor.Default,
|
||||
size: ComponentSize.Small,
|
||||
disabled: false,
|
||||
width: 120,
|
||||
wrapText: false,
|
||||
maxMenuHeight: 250,
|
||||
menuColor: DropdownMenuColors.Sapphire,
|
||||
}
|
||||
|
||||
public static Button = DropdownButton
|
||||
public static Item = DropdownItem
|
||||
public static Divider = DropdownDivider
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const width = `${this.props.width}px`
|
||||
|
||||
this.validateChildren()
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.collapseMenu}>
|
||||
<div className={this.containerClassName} style={{width}}>
|
||||
{this.button}
|
||||
{this.menu}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
private toggleMenu = (): void => {
|
||||
this.setState({expanded: !this.state.expanded})
|
||||
}
|
||||
|
||||
private collapseMenu = (): void => {
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
private get containerClassName(): string {
|
||||
const {color, size, disabled, wrapText, customClass} = this.props
|
||||
|
||||
return classnames(`dropdown dropdown-${size} dropdown-${color}`, {
|
||||
disabled,
|
||||
'dropdown-wrap': wrapText,
|
||||
[customClass]: customClass,
|
||||
})
|
||||
}
|
||||
|
||||
private get button(): JSX.Element {
|
||||
const {selectedItem, disabled, color, size, icon} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
label={selectedItem}
|
||||
active={expanded}
|
||||
color={color}
|
||||
size={size}
|
||||
icon={icon}
|
||||
disabled={disabled}
|
||||
onClick={this.toggleMenu}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get menu(): JSX.Element {
|
||||
const {selectedItem, maxMenuHeight, menuColor} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
if (expanded) {
|
||||
return (
|
||||
<div
|
||||
className={`dropdown--menu-container dropdown--${menuColor}`}
|
||||
style={this.menuStyle}
|
||||
>
|
||||
<FancyScrollbar
|
||||
autoHide={false}
|
||||
autoHeight={true}
|
||||
maxHeight={maxMenuHeight}
|
||||
>
|
||||
<div className="dropdown--menu">
|
||||
{this.flatChildren.map((child: JSX.Element) =>
|
||||
React.cloneElement(child, {
|
||||
...child.props,
|
||||
key: `dropdown-menu--${child.props.text}`,
|
||||
selected: child.props.text === selectedItem,
|
||||
onClick: this.handleItemClick,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private get flatChildren() {
|
||||
const children = React.Children.toArray(this.props.children)
|
||||
|
||||
const childrenWithoutFragments = children.map((child: JSX.Element) => {
|
||||
if (child.type === Fragment) {
|
||||
const childArray = React.Children.toArray(child.props.children)
|
||||
return childArray
|
||||
}
|
||||
|
||||
return child
|
||||
})
|
||||
|
||||
return _.flattenDeep(childrenWithoutFragments)
|
||||
}
|
||||
|
||||
private get menuStyle(): CSSProperties {
|
||||
const {wrapText, width} = this.props
|
||||
|
||||
if (wrapText) {
|
||||
return {
|
||||
width: `${width}px`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
minWidth: `${width}px`,
|
||||
}
|
||||
}
|
||||
|
||||
private handleItemClick = (value: any): void => {
|
||||
const {onChange} = this.props
|
||||
onChange(value)
|
||||
this.collapseMenu()
|
||||
}
|
||||
|
||||
private validateChildren = (): void => {
|
||||
const {children} = this.props
|
||||
|
||||
if (React.Children.count(children) === 0) {
|
||||
throw new Error(
|
||||
'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Dropdown
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Dropdown Button
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import 'src/style/modules/influx-colors';
|
||||
@import 'src/style/modules/variables';
|
||||
|
||||
|
||||
/* Button */
|
||||
.dropdown--button {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown--selected,
|
||||
.dropdown--button > span.icon.dropdown--icon,
|
||||
.dropdown--button > span.icon.dropdown--caret {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
span.icon.dropdown--icon {
|
||||
top: 49%;
|
||||
}
|
||||
|
||||
.dropdown--selected {
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.dropdown--button > span.icon.dropdown--caret {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Button Size Modifiers */
|
||||
@mixin buttonSizing($padding, $font) {
|
||||
.dropdown--icon,
|
||||
.dropdown--selected {
|
||||
left: $padding;
|
||||
}
|
||||
.dropdown--selected {
|
||||
width: calc(100% - #{($padding * 2) + $font});
|
||||
}
|
||||
.dropdown--icon + .dropdown--selected {
|
||||
left: $padding + ($font * 1.5);
|
||||
width: calc(100% - #{($padding * 2) + ($font * 2.5)});
|
||||
}
|
||||
.dropdown--caret {
|
||||
right: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown--button.btn-xs {
|
||||
@include buttonSizing($form-xs-padding, $form-xs-font);
|
||||
}
|
||||
|
||||
.dropdown--button.btn-sm {
|
||||
@include buttonSizing($form-sm-padding, $form-sm-font);
|
||||
}
|
||||
|
||||
.dropdown--button.btn-md {
|
||||
@include buttonSizing($form-md-padding, $form-md-font);
|
||||
}
|
||||
|
||||
.dropdown--button.btn-lg {
|
||||
@include buttonSizing($form-lg-padding, $form-lg-font);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {ComponentColor, ComponentSize, IconFont} from 'src/reusable_ui/types'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import './DropdownButton.scss'
|
||||
|
||||
interface Props {
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
active?: boolean
|
||||
color?: ComponentColor
|
||||
size?: ComponentSize
|
||||
icon?: IconFont
|
||||
label: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class DropdownButton extends Component<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
color: ComponentColor.Default,
|
||||
size: ComponentSize.Small,
|
||||
disabled: false,
|
||||
active: false,
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {onClick, disabled, label} = this.props
|
||||
return (
|
||||
<button className={this.classname} onClick={onClick} disabled={disabled}>
|
||||
{this.icon}
|
||||
<span className="dropdown--selected">{label}</span>
|
||||
<span className="dropdown--caret icon caret-down" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
private get classname(): string {
|
||||
const {disabled, active, color, size} = this.props
|
||||
|
||||
return classnames(`dropdown--button btn btn-${color} btn-${size}`, {
|
||||
disabled,
|
||||
active,
|
||||
})
|
||||
}
|
||||
|
||||
private get icon(): JSX.Element {
|
||||
const {icon} = this.props
|
||||
|
||||
if (icon) {
|
||||
return <span className={`dropdown--icon icon ${icon}`} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default DropdownButton
|
|
@ -0,0 +1,16 @@
|
|||
import React, {SFC} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
text?: string
|
||||
}
|
||||
|
||||
const DropdownDivider: SFC<Props> = ({text}): JSX.Element => (
|
||||
<div className={classnames('dropdown--divider', {line: !text})}>{text}</div>
|
||||
)
|
||||
|
||||
DropdownDivider.defaultProps = {
|
||||
text: '',
|
||||
}
|
||||
|
||||
export default DropdownDivider
|
|
@ -0,0 +1,61 @@
|
|||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
selected?: boolean
|
||||
checkbox?: boolean
|
||||
onClick?: (value: any) => void
|
||||
value: any
|
||||
}
|
||||
|
||||
class DropdownItem extends Component<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
checkbox: false,
|
||||
selected: false,
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {text, selected, checkbox} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('dropdown--item', {
|
||||
active: selected,
|
||||
'multi-select--item': checkbox,
|
||||
})}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{this.checkBox}
|
||||
{this.dot}
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (): void => {
|
||||
const {onClick, value} = this.props
|
||||
|
||||
onClick(value)
|
||||
}
|
||||
|
||||
private get checkBox(): JSX.Element {
|
||||
const {checkbox} = this.props
|
||||
|
||||
if (checkbox) {
|
||||
return <div className="dropdown-item--checkbox" />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private get dot(): JSX.Element {
|
||||
const {checkbox, selected} = this.props
|
||||
|
||||
if (selected && !checkbox) {
|
||||
return <div className="dropdown-item--dot" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DropdownItem
|
|
@ -7,6 +7,13 @@ export enum ComponentColor {
|
|||
Alert = 'alert',
|
||||
}
|
||||
|
||||
export enum DropdownMenuColors {
|
||||
Amethyst = 'amethyst',
|
||||
Malachite = 'malachite',
|
||||
Sapphire = 'sapphire',
|
||||
Onyx = 'onyx',
|
||||
}
|
||||
|
||||
export enum ComponentSize {
|
||||
ExtraSmall = 'xs',
|
||||
Small = 'sm',
|
||||
|
@ -43,3 +50,71 @@ export enum Greys {
|
|||
Ghost = '#fafafc',
|
||||
White = '#ffffff',
|
||||
}
|
||||
|
||||
export enum IconFont {
|
||||
Alerts = 'alerts',
|
||||
AlertTriangle = 'alert-triangle',
|
||||
AuthZero = 'authzero',
|
||||
BarChart = 'bar-chart',
|
||||
Capacitor = 'capacitor2',
|
||||
CaretDown = 'caret-down',
|
||||
CaretLeft = 'caret-left',
|
||||
CaretRight = 'caret-right',
|
||||
CaretUp = 'caret-up',
|
||||
Checkmark = 'checkmark',
|
||||
Circle = 'circle',
|
||||
Clock = 'clock',
|
||||
CogOutline = 'cog-outline',
|
||||
CogThick = 'cog-thick',
|
||||
Collapse = 'collapse',
|
||||
CrownOutline = 'crown-outline',
|
||||
CrownSolid = 'crown2',
|
||||
Cube = 'cube',
|
||||
Cubouniform = 'cubo-uniform',
|
||||
DashF = 'dash-f',
|
||||
DashH = 'dash-h',
|
||||
DashJ = 'dash-j',
|
||||
Disks = 'disks',
|
||||
Download = 'download',
|
||||
Duplicate = 'duplicate',
|
||||
ExpandA = 'expand-a',
|
||||
ExpandB = 'expand-b',
|
||||
Export = 'export',
|
||||
Eye = 'eye',
|
||||
EyeClosed = 'eye-closed',
|
||||
EyeOpen = 'eye-open',
|
||||
GitHub = 'github',
|
||||
Google = 'google',
|
||||
GraphLine = 'graphline2',
|
||||
Group = 'group',
|
||||
Heroku = 'heroku',
|
||||
HerokuSimple = '',
|
||||
Import = 'import',
|
||||
Link = 'link',
|
||||
OAuth = 'oauth',
|
||||
Octagon = 'octagon',
|
||||
Okta = 'okta',
|
||||
Pause = 'pause',
|
||||
Pencil = 'pencil',
|
||||
Play = 'play',
|
||||
Plus = 'plus',
|
||||
Pulse = 'pulse-c',
|
||||
Refresh = 'refresh',
|
||||
Remove = 'remove',
|
||||
Search = 'search',
|
||||
Server = 'server2',
|
||||
Shuffle = 'shuffle',
|
||||
Square = 'square',
|
||||
TextBlock = 'text-block',
|
||||
Trash = 'trash',
|
||||
Triangle = 'triangle',
|
||||
User = 'user',
|
||||
UserAdd = 'user-add',
|
||||
UserOutline = 'user-outline',
|
||||
UserRemove = 'user-remove',
|
||||
Wood = 'wood',
|
||||
Wrench = 'wrench',
|
||||
Star = 'star',
|
||||
Stop = 'stop',
|
||||
Zap = 'zap',
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue