Migrating color pickers to Popper from drop.js pt1

This commit is contained in:
Dominik Prokop 2019-01-18 09:47:39 +01:00
parent c74b39696c
commit 33fa40a1f3
9 changed files with 207 additions and 214 deletions

View File

@ -1,69 +1,45 @@
import React, { Component } from 'react'; import React, { Component, createRef } from 'react';
import ReactDOM from 'react-dom'; import PopperController from '../Tooltip/PopperController';
import Drop from 'tether-drop'; import Popper from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover'; import { ColorPickerPopover } from './ColorPickerPopover';
import { Color } from '../../utils/colorsPalette'; import { ColorDefinition } from '../../utils/colorsPalette';
interface Props { interface Props {
/**
* Value to display, either empty (" ") or "X" / "O".
*
* @default " "
**/
name?: Color;
color: string; color: string;
onChange: (c: string) => void; onChange: (c: string) => void;
} }
export class ColorPicker extends Component<Props, any> { export class ColorPicker extends Component<Props, any> {
private pickerTriggerRef = createRef<HTMLDivElement>();
pickerElem: HTMLElement | null; pickerElem: HTMLElement | null;
colorPickerDrop: any; colorPickerDrop: any;
openColorPicker = () => { onColorSelect = (color: ColorDefinition) => {
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />; this.props.onChange(color.name);
const dropContentElem = document.createElement('div');
ReactDOM.render(dropContent, dropContentElem);
const drop = new Drop({
target: this.pickerElem as Element,
content: dropContentElem,
position: 'top center',
classes: 'drop-popover',
openOn: 'click',
hoverCloseDelay: 200,
tetherOptions: {
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
attachment: 'bottom center',
},
});
drop.on('close', this.closeColorPicker);
this.colorPickerDrop = drop;
this.colorPickerDrop.open();
}; };
closeColorPicker = () => { renderPickerTabs = () => {
setTimeout(() => { return <ColorPickerPopover color="" onColorSelect={() => {}} />;
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
this.colorPickerDrop.destroy();
}
}, 100);
};
onColorSelect = (color: string) => {
this.props.onChange(color);
}; };
render() { render() {
return ( return (
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={element => (this.pickerElem = element)}> <PopperController content={this.renderPickerTabs}>
<div className="sp-preview"> {(showPopper, hidePopper, popperProps) => {
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} /> return (
</div> <>
</div> {this.pickerTriggerRef.current && (
<Popper {...popperProps} referenceElement={this.pickerTriggerRef.current} className="ColorPicker" />
)}
<div ref={this.pickerTriggerRef} onClick={showPopper} className="sp-replacer sp-light">
<div className="sp-preview">
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
</div>
</div>
</>
);
}}
</PopperController>
); );
} }
} }

View File

@ -1,111 +1,76 @@
import React from 'react'; import React, { Children } from 'react';
import $ from 'jquery'; import NamedColorsPicker from './NamedColorsPicker';
import tinycolor from 'tinycolor2'; import { Color } from 'csstype';
import { ColorPalette } from './ColorPalette'; import { ColorDefinition, getColorName } from '../..//utils/colorsPalette';
import { SpectrumPicker } from './SpectrumPicker'; import { SpectrumPicker } from './SpectrumPicker';
import { GrafanaTheme } from '../../types';
const DEFAULT_COLOR = '#000000'; // const DEFAULT_COLOR = '#000000';
export interface Props { export interface Props {
color: string; color: Color | string;
onColorSelect: (c: string) => void; theme?: GrafanaTheme;
onColorSelect: (color: string | ColorDefinition) => void;
} }
export class ColorPickerPopover extends React.Component<Props, any> { type PickerType = 'palette' | 'spectrum';
pickerNavElem: any;
interface State {
activePicker: PickerType;
}
export class ColorPickerPopover extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
tab: 'palette', activePicker: 'spectrum',
color: this.props.color || DEFAULT_COLOR,
colorString: this.props.color || DEFAULT_COLOR,
}; };
} }
setPickerNavElem(elem: any) { handleSpectrumColorSelect = (color: any) => {
this.pickerNavElem = $(elem); this.props.onColorSelect(color.toRgbString());
} };
setColor(color: string) { renderPicker = () => {
const newColor = tinycolor(color); const { activePicker } = this.state;
if (newColor.isValid()) { const { color } = this.props;
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
this.props.onColorSelect(color);
}
}
sampleColorSelected(color: string) { return activePicker === 'spectrum' ? (
this.setColor(color); <SpectrumPicker color={color} onColorSelect={this.handleSpectrumColorSelect} options={{}} />
} ) : (
<NamedColorsPicker selectedColor={getColorName(color)} onChange={this.props.onColorSelect} />
spectrumColorSelected(color: any) { );
const rgbColor = color.toRgbString(); };
this.setColor(rgbColor);
}
onColorStringChange(e: any) {
const colorString = e.target.value;
this.setState({ colorString: colorString });
const newColor = tinycolor(colorString);
if (newColor.isValid()) {
// Update only color state
const newColorString = newColor.toString();
this.setState({ color: newColorString });
this.props.onColorSelect(newColorString);
}
}
onColorStringBlur(e: any) {
const colorString = e.target.value;
this.setColor(colorString);
}
componentDidMount() {
this.pickerNavElem.find('li:first').addClass('active');
this.pickerNavElem.on('show', (e: any) => {
// use href attr (#name => name)
const tab = e.target.hash.slice(1);
this.setState({ tab: tab });
});
}
render() { render() {
const paletteTab = ( const { activePicker } = this.state;
<div id="palette"> const { theme, children } = this.props;
<ColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected.bind(this)} /> const colorPickerTheme = theme || GrafanaTheme.Dark;
</div>
);
const spectrumTab = (
<div id="spectrum">
<SpectrumPicker color={this.state.color} onColorSelect={this.spectrumColorSelected.bind(this)} options={{}} />
</div>
);
const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab;
return ( return (
<div className="gf-color-picker"> <div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
<ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem.bind(this)}> <div className="ColorPickerPopover__tabs">
<li className="gf-tabs-item-colorpicker"> <div
<a href="#palette" data-toggle="tab"> className={`ColorPickerPopover__tab ${activePicker === 'palette' && 'ColorPickerPopover__tab--active'}`}
Colors onClick={() => {
</a> this.setState({ activePicker: 'palette' });
</li> }}
<li className="gf-tabs-item-colorpicker"> >
<a href="#spectrum" data-toggle="tab"> Default
Custom </div>
</a> <div
</li> className={`ColorPickerPopover__tab ${activePicker === 'spectrum' && 'ColorPickerPopover__tab--active'}`}
</ul> onClick={() => {
<div className="gf-color-picker__body">{currentTab}</div> this.setState({ activePicker: 'spectrum' });
<div> }}
<input >
className="gf-form-input gf-form-input--small" Custom
value={this.state.colorString} </div>
onChange={this.onColorStringChange.bind(this)} </div>
onBlur={this.onColorStringBlur.bind(this)}
/> <div className="ColorPickerPopover__content">
{this.renderPicker()}
{children}
</div> </div>
</div> </div>
); );

View File

@ -47,19 +47,22 @@ const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
); );
}; };
const ColorsGroup = ({ interface ColorsGroupProps {
colors: ColorDefinition[];
selectedColor?: Color;
onColorSelect: ColorChangeHandler;
key?: string;
}
const ColorsGroup: FunctionComponent<ColorsGroupProps> = ({
colors, colors,
selectedColor, selectedColor,
onColorSelect, onColorSelect,
}: { ...otherProps
colors: ColorDefinition[];
selectedColor?: Color;
onColorSelect: ColorChangeHandler
}) => { }) => {
const primaryColor = find(colors, color => !!color.isPrimary); const primaryColor = find(colors, color => !!color.isPrimary);
return ( return (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div {...otherProps} style={{ display: 'flex', flexDirection: 'column' }}>
{primaryColor && ( {primaryColor && (
<ColorSwatch <ColorSwatch
isSelected={primaryColor.name === selectedColor} isSelected={primaryColor.name === selectedColor}
@ -75,7 +78,7 @@ const ColorsGroup = ({
}} }}
> >
{colors.map(color => !color.isPrimary && ( {colors.map(color => !color.isPrimary && (
<div style={{ marginRight: '4px' }}> <div key={color.name} style={{ marginRight: '4px' }}>
<ColorSwatch <ColorSwatch
isSelected={color.name === selectedColor} isSelected={color.name === selectedColor}
color={color} color={color}
@ -88,7 +91,6 @@ const ColorsGroup = ({
); );
}; };
interface NamedColorsPickerProps { interface NamedColorsPickerProps {
selectedColor?: Color; selectedColor?: Color;
onChange: ColorChangeHandler; onChange: ColorChangeHandler;
@ -98,9 +100,7 @@ const NamedColorsPicker = ({ selectedColor, onChange }: NamedColorsPickerProps)
ColorsPalete.forEach((colors, hue) => { ColorsPalete.forEach((colors, hue) => {
swatches.push( swatches.push(
<> <ColorsGroup key={hue} selectedColor={selectedColor} colors={colors} onColorSelect={onChange} />
<ColorsGroup selectedColor={selectedColor} colors={colors} onColorSelect={onChange} />
</>
); );
}); });

View File

@ -1,7 +1,9 @@
import React from 'react'; import React, { createRef } from 'react';
import ReactDOM from 'react-dom'; import * as PopperJS from 'popper.js';
import Drop from 'tether-drop';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import PopperController from '../Tooltip/PopperController';
import Popper from '../Tooltip/Popper';
import { GrafanaTheme } from '../../types';
export interface SeriesColorPickerProps { export interface SeriesColorPickerProps {
color: string; color: string;
@ -9,10 +11,12 @@ export interface SeriesColorPickerProps {
optionalClass?: string; optionalClass?: string;
onColorChange: (newColor: string) => void; onColorChange: (newColor: string) => void;
onToggleAxis?: () => void; onToggleAxis?: () => void;
children: JSX.Element;
theme?: GrafanaTheme;
} }
export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> { export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
pickerElem: any; private pickerTriggerRef = createRef<PopperJS.ReferenceObject>();
colorPickerDrop: any; colorPickerDrop: any;
static defaultProps = { static defaultProps = {
@ -21,65 +25,46 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
onToggleAxis: () => {}, onToggleAxis: () => {},
}; };
constructor(props: SeriesColorPickerProps) { renderPickerTabs = () => {
super(props); const { color, yaxis, onColorChange, onToggleAxis, theme } = this.props;
} return (
<SeriesColorPickerPopover
componentWillUnmount() { theme={theme}
this.destroyDrop(); color={color}
} yaxis={yaxis}
onColorChange={onColorChange}
onClickToOpen = () => { onToggleAxis={onToggleAxis}
if (this.colorPickerDrop) { />
this.destroyDrop();
}
const { color, yaxis, onColorChange, onToggleAxis } = this.props;
const dropContent = (
<SeriesColorPickerPopover color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
); );
const dropContentElem = document.createElement('div');
ReactDOM.render(dropContent, dropContentElem);
const drop = new Drop({
target: this.pickerElem,
content: dropContentElem,
position: 'bottom center',
classes: 'drop-popover',
openOn: 'hover',
hoverCloseDelay: 200,
remove: true,
tetherOptions: {
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
attachment: 'bottom center',
},
});
drop.on('close', this.closeColorPicker.bind(this));
this.colorPickerDrop = drop;
this.colorPickerDrop.open();
}; };
closeColorPicker() {
setTimeout(() => {
this.destroyDrop();
}, 100);
}
destroyDrop() {
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
this.colorPickerDrop.destroy();
this.colorPickerDrop = null;
}
}
render() { render() {
const { optionalClass, children } = this.props; const { children } = this.props;
return ( return (
<div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}> <PopperController placement="bottom-start" content={this.renderPickerTabs}>
{children} {(showPopper, hidePopper, popperProps) => {
</div> return (
<>
{this.pickerTriggerRef.current && (
<Popper
{...popperProps}
onMouseEnter={showPopper}
onMouseLeave={hidePopper}
referenceElement={this.pickerTriggerRef.current}
className="ColorPicker"
arrowClassName="popper__arrow"
/>
)}
{React.cloneElement(children, {
ref: this.pickerTriggerRef,
onClick: showPopper,
onMouseLeave: hidePopper,
})}
</>
);
}}
</PopperController>
); );
} }
} }

View File

@ -1,19 +1,24 @@
import React from 'react'; import React from 'react';
import { ColorPickerPopover } from './ColorPickerPopover'; import { ColorPickerPopover } from './ColorPickerPopover';
import { GrafanaTheme } from '../../types';
export interface SeriesColorPickerPopoverProps { export interface SeriesColorPickerPopoverProps {
color: string; color: string;
yaxis?: number; yaxis?: number;
onColorChange: (color: string) => void; onColorChange: (color: string) => void;
onToggleAxis?: () => void; onToggleAxis?: () => void;
theme?: GrafanaTheme;
} }
export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPickerPopoverProps, any> { export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPickerPopoverProps, any> {
render() { render() {
return ( return (
<div className="graph-legend-popover"> <div>
{this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />} <ColorPickerPopover theme={this.props.theme} color={this.props.color} onColorSelect={this.props.onColorChange}>
<ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} /> <div style={{ marginTop: '32px' }}>
{this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />}
</div>
</ColorPickerPopover>
</div> </div>
); );
} }

View File

@ -6,7 +6,7 @@ import '../../vendor/spectrum';
export interface Props { export interface Props {
color: string; color: string;
options: object; options: object;
onColorSelect: (c: string) => void; onColorSelect: (color: string) => void;
} }
export class SpectrumPicker extends React.Component<Props, any> { export class SpectrumPicker extends React.Component<Props, any> {

View File

@ -1,3 +1,54 @@
.ColorPicker {
.popper__arrow {
border-color: #f7f8fa;
}
}
.ColorPickerPopover {
border-radius: 3px;
}
.ColorPickerPopover--light {
color: black;
background: linear-gradient(180deg, #ffffff 0%, #f7f8fa 104.25%);
box-shadow: 0px 2px 4px #dde4ed, 0px 0px 2px #dde4ed;
}
.ColorPickerPopover--dark {
color: #d8d9da;
background: linear-gradient(180deg, #1e2028 0%, #161719 104.25%);
box-shadow: 0px 2px 4px #000000, 0px 0px 2px #000000;
.ColorPickerPopover__tab {
background: #303133;
color: white;
}
.ColorPickerPopover__tab--active {
background: none;
}
}
.ColorPickerPopover__content {
width: 360px;
padding: 32px;
}
.ColorPickerPopover__tabs {
display: flex;
width: 100%;
}
.ColorPickerPopover__tab {
width: 50%;
text-align: center;
padding: 8px 0;
background: #dde4ed;
border-radius: 3px;
}
.ColorPickerPopover__tab--active {
background: white;
}
.sp-replacer { .sp-replacer {
background: inherit; background: inherit;
border: none; border: none;

View File

@ -3,3 +3,8 @@ export * from './time';
export * from './panel'; export * from './panel';
export * from './plugin'; export * from './plugin';
export * from './datasource'; export * from './datasource';
export enum GrafanaTheme {
Light = 'light',
Dark = 'dark',
}

View File

@ -1,6 +1,7 @@
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { getFlotTickDecimals } from 'app/core/utils/ticks'; import { getFlotTickDecimals } from 'app/core/utils/ticks';
import _ from 'lodash'; import _ from 'lodash';
import { ColorDefinition } from '@grafana/ui/src/utils/colorsPalette';
function matchSeriesOverride(aliasOrRegex, seriesAlias) { function matchSeriesOverride(aliasOrRegex, seriesAlias) {
if (!aliasOrRegex) { if (!aliasOrRegex) {
@ -356,8 +357,13 @@ export default class TimeSeries {
return false; return false;
} }
setColor(color) { setColor(color: string | ColorDefinition) {
this.color = color; if (typeof color === 'string') {
this.bars.fillColor = color; this.color = color;
this.bars.fillColor = color;
} else {
this.color = color.variants.dark;
this.bars.fillColor = color.variants.dark;
}
} }
} }