From 1d95a703c6becd4bb817f3a081195782b8deee29 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 26 Jul 2018 14:07:41 -0700 Subject: [PATCH] Introduce reusable Dropdown components --- .../components/dropdowns/Dropdown.scss | 163 +++++++++++++++ .../components/dropdowns/Dropdown.tsx | 191 ++++++++++++++++++ .../components/dropdowns/DropdownButton.scss | 73 +++++++ .../components/dropdowns/DropdownButton.tsx | 57 ++++++ .../components/dropdowns/DropdownDivider.tsx | 16 ++ .../components/dropdowns/DropdownItem.tsx | 61 ++++++ ui/src/reusable_ui/types/index.ts | 75 +++++++ 7 files changed, 636 insertions(+) create mode 100644 ui/src/reusable_ui/components/dropdowns/Dropdown.scss create mode 100644 ui/src/reusable_ui/components/dropdowns/Dropdown.tsx create mode 100644 ui/src/reusable_ui/components/dropdowns/DropdownButton.scss create mode 100644 ui/src/reusable_ui/components/dropdowns/DropdownButton.tsx create mode 100644 ui/src/reusable_ui/components/dropdowns/DropdownDivider.tsx create mode 100644 ui/src/reusable_ui/components/dropdowns/DropdownItem.tsx diff --git a/ui/src/reusable_ui/components/dropdowns/Dropdown.scss b/ui/src/reusable_ui/components/dropdowns/Dropdown.scss new file mode 100644 index 000000000..03ee4adbc --- /dev/null +++ b/ui/src/reusable_ui/components/dropdowns/Dropdown.scss @@ -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; +} diff --git a/ui/src/reusable_ui/components/dropdowns/Dropdown.tsx b/ui/src/reusable_ui/components/dropdowns/Dropdown.tsx new file mode 100644 index 000000000..59a0fac98 --- /dev/null +++ b/ui/src/reusable_ui/components/dropdowns/Dropdown.tsx @@ -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 + 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 { + public static defaultProps: Partial = { + 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 ( + +
+ {this.button} + {this.menu} +
+
+ ) + } + + 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 ( + + ) + } + + private get menu(): JSX.Element { + const {selectedItem, maxMenuHeight, menuColor} = this.props + const {expanded} = this.state + + if (expanded) { + return ( +
+ +
+ {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, + }) + )} +
+
+
+ ) + } + + 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 diff --git a/ui/src/reusable_ui/components/dropdowns/DropdownButton.scss b/ui/src/reusable_ui/components/dropdowns/DropdownButton.scss new file mode 100644 index 000000000..b3db76dfd --- /dev/null +++ b/ui/src/reusable_ui/components/dropdowns/DropdownButton.scss @@ -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); +} diff --git a/ui/src/reusable_ui/components/dropdowns/DropdownButton.tsx b/ui/src/reusable_ui/components/dropdowns/DropdownButton.tsx new file mode 100644 index 000000000..e6e268894 --- /dev/null +++ b/ui/src/reusable_ui/components/dropdowns/DropdownButton.tsx @@ -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 { + public static defaultProps: Partial = { + color: ComponentColor.Default, + size: ComponentSize.Small, + disabled: false, + active: false, + } + + public render() { + const {onClick, disabled, label} = this.props + return ( + + ) + } + + 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 + } + + return null + } +} + +export default DropdownButton diff --git a/ui/src/reusable_ui/components/dropdowns/DropdownDivider.tsx b/ui/src/reusable_ui/components/dropdowns/DropdownDivider.tsx new file mode 100644 index 000000000..5fe87def0 --- /dev/null +++ b/ui/src/reusable_ui/components/dropdowns/DropdownDivider.tsx @@ -0,0 +1,16 @@ +import React, {SFC} from 'react' +import classnames from 'classnames' + +interface Props { + text?: string +} + +const DropdownDivider: SFC = ({text}): JSX.Element => ( +
{text}
+) + +DropdownDivider.defaultProps = { + text: '', +} + +export default DropdownDivider diff --git a/ui/src/reusable_ui/components/dropdowns/DropdownItem.tsx b/ui/src/reusable_ui/components/dropdowns/DropdownItem.tsx new file mode 100644 index 000000000..3104d9979 --- /dev/null +++ b/ui/src/reusable_ui/components/dropdowns/DropdownItem.tsx @@ -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 { + public static defaultProps: Partial = { + checkbox: false, + selected: false, + } + + public render() { + const {text, selected, checkbox} = this.props + + return ( +
+ {this.checkBox} + {this.dot} + {text} +
+ ) + } + + private handleClick = (): void => { + const {onClick, value} = this.props + + onClick(value) + } + + private get checkBox(): JSX.Element { + const {checkbox} = this.props + + if (checkbox) { + return
+ } + + return null + } + + private get dot(): JSX.Element { + const {checkbox, selected} = this.props + + if (selected && !checkbox) { + return
+ } + } +} + +export default DropdownItem diff --git a/ui/src/reusable_ui/types/index.ts b/ui/src/reusable_ui/types/index.ts index b09a8dc1b..e0829aaa1 100644 --- a/ui/src/reusable_ui/types/index.ts +++ b/ui/src/reusable_ui/types/index.ts @@ -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', +}