Skip to content

Commit

Permalink
Color picker component (#4136)
Browse files Browse the repository at this point in the history
  • Loading branch information
kravets-levko authored Sep 16, 2019
1 parent b9a40d1 commit c5e414e
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 29 deletions.
19 changes: 4 additions & 15 deletions client/app/components/ColorBox.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
// ANGULAR_REMOVE_ME
import { react2angular } from 'react2angular';

import './color-box.less';

export function ColorBox({ color }) {
return <span style={{ backgroundColor: color }} />;
}
import ColorPicker from '@/components/ColorPicker';

ColorBox.propTypes = {
color: PropTypes.string,
};

ColorBox.defaultProps = {
color: 'transparent',
};
import './color-box.less';

export default function init(ngModule) {
ngModule.component('colorBox', react2angular(ColorBox));
ngModule.component('colorBox', react2angular(ColorPicker.Swatch));
}

init.init = true;
93 changes: 93 additions & 0 deletions client/app/components/ColorPicker/Input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { isNil, isArray, chunk, map, filter, toPairs } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import tinycolor from 'tinycolor2';
import TextInput from 'antd/lib/input';
import Typography from 'antd/lib/typography';
import Swatch from './Swatch';

import './input.less';

function preparePresets(presetColors, presetColumns) {
presetColors = isArray(presetColors) ? map(presetColors, v => [null, v]) : toPairs(presetColors);
presetColors = map(presetColors, ([title, value]) => {
if (isNil(value)) {
return [title, null];
}
value = tinycolor(value);
if (value.isValid()) {
return [title, '#' + value.toHex().toUpperCase()];
}
return null;
});
return chunk(filter(presetColors), presetColumns);
}

function validateColor(value, callback, prefix = '#') {
if (isNil(value)) {
callback(null);
}
value = tinycolor(value);
if (value.isValid()) {
callback(prefix + value.toHex().toUpperCase());
}
}

export default function Input({ color, presetColors, presetColumns, onChange, onPressEnter }) {
const [inputValue, setInputValue] = useState('');
const [isInputFocused, setIsInputFocused] = useState(false);

const presets = preparePresets(presetColors, presetColumns);

function handleInputChange(value) {
setInputValue(value);
validateColor(value, onChange);
}

useEffect(() => {
if (!isInputFocused) {
validateColor(color, setInputValue, '');
}
}, [color, isInputFocused]);

return (
<React.Fragment>
{map(presets, (group, index) => (
<div className="color-picker-input-swatches" key={`preset-row-${index}`}>
{map(group, ([title, value]) => (
<Swatch key={value} color={value} title={title} size={30} onClick={() => validateColor(value, onChange)} />
))}
</div>
))}
<div className="color-picker-input">
<TextInput
addonBefore={<Typography.Text type="secondary">#</Typography.Text>}
value={inputValue}
onChange={e => handleInputChange(e.target.value)}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onPressEnter={onPressEnter}
/>
</div>
</React.Fragment>
);
}

Input.propTypes = {
color: PropTypes.string,
presetColors: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
PropTypes.objectOf(PropTypes.string), // color name => color value
]),
presetColumns: PropTypes.number,
onChange: PropTypes.func,
onPressEnter: PropTypes.func,
};

Input.defaultProps = {
color: '#FFFFFF',
presetColors: null,
presetColumns: 8,
onChange: () => {},
onPressEnter: () => {},
};
37 changes: 37 additions & 0 deletions client/app/components/ColorPicker/Swatch.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Tooltip from 'antd/lib/tooltip';

import './swatch.less';

export default function Swatch({ className, color, title, size, ...props }) {
const result = (
<span
className={`color-swatch ${className}`}
style={{ backgroundColor: color, width: size }}
{...props}
/>
);

if (isString(title) && (title !== '')) {
return (
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>{result}</Tooltip>
);
}
return result;
}

Swatch.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
color: PropTypes.string,
size: PropTypes.number,
};

Swatch.defaultProps = {
className: '',
title: null,
color: 'transparent',
size: 12,
};
128 changes: 128 additions & 0 deletions client/app/components/ColorPicker/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { toString } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import tinycolor from 'tinycolor2';
import Popover from 'antd/lib/popover';
import Card from 'antd/lib/card';
import Tooltip from 'antd/lib/tooltip';
import Icon from 'antd/lib/icon';

import ColorInput from './Input';
import Swatch from './Swatch';

import './index.less';

function validateColor(value, fallback = null) {
value = tinycolor(value);
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
}

export default function ColorPicker({
color, placement, presetColors, presetColumns, triggerSize, interactive, children, onChange,
}) {
const [visible, setVisible] = useState(false);
const [currentColor, setCurrentColor] = useState('');

function handleApply() {
setVisible(false);
if (!interactive) {
onChange(currentColor);
}
}

function handleCancel() {
setVisible(false);
}

const actions = [];
if (!interactive) {
actions.push((
<Tooltip key="cancel" title="Cancel">
<Icon type="close" onClick={handleCancel} />
</Tooltip>
));
actions.push((
<Tooltip key="apply" title="Apply">
<Icon type="check" onClick={handleApply} />
</Tooltip>
));
}

function handleInputChange(newColor) {
setCurrentColor(newColor);
if (interactive) {
onChange(newColor);
}
}

useEffect(() => {
if (visible) {
setCurrentColor(validateColor(color));
}
}, [color, visible]);

return (
<Popover
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
overlayStyle={{ '--color-picker-selected-color': currentColor }}
content={(
<Card
className="color-picker-panel"
bordered={false}
title={toString(currentColor).toUpperCase()}
headStyle={{
backgroundColor: currentColor,
color: tinycolor(currentColor).isLight() ? '#000000' : '#ffffff',
}}
actions={actions}
>
<ColorInput
color={currentColor}
presetColors={presetColors}
presetColumns={presetColumns}
onChange={handleInputChange}
onPressEnter={handleApply}
/>
</Card>
)}
trigger="click"
placement={placement}
visible={visible}
onVisibleChange={setVisible}
>
{children || (<Swatch className="color-picker-trigger" color={validateColor(color)} size={triggerSize} />)}
</Popover>
);
}

ColorPicker.propTypes = {
color: PropTypes.string,
placement: PropTypes.oneOf([
'top', 'left', 'right', 'bottom',
'topLeft', 'topRight', 'bottomLeft', 'bottomRight',
'leftTop', 'leftBottom', 'rightTop', 'rightBottom',
]),
presetColors: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
PropTypes.objectOf(PropTypes.string), // color name => color value
]),
presetColumns: PropTypes.number,
triggerSize: PropTypes.number,
interactive: PropTypes.bool,
children: PropTypes.node,
onChange: PropTypes.func,
};

ColorPicker.defaultProps = {
color: '#FFFFFF',
placement: 'top',
presetColors: null,
presetColumns: 8,
triggerSize: 30,
interactive: false,
children: null,
onChange: () => {},
};

ColorPicker.Input = ColorInput;
ColorPicker.Swatch = Swatch;
40 changes: 40 additions & 0 deletions client/app/components/ColorPicker/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.color-picker {
&.color-picker-with-actions {
&.ant-popover-placement-top,
&.ant-popover-placement-topLeft,
&.ant-popover-placement-topRight,
&.ant-popover-placement-leftBottom,
&.ant-popover-placement-rightBottom {
> .ant-popover-content > .ant-popover-arrow {
border-color: #fafafa; // same as card actions
}
}
}

&.ant-popover-placement-bottom,
&.ant-popover-placement-bottomLeft,
&.ant-popover-placement-bottomRight,
&.ant-popover-placement-leftTop,
&.ant-popover-placement-rightTop {
> .ant-popover-content > .ant-popover-arrow {
border-color: var(--color-picker-selected-color);
}
}

.ant-popover-inner-content {
padding: 0;
}

.ant-card-head {
text-align: center;
border-bottom-color: rgba(0, 0, 0, 0.1);
}

.ant-card-body {
padding: 10px;
}
}

.color-picker-trigger {
cursor: pointer;
}
19 changes: 19 additions & 0 deletions client/app/components/ColorPicker/input.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.color-picker-input-swatches {
margin: 0 0 10px 0;
text-align: left;
white-space: nowrap;

.color-swatch {
cursor: pointer;
margin: 0 10px 0 0;

&:last-child {
margin-right: 0;
}
}
}

.color-picker-input {
text-align: left;
white-space: nowrap;
}
30 changes: 30 additions & 0 deletions client/app/components/ColorPicker/swatch.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.color-swatch {
display: inline-block;
box-sizing: border-box;
vertical-align: middle;
border-radius: 2px;
overflow: hidden;
width: 12px;

@cell-size: 12px;
@cell-color: rgba(0, 0, 0, 0.1);

background-color: transparent;
background-image:
linear-gradient(45deg, @cell-color 25%, transparent 25%),
linear-gradient(-45deg, @cell-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, @cell-color 75%),
linear-gradient(-45deg, transparent 75%, @cell-color 75%);
background-size: @cell-size @cell-size;
background-position: 0 0, 0 @cell-size/2, @cell-size/2 -@cell-size/2, -@cell-size/2 0px;

&:before {
content: "";
display: block;
padding-top: ~"calc(100% - 2px)";
background-color: inherit;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 2px;
overflow: hidden;
}
}
Loading

0 comments on commit c5e414e

Please sign in to comment.