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,21 +1,42 @@
import React, { Component, createRef } from 'react'; import React, { Component, createRef } from 'react';
import PopperController from '../Tooltip/PopperController'; import PopperController from '../Tooltip/PopperController';
import Popper from '../Tooltip/Popper'; import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover'; import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable, GrafanaTheme } from '../../types'; import { Themeable, GrafanaTheme } from '../../types';
import { getColorFromHexRgbOrName } from '../../utils/colorsPalette';
export interface ColorPickerProps extends Themeable { export interface ColorPickerProps extends Themeable {
color: string; color: string;
onChange: (color: string) => void; onChange: (color: string) => void;
withArrow?: boolean;
children?: JSX.Element;
} }
export class ColorPicker extends Component<ColorPickerProps & Themeable, any> { export const colorPickerFactory = <T extends ColorPickerProps>(
private pickerTriggerRef = createRef<HTMLDivElement>(); popover: React.ComponentType<T>,
displayName?: string,
renderPopoverArrowFunction?: RenderPopperArrowFn
) => {
return class ColorPicker extends Component<T, any> {
static displayName = displayName || 'ColorPicker';
pickerTriggerRef = createRef<HTMLDivElement>();
render() { render() {
const { theme } = this.props; const popoverElement = React.createElement(popover, this.props);
const { theme, withArrow, children } = this.props;
const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => {
return ( return (
<PopperController placement="bottom-start" content={<ColorPickerPopover {...this.props} />}> <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) => { {(showPopper, hidePopper, popperProps) => {
return ( return (
<> <>
@ -23,31 +44,38 @@ export class ColorPicker extends Component<ColorPickerProps & Themeable, any> {
<Popper <Popper
{...popperProps} {...popperProps}
referenceElement={this.pickerTriggerRef.current} referenceElement={this.pickerTriggerRef.current}
className="ColorPicker" wrapperClassName="ColorPicker"
renderArrow={({ arrowProps, placement }) => { renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
return ( onMouseLeave={hidePopper}
<div onMouseEnter={showPopper}
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${
theme === GrafanaTheme.Light ? 'light' : 'dark'
}`}
/>
);
}}
/> />
)} )}
{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 ref={this.pickerTriggerRef} onClick={showPopper} className="sp-replacer sp-light">
<div className="sp-preview"> <div className="sp-preview">
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} /> <div
className="sp-preview-inner"
style={{
backgroundColor: getColorFromHexRgbOrName(this.props.color, theme),
}}
/>
</div> </div>
</div> </div>
)}
</> </>
); );
}} }}
</PopperController> </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 { SpectrumPalette } from './SpectrumPalette';
import { ColorPickerProps } from './ColorPicker'; import { ColorPickerProps } from './ColorPicker';
import { GrafanaTheme, Themeable } from '../../types'; import { GrafanaTheme, Themeable } from '../../types';
import { PopperContentProps } from '../Tooltip/PopperController';
// const DEFAULT_COLOR = '#000000'; // const DEFAULT_COLOR = '#000000';
export interface Props extends ColorPickerProps, Themeable {} export interface Props extends ColorPickerProps, Themeable, PopperContentProps {}
type PickerType = 'palette' | 'spectrum'; type PickerType = 'palette' | 'spectrum';
@ -40,7 +41,7 @@ export class ColorPickerPopover extends React.Component<Props, State> {
render() { render() {
const { activePicker } = this.state; const { activePicker } = this.state;
const { theme, children } = this.props; const { theme, children, updatePopperPosition } = this.props;
const colorPickerTheme = theme || GrafanaTheme.Dark; const colorPickerTheme = theme || GrafanaTheme.Dark;
return ( return (
@ -49,7 +50,11 @@ export class ColorPickerPopover extends React.Component<Props, State> {
<div <div
className={`ColorPickerPopover__tab ${activePicker === 'palette' && 'ColorPickerPopover__tab--active'}`} className={`ColorPickerPopover__tab ${activePicker === 'palette' && 'ColorPickerPopover__tab--active'}`}
onClick={() => { onClick={() => {
this.setState({ activePicker: 'palette' }); this.setState({ activePicker: 'palette' }, () => {
if (updatePopperPosition) {
updatePopperPosition();
}
});
}} }}
> >
Default Default
@ -57,7 +62,11 @@ export class ColorPickerPopover extends React.Component<Props, State> {
<div <div
className={`ColorPickerPopover__tab ${activePicker === 'spectrum' && 'ColorPickerPopover__tab--active'}`} className={`ColorPickerPopover__tab ${activePicker === 'spectrum' && 'ColorPickerPopover__tab--active'}`}
onClick={() => { onClick={() => {
this.setState({ activePicker: 'spectrum' }); this.setState({ activePicker: 'spectrum' }, () => {
if (updatePopperPosition) {
updatePopperPosition();
}
});
}} }}
> >
Custom Custom

View File

@ -1,78 +1,11 @@
import React, { createRef } from 'react';
import * as PopperJS from 'popper.js';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import PopperController from '../Tooltip/PopperController'; import { ColorPickerProps, colorPickerFactory } from './ColorPicker';
import Popper from '../Tooltip/Popper';
import { Themeable, GrafanaTheme } from '../../types';
import { ColorPickerProps } from './ColorPicker';
export interface SeriesColorPickerProps extends ColorPickerProps, Themeable { export interface SeriesColorPickerProps extends ColorPickerProps {
yaxis?: number; yaxis?: number;
optionalClass?: string; optionalClass?: string;
onToggleAxis?: () => void; onToggleAxis?: () => void;
children: JSX.Element; children: JSX.Element;
} }
export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> { export default colorPickerFactory(SeriesColorPickerPopover ,'SeriesColorPicker')
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>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,16 @@
import React from 'react'; import React from 'react';
import * as PopperJS from 'popper.js'; 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 { export interface UsingPopperProps {
show?: boolean; show?: boolean;
placement?: PopperJS.Placement; placement?: PopperJS.Placement;
content: PopperContent; content: PopperContent<any>;
children: JSX.Element; children: JSX.Element;
} }
@ -16,13 +20,13 @@ type PopperControllerRenderProp = (
popperProps: { popperProps: {
show: boolean; show: boolean;
placement: PopperJS.Placement; placement: PopperJS.Placement;
content: PopperContent; content: PopperContent<any>;
} }
) => JSX.Element; ) => JSX.Element;
interface Props { interface Props {
placement?: PopperJS.Placement; placement?: PopperJS.Placement;
content: PopperContent; content: PopperContent<any>;
className?: string; className?: string;
children: PopperControllerRenderProp; children: PopperControllerRenderProp;
} }

View File

@ -125,7 +125,11 @@ const isHex = (color: string) => {
return hexRegex.test(color); 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) { if (color.indexOf('rgb') > -1) {
return undefined; return undefined;
} }

View File

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