Refactor color picker to remove code duplicartion (introduced colorPickerFactory). Allow popver position update on content change

This commit is contained in:
Dominik Prokop 2019-01-21 12:15:42 +01:00
parent 4f516faa82
commit a214b5748e
10 changed files with 125 additions and 138 deletions

View File

@ -1,53 +1,81 @@
import React, { Component, createRef } from 'react';
import PopperController from '../Tooltip/PopperController';
import Popper from '../Tooltip/Popper';
import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable, GrafanaTheme } from '../../types';
import { getColorFromHexRgbOrName } from '../../utils/colorsPalette';
export interface ColorPickerProps extends Themeable {
color: string;
onChange: (color: string) => void;
withArrow?: boolean;
children?: JSX.Element;
}
export class ColorPicker extends Component<ColorPickerProps & Themeable, any> {
private pickerTriggerRef = createRef<HTMLDivElement>();
export const colorPickerFactory = <T extends ColorPickerProps>(
popover: React.ComponentType<T>,
displayName?: string,
renderPopoverArrowFunction?: RenderPopperArrowFn
) => {
return class ColorPicker extends Component<T, any> {
static displayName = displayName || 'ColorPicker';
pickerTriggerRef = createRef<HTMLDivElement>();
render() {
const { theme } = this.props;
return (
<PopperController placement="bottom-start" content={<ColorPickerPopover {...this.props} />}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.pickerTriggerRef.current && (
<Popper
{...popperProps}
referenceElement={this.pickerTriggerRef.current}
className="ColorPicker"
renderArrow={({ arrowProps, placement }) => {
return (
render() {
const popoverElement = React.createElement(popover, this.props);
const { theme, withArrow, children } = this.props;
const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => {
return (
<div
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${theme === GrafanaTheme.Light ? 'light' : 'dark'}`}
/>
);
};
return (
<PopperController content={popoverElement} placement="bottom-start">
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.pickerTriggerRef.current && (
<Popper
{...popperProps}
referenceElement={this.pickerTriggerRef.current}
wrapperClassName="ColorPicker"
renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
/>
)}
{children ? (
React.cloneElement(children as JSX.Element, {
ref: this.pickerTriggerRef,
onClick: showPopper,
onMouseLeave: hidePopper,
})
) : (
<div ref={this.pickerTriggerRef} onClick={showPopper} className="sp-replacer sp-light">
<div className="sp-preview">
<div
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${
theme === GrafanaTheme.Light ? 'light' : 'dark'
}`}
className="sp-preview-inner"
style={{
backgroundColor: getColorFromHexRgbOrName(this.props.color, theme),
}}
/>
);
}}
/>
)}
<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>
);
}
}
</div>
</div>
)}
</>
);
}}
</PopperController>
);
}
};
};
export default ColorPicker;
export default colorPickerFactory(ColorPickerPopover, 'ColorPicker');

View File

@ -4,10 +4,11 @@ import { getColorName } from '../..//utils/colorsPalette';
import { SpectrumPalette } from './SpectrumPalette';
import { ColorPickerProps } from './ColorPicker';
import { GrafanaTheme, Themeable } from '../../types';
import { PopperContentProps } from '../Tooltip/PopperController';
// const DEFAULT_COLOR = '#000000';
export interface Props extends ColorPickerProps, Themeable {}
export interface Props extends ColorPickerProps, Themeable, PopperContentProps {}
type PickerType = 'palette' | 'spectrum';
@ -40,7 +41,7 @@ export class ColorPickerPopover extends React.Component<Props, State> {
render() {
const { activePicker } = this.state;
const { theme, children } = this.props;
const { theme, children, updatePopperPosition } = this.props;
const colorPickerTheme = theme || GrafanaTheme.Dark;
return (
@ -49,7 +50,11 @@ export class ColorPickerPopover extends React.Component<Props, State> {
<div
className={`ColorPickerPopover__tab ${activePicker === 'palette' && 'ColorPickerPopover__tab--active'}`}
onClick={() => {
this.setState({ activePicker: 'palette' });
this.setState({ activePicker: 'palette' }, () => {
if (updatePopperPosition) {
updatePopperPosition();
}
});
}}
>
Default
@ -57,7 +62,11 @@ export class ColorPickerPopover extends React.Component<Props, State> {
<div
className={`ColorPickerPopover__tab ${activePicker === 'spectrum' && 'ColorPickerPopover__tab--active'}`}
onClick={() => {
this.setState({ activePicker: 'spectrum' });
this.setState({ activePicker: 'spectrum' }, () => {
if (updatePopperPosition) {
updatePopperPosition();
}
});
}}
>
Custom

View File

@ -1,78 +1,11 @@
import React, { createRef } from 'react';
import * as PopperJS from 'popper.js';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import PopperController from '../Tooltip/PopperController';
import Popper from '../Tooltip/Popper';
import { Themeable, GrafanaTheme } from '../../types';
import { ColorPickerProps } from './ColorPicker';
import { ColorPickerProps, colorPickerFactory } from './ColorPicker';
export interface SeriesColorPickerProps extends ColorPickerProps, Themeable {
export interface SeriesColorPickerProps extends ColorPickerProps {
yaxis?: number;
optionalClass?: string;
onToggleAxis?: () => void;
children: JSX.Element;
}
export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
private pickerTriggerRef = createRef<PopperJS.ReferenceObject>();
colorPickerDrop: any;
static defaultProps = {
optionalClass: '',
yaxis: undefined,
onToggleAxis: () => {},
};
renderPickerTabs = () => {
const { color, yaxis, onChange, onToggleAxis, theme } = this.props;
return (
<SeriesColorPickerPopover
theme={theme}
color={color}
yaxis={yaxis}
onChange={onChange}
onToggleAxis={onToggleAxis}
/>
);
};
render() {
const { children, theme } = this.props;
return (
<PopperController placement="bottom-start" content={this.renderPickerTabs()}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.pickerTriggerRef.current && (
<Popper
{...popperProps}
onMouseEnter={showPopper}
onMouseLeave={hidePopper}
referenceElement={this.pickerTriggerRef.current}
wrapperClassName="ColorPicker"
renderArrow={({ arrowProps, placement }) => {
return (
<div
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${
theme === GrafanaTheme.Light ? 'light' : 'dark'
}`}
/>
);
}}
/>
)}
{React.cloneElement(children, {
ref: this.pickerTriggerRef,
onClick: showPopper,
onMouseLeave: hidePopper,
})}
</>
);
}}
</PopperController>
);
}
}
export default colorPickerFactory(SeriesColorPickerPopover ,'SeriesColorPicker')

View File

@ -2,8 +2,9 @@ import React, { FunctionComponent } from 'react';
import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable } from '../../types';
import { ColorPickerProps } from './ColorPicker';
import { PopperContentProps } from '../Tooltip/PopperController';
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, Themeable {
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, Themeable, PopperContentProps {
yaxis?: number;
onToggleAxis?: () => void;
}
@ -14,9 +15,10 @@ export const SeriesColorPickerPopover: FunctionComponent<SeriesColorPickerPopove
theme,
yaxis,
onToggleAxis,
updatePopperPosition
}) => {
return (
<ColorPickerPopover theme={theme} color={color} onChange={onChange}>
<ColorPickerPopover theme={theme} color={color} onChange={onChange} updatePopperPosition={updatePopperPosition}>
<div style={{ marginTop: '32px' }}>{yaxis && <AxisSelector yaxis={yaxis} onToggleAxis={onToggleAxis} />}</div>
</ColorPickerPopover>
);

View File

@ -1,4 +1,4 @@
$arrowSize: 10px;
$arrowSize: 15px;
.ColorPicker {
@extend .popper;
}
@ -16,7 +16,7 @@ $arrowSize: 10px;
border-right-color: transparent;
border-bottom-color: transparent;
bottom: -$arrowSize;
left: calc(50% - $arrowSize);
left: calc(50%-#{$arrowSize});
padding-top: $arrowSize;
}
@ -26,7 +26,7 @@ $arrowSize: 10px;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(50% - $arrowSize);
left: calc(50%-#{$arrowSize});
}
&[data-placement^='bottom-start'] {
@ -44,7 +44,7 @@ $arrowSize: 10px;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(100% - $arrowSize);
left: calc(100% -$arrowSize);
}
&[data-placement^='right'] {
@ -53,7 +53,7 @@ $arrowSize: 10px;
border-top-color: transparent;
border-bottom-color: transparent;
left: 0;
top: calc(50% - $arrowSize);
top: calc(50%-#{$arrowSize});
}
&[data-placement^='left'] {
@ -62,7 +62,7 @@ $arrowSize: 10px;
border-right-color: transparent;
border-bottom-color: transparent;
right: -$arrowSize;
top: calc(50% - $arrowSize);
top: calc(50%-#{$arrowSize});
}
}
@ -148,11 +148,13 @@ $arrowSize: 10px;
.ColorPickerPopover__tab--active {
background: white;
}
.sp-replacer {
background: inherit;
border: none;
color: inherit;
padding: 0;
border-radius: 10px;
}
.sp-replacer:hover,

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
// import tinycolor, { ColorInput } from 'tinycolor2';
import { Threshold } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import ColorPicker from '../ColorPicker/ColorPicker';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
import { colors } from '../../utils';

View File

@ -17,18 +17,20 @@ const transitionStyles: { [key: string]: object } = {
exiting: { opacity: 0 },
};
export type RenderPopperArrowFn = (
props: {
arrowProps: PopperArrowProps;
placement: string;
}
) => JSX.Element;
interface Props extends React.HTMLAttributes<HTMLDivElement> {
show: boolean;
placement?: PopperJS.Placement;
content: PopperContent;
content: PopperContent<any>;
referenceElement: PopperJS.ReferenceObject;
wrapperClassName?: string;
renderArrow?: (
props: {
arrowProps: PopperArrowProps;
placement: string;
}
) => JSX.Element;
renderArrow?: RenderPopperArrowFn;
}
class Popper extends PureComponent<Props> {
@ -47,7 +49,7 @@ class Popper extends PureComponent<Props> {
// TODO: move modifiers config to popper controller
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
>
{({ ref, style, placement, arrowProps }) => {
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
return (
<div
onMouseEnter={onMouseEnter}
@ -62,7 +64,11 @@ class Popper extends PureComponent<Props> {
className={`${wrapperClassName}`}
>
<div className={className}>
{content}
{typeof content === 'string'
? content
: React.cloneElement(content, {
updatePopperPosition: scheduleUpdate,
})}
{renderArrow &&
renderArrow({
arrowProps,

View File

@ -1,12 +1,16 @@
import React from 'react';
import * as PopperJS from 'popper.js';
export type PopperContent = string | JSX.Element;
// This API allows popovers to update Popper's position when e.g. popover content chaanges
// updatePopperPosition is delivered to content by react-popper
export interface PopperContentProps { updatePopperPosition?: () => void; }
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T>;
export interface UsingPopperProps {
show?: boolean;
placement?: PopperJS.Placement;
content: PopperContent;
content: PopperContent<any>;
children: JSX.Element;
}
@ -16,13 +20,13 @@ type PopperControllerRenderProp = (
popperProps: {
show: boolean;
placement: PopperJS.Placement;
content: PopperContent;
content: PopperContent<any>;
}
) => JSX.Element;
interface Props {
placement?: PopperJS.Placement;
content: PopperContent;
content: PopperContent<any>;
className?: string;
children: PopperControllerRenderProp;
}

View File

@ -125,7 +125,11 @@ const isHex = (color: string) => {
return hexRegex.test(color);
};
export const getColorName = (color: string): Color | undefined => {
export const getColorName = (color?: string): Color | undefined => {
if (!color) {
return undefined;
}
if (color.indexOf('rgb') > -1) {
return undefined;
}

View File

@ -172,14 +172,13 @@ class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeries
{theme => {
return (
<SeriesColorPicker
optionalClass="graph-legend-icon"
yaxis={this.props.yaxis}
color={this.props.color}
onChange={this.props.onColorChange}
onToggleAxis={this.props.onToggleAxis}
theme={theme}
>
<span>
<span className="graph-legend-icon">
<SeriesIcon color={this.props.color} />
</span>
</SeriesColorPicker>