Merge pull request #15926 from grafana/create-color-picker-trigger-component

Create default ColorPickerTrigger component
This commit is contained in:
Andrej Ocenas 2019-03-13 12:47:52 +01:00 committed by GitHub
commit f7a8df6ebd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 124 additions and 78 deletions

View File

@ -50,7 +50,16 @@ ColorPickerStories.add('Series color picker', () => {
color={selectedColor} color={selectedColor}
onChange={color => updateSelectedColor(color)} onChange={color => updateSelectedColor(color)}
> >
<div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div> {({ ref, showColorPicker, hideColorPicker }) => (
<div
ref={ref}
onMouseLeave={hideColorPicker}
onClick={showColorPicker}
style={{ color: selectedColor, cursor: 'pointer' }}
>
Open color picker
</div>
)}
</SeriesColorPicker> </SeriesColorPicker>
); );
}} }}

View File

@ -0,0 +1,23 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { ColorPicker } from './ColorPicker';
import { ColorPickerTrigger } from './ColorPickerTrigger';
describe('ColorPicker', () => {
it('renders ColorPickerTrigger component by default', () => {
expect(
renderer.create(<ColorPicker color="#EAB839" onChange={() => {}} />).root.findByType(ColorPickerTrigger)
).toBeTruthy();
});
it('renders custom trigger when supplied', () => {
const div = renderer
.create(
<ColorPicker color="#EAB839" onChange={() => {}}>
{() => <div>Custom trigger</div>}
</ColorPicker>
)
.root.findByType('div');
expect(div.children[0]).toBe('Custom trigger');
});
});

View File

@ -1,4 +1,5 @@
import React, { Component, createRef } from 'react'; import React, { Component, createRef } from 'react';
import { omit } from 'lodash';
import { PopperController } from '../Tooltip/PopperController'; import { PopperController } from '../Tooltip/PopperController';
import { Popper } from '../Tooltip/Popper'; import { Popper } from '../Tooltip/Popper';
import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover'; import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover';
@ -6,14 +7,29 @@ import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import { withTheme } from '../../themes/ThemeContext'; import { withTheme } from '../../themes/ThemeContext';
import { ColorPickerTrigger } from './ColorPickerTrigger';
/**
* If you need custom trigger for the color picker you can do that with a render prop pattern and supply a function
* as a child. You will get show/hide function which you can map to desired interaction (like onClick or onMouseLeave)
* and a ref which needs to be passed to an HTMLElement for correct positioning. If you want to use class or functional
* component as a custom trigger you will need to forward the reference to first HTMLElement child.
*/
type ColorPickerTriggerRenderer = (props: {
// This should be a React.RefObject<HTMLElement> but due to how object refs are defined you cannot downcast from that
// to a specific type like React.RefObject<HTMLDivElement> even though it would be fine in runtime.
ref: React.RefObject<any>;
showColorPicker: () => void;
hideColorPicker: () => void;
}) => React.ReactNode;
export const colorPickerFactory = <T extends ColorPickerProps>( export const colorPickerFactory = <T extends ColorPickerProps>(
popover: React.ComponentType<T>, popover: React.ComponentType<T>,
displayName = 'ColorPicker' displayName = 'ColorPicker'
) => { ) => {
return class ColorPicker extends Component<T, any> { return class ColorPicker extends Component<T & { children?: ColorPickerTriggerRenderer }, any> {
static displayName = displayName; static displayName = displayName;
pickerTriggerRef = createRef<HTMLDivElement>(); pickerTriggerRef = createRef<any>();
onColorChange = (color: string) => { onColorChange = (color: string) => {
const { onColorChange, onChange } = this.props; const { onColorChange, onChange } = this.props;
@ -23,11 +39,11 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
}; };
render() { render() {
const { theme, children } = this.props;
const popoverElement = React.createElement(popover, { const popoverElement = React.createElement(popover, {
...this.props, ...omit(this.props, 'children'),
onChange: this.onColorChange, onChange: this.onColorChange,
}); });
const { theme, children } = this.props;
return ( return (
<PopperController content={popoverElement} hideAfter={300}> <PopperController content={popoverElement} hideAfter={300}>
@ -45,27 +61,21 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
)} )}
{children ? ( {children ? (
React.cloneElement(children as JSX.Element, { // Children have a bit weird type due to intersection used in the definition so we need to cast here,
// but the definition is correct and should not allow to pass a children that does not conform to
// ColorPickerTriggerRenderer type.
(children as ColorPickerTriggerRenderer)({
ref: this.pickerTriggerRef, ref: this.pickerTriggerRef,
onClick: showPopper, showColorPicker: showPopper,
onMouseLeave: hidePopper, hideColorPicker: hidePopper,
}) })
) : ( ) : (
<div <ColorPickerTrigger
ref={this.pickerTriggerRef} ref={this.pickerTriggerRef}
onClick={showPopper} onClick={showPopper}
onMouseLeave={hidePopper} onMouseLeave={hidePopper}
className="sp-replacer sp-light" color={getColorFromHexRgbOrName(this.props.color || '#000000', theme.type)}
> />
<div className="sp-preview">
<div
className="sp-preview-inner"
style={{
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme.type),
}}
/>
</div>
</div>
)} )}
</> </>
); );

View File

@ -17,8 +17,8 @@ export interface ColorPickerProps extends Themeable {
*/ */
onColorChange?: ColorPickerChangeHandler; onColorChange?: ColorPickerChangeHandler;
enableNamedColors?: boolean; enableNamedColors?: boolean;
children?: JSX.Element;
} }
export interface Props<T> extends ColorPickerProps, PopperContentProps { export interface Props<T> extends ColorPickerProps, PopperContentProps {
customPickers?: T; customPickers?: T;
} }

View File

@ -0,0 +1,56 @@
import React, { forwardRef } from 'react';
interface ColorPickerTriggerProps {
onClick: () => void;
onMouseLeave: () => void;
color: string;
}
export const ColorPickerTrigger = forwardRef(function ColorPickerTrigger(
props: ColorPickerTriggerProps,
ref: React.Ref<HTMLDivElement>
) {
return (
<div
ref={ref}
onClick={props.onClick}
onMouseLeave={props.onMouseLeave}
style={{
overflow: 'hidden',
background: 'inherit',
border: 'none',
color: 'inherit',
padding: 0,
borderRadius: 10,
cursor: 'pointer',
}}
>
<div
style={{
position: 'relative',
width: 15,
height: 15,
border: 'none',
margin: 0,
float: 'left',
zIndex: 0,
backgroundImage:
// tslint:disable-next-line:max-line-length
'url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)',
}}
>
<div
style={{
backgroundColor: props.color,
display: 'block',
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
}}
/>
</div>
</div>
);
});

View File

@ -161,59 +161,6 @@ $arrowSize: 15px;
flex-grow: 1; flex-grow: 1;
} }
.sp-replacer {
background: inherit;
border: none;
color: inherit;
padding: 0;
border-radius: 10px;
cursor: pointer;
}
.sp-replacer:hover,
.sp-replacer.sp-active {
border-color: inherit;
color: inherit;
}
.sp-container {
border-radius: 0;
background-color: $dropdownBackground;
border: none;
padding: 0;
}
.sp-palette-container,
.sp-picker-container {
border: none;
}
.sp-dd {
display: none;
}
.sp-preview {
position: relative;
width: 15px;
height: 15px;
border: none;
margin: 0;
float: left;
z-index: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
}
.sp-preview-inner,
.sp-alpha-inner,
.sp-thumb-inner {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.gf-color-picker__body { .gf-color-picker__body {
padding-bottom: $arrowSize; padding-bottom: $arrowSize;
padding-left: 6px; padding-left: 6px;

View File

@ -86,7 +86,6 @@
.thresholds-row-input-inner-color-colorpicker { .thresholds-row-input-inner-color-colorpicker {
border-radius: 10px; border-radius: 10px;
overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);

View File

@ -174,9 +174,11 @@ class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeries
onToggleAxis={this.props.onToggleAxis} onToggleAxis={this.props.onToggleAxis}
enableNamedColors enableNamedColors
> >
<span className="graph-legend-icon"> {({ ref, showColorPicker, hideColorPicker }) => (
<SeriesIcon color={this.props.color} /> <span ref={ref} onClick={showColorPicker} onMouseLeave={hideColorPicker} className="graph-legend-icon">
</span> <SeriesIcon color={this.props.color} />
</span>
)}
</SeriesColorPicker> </SeriesColorPicker>
); );
} }