Introduce reusable Dropdown components

pull/4041/head
Alex P 2018-07-26 14:07:41 -07:00
parent 42ef7bd8bc
commit 1d95a703c6
7 changed files with 636 additions and 0 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',
}