Browse Source

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

Dominik Prokop 7 years ago
parent
commit
a214b5748e

+ 67 - 39
packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

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

+ 13 - 4
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

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

+ 3 - 70
packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx

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

+ 4 - 2
packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx

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

+ 8 - 6
packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss

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

+ 1 - 1
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

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

+ 15 - 9
packages/grafana-ui/src/components/Tooltip/Popper.tsx

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

+ 8 - 4
packages/grafana-ui/src/components/Tooltip/PopperController.tsx

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

+ 5 - 1
packages/grafana-ui/src/utils/colorsPalette.ts

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

+ 1 - 2
public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx

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