mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #15926 from grafana/create-color-picker-trigger-component
Create default ColorPickerTrigger component
This commit is contained in:
commit
f7a8df6ebd
@ -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>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
});
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user