From fcfd4a624a143077891614296828befdd0cc0f40 Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Thu, 13 Aug 2020 18:13:12 -0400 Subject: [PATCH 01/11] feat: implement ColorPicker component --- src/components/ColorPicker/alpha.js | 28 ++ src/components/ColorPicker/colors.js | 37 +++ .../ColorPicker/helpers/getAlpha.js | 3 + src/components/ColorPicker/helpers/index.js | 3 + .../ColorPicker/helpers/isAchromatic.js | 7 + .../ColorPicker/helpers/normalazeColor.js | 6 + src/components/ColorPicker/hexColor.js | 58 ++++ src/components/ColorPicker/hue.js | 35 +++ src/components/ColorPicker/index.d.ts | 9 + src/components/ColorPicker/index.js | 105 +++++++ src/components/ColorPicker/readme.md | 97 +++++++ src/components/ColorPicker/rgbaColor.js | 73 +++++ .../ColorPicker/saturation/index.js | 74 +++++ .../ColorPicker/saturation/styled.js | 39 +++ src/components/ColorPicker/slider.js | 31 ++ src/components/ColorPicker/styled.js | 265 ++++++++++++++++++ src/components/Input/index.d.ts | 2 +- src/components/Input/index.js | 2 +- src/components/Input/inputBase/index.js | 2 +- src/components/index.d.ts | 1 + src/components/index.js | 1 + .../helpers/color/__test__/isHexColor.spec.js | 17 ++ src/styles/helpers/color/bound01.js | 25 ++ src/styles/helpers/color/decomposeColor.js | 4 +- src/styles/helpers/color/hexToRgb.js | 32 +-- src/styles/helpers/color/hsvToRgb.js | 26 ++ src/styles/helpers/color/index.js | 3 + src/styles/helpers/color/isHexColor.js | 5 + src/styles/helpers/color/isOnePointZero.js | 3 + src/styles/helpers/color/isPercentage.js | 3 + src/styles/helpers/color/recomposeColor.js | 19 +- src/styles/helpers/color/rgbToHsv.js | 39 +++ src/styles/helpers/color/rgbaToHex.js | 21 ++ 33 files changed, 1042 insertions(+), 33 deletions(-) create mode 100644 src/components/ColorPicker/alpha.js create mode 100644 src/components/ColorPicker/colors.js create mode 100644 src/components/ColorPicker/helpers/getAlpha.js create mode 100644 src/components/ColorPicker/helpers/index.js create mode 100644 src/components/ColorPicker/helpers/isAchromatic.js create mode 100644 src/components/ColorPicker/helpers/normalazeColor.js create mode 100644 src/components/ColorPicker/hexColor.js create mode 100644 src/components/ColorPicker/hue.js create mode 100644 src/components/ColorPicker/index.d.ts create mode 100644 src/components/ColorPicker/index.js create mode 100644 src/components/ColorPicker/readme.md create mode 100644 src/components/ColorPicker/rgbaColor.js create mode 100644 src/components/ColorPicker/saturation/index.js create mode 100644 src/components/ColorPicker/saturation/styled.js create mode 100644 src/components/ColorPicker/slider.js create mode 100644 src/components/ColorPicker/styled.js create mode 100644 src/styles/helpers/color/__test__/isHexColor.spec.js create mode 100644 src/styles/helpers/color/bound01.js create mode 100644 src/styles/helpers/color/hsvToRgb.js create mode 100644 src/styles/helpers/color/isHexColor.js create mode 100644 src/styles/helpers/color/isOnePointZero.js create mode 100644 src/styles/helpers/color/isPercentage.js create mode 100644 src/styles/helpers/color/rgbToHsv.js create mode 100644 src/styles/helpers/color/rgbaToHex.js diff --git a/src/components/ColorPicker/alpha.js b/src/components/ColorPicker/alpha.js new file mode 100644 index 000000000..f1cc1d435 --- /dev/null +++ b/src/components/ColorPicker/alpha.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledAlphaSlider } from './styled'; +import { recomposeColor } from '../../styles/helpers/color'; +import { getAlpha } from './helpers'; + +export default function Alpha(props) { + const { rgbaColor, onChange } = props; + const a = getAlpha(rgbaColor); + + const handleChange = event => { + const alpha = parseInt(event.target.value, 10); + rgbaColor.values[3] = alpha / 100; + onChange(recomposeColor(rgbaColor)); + }; + + return ; +} + +Alpha.propTypes = { + rgbaColor: PropTypes.object, + onChange: PropTypes.func, +}; + +Alpha.defaultProps = { + rgbaColor: '', + onChange: () => {}, +}; diff --git a/src/components/ColorPicker/colors.js b/src/components/ColorPicker/colors.js new file mode 100644 index 000000000..cd96b6914 --- /dev/null +++ b/src/components/ColorPicker/colors.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledDefaulColor, StyledColorsContainer } from './styled'; +import { colorToRgba, isValidColor } from '../../styles/helpers/color'; + +const Colors = React.memo(props => { + const { colors, onChange } = props; + + const handleChange = color => { + const rgbaColor = colorToRgba(color); + if (isValidColor(rgbaColor)) { + onChange(rgbaColor); + } + }; + const ListColors = () => + colors.map(color => ( + handleChange(color)} /> + )); + + return ( + + + + ); +}); + +Colors.propTypes = { + colors: PropTypes.array, + onChange: PropTypes.func, +}; + +Colors.defaultProps = { + colors: [], + onChange: () => {}, +}; + +export default Colors; diff --git a/src/components/ColorPicker/helpers/getAlpha.js b/src/components/ColorPicker/helpers/getAlpha.js new file mode 100644 index 000000000..de91c8e1f --- /dev/null +++ b/src/components/ColorPicker/helpers/getAlpha.js @@ -0,0 +1,3 @@ +export default function getAlpha(rgbaColor) { + return Math.round(rgbaColor.values[3] * 100); +} diff --git a/src/components/ColorPicker/helpers/index.js b/src/components/ColorPicker/helpers/index.js new file mode 100644 index 000000000..a808a36d9 --- /dev/null +++ b/src/components/ColorPicker/helpers/index.js @@ -0,0 +1,3 @@ +export { default as getAlpha } from './getAlpha'; +export { default as normalazeColor } from './normalazeColor'; +export { default as isAchromatic } from './isAchromatic'; diff --git a/src/components/ColorPicker/helpers/isAchromatic.js b/src/components/ColorPicker/helpers/isAchromatic.js new file mode 100644 index 000000000..31481aa7a --- /dev/null +++ b/src/components/ColorPicker/helpers/isAchromatic.js @@ -0,0 +1,7 @@ +export default function isAchromatic(color) { + const [r, g, b] = color.values; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + return max === min; +} diff --git a/src/components/ColorPicker/helpers/normalazeColor.js b/src/components/ColorPicker/helpers/normalazeColor.js new file mode 100644 index 000000000..73ce3a157 --- /dev/null +++ b/src/components/ColorPicker/helpers/normalazeColor.js @@ -0,0 +1,6 @@ +import { colorToRgba } from '../../../styles/helpers/color'; + +export default function normalazeColor(color) { + const rgbaColor = colorToRgba(color); + return rgbaColor !== '' ? rgbaColor : 'rgba(0, 0, 0, 1)'; +} diff --git a/src/components/ColorPicker/hexColor.js b/src/components/ColorPicker/hexColor.js new file mode 100644 index 000000000..b5a791869 --- /dev/null +++ b/src/components/ColorPicker/hexColor.js @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import Input from '../Input'; +import { StyledHexColorIcon } from './styled'; +import { hexToRgba } from '../../styles/helpers/color'; + +export default function HexColor(props) { + const { hexColor, onChange } = props; + const [color, setColor] = useState(); + const [isFocused, setIsFocused] = useState(false); + + useEffect(() => { + if (!isFocused) { + setColor(hexColor); + } + }, [hexColor, isFocused]); + + const handleChange = event => { + const value = event.target.value; + setColor(value); + const hex = `#${value}`; + const rgbaColor = hexToRgba(hex); + if (rgbaColor !== '') { + onChange(rgbaColor); + } + }; + + const handleBlur = event => { + setIsFocused(false); + const value = event.target.value; + const hex = `#${value}`; + const rgbaColor = hexToRgba(hex); + if (rgbaColor !== '') { + onChange(rgbaColor); + } + }; + + return ( + setIsFocused(true)} + onBlur={handleBlur} + icon={#} + /> + ); +} + +HexColor.propTypes = { + hexColor: PropTypes.string, + onChange: PropTypes.func, +}; + +HexColor.defaultProps = { + hexColor: '', + onChange: () => {}, +}; diff --git a/src/components/ColorPicker/hue.js b/src/components/ColorPicker/hue.js new file mode 100644 index 000000000..b6132c5af --- /dev/null +++ b/src/components/ColorPicker/hue.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledHueSlider } from './styled'; +import { hsvToRgb, rgbToRgba } from '../../styles/helpers/color'; + +export default function Hue(props) { + const { hsvColor, hue, setHue, alpha, onChange } = props; + + const handleChange = event => { + const value = parseInt(event.target.value, 10); + setHue(value); + hsvColor.values[0] = value; + const rgbColor = hsvToRgb(hsvColor); + const rgbaColor = rgbToRgba(rgbColor, alpha); + onChange(rgbaColor); + }; + + return ; +} + +Hue.propTypes = { + hsvColor: PropTypes.object, + alpha: PropTypes.number, + hue: PropTypes.number, + setHue: PropTypes.func, + onChange: PropTypes.func, +}; + +Hue.defaultProps = { + hsvColor: undefined, + alpha: undefined, + hue: undefined, + setHue: () => {}, + onChange: () => {}, +}; diff --git a/src/components/ColorPicker/index.d.ts b/src/components/ColorPicker/index.d.ts new file mode 100644 index 000000000..e61a3eeb6 --- /dev/null +++ b/src/components/ColorPicker/index.d.ts @@ -0,0 +1,9 @@ +import { BaseProps } from '../types'; + +export interface ColorPickerProps extends BaseProps { + id?: string; + color?: string; + onChange?: (value: string) => void; +} + +export default function(props: ColorPickerProps): JSX.Element | null; diff --git a/src/components/ColorPicker/index.js b/src/components/ColorPicker/index.js new file mode 100644 index 000000000..084474c53 --- /dev/null +++ b/src/components/ColorPicker/index.js @@ -0,0 +1,105 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import RenderIf from '../RenderIf'; +import Saturation from './saturation'; +import RgbaColor from './rgbaColor'; +import HexColor from './hexColor'; +import Alpha from './alpha'; +import Hue from './hue'; +import Colors from './colors'; +import { + StyledContainer, + StyledPreview, + StyledFlexContainer, + StyledLabel, + StyledSaturationContainer, + StyledSlidersContainer, + StyledHexColorContainer, + StyledRgbaColorContainer, +} from './styled'; +import { rgbaToHex, rgbToHsv, decomposeColor } from '../../styles/helpers/color'; +import { normalazeColor, isAchromatic } from './helpers'; + +export default function ColorPicker(props) { + const { id, color: colorProp, colors, label, labelColors, onChange, className, style } = props; + const [hue, setHue] = useState(0); + + const color = normalazeColor(colorProp); + const rgbaColor = decomposeColor(color); + const hexColor = rgbaToHex(rgbaColor); + const hsvColor = decomposeColor(rgbToHsv(color)); + const alpha = rgbaColor.values[3]; + + useEffect(() => { + if (!isAchromatic(rgbaColor)) { + setHue(hsvColor.values[0]); + } + }, [hsvColor.values, rgbaColor]); + + const hasColors = Array.isArray(colors) && colors.length > 0; + + return ( + + {label} + + + + + + + + + + + + + + + + + + + + + {labelColors} + + + + ); +} + +ColorPicker.propTypes = { + /** The id of the outer element. */ + id: PropTypes.string, + /** Specifies the color of ColorPicker. */ + color: PropTypes.string, + /** Specifies the default colors to choice. */ + colors: PropTypes.array, + /** Text label for the input. */ + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** Text label for the default colors. */ + labelColors: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** The action triggered when the value changes. */ + onChange: PropTypes.func, + /** A CSS class for the outer element, in addition to the component's base classes. */ + className: PropTypes.string, + /** An object with custom style applied to the outer element. */ + style: PropTypes.object, +}; + +ColorPicker.defaultProps = { + id: undefined, + color: '', + colors: [], + label: 'Color Picker', + labelColors: 'Defualt', + onChange: () => {}, + className: undefined, + style: undefined, +}; diff --git a/src/components/ColorPicker/readme.md b/src/components/ColorPicker/readme.md new file mode 100644 index 000000000..4f941b89d --- /dev/null +++ b/src/components/ColorPicker/readme.md @@ -0,0 +1,97 @@ +##### ColorPicker info + +```js +import React, { useState } from 'react'; +import styled from 'styled-components' +import { ColorPicker, Card } from 'react-rainbow-components'; + +const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 50px; +`; + +const StyledCard = styled(Card)` + width: 26rem; + height: 27rem; +`; + +const ColorPickerExample = () => { + const [color, setColor] = useState('#ff0'); + const handleChange = value => { + // console.log(value); + setColor(value); + } + return ; +} + + + + + + +``` + +##### ColorPicker with default colors + +```js +import React, {useState, useCallback} from 'react'; +import styled from 'styled-components' +import { ColorPicker, Card } from 'react-rainbow-components'; + +const colors = [ + '#e3aaec', + '#c3dbf7', + '#9fd6ff', + '#9de7da', + '#9ef0bf', + '#fef199', + '#fdd499', + '#d174e0', + '#86baf3', + '#5ebbff', + '#42d8be', + '#3be282', + '#ffe654', + '#ffb758', + '#bd35bd', + '#5779c1', + '#4A90E2', + '#06aea9', + '#3dba4c', + '#f5bc24', + '#f99222', + '#570e8c', + '#021970', + '#0b2399', + '#0d7477', + '#0a6b50', + '#b67e12', + '#b75d0c', +]; + +const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 50px; +`; + +const StyledCard = styled(Card)` + width: 26rem; + height: 40rem; +`; + +const ColorPickerExample = () => { + const [color, setColor] = useState('#ff0'); + + return ; +} + + + + + + +``` \ No newline at end of file diff --git a/src/components/ColorPicker/rgbaColor.js b/src/components/ColorPicker/rgbaColor.js new file mode 100644 index 000000000..007166e21 --- /dev/null +++ b/src/components/ColorPicker/rgbaColor.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledFlexContainer, StyledNumberInput } from './styled'; +import Input from '../Input'; +import { getAlpha } from './helpers'; +import { recomposeColor } from '../../styles/helpers/color'; + +export default function RgbaColor(props) { + const { rgbaColor, onChange } = props; + const [r, g, b] = rgbaColor.values; + const a = getAlpha(rgbaColor); + + const handleAlphaChange = event => { + const alpha = parseInt(event.target.value, 10); + + if (isNaN(alpha)) { + rgbaColor.values[3] = 0; + } else { + rgbaColor.values[3] = Math.max(0, Math.min(alpha, 100)) / 100; + } + onChange(recomposeColor(rgbaColor)); + }; + + const handleChange = (index, event) => { + const value = parseInt(event.target.value, 10); + + if (isNaN(value)) { + rgbaColor.values[index] = 0; + } else { + rgbaColor.values[index] = Math.max(0, Math.min(value, 255)); + } + onChange(recomposeColor(rgbaColor)); + }; + + return ( + + handleChange(0, event)} + /> + handleChange(1, event)} + /> + handleChange(2, event)} + /> + + + ); +} + +RgbaColor.propTypes = { + rgbaColor: PropTypes.object, + onChange: PropTypes.func, +}; + +RgbaColor.defaultProps = { + rgbaColor: '', + onChange: () => {}, +}; diff --git a/src/components/ColorPicker/saturation/index.js b/src/components/ColorPicker/saturation/index.js new file mode 100644 index 000000000..9cbcbd520 --- /dev/null +++ b/src/components/ColorPicker/saturation/index.js @@ -0,0 +1,74 @@ +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { hsvToRgb, rgbToRgba } from '../../../styles/helpers/color'; +import { StyledColor, StyledWhite, StyledBlack, StyledPointer, StyledCircle } from './styled'; + +export default function Saturation(props) { + const { rgbaColor, hsvColor, onChange } = props; + const hslColor = `hsl(${hsvColor.values[0]},100%, 50%)`; + const alpha = rgbaColor.values[3]; + const containerRef = useRef(); + + const handleChange = event => { + const rect = containerRef.current.getBoundingClientRect(); + const { width, height } = rect; + const x = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX; + const y = typeof event.pageY === 'number' ? event.pageY : event.touches[0].pageY; + const left = Math.min(Math.max(0, x - (rect.left + window.pageXOffset)), width); + const top = Math.min(Math.max(0, y - (rect.top + window.pageYOffset)), height); + + const saturation = Math.round((left / width) * 100); + const bright = Math.round((1 - top / height) * 100); + + hsvColor.values[1] = saturation; + hsvColor.values[2] = bright; + const color = rgbToRgba(hsvToRgb(hsvColor), alpha); + onChange(color); + }; + + const unbindEventListeners = () => { + window.removeEventListener('mousemove', handleChange); + window.removeEventListener('mouseup', unbindEventListeners); + }; + + const handleMouseDown = event => { + handleChange(event); + window.addEventListener('mousemove', handleChange); + window.addEventListener('mouseup', unbindEventListeners); + }; + + useEffect(() => unbindEventListeners, []); + + const top = 100 - hsvColor.values[2]; + const left = hsvColor.values[1]; + + return ( + + + + + + + + + + ); +} + +Saturation.propTypes = { + rgbaColor: PropTypes.object, + hsvColor: PropTypes.object, + onChange: PropTypes.func, +}; + +Saturation.defaultProps = { + rgbaColor: undefined, + hsvColor: undefined, + onChange: () => {}, +}; diff --git a/src/components/ColorPicker/saturation/styled.js b/src/components/ColorPicker/saturation/styled.js new file mode 100644 index 000000000..7d999a9c2 --- /dev/null +++ b/src/components/ColorPicker/saturation/styled.js @@ -0,0 +1,39 @@ +import styled, { css } from 'styled-components'; + +const StyledAbsolute = css` + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; +`; + +export const StyledColor = styled.div` + ${StyledAbsolute} + background: ${props => props.hslColor}; +`; + +export const StyledWhite = styled.div` + ${StyledAbsolute} + background: linear-gradient(to right, rgb(255, 255, 255), rgba(255, 255, 255, 0)); +`; + +export const StyledBlack = styled.div` + ${StyledAbsolute} + background: linear-gradient(to top, rgb(0, 0, 0), rgba(0, 0, 0, 0)); +`; + +export const StyledPointer = styled.div` + position: absolute; + top: ${props => props.$top}%; + left: ${props => props.$left}%; + cursor: default; +`; + +export const StyledCircle = styled.div` + width: 12px; + height: 12px; + border-radius: 6px; + box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset; + transform: translate(-6px, -6px); +`; diff --git a/src/components/ColorPicker/slider.js b/src/components/ColorPicker/slider.js new file mode 100644 index 000000000..49f930441 --- /dev/null +++ b/src/components/ColorPicker/slider.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledSlider, StyledInputRange } from './styled'; + +export default function Slider(props) { + const { value, min, max, onChange, style, className } = props; + + return ( + + + + ); +} + +Slider.propTypes = { + value: PropTypes.number, + min: PropTypes.number, + max: PropTypes.number, + onChange: PropTypes.func, + className: PropTypes.string, + style: PropTypes.object, +}; + +Slider.defaultProps = { + value: undefined, + min: 0, + max: 100, + onChange: () => {}, + className: undefined, + style: undefined, +}; diff --git a/src/components/ColorPicker/styled.js b/src/components/ColorPicker/styled.js new file mode 100644 index 000000000..61c580252 --- /dev/null +++ b/src/components/ColorPicker/styled.js @@ -0,0 +1,265 @@ +import styled, { css } from 'styled-components'; +import { FONT_SIZE_TEXT_MEDIUM } from '../../../styles/fontSizes'; +import { PADDING_SMALL, PADDING_XX_SMALL } from '../../../styles/paddings'; +import { BORDER_RADIUS_3, BORDER_RADIUS_2 } from '../../../styles/borderRadius'; +import { COLOR_GRAY_1 } from '../../styles/colors'; +import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; +import Input from '../Input'; +import Slider from './slider'; + +const StyledCircleColor = css` + background-color: ${props => props.$color}; + border-radius: 50%; + border: 1px solid ${props => props.palette.border.divider}; +`; + +export const StyledPreview = attachThemeAttrs(styled.div)` + width: 50px; + height: 50px; + margin: 10px 0; + ${StyledCircleColor} +`; + +export const StyledDefaulColor = attachThemeAttrs(styled.div)` + width: 40px; + height: 40px; + margin: 0px 15px 15px 0px + flex: 0 0 auto; + ${StyledCircleColor} + + :hover { + cursor: pointer; + } +`; + +export const StyledFlexContainer = styled.div` + display: flex; + flex: 0 0 auto; +`; + +export const StyledContainer = styled.div` + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: 15px; +`; + +export const StyledSaturationContainer = attachThemeAttrs(styled.div)` + position: relative; + flex: 1 0 auto; + border-radius: 12px; + overflow: hidden; + border: 1px solid ${props => props.palette.border.divider}; +`; + +export const StyledSlidersContainer = styled.div` + flex: 1 0 auto; + margin-right: 15px; + margin-top: 5px; +`; + +export const StyledHexColorContainer = styled.div` + flex: 0 3 auto; +`; + +export const StyledRgbaColorContainer = styled.div` + flex: 0 4 auto; +`; + +export const StyledColorsContainer = styled.div` + display: flex; + flex-wrap: wrap; + flex: 0 0 auto; + margin: 0px -15px; + padding: 15px 0px 0px 15px; +`; + +export const StyledLabel = attachThemeAttrs(styled.span)` + color: ${props => props.palette.text.label}; + font-size: ${FONT_SIZE_TEXT_MEDIUM}; + line-height: 1.5; + margin-bottom: 0.125rem; + box-sizing: border-box; + text-transform: uppercase; + flex: 0 0 auto; +`; + +export const StyledHexColorIcon = attachThemeAttrs(styled.span)` + font: inherit; + line-height: 2.5rem; + font-size: 1rem; +`; + +export const StyledNumberInput = styled(Input)` + margin-left: 5px; + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + input::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } +`; + +export const StyledAlphaSlider = styled(Slider)` + input::-webkit-slider-runnable-track { + background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%); + } + + input::-moz-range-track { + background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%) !important; + } +`; + +export const StyledHueSlider = styled(Slider)` + input::-webkit-slider-runnable-track { + background: linear-gradient( + to right, + rgb(255, 0, 0) 0%, + rgb(255, 255, 0) 17%, + rgb(0, 255, 0) 33%, + rgb(0, 255, 255) 50%, + rgb(0, 0, 255) 67%, + rgb(255, 0, 255) 83%, + rgb(255, 0, 0) 100% + ) !important; + } + + input::-moz-range-track { + background: linear-gradient( + to right, + rgb(255, 0, 0) 0%, + rgb(255, 255, 0) 17%, + rgb(0, 255, 0) 33%, + rgb(0, 255, 255) 50%, + rgb(0, 0, 255) 67%, + rgb(255, 0, 255) 83%, + rgb(255, 0, 0) 100% + ) !important; + } +`; + +export const StyledSlider = styled.div` + display: flex; + position: relative; + width: 100%; + padding: ${PADDING_XX_SMALL} 0 ${PADDING_SMALL} 0; +`; + +export const StyledInputRange = attachThemeAttrs(styled.input)` + appearance: none; + width: 100%; + margin: 0.5rem 0; + background: transparent; + border-radius: ${BORDER_RADIUS_3}; + box-sizing: border-box; + color: inherit; + font: inherit; + line-height: normal; + + ::-moz-focus-inner { + border: 0; + padding: 0; + } + + &::-webkit-slider-thumb { + appearance: none; + width: 1rem; + height: 1rem; + border-radius: 50%; + background: #fff; + border: 0; + box-shadow: ${props => props.shadows.shadow_1}; + cursor: pointer; + transition: all 0.3s ease 0s; + margin-top: calc(((1rem / 2) - (4px / 2)) * -1); + } + + &::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + background: ${props => props.palette.background.highlight}; + border-radius: ${BORDER_RADIUS_3}; + } + + &::-moz-range-thumb { + appearance: none; + width: 1rem; + height: 1rem; + border-radius: 50%; + background: #fff; + border: 0; + box-shadow: ${props => props.shadows.shadow_1}; + cursor: pointer; + transition: background 0.15s ease-in-out; + } + + &::-moz-range-track { + width: 100%; + height: 4px; + cursor: pointer; + background: ${props => props.palette.background.highlight}; + border-radius: ${BORDER_RADIUS_3}; + } + + &::-ms-track { + width: 100%; + height: 4px; + cursor: pointer; + border-radius: ${BORDER_RADIUS_3}; + background: transparent; + border-color: transparent; + color: transparent; + } + + &::-ms-thumb { + width: 1rem; + height: 1rem; + border-radius: ${BORDER_RADIUS_2}; + background: #fff; + border: 0; + box-shadow: rgba(0, 0, 0, 0.16) 0 2px 3px; + cursor: pointer; + transition: background 0.15s ease-in-out; + } + + &:focus { + outline: 0; + } + + &::-webkit-slider-thumb:hover { + background-color: ${COLOR_GRAY_1}; + } + + &::-moz-range-thumb:hover { + background-color: ${COLOR_GRAY_1}; + } + + &::-ms-thumb:hover { + background-color: ${COLOR_GRAY_1}; + } + + &:focus::-webkit-slider-thumb { + background-color: #fff; + box-shadow: $shadow-outline; + } + + &:active::-webkit-slider-thumb { + background-color: #fff; + transition: all 0.3s ease 0s; + transform: scale3d(1.5, 1.5, 1); + } + + &:focus::-moz-range-thumb { + background-color: #fff; + box-shadow: $shadow-outline; + } + + &:active::-moz-range-thumb { + background-color: #fff; + } +`; diff --git a/src/components/Input/index.d.ts b/src/components/Input/index.d.ts index b4f3af402..bad2f14f8 100644 --- a/src/components/Input/index.d.ts +++ b/src/components/Input/index.d.ts @@ -27,7 +27,7 @@ type InputType = | 'checkbox'; export interface InputProps extends BaseProps { - value?: string | boolean; + value?: string | boolean | number; name?: string; type?: InputType; label?: ReactNode; diff --git a/src/components/Input/index.js b/src/components/Input/index.js index b7bb61443..a03cd825b 100644 --- a/src/components/Input/index.js +++ b/src/components/Input/index.js @@ -54,7 +54,7 @@ class Input extends Component { Input.propTypes = { /** Specifies the value of an input element. */ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]), /** The name of the input. */ name: PropTypes.string, /** The type of the input. This value defaults to text. */ diff --git a/src/components/Input/inputBase/index.js b/src/components/Input/inputBase/index.js index 4fdfc0087..59dbfae04 100644 --- a/src/components/Input/inputBase/index.js +++ b/src/components/Input/inputBase/index.js @@ -157,7 +157,7 @@ export default class InputBase extends Component { } InputBase.propTypes = { - value: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), name: PropTypes.string, type: PropTypes.oneOf([ 'text', diff --git a/src/components/index.d.ts b/src/components/index.d.ts index dbe7cd15f..f297c239e 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -26,6 +26,7 @@ export { default as CheckboxGroup } from './CheckboxGroup'; export { default as CheckboxToggle } from './CheckboxToggle'; export { default as Chip } from './Chip'; export { default as CodeInput } from './CodeInput'; +export { default as ColorPicker } from './ColorPicker'; export { default as Column } from './Column'; export { default as Dataset } from './Dataset'; export { default as DatePicker } from './DatePicker'; diff --git a/src/components/index.js b/src/components/index.js index bd9bdeb77..870393113 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -24,6 +24,7 @@ export { default as CheckboxGroup } from './CheckboxGroup'; export { default as CheckboxToggle } from './CheckboxToggle'; export { default as Chip } from './Chip'; export { default as CodeInput } from './CodeInput'; +export { default as ColorPicker } from './ColorPicker'; export { default as Column } from './Column'; export { default as Dataset } from './Dataset'; export { default as DatePicker } from './DatePicker'; diff --git a/src/styles/helpers/color/__test__/isHexColor.spec.js b/src/styles/helpers/color/__test__/isHexColor.spec.js new file mode 100644 index 000000000..44d90b402 --- /dev/null +++ b/src/styles/helpers/color/__test__/isHexColor.spec.js @@ -0,0 +1,17 @@ +import isHexColor from '../isHexColor'; + +describe('isHexColor', () => { + it('should recognize 6-digit and 3-digit hex colors', () => { + const colors = ['#ffffff', '#FF0000', '#f0c34a', '#fff', '#F00', '#f89']; + colors.forEach(color => { + expect(isHexColor(color)).toBeTrue(); + }); + }); + + it('should recognize non hex colors', () => { + const colors = ['a', 'FFFFFF', 232323]; + colors.forEach(color => { + expect(isHexColor(color)).toBeFalse(); + }); + }); +}); diff --git a/src/styles/helpers/color/bound01.js b/src/styles/helpers/color/bound01.js new file mode 100644 index 000000000..1de28d2dd --- /dev/null +++ b/src/styles/helpers/color/bound01.js @@ -0,0 +1,25 @@ +/* eslint-disable no-param-reassign */ +import isOnePointZero from './isOnePointZero'; +import isPercentage from './isPercentage'; + +export default function bound01(n, max) { + if (isOnePointZero(n)) { + n = '100%'; + } + + const processPercent = isPercentage(n); + n = Math.min(max, Math.max(0, parseFloat(n))); + + // Automatically convert percentage into number + if (processPercent) { + n = parseInt(n * max, 10) / 100; + } + + // Handle floating point rounding errors + if (Math.abs(n - max) < 0.000001) { + return 1; + } + + // Convert into [0, 1] range if it isn't already + return (n % max) / parseFloat(max); +} diff --git a/src/styles/helpers/color/decomposeColor.js b/src/styles/helpers/color/decomposeColor.js index faf50ec8f..58bbe9eda 100644 --- a/src/styles/helpers/color/decomposeColor.js +++ b/src/styles/helpers/color/decomposeColor.js @@ -13,11 +13,11 @@ export default function decomposeColor(color) { const marker = color.indexOf('('); const type = color.substring(0, marker); - if (['rgb', 'rgba', 'hsl', 'hsla'].indexOf(type) === -1) { + if (['rgb', 'rgba', 'hsl', 'hsla', 'hsv'].indexOf(type) === -1) { throw new Error( [ `Unsupported \`${color}\` color.`, - 'We support the following formats: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla().', + 'We support the following formats: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), hsv().', ].join('\n'), ); } diff --git a/src/styles/helpers/color/hexToRgb.js b/src/styles/helpers/color/hexToRgb.js index c4325ef7a..b85587074 100644 --- a/src/styles/helpers/color/hexToRgb.js +++ b/src/styles/helpers/color/hexToRgb.js @@ -1,21 +1,21 @@ -/* eslint-disable no-param-reassign */ -export default function hexToRgb(color) { - if (color.charAt(0) !== '#') { - return ''; - } - - color = color.substr(1); +import isHexColor from './isHexColor'; - if (color.length !== 3 && color.length !== 6) { - return ''; - } +export default function hexToRgba(color, alpha = 1) { + if (isHexColor(color)) { + const hexColor = color.substr(1); + const re = new RegExp(`.{1,${hexColor.length / 3}}`, 'g'); + const regColors = hexColor.match(re); - const re = new RegExp(`.{1,${color.length / 3}}`, 'g'); - let colors = color.match(re); + if (regColors) { + const colors = regColors.map(n => { + if (n.length === 1) { + return n + n; + } + return n; + }); - if (colors && colors[0].length === 1) { - colors = colors.map(n => n + n); + return `rgba(${colors.map(n => parseInt(n, 16)).join(', ')}, ${alpha})`; + } } - - return colors ? `rgb(${colors.map(n => parseInt(n, 16)).join(', ')})` : ''; + return ''; } diff --git a/src/styles/helpers/color/hsvToRgb.js b/src/styles/helpers/color/hsvToRgb.js new file mode 100644 index 000000000..e81f21877 --- /dev/null +++ b/src/styles/helpers/color/hsvToRgb.js @@ -0,0 +1,26 @@ +import decomposeColor from './decomposeColor'; +import bound01 from './bound01'; + +export default function hsvToRgb(color) { + const { type, values } = decomposeColor(color); + if (!type || !values || type !== 'hsv') { + return ''; + } + + const h = bound01(values[0], 360) * 6; + const s = bound01(values[1], 100); + const v = bound01(values[2], 100); + + const i = Math.floor(h); + const f = h - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + const mod = i % 6; + + const r = [v, q, p, p, t, v][mod]; + const g = [t, v, v, q, p, p][mod]; + const b = [p, p, t, v, v, q][mod]; + + return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`; +} diff --git a/src/styles/helpers/color/index.js b/src/styles/helpers/color/index.js index 08b102d6b..2d38d08e2 100644 --- a/src/styles/helpers/color/index.js +++ b/src/styles/helpers/color/index.js @@ -14,3 +14,6 @@ export { default as isValidColor } from './isValidColor'; export { default as getBrightness } from './getBrightness'; export { default as isDark } from './isDark'; export { default as colorToRgba } from './colorToRgba'; +export { default as rgbaToHex } from './rgbaToHex'; +export { default as rgbToHsv } from './rgbToHsv'; +export { default as hsvToRgb } from './hsvToRgb'; diff --git a/src/styles/helpers/color/isHexColor.js b/src/styles/helpers/color/isHexColor.js new file mode 100644 index 000000000..ef79dde18 --- /dev/null +++ b/src/styles/helpers/color/isHexColor.js @@ -0,0 +1,5 @@ +const matcher = /^#[a-f0-9]{3}([a-f0-9]{3})?$/i; + +export default function isHexColor(string) { + return matcher.test(string); +} diff --git a/src/styles/helpers/color/isOnePointZero.js b/src/styles/helpers/color/isOnePointZero.js new file mode 100644 index 000000000..59055cba1 --- /dev/null +++ b/src/styles/helpers/color/isOnePointZero.js @@ -0,0 +1,3 @@ +export default function isOnePointZero(n) { + return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1; +} diff --git a/src/styles/helpers/color/isPercentage.js b/src/styles/helpers/color/isPercentage.js new file mode 100644 index 000000000..c44135f19 --- /dev/null +++ b/src/styles/helpers/color/isPercentage.js @@ -0,0 +1,3 @@ +export default function isPercentage(n) { + return typeof n === 'string' && n.indexOf('%') !== -1; +} diff --git a/src/styles/helpers/color/recomposeColor.js b/src/styles/helpers/color/recomposeColor.js index 06c675bbb..a65e1919b 100644 --- a/src/styles/helpers/color/recomposeColor.js +++ b/src/styles/helpers/color/recomposeColor.js @@ -1,20 +1,15 @@ export default function recomposeColor(color) { - const { type } = color; - const { values } = color; - let valuesOutput = []; - + const { type, values } = color; if (!type || !values) { return ''; } - if (type.indexOf('rgb') !== -1) { - // Only convert the first 3 values to int (i.e. not alpha) - valuesOutput = values.map((n, i) => (i < 3 ? parseInt(n, 10) : n)); - } else if (type.indexOf('hsl') !== -1) { - valuesOutput[0] = values[0]; - valuesOutput[1] = `${values[1]}%`; - valuesOutput[2] = `${values[2]}%`; - } + const valuesOutput = values.map((value, index) => { + if (['hsl', 'hsla', 'hsv'].includes(type) && index > 0 && index < 3) { + return `${value}%`; + } + return value; + }); return `${type}(${valuesOutput.join(', ')})`; } diff --git a/src/styles/helpers/color/rgbToHsv.js b/src/styles/helpers/color/rgbToHsv.js new file mode 100644 index 000000000..523f78aae --- /dev/null +++ b/src/styles/helpers/color/rgbToHsv.js @@ -0,0 +1,39 @@ +/* eslint-disable default-case */ +import bound01 from './bound01'; +import { decomposeColor } from '.'; + +export default function rgbToHsv(color) { + const { type, values } = decomposeColor(color); + if (!type || !values || type.indexOf('rgb') === -1) { + return ''; + } + + const r = bound01(values[0], 255); + const g = bound01(values[1], 255); + const b = bound01(values[2], 255); + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const v = max; + const d = max - min; + const s = max === 0 ? 0 : d / max; + + let h; + if (max === min) { + h = 0; // achromatic + } else { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + return `hsv(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(v * 100)}%)`; +} diff --git a/src/styles/helpers/color/rgbaToHex.js b/src/styles/helpers/color/rgbaToHex.js new file mode 100644 index 000000000..e7750d36c --- /dev/null +++ b/src/styles/helpers/color/rgbaToHex.js @@ -0,0 +1,21 @@ +import { decomposeColor } from '.'; + +function pad2(c) { + return c.length === 1 ? `0${c}` : `${c}`; +} + +export default function rgbaToHex(color) { + const rgbaColor = decomposeColor(color); + + if (rgbaColor.type !== 'rgba') { + return ''; + } + + const hex = [ + pad2(Math.round(rgbaColor.values[0]).toString(16)), + pad2(Math.round(rgbaColor.values[1]).toString(16)), + pad2(Math.round(rgbaColor.values[2]).toString(16)), + ]; + + return hex.join(''); +} From 007d6c6cf1cdfe16002aaada82a20c68fbac1ac2 Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Fri, 21 Aug 2020 01:13:57 -0400 Subject: [PATCH 02/11] feat: implement default color section --- src/components/ColorPicker/alpha.js | 14 +- src/components/ColorPicker/colors.js | 37 ----- .../ColorPicker/defaultColors/color.js | 53 ++++++++ .../ColorPicker/defaultColors/index.js | 53 ++++++++ .../ColorPicker/defaultColors/styled.js | 38 ++++++ src/components/ColorPicker/helpers/index.js | 2 +- .../{normalazeColor.js => normalizeColor.js} | 2 +- src/components/ColorPicker/hexColor.js | 42 +++--- .../ColorPicker/hooks/useNormalizeColors.js | 11 ++ src/components/ColorPicker/hue.js | 14 +- src/components/ColorPicker/index.js | 127 ++++++++++++++---- src/components/ColorPicker/readme.md | 73 +--------- src/components/ColorPicker/rgbaColor.js | 9 +- .../ColorPicker/saturation/index.js | 88 +++++++++--- .../ColorPicker/saturation/styled.js | 48 +++---- src/components/ColorPicker/slider.js | 13 +- src/components/ColorPicker/styled.js | 31 +---- 17 files changed, 420 insertions(+), 235 deletions(-) delete mode 100644 src/components/ColorPicker/colors.js create mode 100644 src/components/ColorPicker/defaultColors/color.js create mode 100644 src/components/ColorPicker/defaultColors/index.js create mode 100644 src/components/ColorPicker/defaultColors/styled.js rename src/components/ColorPicker/helpers/{normalazeColor.js => normalizeColor.js} (77%) create mode 100644 src/components/ColorPicker/hooks/useNormalizeColors.js diff --git a/src/components/ColorPicker/alpha.js b/src/components/ColorPicker/alpha.js index f1cc1d435..ce184b0a3 100644 --- a/src/components/ColorPicker/alpha.js +++ b/src/components/ColorPicker/alpha.js @@ -5,7 +5,7 @@ import { recomposeColor } from '../../styles/helpers/color'; import { getAlpha } from './helpers'; export default function Alpha(props) { - const { rgbaColor, onChange } = props; + const { rgbaColor, tabIndex, onChange } = props; const a = getAlpha(rgbaColor); const handleChange = event => { @@ -14,15 +14,25 @@ export default function Alpha(props) { onChange(recomposeColor(rgbaColor)); }; - return ; + return ( + + ); } Alpha.propTypes = { rgbaColor: PropTypes.object, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChange: PropTypes.func, }; Alpha.defaultProps = { rgbaColor: '', + tabIndex: undefined, onChange: () => {}, }; diff --git a/src/components/ColorPicker/colors.js b/src/components/ColorPicker/colors.js deleted file mode 100644 index cd96b6914..000000000 --- a/src/components/ColorPicker/colors.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { StyledDefaulColor, StyledColorsContainer } from './styled'; -import { colorToRgba, isValidColor } from '../../styles/helpers/color'; - -const Colors = React.memo(props => { - const { colors, onChange } = props; - - const handleChange = color => { - const rgbaColor = colorToRgba(color); - if (isValidColor(rgbaColor)) { - onChange(rgbaColor); - } - }; - const ListColors = () => - colors.map(color => ( - handleChange(color)} /> - )); - - return ( - - - - ); -}); - -Colors.propTypes = { - colors: PropTypes.array, - onChange: PropTypes.func, -}; - -Colors.defaultProps = { - colors: [], - onChange: () => {}, -}; - -export default Colors; diff --git a/src/components/ColorPicker/defaultColors/color.js b/src/components/ColorPicker/defaultColors/color.js new file mode 100644 index 000000000..cfb1d2096 --- /dev/null +++ b/src/components/ColorPicker/defaultColors/color.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledColor, StyledInput, StyledLabel } from './styled'; +import AssistiveText from '../../AssistiveText'; +import { useUniqueIdentifier } from '../../../libs/hooks'; +import { colorToRgba } from '../../../styles/helpers/color'; + +const Color = React.forwardRef((props, ref) => { + const { color, tabIndex, isChecked, onChange } = props; + const colorId = useUniqueIdentifier('color-picker-default'); + + const handleChange = () => { + const newColor = colorToRgba(color); + if (newColor !== '') { + onChange(newColor); + } + }; + + const style = { backgroundColor: color }; + return ( + + + + {color} + + + ); +}); + +Color.propTypes = { + color: PropTypes.string.isRequired, + isChecked: PropTypes.bool, + onChange: PropTypes.func, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), +}; + +Color.defaultProps = { + color: undefined, + isChecked: false, + onChange: () => {}, + tapIndex: undefined, +}; + +export default Color; diff --git a/src/components/ColorPicker/defaultColors/index.js b/src/components/ColorPicker/defaultColors/index.js new file mode 100644 index 000000000..3b8d0bada --- /dev/null +++ b/src/components/ColorPicker/defaultColors/index.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledContainer, StyledColors } from './styled'; +import { StyledLabel } from '../styled'; +import { colorToRgba, recomposeColor } from '../../../styles/helpers/color'; +import Color from './color'; + +const DefaultColors = React.forwardRef((props, ref) => { + const { colors, rgbaColor: rgbaColorProps, title, tabIndex: tabIndexProp, onChange } = props; + const rgbaColor = recomposeColor(rgbaColorProps); + + const listColors = colors.map((color, index) => { + const tabIndex = index === 0 ? tabIndexProp : -1; + const isSelected = colorToRgba(color) === rgbaColor; + const isFirstInput = index === 0; + const inputRef = isFirstInput ? ref : undefined; + return ( + + ); + }); + + return ( + + {title} + {listColors} + + ); +}); + +DefaultColors.propTypes = { + colors: PropTypes.array.isRequired, + rgbaColor: PropTypes.object.isRequired, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + onChange: PropTypes.func, +}; + +DefaultColors.defaultProps = { + colors: [], + rgbaColor: undefined, + title: 'Default Colors', + tabIndex: undefined, + onChange: () => {}, +}; + +export default DefaultColors; diff --git a/src/components/ColorPicker/defaultColors/styled.js b/src/components/ColorPicker/defaultColors/styled.js new file mode 100644 index 000000000..f0ea19ec5 --- /dev/null +++ b/src/components/ColorPicker/defaultColors/styled.js @@ -0,0 +1,38 @@ +import styled from 'styled-components'; +import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; +import HiddenElement from '../../Structural/hiddenElement'; +import { CssCircleColor } from '../styled'; + +export const StyledContainer = styled.div` + flex: 0 0 auto; + padding: 0.5rem 0 0.25rem; +`; + +export const StyledColors = styled.div` + text-align: center; +`; + +export const StyledColor = styled.span` + line-height: inherit; + height: inherit; +`; + +export const StyledInput = attachThemeAttrs(styled(HiddenElement))` + &:focus + label { + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; + } +`; + +export const StyledLabel = attachThemeAttrs(styled.label)` + display: inline-block; + margin: 0.25rem 0.45rem; + width: 40px; + height: 40px; + padding: 0; + ${CssCircleColor} + + &:hover { + cursor: pointer; + } +`; diff --git a/src/components/ColorPicker/helpers/index.js b/src/components/ColorPicker/helpers/index.js index a808a36d9..7f785bea3 100644 --- a/src/components/ColorPicker/helpers/index.js +++ b/src/components/ColorPicker/helpers/index.js @@ -1,3 +1,3 @@ export { default as getAlpha } from './getAlpha'; -export { default as normalazeColor } from './normalazeColor'; +export { default as normalizeColor } from './normalizeColor'; export { default as isAchromatic } from './isAchromatic'; diff --git a/src/components/ColorPicker/helpers/normalazeColor.js b/src/components/ColorPicker/helpers/normalizeColor.js similarity index 77% rename from src/components/ColorPicker/helpers/normalazeColor.js rename to src/components/ColorPicker/helpers/normalizeColor.js index 73ce3a157..dfe11b966 100644 --- a/src/components/ColorPicker/helpers/normalazeColor.js +++ b/src/components/ColorPicker/helpers/normalizeColor.js @@ -1,6 +1,6 @@ import { colorToRgba } from '../../../styles/helpers/color'; -export default function normalazeColor(color) { +export default function normalizeColor(color) { const rgbaColor = colorToRgba(color); return rgbaColor !== '' ? rgbaColor : 'rgba(0, 0, 0, 1)'; } diff --git a/src/components/ColorPicker/hexColor.js b/src/components/ColorPicker/hexColor.js index b5a791869..a1d710508 100644 --- a/src/components/ColorPicker/hexColor.js +++ b/src/components/ColorPicker/hexColor.js @@ -2,57 +2,55 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import Input from '../Input'; import { StyledHexColorIcon } from './styled'; -import { hexToRgba } from '../../styles/helpers/color'; +import { hexToRgba, rgbaToHex } from '../../styles/helpers/color'; export default function HexColor(props) { - const { hexColor, onChange } = props; + const { rgbaColor, tabIndex, onChange } = props; const [color, setColor] = useState(); const [isFocused, setIsFocused] = useState(false); useEffect(() => { if (!isFocused) { - setColor(hexColor); + const hexColor = rgbaToHex(rgbaColor); + setColor(hexColor.substr(1)); } - }, [hexColor, isFocused]); + }, [isFocused, rgbaColor]); - const handleChange = event => { - const value = event.target.value; - setColor(value); - const hex = `#${value}`; - const rgbaColor = hexToRgba(hex); - if (rgbaColor !== '') { - onChange(rgbaColor); + useEffect(() => { + const hex = `#${color}`; + const newColor = hexToRgba(hex); + if (newColor !== '') { + onChange(newColor); } - }; + }, [color, onChange]); - const handleBlur = event => { + const handleBlur = () => { setIsFocused(false); - const value = event.target.value; - const hex = `#${value}`; - const rgbaColor = hexToRgba(hex); - if (rgbaColor !== '') { - onChange(rgbaColor); - } + const hexColor = rgbaToHex(rgbaColor); + setColor(hexColor.substr(1)); }; return ( setColor(event.target.value)} onFocus={() => setIsFocused(true)} onBlur={handleBlur} icon={#} + tabIndex={tabIndex} /> ); } HexColor.propTypes = { - hexColor: PropTypes.string, + rgbaColor: PropTypes.object, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChange: PropTypes.func, }; HexColor.defaultProps = { - hexColor: '', + rgbaColor: '', + tabIndex: undefined, onChange: () => {}, }; diff --git a/src/components/ColorPicker/hooks/useNormalizeColors.js b/src/components/ColorPicker/hooks/useNormalizeColors.js new file mode 100644 index 000000000..11eac63bd --- /dev/null +++ b/src/components/ColorPicker/hooks/useNormalizeColors.js @@ -0,0 +1,11 @@ +import { useMemo } from 'react'; +import isValidColor from '../../../../styles/helpers/color/isValidColor'; + +export default function useNormalizeColors(colors) { + return useMemo(() => { + if (Array.isArray(colors)) { + return colors.filter(color => isValidColor(color)); + } + return []; + }, [colors]); +} diff --git a/src/components/ColorPicker/hue.js b/src/components/ColorPicker/hue.js index b6132c5af..2a2cbf341 100644 --- a/src/components/ColorPicker/hue.js +++ b/src/components/ColorPicker/hue.js @@ -4,7 +4,7 @@ import { StyledHueSlider } from './styled'; import { hsvToRgb, rgbToRgba } from '../../styles/helpers/color'; export default function Hue(props) { - const { hsvColor, hue, setHue, alpha, onChange } = props; + const { hsvColor, hue, setHue, alpha, tabIndex, onChange } = props; const handleChange = event => { const value = parseInt(event.target.value, 10); @@ -15,7 +15,15 @@ export default function Hue(props) { onChange(rgbaColor); }; - return ; + return ( + + ); } Hue.propTypes = { @@ -23,6 +31,7 @@ Hue.propTypes = { alpha: PropTypes.number, hue: PropTypes.number, setHue: PropTypes.func, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChange: PropTypes.func, }; @@ -31,5 +40,6 @@ Hue.defaultProps = { alpha: undefined, hue: undefined, setHue: () => {}, + tabIndex: undefined, onChange: () => {}, }; diff --git a/src/components/ColorPicker/index.js b/src/components/ColorPicker/index.js index 084474c53..c6fabb8c0 100644 --- a/src/components/ColorPicker/index.js +++ b/src/components/ColorPicker/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'; import PropTypes from 'prop-types'; import RenderIf from '../RenderIf'; import Saturation from './saturation'; @@ -6,7 +6,7 @@ import RgbaColor from './rgbaColor'; import HexColor from './hexColor'; import Alpha from './alpha'; import Hue from './hue'; -import Colors from './colors'; +import DefaultColors from './defaultColors'; import { StyledContainer, StyledPreview, @@ -17,32 +17,66 @@ import { StyledHexColorContainer, StyledRgbaColorContainer, } from './styled'; -import { rgbaToHex, rgbToHsv, decomposeColor } from '../../styles/helpers/color'; -import { normalazeColor, isAchromatic } from './helpers'; +import { rgbToHsv, decomposeColor } from '../../styles/helpers/color'; +import { normalizeColor, isAchromatic } from './helpers'; +import useNormalizeColors from './hooks/useNormalizeColors'; -export default function ColorPicker(props) { - const { id, color: colorProp, colors, label, labelColors, onChange, className, style } = props; +const ColorPicker = React.forwardRef((props, ref) => { + const { + id, + color: colorProp, + colors: colorsProp, + title, + titleColors, + tabIndex, + onChange, + className, + style, + } = props; const [hue, setHue] = useState(0); - const color = normalazeColor(colorProp); + const color = normalizeColor(colorProp); const rgbaColor = decomposeColor(color); - const hexColor = rgbaToHex(rgbaColor); const hsvColor = decomposeColor(rgbToHsv(color)); const alpha = rgbaColor.values[3]; + const firstRef = useRef(); + const lastRef = useRef(); + + useImperativeHandle(ref, () => ({ + focus: () => { + firstRef.current.focus(); + }, + click: () => { + firstRef.current.click(); + }, + blur: () => { + lastRef.current.blur(); + }, + })); + useEffect(() => { if (!isAchromatic(rgbaColor)) { setHue(hsvColor.values[0]); } }, [hsvColor.values, rgbaColor]); - const hasColors = Array.isArray(colors) && colors.length > 0; + const colors = useNormalizeColors(colorsProp); + const hasColors = colors.length > 0; + const styleColor = { backgroundColor: color }; return ( - {label} + {title} - + @@ -52,27 +86,34 @@ export default function ColorPicker(props) { hue={hue} setHue={setHue} onChange={onChange} + tabIndex={tabIndex} /> - + - + - + - + - {labelColors} - + ); -} +}); ColorPicker.propTypes = { /** The id of the outer element. */ @@ -81,10 +122,12 @@ ColorPicker.propTypes = { color: PropTypes.string, /** Specifies the default colors to choice. */ colors: PropTypes.array, - /** Text label for the input. */ - label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - /** Text label for the default colors. */ - labelColors: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** Text title for the Color Picker. */ + title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** Text title for the Default Colors section. */ + titleColors: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** Specifies the tab order of an element (when the tab button is used for navigating). */ + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** The action triggered when the value changes. */ onChange: PropTypes.func, /** A CSS class for the outer element, in addition to the component's base classes. */ @@ -95,11 +138,43 @@ ColorPicker.propTypes = { ColorPicker.defaultProps = { id: undefined, - color: '', - colors: [], - label: 'Color Picker', - labelColors: 'Defualt', + color: '#000000', + colors: [ + '#e3aaec', + '#c3dbf7', + '#9fd6ff', + '#9de7da', + '#9ef0bf', + '#fef199', + '#fdd499', + '#d174e0', + '#86baf3', + '#5ebbff', + '#42d8be', + '#3be282', + '#ffe654', + '#ffb758', + '#bd35bd', + '#5779c1', + '#4A90E2', + '#06aea9', + '#3dba4c', + '#f5bc24', + '#f99222', + '#570e8c', + '#021970', + '#0b2399', + '#0d7477', + '#0a6b50', + '#b67e12', + '#b75d0c', + ], + title: 'Color Picker', + titleColors: 'Defualt', + tabIndex: undefined, onChange: () => {}, className: undefined, style: undefined, }; + +export default ColorPicker; diff --git a/src/components/ColorPicker/readme.md b/src/components/ColorPicker/readme.md index 4f941b89d..ee440d7d4 100644 --- a/src/components/ColorPicker/readme.md +++ b/src/components/ColorPicker/readme.md @@ -12,81 +12,14 @@ const Container = styled.div` padding: 50px; `; -const StyledCard = styled(Card)` - width: 26rem; - height: 27rem; -`; - -const ColorPickerExample = () => { - const [color, setColor] = useState('#ff0'); - const handleChange = value => { - // console.log(value); - setColor(value); - } - return ; -} - - - - - - -``` - -##### ColorPicker with default colors - -```js -import React, {useState, useCallback} from 'react'; -import styled from 'styled-components' -import { ColorPicker, Card } from 'react-rainbow-components'; - -const colors = [ - '#e3aaec', - '#c3dbf7', - '#9fd6ff', - '#9de7da', - '#9ef0bf', - '#fef199', - '#fdd499', - '#d174e0', - '#86baf3', - '#5ebbff', - '#42d8be', - '#3be282', - '#ffe654', - '#ffb758', - '#bd35bd', - '#5779c1', - '#4A90E2', - '#06aea9', - '#3dba4c', - '#f5bc24', - '#f99222', - '#570e8c', - '#021970', - '#0b2399', - '#0d7477', - '#0a6b50', - '#b67e12', - '#b75d0c', -]; - -const Container = styled.div` - display: flex; - justify-content: center; - align-items: center; - padding: 50px; -`; - const StyledCard = styled(Card)` width: 26rem; height: 40rem; `; const ColorPickerExample = () => { - const [color, setColor] = useState('#ff0'); - - return ; + const [color, setColor] = useState(); + return ; } @@ -94,4 +27,4 @@ const ColorPickerExample = () => { -``` \ No newline at end of file +``` diff --git a/src/components/ColorPicker/rgbaColor.js b/src/components/ColorPicker/rgbaColor.js index 007166e21..1a3f699a9 100644 --- a/src/components/ColorPicker/rgbaColor.js +++ b/src/components/ColorPicker/rgbaColor.js @@ -1,12 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { StyledFlexContainer, StyledNumberInput } from './styled'; -import Input from '../Input'; import { getAlpha } from './helpers'; import { recomposeColor } from '../../styles/helpers/color'; export default function RgbaColor(props) { - const { rgbaColor, onChange } = props; + const { rgbaColor, tabIndex, onChange } = props; const [r, g, b] = rgbaColor.values; const a = getAlpha(rgbaColor); @@ -39,24 +38,28 @@ export default function RgbaColor(props) { value={r} bottomHelpText="R" onChange={event => handleChange(0, event)} + tabIndex={tabIndex} /> handleChange(1, event)} + tabIndex={tabIndex} /> handleChange(2, event)} + tabIndex={tabIndex} /> ); @@ -64,10 +67,12 @@ export default function RgbaColor(props) { RgbaColor.propTypes = { rgbaColor: PropTypes.object, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChange: PropTypes.func, }; RgbaColor.defaultProps = { rgbaColor: '', + tabIndex: undefined, onChange: () => {}, }; diff --git a/src/components/ColorPicker/saturation/index.js b/src/components/ColorPicker/saturation/index.js index 9cbcbd520..4c14fc7c6 100644 --- a/src/components/ColorPicker/saturation/index.js +++ b/src/components/ColorPicker/saturation/index.js @@ -1,14 +1,26 @@ import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { UP_KEY, DOWN_KEY, RIGHT_KEY, LEFT_KEY } from '../../../../libs/constants'; import { hsvToRgb, rgbToRgba } from '../../../styles/helpers/color'; -import { StyledColor, StyledWhite, StyledBlack, StyledPointer, StyledCircle } from './styled'; +import { StyledColor, StyledPointer, StyledCircle } from './styled'; -export default function Saturation(props) { - const { rgbaColor, hsvColor, onChange } = props; - const hslColor = `hsl(${hsvColor.values[0]},100%, 50%)`; +const Saturation = React.forwardRef((props, ref) => { + const { rgbaColor, hsvColor, tabIndex, hue, onChange } = props; + const hslColor = `hsl(${hue},100%, 50%)`; const alpha = rgbaColor.values[3]; const containerRef = useRef(); + const change = ({ saturation, bright }) => { + if (saturation) { + hsvColor.values[1] = saturation; + } + if (bright) { + hsvColor.values[2] = bright; + } + const color = rgbToRgba(hsvToRgb(hsvColor), alpha); + onChange(color); + }; + const handleChange = event => { const rect = containerRef.current.getBoundingClientRect(); const { width, height } = rect; @@ -20,10 +32,7 @@ export default function Saturation(props) { const saturation = Math.round((left / width) * 100); const bright = Math.round((1 - top / height) * 100); - hsvColor.values[1] = saturation; - hsvColor.values[2] = bright; - const color = rgbToRgba(hsvToRgb(hsvColor), alpha); - onChange(color); + change({ saturation, bright }); }; const unbindEventListeners = () => { @@ -37,38 +46,75 @@ export default function Saturation(props) { window.addEventListener('mouseup', unbindEventListeners); }; + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => unbindEventListeners, []); - const top = 100 - hsvColor.values[2]; - const left = hsvColor.values[1]; + const keyHandlerMap = { + [UP_KEY]: () => { + const bright = Math.min(100, hsvColor.values[2] + 1); + change({ bright }); + }, + [DOWN_KEY]: () => { + const bright = Math.max(0, hsvColor.values[2] - 1); + change({ bright }); + }, + [LEFT_KEY]: () => { + const saturation = Math.max(0, hsvColor.values[1] - 1); + change({ saturation }); + }, + [RIGHT_KEY]: () => { + const saturation = Math.min(100, hsvColor.values[1] + 1); + change({ saturation }); + }, + }; + + const handleKeyDown = event => { + const { keyCode } = event; + if (keyHandlerMap[keyCode]) { + event.preventDefault(); + event.stopPropagation(); + keyHandlerMap[keyCode](); + } + }; + + const handleClick = () => { + ref.current.focus(); + }; + + const styleColor = { background: hslColor }; + const stylePointer = { top: `${100 - hsvColor.values[2]}%`, left: `${hsvColor.values[1]}%` }; return ( - - - - - - - + + + ); -} +}); Saturation.propTypes = { - rgbaColor: PropTypes.object, - hsvColor: PropTypes.object, + rgbaColor: PropTypes.object.isRequired, + hsvColor: PropTypes.object.isRequired, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + hue: PropTypes.number.isRequired, onChange: PropTypes.func, }; Saturation.defaultProps = { rgbaColor: undefined, hsvColor: undefined, + tabIndex: undefined, + hue: undefined, onChange: () => {}, }; + +export default Saturation; diff --git a/src/components/ColorPicker/saturation/styled.js b/src/components/ColorPicker/saturation/styled.js index 7d999a9c2..62a7f9573 100644 --- a/src/components/ColorPicker/saturation/styled.js +++ b/src/components/ColorPicker/saturation/styled.js @@ -1,39 +1,39 @@ -import styled, { css } from 'styled-components'; - -const StyledAbsolute = css` - position: absolute; - top: 0px; - right: 0px; - bottom: 0px; - left: 0px; -`; +import styled from 'styled-components'; +import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; export const StyledColor = styled.div` - ${StyledAbsolute} - background: ${props => props.hslColor}; -`; + position: relative; + height: 100%; -export const StyledWhite = styled.div` - ${StyledAbsolute} - background: linear-gradient(to right, rgb(255, 255, 255), rgba(255, 255, 255, 0)); -`; - -export const StyledBlack = styled.div` - ${StyledAbsolute} - background: linear-gradient(to top, rgb(0, 0, 0), rgba(0, 0, 0, 0)); + &:before { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: linear-gradient(0deg, #000, rgba(0, 0, 0, 0.9) 1%, transparent 99%), + linear-gradient(90deg, #fff 1%, hsla(0, 0%, 100%, 0)); + } `; export const StyledPointer = styled.div` position: absolute; - top: ${props => props.$top}%; - left: ${props => props.$left}%; - cursor: default; `; -export const StyledCircle = styled.div` +export const StyledCircle = attachThemeAttrs(styled.button)` width: 12px; height: 12px; border-radius: 6px; box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset; transform: translate(-6px, -6px); + padding: 0; + border: 1px solid ${props => props.palette.border.divider}; + + &:focus, + &:active { + outline: 0; + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; + } `; diff --git a/src/components/ColorPicker/slider.js b/src/components/ColorPicker/slider.js index 49f930441..82495122a 100644 --- a/src/components/ColorPicker/slider.js +++ b/src/components/ColorPicker/slider.js @@ -3,11 +3,18 @@ import PropTypes from 'prop-types'; import { StyledSlider, StyledInputRange } from './styled'; export default function Slider(props) { - const { value, min, max, onChange, style, className } = props; + const { value, min, max, tabIndex, onChange, style, className } = props; return ( - + ); } @@ -16,6 +23,7 @@ Slider.propTypes = { value: PropTypes.number, min: PropTypes.number, max: PropTypes.number, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChange: PropTypes.func, className: PropTypes.string, style: PropTypes.object, @@ -25,6 +33,7 @@ Slider.defaultProps = { value: undefined, min: 0, max: 100, + tabIndex: undefined, onChange: () => {}, className: undefined, style: undefined, diff --git a/src/components/ColorPicker/styled.js b/src/components/ColorPicker/styled.js index 61c580252..30b229c42 100644 --- a/src/components/ColorPicker/styled.js +++ b/src/components/ColorPicker/styled.js @@ -7,8 +7,7 @@ import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; import Input from '../Input'; import Slider from './slider'; -const StyledCircleColor = css` - background-color: ${props => props.$color}; +export const CssCircleColor = css` border-radius: 50%; border: 1px solid ${props => props.palette.border.divider}; `; @@ -17,19 +16,7 @@ export const StyledPreview = attachThemeAttrs(styled.div)` width: 50px; height: 50px; margin: 10px 0; - ${StyledCircleColor} -`; - -export const StyledDefaulColor = attachThemeAttrs(styled.div)` - width: 40px; - height: 40px; - margin: 0px 15px 15px 0px - flex: 0 0 auto; - ${StyledCircleColor} - - :hover { - cursor: pointer; - } + ${CssCircleColor} `; export const StyledFlexContainer = styled.div` @@ -68,14 +55,6 @@ export const StyledRgbaColorContainer = styled.div` flex: 0 4 auto; `; -export const StyledColorsContainer = styled.div` - display: flex; - flex-wrap: wrap; - flex: 0 0 auto; - margin: 0px -15px; - padding: 15px 0px 0px 15px; -`; - export const StyledLabel = attachThemeAttrs(styled.span)` color: ${props => props.palette.text.label}; font-size: ${FONT_SIZE_TEXT_MEDIUM}; @@ -245,7 +224,8 @@ export const StyledInputRange = attachThemeAttrs(styled.input)` &:focus::-webkit-slider-thumb { background-color: #fff; - box-shadow: $shadow-outline; + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; } &:active::-webkit-slider-thumb { @@ -256,7 +236,8 @@ export const StyledInputRange = attachThemeAttrs(styled.input)` &:focus::-moz-range-thumb { background-color: #fff; - box-shadow: $shadow-outline; + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; } &:active::-moz-range-thumb { From 0cb522f2110f854bef2881aa6d118872311e44c6 Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Tue, 25 Aug 2020 05:43:40 -0400 Subject: [PATCH 03/11] fix: change saturation component --- src/components/ColorPicker/alpha.js | 31 ++-- src/components/ColorPicker/context.js | 4 + .../ColorPicker/defaultColors/color.js | 5 +- .../ColorPicker/defaultColors/index.js | 46 +++--- src/components/ColorPicker/hexColor.js | 29 ++-- src/components/ColorPicker/hue.js | 40 +---- src/components/ColorPicker/index.d.ts | 7 +- src/components/ColorPicker/index.js | 138 +++++++++--------- src/components/ColorPicker/preview.js | 11 ++ src/components/ColorPicker/readme.md | 2 +- src/components/ColorPicker/rgbaColor.js | 48 +++--- .../ColorPicker/saturation/index.js | 104 +++++++------ .../ColorPicker/saturation/styled.js | 8 +- src/components/ColorPicker/styled.js | 6 +- 14 files changed, 222 insertions(+), 257 deletions(-) create mode 100644 src/components/ColorPicker/context.js create mode 100644 src/components/ColorPicker/preview.js diff --git a/src/components/ColorPicker/alpha.js b/src/components/ColorPicker/alpha.js index ce184b0a3..0b23f1a07 100644 --- a/src/components/ColorPicker/alpha.js +++ b/src/components/ColorPicker/alpha.js @@ -1,22 +1,21 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; import { StyledAlphaSlider } from './styled'; -import { recomposeColor } from '../../styles/helpers/color'; -import { getAlpha } from './helpers'; +import { ColorPickerContext } from './context'; -export default function Alpha(props) { - const { rgbaColor, tabIndex, onChange } = props; - const a = getAlpha(rgbaColor); +export default function Alpha() { + const { r, g, b, a, tabIndex, onChange } = useContext(ColorPickerContext); const handleChange = event => { const alpha = parseInt(event.target.value, 10); - rgbaColor.values[3] = alpha / 100; - onChange(recomposeColor(rgbaColor)); + const rgba = `rgba(${r}, ${g}, ${b}, ${alpha / 100})`; + onChange(rgba); }; + const value = Math.round(a * 100); + return ( ); } - -Alpha.propTypes = { - rgbaColor: PropTypes.object, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - onChange: PropTypes.func, -}; - -Alpha.defaultProps = { - rgbaColor: '', - tabIndex: undefined, - onChange: () => {}, -}; diff --git a/src/components/ColorPicker/context.js b/src/components/ColorPicker/context.js new file mode 100644 index 000000000..0db18df2d --- /dev/null +++ b/src/components/ColorPicker/context.js @@ -0,0 +1,4 @@ +import React from 'react'; + +export const ColorPickerContext = React.createContext(); +export const { Provider, Consumer } = ColorPickerContext; diff --git a/src/components/ColorPicker/defaultColors/color.js b/src/components/ColorPicker/defaultColors/color.js index cfb1d2096..9e681433c 100644 --- a/src/components/ColorPicker/defaultColors/color.js +++ b/src/components/ColorPicker/defaultColors/color.js @@ -6,7 +6,7 @@ import { useUniqueIdentifier } from '../../../libs/hooks'; import { colorToRgba } from '../../../styles/helpers/color'; const Color = React.forwardRef((props, ref) => { - const { color, tabIndex, isChecked, onChange } = props; + const { color, name, tabIndex, isChecked, onChange } = props; const colorId = useUniqueIdentifier('color-picker-default'); const handleChange = () => { @@ -24,6 +24,7 @@ const Color = React.forwardRef((props, ref) => { as="input" name={name} checked={isChecked} + value={color} type="radio" onChange={handleChange} ref={ref} @@ -38,6 +39,7 @@ const Color = React.forwardRef((props, ref) => { Color.propTypes = { color: PropTypes.string.isRequired, + name: PropTypes.string, isChecked: PropTypes.bool, onChange: PropTypes.func, tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), @@ -45,6 +47,7 @@ Color.propTypes = { Color.defaultProps = { color: undefined, + name: '', isChecked: false, onChange: () => {}, tapIndex: undefined, diff --git a/src/components/ColorPicker/defaultColors/index.js b/src/components/ColorPicker/defaultColors/index.js index 3b8d0bada..bdf992128 100644 --- a/src/components/ColorPicker/defaultColors/index.js +++ b/src/components/ColorPicker/defaultColors/index.js @@ -1,23 +1,26 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; +import { ColorPickerContext } from '../context'; import { StyledContainer, StyledColors } from './styled'; -import { StyledLabel } from '../styled'; -import { colorToRgba, recomposeColor } from '../../../styles/helpers/color'; +import { colorToRgba } from '../../../styles/helpers/color'; +import { useUniqueIdentifier } from '../../../libs/hooks'; import Color from './color'; +import RenderIf from '../../RenderIf'; -const DefaultColors = React.forwardRef((props, ref) => { - const { colors, rgbaColor: rgbaColorProps, title, tabIndex: tabIndexProp, onChange } = props; - const rgbaColor = recomposeColor(rgbaColorProps); +const DefaultColors = React.forwardRef((_props, ref) => { + const { colors, r, g, b, a, tabIndex: tabIndexProp, onChange } = useContext(ColorPickerContext); + const rgba = `rgba(${r}, ${g}, ${b}, ${a})`; + const name = useUniqueIdentifier('color-picker-default'); const listColors = colors.map((color, index) => { const tabIndex = index === 0 ? tabIndexProp : -1; - const isSelected = colorToRgba(color) === rgbaColor; + const isSelected = colorToRgba(color) === rgba; const isFirstInput = index === 0; const inputRef = isFirstInput ? ref : undefined; return ( { ); }); + const hasColors = colors.length > 0; + return ( - - {title} - {listColors} - + + + {listColors} + + ); }); -DefaultColors.propTypes = { - colors: PropTypes.array.isRequired, - rgbaColor: PropTypes.object.isRequired, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - onChange: PropTypes.func, -}; - -DefaultColors.defaultProps = { - colors: [], - rgbaColor: undefined, - title: 'Default Colors', - tabIndex: undefined, - onChange: () => {}, -}; - export default DefaultColors; diff --git a/src/components/ColorPicker/hexColor.js b/src/components/ColorPicker/hexColor.js index a1d710508..540c67468 100644 --- a/src/components/ColorPicker/hexColor.js +++ b/src/components/ColorPicker/hexColor.js @@ -1,20 +1,21 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; +import React, { useState, useEffect, useContext } from 'react'; import Input from '../Input'; import { StyledHexColorIcon } from './styled'; import { hexToRgba, rgbaToHex } from '../../styles/helpers/color'; +import { ColorPickerContext } from './context'; -export default function HexColor(props) { - const { rgbaColor, tabIndex, onChange } = props; - const [color, setColor] = useState(); +export default function HexColor() { + const { r, g, b, a, tabIndex, onChange } = useContext(ColorPickerContext); + const rgba = `rgba(${r}, ${g}, ${b}, ${a})`; + const [color, setColor] = useState(rgbaToHex(rgba).substr(1)); const [isFocused, setIsFocused] = useState(false); useEffect(() => { if (!isFocused) { - const hexColor = rgbaToHex(rgbaColor); + const hexColor = rgbaToHex(rgba); setColor(hexColor.substr(1)); } - }, [isFocused, rgbaColor]); + }, [isFocused, rgba]); useEffect(() => { const hex = `#${color}`; @@ -26,7 +27,7 @@ export default function HexColor(props) { const handleBlur = () => { setIsFocused(false); - const hexColor = rgbaToHex(rgbaColor); + const hexColor = rgbaToHex(rgba); setColor(hexColor.substr(1)); }; @@ -42,15 +43,3 @@ export default function HexColor(props) { /> ); } - -HexColor.propTypes = { - rgbaColor: PropTypes.object, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - onChange: PropTypes.func, -}; - -HexColor.defaultProps = { - rgbaColor: '', - tabIndex: undefined, - onChange: () => {}, -}; diff --git a/src/components/ColorPicker/hue.js b/src/components/ColorPicker/hue.js index 2a2cbf341..7b454a39b 100644 --- a/src/components/ColorPicker/hue.js +++ b/src/components/ColorPicker/hue.js @@ -1,45 +1,21 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; import { StyledHueSlider } from './styled'; import { hsvToRgb, rgbToRgba } from '../../styles/helpers/color'; +import { ColorPickerContext } from './context'; -export default function Hue(props) { - const { hsvColor, hue, setHue, alpha, tabIndex, onChange } = props; +export default function Hue() { + const { h, s, v, a, setHue, tabIndex, onChange } = useContext(ColorPickerContext); const handleChange = event => { const value = parseInt(event.target.value, 10); setHue(value); - hsvColor.values[0] = value; - const rgbColor = hsvToRgb(hsvColor); - const rgbaColor = rgbToRgba(rgbColor, alpha); + const hsv = `hsv(${value}, ${s}, ${v})`; + const rgbColor = hsvToRgb(hsv); + const rgbaColor = rgbToRgba(rgbColor, a); onChange(rgbaColor); }; return ( - + ); } - -Hue.propTypes = { - hsvColor: PropTypes.object, - alpha: PropTypes.number, - hue: PropTypes.number, - setHue: PropTypes.func, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - onChange: PropTypes.func, -}; - -Hue.defaultProps = { - hsvColor: undefined, - alpha: undefined, - hue: undefined, - setHue: () => {}, - tabIndex: undefined, - onChange: () => {}, -}; diff --git a/src/components/ColorPicker/index.d.ts b/src/components/ColorPicker/index.d.ts index e61a3eeb6..dcf4e722a 100644 --- a/src/components/ColorPicker/index.d.ts +++ b/src/components/ColorPicker/index.d.ts @@ -2,8 +2,13 @@ import { BaseProps } from '../types'; export interface ColorPickerProps extends BaseProps { id?: string; - color?: string; + value?: string; + colors?: Array[string]; + tabIndex?: stirng | number; onChange?: (value: string) => void; + onClick?: (event: MouseEvent) => void; + onFocus?: (event: FocusEvent) => void; + onBlur?: (event: FocusEvent) => void; } export default function(props: ColorPickerProps): JSX.Element | null; diff --git a/src/components/ColorPicker/index.js b/src/components/ColorPicker/index.js index c6fabb8c0..18b9c79fe 100644 --- a/src/components/ColorPicker/index.js +++ b/src/components/ColorPicker/index.js @@ -1,15 +1,16 @@ +/* eslint-disable import/no-unresolved */ import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'; import PropTypes from 'prop-types'; -import RenderIf from '../RenderIf'; +import { Provider } from './context'; import Saturation from './saturation'; import RgbaColor from './rgbaColor'; import HexColor from './hexColor'; import Alpha from './alpha'; import Hue from './hue'; import DefaultColors from './defaultColors'; +import Preview from './preview'; import { StyledContainer, - StyledPreview, StyledFlexContainer, StyledLabel, StyledSaturationContainer, @@ -24,21 +25,16 @@ import useNormalizeColors from './hooks/useNormalizeColors'; const ColorPicker = React.forwardRef((props, ref) => { const { id, - color: colorProp, + value, colors: colorsProp, - title, - titleColors, tabIndex, + onClick, onChange, + onFocus, + onBlur, className, style, } = props; - const [hue, setHue] = useState(0); - - const color = normalizeColor(colorProp); - const rgbaColor = decomposeColor(color); - const hsvColor = decomposeColor(rgbToHsv(color)); - const alpha = rgbaColor.values[3]; const firstRef = useRef(); const lastRef = useRef(); @@ -55,62 +51,61 @@ const ColorPicker = React.forwardRef((props, ref) => { }, })); - useEffect(() => { - if (!isAchromatic(rgbaColor)) { - setHue(hsvColor.values[0]); - } - }, [hsvColor.values, rgbaColor]); + const [hue, setHue] = useState(0); + + const color = normalizeColor(value); + const rgba = decomposeColor(color); + const [r, g, b, a] = rgba.values; + const [h, s, v] = decomposeColor(rgbToHsv(color)).values; const colors = useNormalizeColors(colorsProp); - const hasColors = colors.length > 0; - const styleColor = { backgroundColor: color }; + const context = { + r, + g, + b, + a, + h: hue, + s, + v, + colors, + tabIndex, + onClick, + onChange, + onFocus, + onBlur, + setHue, + }; + + useEffect(() => { + if (!isAchromatic(rgba)) { + setHue(h); + } + }, [h, rgba]); return ( - {title} - - - - - - - - - - - - - - - - - - - - - - + + Color Picker + + + + + + + + + + + + + + + + + + + + ); }); @@ -119,17 +114,19 @@ ColorPicker.propTypes = { /** The id of the outer element. */ id: PropTypes.string, /** Specifies the color of ColorPicker. */ - color: PropTypes.string, + value: PropTypes.string, /** Specifies the default colors to choice. */ colors: PropTypes.array, - /** Text title for the Color Picker. */ - title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - /** Text title for the Default Colors section. */ - titleColors: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), /** Specifies the tab order of an element (when the tab button is used for navigating). */ tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** The action triggered when the element is clicked. */ + onClick: PropTypes.func, /** The action triggered when the value changes. */ onChange: PropTypes.func, + /** The action triggered when the element receives the focus. */ + onFocus: PropTypes.func, + /** The action triggered when the element releases focus. */ + onBlur: PropTypes.func, /** A CSS class for the outer element, in addition to the component's base classes. */ className: PropTypes.string, /** An object with custom style applied to the outer element. */ @@ -138,7 +135,7 @@ ColorPicker.propTypes = { ColorPicker.defaultProps = { id: undefined, - color: '#000000', + value: '#000000', colors: [ '#e3aaec', '#c3dbf7', @@ -169,10 +166,11 @@ ColorPicker.defaultProps = { '#b67e12', '#b75d0c', ], - title: 'Color Picker', - titleColors: 'Defualt', tabIndex: undefined, + onClick: () => {}, onChange: () => {}, + onFocus: () => {}, + onBlur: () => {}, className: undefined, style: undefined, }; diff --git a/src/components/ColorPicker/preview.js b/src/components/ColorPicker/preview.js new file mode 100644 index 000000000..30a304036 --- /dev/null +++ b/src/components/ColorPicker/preview.js @@ -0,0 +1,11 @@ +import React, { useContext } from 'react'; +import { ColorPickerContext } from './context'; +import { StyledPreview } from './styled'; + +export default function Preview() { + const { r, g, b, a } = useContext(ColorPickerContext); + const rgba = `rgba(${r}, ${g}, ${b}, ${a})`; + const style = { backgroundColor: rgba }; + + return ; +} diff --git a/src/components/ColorPicker/readme.md b/src/components/ColorPicker/readme.md index ee440d7d4..363d7467b 100644 --- a/src/components/ColorPicker/readme.md +++ b/src/components/ColorPicker/readme.md @@ -19,7 +19,7 @@ const StyledCard = styled(Card)` const ColorPickerExample = () => { const [color, setColor] = useState(); - return ; + return ; } diff --git a/src/components/ColorPicker/rgbaColor.js b/src/components/ColorPicker/rgbaColor.js index 1a3f699a9..fb16d2332 100644 --- a/src/components/ColorPicker/rgbaColor.js +++ b/src/components/ColorPicker/rgbaColor.js @@ -1,36 +1,34 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; +import { ColorPickerContext } from './context'; import { StyledFlexContainer, StyledNumberInput } from './styled'; -import { getAlpha } from './helpers'; -import { recomposeColor } from '../../styles/helpers/color'; +import { recomposeColor, decomposeColor } from '../../styles/helpers/color'; -export default function RgbaColor(props) { - const { rgbaColor, tabIndex, onChange } = props; - const [r, g, b] = rgbaColor.values; - const a = getAlpha(rgbaColor); +export default function RgbaColor() { + const { r, g, b, a, tabIndex, onChange } = useContext(ColorPickerContext); const handleAlphaChange = event => { - const alpha = parseInt(event.target.value, 10); + const value = parseInt(event.target.value, 10); + const newApha = isNaN(value) ? 0 : Math.max(0, Math.min(value, 100)) / 100; + const rgba = `rgba(${r}, ${g}, ${b}, ${newApha})`; - if (isNaN(alpha)) { - rgbaColor.values[3] = 0; - } else { - rgbaColor.values[3] = Math.max(0, Math.min(alpha, 100)) / 100; - } - onChange(recomposeColor(rgbaColor)); + onChange(rgba); }; const handleChange = (index, event) => { const value = parseInt(event.target.value, 10); + const rgba = decomposeColor(`rgba(${r}, ${g}, ${b}, ${a})`); if (isNaN(value)) { - rgbaColor.values[index] = 0; + rgba.values[index] = 0; } else { - rgbaColor.values[index] = Math.max(0, Math.min(value, 255)); + rgba.values[index] = Math.max(0, Math.min(value, 255)); } - onChange(recomposeColor(rgbaColor)); + + onChange(recomposeColor(rgba)); }; + const alpha = Math.round(a * 100); + return ( ); } - -RgbaColor.propTypes = { - rgbaColor: PropTypes.object, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - onChange: PropTypes.func, -}; - -RgbaColor.defaultProps = { - rgbaColor: '', - tabIndex: undefined, - onChange: () => {}, -}; diff --git a/src/components/ColorPicker/saturation/index.js b/src/components/ColorPicker/saturation/index.js index 4c14fc7c6..6f22c458f 100644 --- a/src/components/ColorPicker/saturation/index.js +++ b/src/components/ColorPicker/saturation/index.js @@ -1,23 +1,20 @@ -import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; +import React, { useRef, useEffect, useContext, useState } from 'react'; import { UP_KEY, DOWN_KEY, RIGHT_KEY, LEFT_KEY } from '../../../../libs/constants'; import { hsvToRgb, rgbToRgba } from '../../../styles/helpers/color'; -import { StyledColor, StyledPointer, StyledCircle } from './styled'; +import { StyledColor, StyledCircle } from './styled'; +import { ColorPickerContext } from '../context'; -const Saturation = React.forwardRef((props, ref) => { - const { rgbaColor, hsvColor, tabIndex, hue, onChange } = props; - const hslColor = `hsl(${hue},100%, 50%)`; - const alpha = rgbaColor.values[3]; +const Saturation = React.forwardRef((_props, ref) => { + const { h, s, v, a, tabIndex, onChange } = useContext(ColorPickerContext); const containerRef = useRef(); - - const change = ({ saturation, bright }) => { - if (saturation) { - hsvColor.values[1] = saturation; - } - if (bright) { - hsvColor.values[2] = bright; - } - const color = rgbToRgba(hsvToRgb(hsvColor), alpha); + const [saturation, setSaturation] = useState(s); + const [bright, setBright] = useState(v); + + const change = values => { + const newSaturation = values.saturation || saturation; + const newBright = values.bright || bright; + const newHsv = `hsv(${h}, ${newSaturation}, ${newBright})`; + const color = rgbToRgba(hsvToRgb(newHsv), a); onChange(color); }; @@ -29,10 +26,12 @@ const Saturation = React.forwardRef((props, ref) => { const left = Math.min(Math.max(0, x - (rect.left + window.pageXOffset)), width); const top = Math.min(Math.max(0, y - (rect.top + window.pageYOffset)), height); - const saturation = Math.round((left / width) * 100); - const bright = Math.round((1 - top / height) * 100); + const newSaturation = Math.round((left / width) * 100); + const newBright = Math.round((1 - top / height) * 100); - change({ saturation, bright }); + setSaturation(newSaturation); + setBright(newBright); + change({ saturation: newSaturation, bright: newBright }); }; const unbindEventListeners = () => { @@ -51,20 +50,24 @@ const Saturation = React.forwardRef((props, ref) => { const keyHandlerMap = { [UP_KEY]: () => { - const bright = Math.min(100, hsvColor.values[2] + 1); - change({ bright }); + const newBright = Math.min(100, bright + 1); + setBright(newBright); + change({ bright: newBright }); }, [DOWN_KEY]: () => { - const bright = Math.max(0, hsvColor.values[2] - 1); - change({ bright }); + const newBright = Math.max(0, bright - 1); + setBright(newBright); + change({ bright: newBright }); }, [LEFT_KEY]: () => { - const saturation = Math.max(0, hsvColor.values[1] - 1); - change({ saturation }); + const newSaturation = Math.max(0, saturation - 1); + setSaturation(newSaturation); + change({ saturation: newSaturation }); }, [RIGHT_KEY]: () => { - const saturation = Math.min(100, hsvColor.values[1] + 1); - change({ saturation }); + const newSaturation = Math.min(100, saturation + 1); + setSaturation(newSaturation); + change({ saturation: newSaturation }); }, }; @@ -81,8 +84,24 @@ const Saturation = React.forwardRef((props, ref) => { ref.current.focus(); }; - const styleColor = { background: hslColor }; - const stylePointer = { top: `${100 - hsvColor.values[2]}%`, left: `${hsvColor.values[1]}%` }; + const isFocusedRef = useRef(false); + const handleFocus = () => { + isFocusedRef.current = true; + }; + const handleBlur = () => { + isFocusedRef.current = false; + }; + + useEffect(() => { + if (!isFocusedRef.current) { + setSaturation(s); + setBright(v); + } + }, [s, v]); + + const hsl = `hsl(${h},100%, 50%)`; + const styleColor = { background: hsl }; + const stylePointer = { top: `${100 - bright}%`, left: `${saturation}%` }; return ( { onKeyDown={handleKeyDown} onClick={handleClick} > - - - + ); }); -Saturation.propTypes = { - rgbaColor: PropTypes.object.isRequired, - hsvColor: PropTypes.object.isRequired, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - hue: PropTypes.number.isRequired, - onChange: PropTypes.func, -}; - -Saturation.defaultProps = { - rgbaColor: undefined, - hsvColor: undefined, - tabIndex: undefined, - hue: undefined, - onChange: () => {}, -}; - export default Saturation; diff --git a/src/components/ColorPicker/saturation/styled.js b/src/components/ColorPicker/saturation/styled.js index 62a7f9573..4bc4de6f5 100644 --- a/src/components/ColorPicker/saturation/styled.js +++ b/src/components/ColorPicker/saturation/styled.js @@ -4,9 +4,11 @@ import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; export const StyledColor = styled.div` position: relative; height: 100%; + overflow: hidden; - &:before { + &::before { content: ''; + overflow: hidden; position: absolute; top: 0; right: 0; @@ -19,15 +21,19 @@ export const StyledColor = styled.div` export const StyledPointer = styled.div` position: absolute; + width: 0; + height: 0; `; export const StyledCircle = attachThemeAttrs(styled.button)` + position: absolute; width: 12px; height: 12px; border-radius: 6px; box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset; transform: translate(-6px, -6px); padding: 0; + margin: 0; border: 1px solid ${props => props.palette.border.divider}; &:focus, diff --git a/src/components/ColorPicker/styled.js b/src/components/ColorPicker/styled.js index 30b229c42..309dbbb94 100644 --- a/src/components/ColorPicker/styled.js +++ b/src/components/ColorPicker/styled.js @@ -73,12 +73,14 @@ export const StyledHexColorIcon = attachThemeAttrs(styled.span)` export const StyledNumberInput = styled(Input)` margin-left: 5px; + input::-webkit-inner-spin-button { - -webkit-appearance: none; + appearance: none; margin: 0; } + input::-webkit-outer-spin-button { - -webkit-appearance: none; + appearance: none; margin: 0; } `; From 1cc5d1fa4eada1f653a1c07ef7344c93b943103b Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Tue, 25 Aug 2020 13:40:47 -0400 Subject: [PATCH 04/11] fix: unresolve import --- src/components/ColorPicker/hooks/useNormalizeColors.js | 2 +- src/components/ColorPicker/saturation/index.js | 2 +- src/components/ColorPicker/styled.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ColorPicker/hooks/useNormalizeColors.js b/src/components/ColorPicker/hooks/useNormalizeColors.js index 11eac63bd..400dc6783 100644 --- a/src/components/ColorPicker/hooks/useNormalizeColors.js +++ b/src/components/ColorPicker/hooks/useNormalizeColors.js @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import isValidColor from '../../../../styles/helpers/color/isValidColor'; +import isValidColor from '../../../styles/helpers/color/isValidColor'; export default function useNormalizeColors(colors) { return useMemo(() => { diff --git a/src/components/ColorPicker/saturation/index.js b/src/components/ColorPicker/saturation/index.js index 6f22c458f..bbc7c487f 100644 --- a/src/components/ColorPicker/saturation/index.js +++ b/src/components/ColorPicker/saturation/index.js @@ -1,5 +1,5 @@ import React, { useRef, useEffect, useContext, useState } from 'react'; -import { UP_KEY, DOWN_KEY, RIGHT_KEY, LEFT_KEY } from '../../../../libs/constants'; +import { UP_KEY, DOWN_KEY, RIGHT_KEY, LEFT_KEY } from '../../../libs/constants'; import { hsvToRgb, rgbToRgba } from '../../../styles/helpers/color'; import { StyledColor, StyledCircle } from './styled'; import { ColorPickerContext } from '../context'; diff --git a/src/components/ColorPicker/styled.js b/src/components/ColorPicker/styled.js index 309dbbb94..48950c61d 100644 --- a/src/components/ColorPicker/styled.js +++ b/src/components/ColorPicker/styled.js @@ -1,9 +1,9 @@ import styled, { css } from 'styled-components'; -import { FONT_SIZE_TEXT_MEDIUM } from '../../../styles/fontSizes'; -import { PADDING_SMALL, PADDING_XX_SMALL } from '../../../styles/paddings'; -import { BORDER_RADIUS_3, BORDER_RADIUS_2 } from '../../../styles/borderRadius'; +import { FONT_SIZE_TEXT_MEDIUM } from '../../styles/fontSizes'; +import { PADDING_SMALL, PADDING_XX_SMALL } from '../../styles/paddings'; +import { BORDER_RADIUS_3, BORDER_RADIUS_2 } from '../../styles/borderRadius'; import { COLOR_GRAY_1 } from '../../styles/colors'; -import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; +import attachThemeAttrs from '../../styles/helpers/attachThemeAttrs'; import Input from '../Input'; import Slider from './slider'; From fc6252e9ad2a8c005a01e016d47c4bb77a2d0aa9 Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Tue, 25 Aug 2020 15:17:25 -0400 Subject: [PATCH 05/11] fix: fix isHexColor test --- src/styles/helpers/color/__test__/isHexColor.spec.js | 4 ++-- src/styles/helpers/color/hexToRgb.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/styles/helpers/color/__test__/isHexColor.spec.js b/src/styles/helpers/color/__test__/isHexColor.spec.js index 44d90b402..75a1e5200 100644 --- a/src/styles/helpers/color/__test__/isHexColor.spec.js +++ b/src/styles/helpers/color/__test__/isHexColor.spec.js @@ -4,14 +4,14 @@ describe('isHexColor', () => { it('should recognize 6-digit and 3-digit hex colors', () => { const colors = ['#ffffff', '#FF0000', '#f0c34a', '#fff', '#F00', '#f89']; colors.forEach(color => { - expect(isHexColor(color)).toBeTrue(); + expect(isHexColor(color)).toBe(true); }); }); it('should recognize non hex colors', () => { const colors = ['a', 'FFFFFF', 232323]; colors.forEach(color => { - expect(isHexColor(color)).toBeFalse(); + expect(isHexColor(color)).toBe(false); }); }); }); diff --git a/src/styles/helpers/color/hexToRgb.js b/src/styles/helpers/color/hexToRgb.js index b85587074..c5b1de32b 100644 --- a/src/styles/helpers/color/hexToRgb.js +++ b/src/styles/helpers/color/hexToRgb.js @@ -1,6 +1,6 @@ import isHexColor from './isHexColor'; -export default function hexToRgba(color, alpha = 1) { +export default function hexToRgb(color) { if (isHexColor(color)) { const hexColor = color.substr(1); const re = new RegExp(`.{1,${hexColor.length / 3}}`, 'g'); @@ -14,7 +14,7 @@ export default function hexToRgba(color, alpha = 1) { return n; }); - return `rgba(${colors.map(n => parseInt(n, 16)).join(', ')}, ${alpha})`; + return `rgb(${colors.map(n => parseInt(n, 16)).join(', ')})`; } } return ''; From d28e630a0a74b6f6dcc914bff6f78dd22be6c10f Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Sat, 29 Aug 2020 19:01:17 -0400 Subject: [PATCH 06/11] fix: change the api --- src/components/ColorPicker/alpha.js | 25 -- .../ColorPicker/defaultColors/color.js | 56 ---- .../ColorPicker/defaultColors/index.js | 43 ---- .../ColorPicker/defaultColors/styled.js | 38 --- .../helpers/__test__/normalizeColor.spec.js | 22 ++ .../ColorPicker/helpers/getAlpha.js | 3 - src/components/ColorPicker/helpers/index.js | 3 - .../ColorPicker/helpers/isAchromatic.js | 7 - .../ColorPicker/helpers/normalizeColor.js | 49 +++- src/components/ColorPicker/hexColor.js | 45 ---- .../hooks/__test__/useNormalizeColors.spec.js | 50 ++++ .../ColorPicker/hooks/useNormalizeColors.js | 53 +++- src/components/ColorPicker/hue.js | 21 -- src/components/ColorPicker/index.d.ts | 19 +- src/components/ColorPicker/index.js | 124 ++------- src/components/ColorPicker/preview.js | 11 - src/components/ColorPicker/readme.md | 105 +++++++- src/components/ColorPicker/rgbaColor.js | 64 ----- .../ColorPicker/saturation/index.js | 128 ---------- .../ColorPicker/saturation/styled.js | 45 ---- src/components/ColorPicker/slider.js | 40 --- src/components/ColorPicker/styled.js | 240 +----------------- .../ColorPicker/variants/colorsFixed.js | 22 ++ .../ColorPicker/variants/default.js | 64 +++++ src/components/ColorPicker/variants/index.js | 2 + src/components/ColorPicker/variants/styled.js | 24 ++ src/styles/helpers/color/index.js | 1 + src/styles/helpers/color/isHsvColor.js | 16 ++ 28 files changed, 432 insertions(+), 888 deletions(-) delete mode 100644 src/components/ColorPicker/alpha.js delete mode 100644 src/components/ColorPicker/defaultColors/color.js delete mode 100644 src/components/ColorPicker/defaultColors/index.js delete mode 100644 src/components/ColorPicker/defaultColors/styled.js create mode 100644 src/components/ColorPicker/helpers/__test__/normalizeColor.spec.js delete mode 100644 src/components/ColorPicker/helpers/getAlpha.js delete mode 100644 src/components/ColorPicker/helpers/index.js delete mode 100644 src/components/ColorPicker/helpers/isAchromatic.js delete mode 100644 src/components/ColorPicker/hexColor.js create mode 100644 src/components/ColorPicker/hooks/__test__/useNormalizeColors.spec.js delete mode 100644 src/components/ColorPicker/hue.js delete mode 100644 src/components/ColorPicker/preview.js delete mode 100644 src/components/ColorPicker/rgbaColor.js delete mode 100644 src/components/ColorPicker/saturation/index.js delete mode 100644 src/components/ColorPicker/saturation/styled.js delete mode 100644 src/components/ColorPicker/slider.js create mode 100644 src/components/ColorPicker/variants/colorsFixed.js create mode 100644 src/components/ColorPicker/variants/default.js create mode 100644 src/components/ColorPicker/variants/index.js create mode 100644 src/components/ColorPicker/variants/styled.js create mode 100644 src/styles/helpers/color/isHsvColor.js diff --git a/src/components/ColorPicker/alpha.js b/src/components/ColorPicker/alpha.js deleted file mode 100644 index 0b23f1a07..000000000 --- a/src/components/ColorPicker/alpha.js +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useContext } from 'react'; -import { StyledAlphaSlider } from './styled'; -import { ColorPickerContext } from './context'; - -export default function Alpha() { - const { r, g, b, a, tabIndex, onChange } = useContext(ColorPickerContext); - - const handleChange = event => { - const alpha = parseInt(event.target.value, 10); - const rgba = `rgba(${r}, ${g}, ${b}, ${alpha / 100})`; - onChange(rgba); - }; - - const value = Math.round(a * 100); - - return ( - - ); -} diff --git a/src/components/ColorPicker/defaultColors/color.js b/src/components/ColorPicker/defaultColors/color.js deleted file mode 100644 index 9e681433c..000000000 --- a/src/components/ColorPicker/defaultColors/color.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { StyledColor, StyledInput, StyledLabel } from './styled'; -import AssistiveText from '../../AssistiveText'; -import { useUniqueIdentifier } from '../../../libs/hooks'; -import { colorToRgba } from '../../../styles/helpers/color'; - -const Color = React.forwardRef((props, ref) => { - const { color, name, tabIndex, isChecked, onChange } = props; - const colorId = useUniqueIdentifier('color-picker-default'); - - const handleChange = () => { - const newColor = colorToRgba(color); - if (newColor !== '') { - onChange(newColor); - } - }; - - const style = { backgroundColor: color }; - return ( - - - - {color} - - - ); -}); - -Color.propTypes = { - color: PropTypes.string.isRequired, - name: PropTypes.string, - isChecked: PropTypes.bool, - onChange: PropTypes.func, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -Color.defaultProps = { - color: undefined, - name: '', - isChecked: false, - onChange: () => {}, - tapIndex: undefined, -}; - -export default Color; diff --git a/src/components/ColorPicker/defaultColors/index.js b/src/components/ColorPicker/defaultColors/index.js deleted file mode 100644 index bdf992128..000000000 --- a/src/components/ColorPicker/defaultColors/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useContext } from 'react'; -import { ColorPickerContext } from '../context'; -import { StyledContainer, StyledColors } from './styled'; -import { colorToRgba } from '../../../styles/helpers/color'; -import { useUniqueIdentifier } from '../../../libs/hooks'; -import Color from './color'; -import RenderIf from '../../RenderIf'; - -const DefaultColors = React.forwardRef((_props, ref) => { - const { colors, r, g, b, a, tabIndex: tabIndexProp, onChange } = useContext(ColorPickerContext); - const rgba = `rgba(${r}, ${g}, ${b}, ${a})`; - const name = useUniqueIdentifier('color-picker-default'); - - const listColors = colors.map((color, index) => { - const tabIndex = index === 0 ? tabIndexProp : -1; - const isSelected = colorToRgba(color) === rgba; - const isFirstInput = index === 0; - const inputRef = isFirstInput ? ref : undefined; - return ( - - ); - }); - - const hasColors = colors.length > 0; - - return ( - - - {listColors} - - - ); -}); - -export default DefaultColors; diff --git a/src/components/ColorPicker/defaultColors/styled.js b/src/components/ColorPicker/defaultColors/styled.js deleted file mode 100644 index f0ea19ec5..000000000 --- a/src/components/ColorPicker/defaultColors/styled.js +++ /dev/null @@ -1,38 +0,0 @@ -import styled from 'styled-components'; -import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; -import HiddenElement from '../../Structural/hiddenElement'; -import { CssCircleColor } from '../styled'; - -export const StyledContainer = styled.div` - flex: 0 0 auto; - padding: 0.5rem 0 0.25rem; -`; - -export const StyledColors = styled.div` - text-align: center; -`; - -export const StyledColor = styled.span` - line-height: inherit; - height: inherit; -`; - -export const StyledInput = attachThemeAttrs(styled(HiddenElement))` - &:focus + label { - border: 1px solid ${props => props.palette.brand.main}; - box-shadow: ${props => props.shadows.brand}; - } -`; - -export const StyledLabel = attachThemeAttrs(styled.label)` - display: inline-block; - margin: 0.25rem 0.45rem; - width: 40px; - height: 40px; - padding: 0; - ${CssCircleColor} - - &:hover { - cursor: pointer; - } -`; diff --git a/src/components/ColorPicker/helpers/__test__/normalizeColor.spec.js b/src/components/ColorPicker/helpers/__test__/normalizeColor.spec.js new file mode 100644 index 000000000..8d244706a --- /dev/null +++ b/src/components/ColorPicker/helpers/__test__/normalizeColor.spec.js @@ -0,0 +1,22 @@ +import normalizeColor, { defaultColor } from '../normalizeColor'; + +describe('normalizeColor', () => { + it('should return default value', () => { + const values = ['', 1, [], {}, true, { hex: '', rgba: '', hvs: '' }]; + values.forEach(value => { + expect(normalizeColor(value)).toBe(defaultColor); + }); + }); + it('should return the expect value', () => { + const values = [ + { hex: '#ffffff' }, + { rgba: [255, 255, 255, 1] }, + { hsv: [0, 0, 100] }, + { hex: '#ffffff', rgba: [255, 255, 255, 1] }, + ]; + const result = { hex: '#ffffff', rgba: [255, 255, 255, 1], hsv: [0, 0, 100] }; + values.forEach(value => { + expect(normalizeColor(value)).toStrictEqual(result); + }); + }); +}); diff --git a/src/components/ColorPicker/helpers/getAlpha.js b/src/components/ColorPicker/helpers/getAlpha.js deleted file mode 100644 index de91c8e1f..000000000 --- a/src/components/ColorPicker/helpers/getAlpha.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getAlpha(rgbaColor) { - return Math.round(rgbaColor.values[3] * 100); -} diff --git a/src/components/ColorPicker/helpers/index.js b/src/components/ColorPicker/helpers/index.js deleted file mode 100644 index 7f785bea3..000000000 --- a/src/components/ColorPicker/helpers/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { default as getAlpha } from './getAlpha'; -export { default as normalizeColor } from './normalizeColor'; -export { default as isAchromatic } from './isAchromatic'; diff --git a/src/components/ColorPicker/helpers/isAchromatic.js b/src/components/ColorPicker/helpers/isAchromatic.js deleted file mode 100644 index 31481aa7a..000000000 --- a/src/components/ColorPicker/helpers/isAchromatic.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function isAchromatic(color) { - const [r, g, b] = color.values; - - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - return max === min; -} diff --git a/src/components/ColorPicker/helpers/normalizeColor.js b/src/components/ColorPicker/helpers/normalizeColor.js index dfe11b966..7701aa99a 100644 --- a/src/components/ColorPicker/helpers/normalizeColor.js +++ b/src/components/ColorPicker/helpers/normalizeColor.js @@ -1,6 +1,47 @@ -import { colorToRgba } from '../../../styles/helpers/color'; +import { + isValidColor, + recomposeColor, + hexToRgba, + decomposeColor, + hsvToRgb, + rgbaToHex, + rgbToHsv, + rgbToRgba, + isHsvColor, +} from '../../../styles/helpers/color'; -export default function normalizeColor(color) { - const rgbaColor = colorToRgba(color); - return rgbaColor !== '' ? rgbaColor : 'rgba(0, 0, 0, 1)'; +function getRgba(value, isValidHex) { + if (isValidHex) { + return hexToRgba(value.hex); + } + return rgbToRgba(hsvToRgb(recomposeColor({ type: 'hsv', values: value.hsv })), 1); +} + +export const defaultColor = { + hex: '#000000', + rgba: [0, 0, 0, 1], + hsv: [0, 0, 0], +}; + +export default function normalizeColor(value) { + const { hex, rgba, hsv } = value; + const isValidHex = isValidColor(hex); + const isValidRgba = isValidColor(recomposeColor({ type: 'rgba', values: rgba })); + const isValidHsv = isHsvColor(recomposeColor({ type: 'hsv', values: hsv })); + + if (!isValidHex && !isValidRgba && !isValidHsv) { + return defaultColor; + } + + const rgbaColor = isValidRgba + ? recomposeColor({ type: 'rgba', values: rgba }) + : getRgba(value, isValidHex); + const hexColor = isValidHex ? hex : `#${rgbaToHex(rgbaColor)}`; + const hsvColor = isValidHsv ? hsv : decomposeColor(rgbToHsv(rgbaColor)).values; + + return { + hex: hexColor, + rgba: decomposeColor(rgbaColor).values, + hsv: hsvColor, + }; } diff --git a/src/components/ColorPicker/hexColor.js b/src/components/ColorPicker/hexColor.js deleted file mode 100644 index 540c67468..000000000 --- a/src/components/ColorPicker/hexColor.js +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import Input from '../Input'; -import { StyledHexColorIcon } from './styled'; -import { hexToRgba, rgbaToHex } from '../../styles/helpers/color'; -import { ColorPickerContext } from './context'; - -export default function HexColor() { - const { r, g, b, a, tabIndex, onChange } = useContext(ColorPickerContext); - const rgba = `rgba(${r}, ${g}, ${b}, ${a})`; - const [color, setColor] = useState(rgbaToHex(rgba).substr(1)); - const [isFocused, setIsFocused] = useState(false); - - useEffect(() => { - if (!isFocused) { - const hexColor = rgbaToHex(rgba); - setColor(hexColor.substr(1)); - } - }, [isFocused, rgba]); - - useEffect(() => { - const hex = `#${color}`; - const newColor = hexToRgba(hex); - if (newColor !== '') { - onChange(newColor); - } - }, [color, onChange]); - - const handleBlur = () => { - setIsFocused(false); - const hexColor = rgbaToHex(rgba); - setColor(hexColor.substr(1)); - }; - - return ( - setColor(event.target.value)} - onFocus={() => setIsFocused(true)} - onBlur={handleBlur} - icon={#} - tabIndex={tabIndex} - /> - ); -} diff --git a/src/components/ColorPicker/hooks/__test__/useNormalizeColors.spec.js b/src/components/ColorPicker/hooks/__test__/useNormalizeColors.spec.js new file mode 100644 index 000000000..e5bd8fb59 --- /dev/null +++ b/src/components/ColorPicker/hooks/__test__/useNormalizeColors.spec.js @@ -0,0 +1,50 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useNormalizeColors, { defaultColors } from '../useNormalizeColors'; + +describe('useNormalizeColors', () => { + it('should return empty array', () => { + const values = ['', null, true, 0, {}, [], ['', 'qwww']]; + + values.forEach(value => { + const { result } = renderHook(() => useNormalizeColors({ defaultColors: value })); + expect(result.current).toStrictEqual([]); + }); + }); + + it('should return array of valid colors', () => { + const values = [ + ['#5ebbff', '#42d8be', '#3be282'], + ['#5ebbff', 'qwe', '#42d8be', '#3be282'], + ]; + + values.forEach(value => { + const { result } = renderHook(() => useNormalizeColors({ defaultColors: value })); + expect(result.current).toStrictEqual(values[0]); + }); + }); + + it('should return default colors', () => { + const values = [null, [], ['', 'qwww']]; + + values.forEach(value => { + const { result } = renderHook(() => + useNormalizeColors({ defaultColors: value, variant: 'colors-fixed' }), + ); + expect(result.current).toStrictEqual(defaultColors); + }); + }); + + it('should return array of valid colors when the variant is colors-fixed', () => { + const values = [ + ['#5ebbff', '#42d8be', '#3be282'], + ['#5ebbff', 'qwe', '#42d8be', '#3be282'], + ]; + + values.forEach(value => { + const { result } = renderHook(() => + useNormalizeColors({ defaultColors: value, variant: 'colors-fixed' }), + ); + expect(result.current).toStrictEqual(values[0]); + }); + }); +}); diff --git a/src/components/ColorPicker/hooks/useNormalizeColors.js b/src/components/ColorPicker/hooks/useNormalizeColors.js index 400dc6783..0c05f5bef 100644 --- a/src/components/ColorPicker/hooks/useNormalizeColors.js +++ b/src/components/ColorPicker/hooks/useNormalizeColors.js @@ -1,11 +1,54 @@ import { useMemo } from 'react'; import isValidColor from '../../../styles/helpers/color/isValidColor'; -export default function useNormalizeColors(colors) { +export const defaultColors = [ + '#e3aaec', + '#c3dbf7', + '#9fd6ff', + '#9de7da', + '#9ef0bf', + '#fef199', + '#fdd499', + '#d174e0', + '#86baf3', + '#5ebbff', + '#42d8be', + '#3be282', + '#ffe654', + '#ffb758', + '#bd35bd', + '#5779c1', + '#4A90E2', + '#06aea9', + '#3dba4c', + '#f5bc24', + '#f99222', + '#570e8c', + '#021970', + '#0b2399', + '#0d7477', + '#0a6b50', + '#b67e12', + '#b75d0c', +]; + +export default function useNormalizeColors(props) { + const { defaultColors: colors, variant } = props; return useMemo(() => { - if (Array.isArray(colors)) { - return colors.filter(color => isValidColor(color)); + const validColors = Array.isArray(colors) + ? colors.filter(color => isValidColor(color)) + : []; + + if (variant === 'colors-fixed') { + if (!validColors.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'If the props variant is colors-fixed, the defaultColors prop must contain at least one valid color', + ); + return defaultColors; + } } - return []; - }, [colors]); + + return validColors; + }, [colors, variant]); } diff --git a/src/components/ColorPicker/hue.js b/src/components/ColorPicker/hue.js deleted file mode 100644 index 7b454a39b..000000000 --- a/src/components/ColorPicker/hue.js +++ /dev/null @@ -1,21 +0,0 @@ -import React, { useContext } from 'react'; -import { StyledHueSlider } from './styled'; -import { hsvToRgb, rgbToRgba } from '../../styles/helpers/color'; -import { ColorPickerContext } from './context'; - -export default function Hue() { - const { h, s, v, a, setHue, tabIndex, onChange } = useContext(ColorPickerContext); - - const handleChange = event => { - const value = parseInt(event.target.value, 10); - setHue(value); - const hsv = `hsv(${value}, ${s}, ${v})`; - const rgbColor = hsvToRgb(hsv); - const rgbaColor = rgbToRgba(rgbColor, a); - onChange(rgbaColor); - }; - - return ( - - ); -} diff --git a/src/components/ColorPicker/index.d.ts b/src/components/ColorPicker/index.d.ts index dcf4e722a..75ff9de90 100644 --- a/src/components/ColorPicker/index.d.ts +++ b/src/components/ColorPicker/index.d.ts @@ -1,14 +1,19 @@ import { BaseProps } from '../types'; +import { ReactNode, MouseEvent, FocusEvent, KeyboardEvent } from 'react'; + +export interface ColorPickerValue { + hex?: string; + rgba?: Array; + hsv?: Array; +} export interface ColorPickerProps extends BaseProps { id?: string; - value?: string; - colors?: Array[string]; - tabIndex?: stirng | number; - onChange?: (value: string) => void; - onClick?: (event: MouseEvent) => void; - onFocus?: (event: FocusEvent) => void; - onBlur?: (event: FocusEvent) => void; + value?: ColorPickerValue; + defaultColors?: Array; + variant: 'default' | 'colors-fixed'; + tabIndex?: string | number; + onChange?: (value: ColorPickerValue) => void; } export default function(props: ColorPickerProps): JSX.Element | null; diff --git a/src/components/ColorPicker/index.js b/src/components/ColorPicker/index.js index 18b9c79fe..043a941ac 100644 --- a/src/components/ColorPicker/index.js +++ b/src/components/ColorPicker/index.js @@ -1,110 +1,32 @@ /* eslint-disable import/no-unresolved */ -import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { Provider } from './context'; -import Saturation from './saturation'; -import RgbaColor from './rgbaColor'; -import HexColor from './hexColor'; -import Alpha from './alpha'; -import Hue from './hue'; -import DefaultColors from './defaultColors'; -import Preview from './preview'; -import { - StyledContainer, - StyledFlexContainer, - StyledLabel, - StyledSaturationContainer, - StyledSlidersContainer, - StyledHexColorContainer, - StyledRgbaColorContainer, -} from './styled'; -import { rgbToHsv, decomposeColor } from '../../styles/helpers/color'; -import { normalizeColor, isAchromatic } from './helpers'; +import { StyledContainer } from './styled'; +import normalizeColor from './helpers/normalizeColor'; import useNormalizeColors from './hooks/useNormalizeColors'; +import { Default, ColorsFixed } from './variants'; -const ColorPicker = React.forwardRef((props, ref) => { - const { - id, - value, - colors: colorsProp, - tabIndex, - onClick, - onChange, - onFocus, - onBlur, - className, - style, - } = props; - - const firstRef = useRef(); - const lastRef = useRef(); +const variantMap = { default: Default, 'colors-fixed': ColorsFixed }; - useImperativeHandle(ref, () => ({ - focus: () => { - firstRef.current.focus(); - }, - click: () => { - firstRef.current.click(); - }, - blur: () => { - lastRef.current.blur(); - }, - })); - - const [hue, setHue] = useState(0); +const ColorPicker = React.forwardRef((props, ref) => { + const { id, value, defaultColors, variant, tabIndex, onChange, className, style } = props; const color = normalizeColor(value); - const rgba = decomposeColor(color); - const [r, g, b, a] = rgba.values; - const [h, s, v] = decomposeColor(rgbToHsv(color)).values; - - const colors = useNormalizeColors(colorsProp); + const colors = useNormalizeColors({ defaultColors, variant }); const context = { - r, - g, - b, - a, - h: hue, - s, - v, + ...color, colors, tabIndex, - onClick, onChange, - onFocus, - onBlur, - setHue, }; - useEffect(() => { - if (!isAchromatic(rgba)) { - setHue(h); - } - }, [h, rgba]); + const Variant = variantMap[variant] || Default; return ( - Color Picker - - - - - - - - - - - - - - - - - - - + ); @@ -114,19 +36,19 @@ ColorPicker.propTypes = { /** The id of the outer element. */ id: PropTypes.string, /** Specifies the color of ColorPicker. */ - value: PropTypes.string, + value: PropTypes.shape({ + hex: PropTypes.string, + rgba: PropTypes.arrayOf(PropTypes.number), + hsv: PropTypes.arrayOf(PropTypes.number), + }), /** Specifies the default colors to choice. */ - colors: PropTypes.array, + defaultColors: PropTypes.array, + /** The variant changes the appearance of the Chip. Accepted variants include default, color-fixed */ + variant: PropTypes.oneOf(['default', 'colors-fixed']), /** Specifies the tab order of an element (when the tab button is used for navigating). */ tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** The action triggered when the element is clicked. */ - onClick: PropTypes.func, /** The action triggered when the value changes. */ onChange: PropTypes.func, - /** The action triggered when the element receives the focus. */ - onFocus: PropTypes.func, - /** The action triggered when the element releases focus. */ - onBlur: PropTypes.func, /** A CSS class for the outer element, in addition to the component's base classes. */ className: PropTypes.string, /** An object with custom style applied to the outer element. */ @@ -135,8 +57,8 @@ ColorPicker.propTypes = { ColorPicker.defaultProps = { id: undefined, - value: '#000000', - colors: [ + value: { hex: '#000000' }, + defaultColors: [ '#e3aaec', '#c3dbf7', '#9fd6ff', @@ -166,11 +88,9 @@ ColorPicker.defaultProps = { '#b67e12', '#b75d0c', ], + variant: 'default', tabIndex: undefined, - onClick: () => {}, onChange: () => {}, - onFocus: () => {}, - onBlur: () => {}, className: undefined, style: undefined, }; diff --git a/src/components/ColorPicker/preview.js b/src/components/ColorPicker/preview.js deleted file mode 100644 index 30a304036..000000000 --- a/src/components/ColorPicker/preview.js +++ /dev/null @@ -1,11 +0,0 @@ -import React, { useContext } from 'react'; -import { ColorPickerContext } from './context'; -import { StyledPreview } from './styled'; - -export default function Preview() { - const { r, g, b, a } = useContext(ColorPickerContext); - const rgba = `rgba(${r}, ${g}, ${b}, ${a})`; - const style = { backgroundColor: rgba }; - - return ; -} diff --git a/src/components/ColorPicker/readme.md b/src/components/ColorPicker/readme.md index 363d7467b..521a5e7bc 100644 --- a/src/components/ColorPicker/readme.md +++ b/src/components/ColorPicker/readme.md @@ -1,4 +1,4 @@ -##### ColorPicker info +##### ColorPicker default variant ```js import React, { useState } from 'react'; @@ -13,8 +13,17 @@ const Container = styled.div` `; const StyledCard = styled(Card)` - width: 26rem; - height: 40rem; + padding: 10px; +`; + +const StyledContent = styled.div` + width: 19rem; + height: 26rem; +`; + +const StyledLabel = styled.span` + color: ${props => props.theme.rainbow.palette.text.label}; + text-transform: uppercase; `; const ColorPickerExample = () => { @@ -24,7 +33,95 @@ const ColorPickerExample = () => { - + Color Picker + + + + + +``` + +##### ColorPicker without default colors + +```js +import React, { useState } from 'react'; +import styled from 'styled-components' +import { ColorPicker, Card } from 'react-rainbow-components'; + +const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 50px; +`; + +const StyledCard = styled(Card)` + padding: 10px; +`; + +const StyledContent = styled.div` + width: 19rem; + height: 16rem; +`; + +const StyledLabel = styled.span` + color: ${props => props.theme.rainbow.palette.text.label}; + text-transform: uppercase; +`; + +const ColorPickerExample = () => { + const [color, setColor] = useState(); + return ; +} + + + + Color Picker + + + ``` + +##### ColorPicker colors-fixed variant + +```js +import React, { useState } from 'react'; +import styled from 'styled-components' +import { ColorPicker, Card } from 'react-rainbow-components'; + +const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 50px; +`; + +const StyledCard = styled(Card)` + width: 20rem; + padding: 10px; +`; + +const StyledLabel = styled.label` + color: ${props => props.theme.rainbow.palette.text.label}; + text-transform: uppercase; +`; + +const ColorsFixedPicker = () => { + const [color, setColor] = useState(); + return ( + <> + + {!!color && color.hex} + + ); +} + + + + Default Colors + + + +``` \ No newline at end of file diff --git a/src/components/ColorPicker/rgbaColor.js b/src/components/ColorPicker/rgbaColor.js deleted file mode 100644 index fb16d2332..000000000 --- a/src/components/ColorPicker/rgbaColor.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useContext } from 'react'; -import { ColorPickerContext } from './context'; -import { StyledFlexContainer, StyledNumberInput } from './styled'; -import { recomposeColor, decomposeColor } from '../../styles/helpers/color'; - -export default function RgbaColor() { - const { r, g, b, a, tabIndex, onChange } = useContext(ColorPickerContext); - - const handleAlphaChange = event => { - const value = parseInt(event.target.value, 10); - const newApha = isNaN(value) ? 0 : Math.max(0, Math.min(value, 100)) / 100; - const rgba = `rgba(${r}, ${g}, ${b}, ${newApha})`; - - onChange(rgba); - }; - - const handleChange = (index, event) => { - const value = parseInt(event.target.value, 10); - const rgba = decomposeColor(`rgba(${r}, ${g}, ${b}, ${a})`); - - if (isNaN(value)) { - rgba.values[index] = 0; - } else { - rgba.values[index] = Math.max(0, Math.min(value, 255)); - } - - onChange(recomposeColor(rgba)); - }; - - const alpha = Math.round(a * 100); - - return ( - - handleChange(0, event)} - tabIndex={tabIndex} - /> - handleChange(1, event)} - tabIndex={tabIndex} - /> - handleChange(2, event)} - tabIndex={tabIndex} - /> - - - ); -} diff --git a/src/components/ColorPicker/saturation/index.js b/src/components/ColorPicker/saturation/index.js deleted file mode 100644 index bbc7c487f..000000000 --- a/src/components/ColorPicker/saturation/index.js +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useRef, useEffect, useContext, useState } from 'react'; -import { UP_KEY, DOWN_KEY, RIGHT_KEY, LEFT_KEY } from '../../../libs/constants'; -import { hsvToRgb, rgbToRgba } from '../../../styles/helpers/color'; -import { StyledColor, StyledCircle } from './styled'; -import { ColorPickerContext } from '../context'; - -const Saturation = React.forwardRef((_props, ref) => { - const { h, s, v, a, tabIndex, onChange } = useContext(ColorPickerContext); - const containerRef = useRef(); - const [saturation, setSaturation] = useState(s); - const [bright, setBright] = useState(v); - - const change = values => { - const newSaturation = values.saturation || saturation; - const newBright = values.bright || bright; - const newHsv = `hsv(${h}, ${newSaturation}, ${newBright})`; - const color = rgbToRgba(hsvToRgb(newHsv), a); - onChange(color); - }; - - const handleChange = event => { - const rect = containerRef.current.getBoundingClientRect(); - const { width, height } = rect; - const x = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX; - const y = typeof event.pageY === 'number' ? event.pageY : event.touches[0].pageY; - const left = Math.min(Math.max(0, x - (rect.left + window.pageXOffset)), width); - const top = Math.min(Math.max(0, y - (rect.top + window.pageYOffset)), height); - - const newSaturation = Math.round((left / width) * 100); - const newBright = Math.round((1 - top / height) * 100); - - setSaturation(newSaturation); - setBright(newBright); - change({ saturation: newSaturation, bright: newBright }); - }; - - const unbindEventListeners = () => { - window.removeEventListener('mousemove', handleChange); - window.removeEventListener('mouseup', unbindEventListeners); - }; - - const handleMouseDown = event => { - handleChange(event); - window.addEventListener('mousemove', handleChange); - window.addEventListener('mouseup', unbindEventListeners); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => unbindEventListeners, []); - - const keyHandlerMap = { - [UP_KEY]: () => { - const newBright = Math.min(100, bright + 1); - setBright(newBright); - change({ bright: newBright }); - }, - [DOWN_KEY]: () => { - const newBright = Math.max(0, bright - 1); - setBright(newBright); - change({ bright: newBright }); - }, - [LEFT_KEY]: () => { - const newSaturation = Math.max(0, saturation - 1); - setSaturation(newSaturation); - change({ saturation: newSaturation }); - }, - [RIGHT_KEY]: () => { - const newSaturation = Math.min(100, saturation + 1); - setSaturation(newSaturation); - change({ saturation: newSaturation }); - }, - }; - - const handleKeyDown = event => { - const { keyCode } = event; - if (keyHandlerMap[keyCode]) { - event.preventDefault(); - event.stopPropagation(); - keyHandlerMap[keyCode](); - } - }; - - const handleClick = () => { - ref.current.focus(); - }; - - const isFocusedRef = useRef(false); - const handleFocus = () => { - isFocusedRef.current = true; - }; - const handleBlur = () => { - isFocusedRef.current = false; - }; - - useEffect(() => { - if (!isFocusedRef.current) { - setSaturation(s); - setBright(v); - } - }, [s, v]); - - const hsl = `hsl(${h},100%, 50%)`; - const styleColor = { background: hsl }; - const stylePointer = { top: `${100 - bright}%`, left: `${saturation}%` }; - - return ( - - - - ); -}); - -export default Saturation; diff --git a/src/components/ColorPicker/saturation/styled.js b/src/components/ColorPicker/saturation/styled.js deleted file mode 100644 index 4bc4de6f5..000000000 --- a/src/components/ColorPicker/saturation/styled.js +++ /dev/null @@ -1,45 +0,0 @@ -import styled from 'styled-components'; -import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; - -export const StyledColor = styled.div` - position: relative; - height: 100%; - overflow: hidden; - - &::before { - content: ''; - overflow: hidden; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: linear-gradient(0deg, #000, rgba(0, 0, 0, 0.9) 1%, transparent 99%), - linear-gradient(90deg, #fff 1%, hsla(0, 0%, 100%, 0)); - } -`; - -export const StyledPointer = styled.div` - position: absolute; - width: 0; - height: 0; -`; - -export const StyledCircle = attachThemeAttrs(styled.button)` - position: absolute; - width: 12px; - height: 12px; - border-radius: 6px; - box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset; - transform: translate(-6px, -6px); - padding: 0; - margin: 0; - border: 1px solid ${props => props.palette.border.divider}; - - &:focus, - &:active { - outline: 0; - border: 1px solid ${props => props.palette.brand.main}; - box-shadow: ${props => props.shadows.brand}; - } -`; diff --git a/src/components/ColorPicker/slider.js b/src/components/ColorPicker/slider.js deleted file mode 100644 index 82495122a..000000000 --- a/src/components/ColorPicker/slider.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { StyledSlider, StyledInputRange } from './styled'; - -export default function Slider(props) { - const { value, min, max, tabIndex, onChange, style, className } = props; - - return ( - - - - ); -} - -Slider.propTypes = { - value: PropTypes.number, - min: PropTypes.number, - max: PropTypes.number, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - onChange: PropTypes.func, - className: PropTypes.string, - style: PropTypes.object, -}; - -Slider.defaultProps = { - value: undefined, - min: 0, - max: 100, - tabIndex: undefined, - onChange: () => {}, - className: undefined, - style: undefined, -}; diff --git a/src/components/ColorPicker/styled.js b/src/components/ColorPicker/styled.js index 48950c61d..e925b6886 100644 --- a/src/components/ColorPicker/styled.js +++ b/src/components/ColorPicker/styled.js @@ -1,28 +1,4 @@ -import styled, { css } from 'styled-components'; -import { FONT_SIZE_TEXT_MEDIUM } from '../../styles/fontSizes'; -import { PADDING_SMALL, PADDING_XX_SMALL } from '../../styles/paddings'; -import { BORDER_RADIUS_3, BORDER_RADIUS_2 } from '../../styles/borderRadius'; -import { COLOR_GRAY_1 } from '../../styles/colors'; -import attachThemeAttrs from '../../styles/helpers/attachThemeAttrs'; -import Input from '../Input'; -import Slider from './slider'; - -export const CssCircleColor = css` - border-radius: 50%; - border: 1px solid ${props => props.palette.border.divider}; -`; - -export const StyledPreview = attachThemeAttrs(styled.div)` - width: 50px; - height: 50px; - margin: 10px 0; - ${CssCircleColor} -`; - -export const StyledFlexContainer = styled.div` - display: flex; - flex: 0 0 auto; -`; +import styled from 'styled-components'; export const StyledContainer = styled.div` position: relative; @@ -30,219 +6,9 @@ export const StyledContainer = styled.div` height: 100%; display: flex; flex-direction: column; - padding: 15px; -`; - -export const StyledSaturationContainer = attachThemeAttrs(styled.div)` - position: relative; - flex: 1 0 auto; - border-radius: 12px; - overflow: hidden; - border: 1px solid ${props => props.palette.border.divider}; -`; - -export const StyledSlidersContainer = styled.div` - flex: 1 0 auto; - margin-right: 15px; - margin-top: 5px; `; -export const StyledHexColorContainer = styled.div` - flex: 0 3 auto; -`; - -export const StyledRgbaColorContainer = styled.div` - flex: 0 4 auto; -`; - -export const StyledLabel = attachThemeAttrs(styled.span)` - color: ${props => props.palette.text.label}; - font-size: ${FONT_SIZE_TEXT_MEDIUM}; - line-height: 1.5; - margin-bottom: 0.125rem; - box-sizing: border-box; - text-transform: uppercase; - flex: 0 0 auto; -`; - -export const StyledHexColorIcon = attachThemeAttrs(styled.span)` - font: inherit; - line-height: 2.5rem; - font-size: 1rem; -`; - -export const StyledNumberInput = styled(Input)` - margin-left: 5px; - - input::-webkit-inner-spin-button { - appearance: none; - margin: 0; - } - - input::-webkit-outer-spin-button { - appearance: none; - margin: 0; - } -`; - -export const StyledAlphaSlider = styled(Slider)` - input::-webkit-slider-runnable-track { - background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%); - } - - input::-moz-range-track { - background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%) !important; - } -`; - -export const StyledHueSlider = styled(Slider)` - input::-webkit-slider-runnable-track { - background: linear-gradient( - to right, - rgb(255, 0, 0) 0%, - rgb(255, 255, 0) 17%, - rgb(0, 255, 0) 33%, - rgb(0, 255, 255) 50%, - rgb(0, 0, 255) 67%, - rgb(255, 0, 255) 83%, - rgb(255, 0, 0) 100% - ) !important; - } - - input::-moz-range-track { - background: linear-gradient( - to right, - rgb(255, 0, 0) 0%, - rgb(255, 255, 0) 17%, - rgb(0, 255, 0) 33%, - rgb(0, 255, 255) 50%, - rgb(0, 0, 255) 67%, - rgb(255, 0, 255) 83%, - rgb(255, 0, 0) 100% - ) !important; - } -`; - -export const StyledSlider = styled.div` +export const StyledFlexContainer = styled.div` display: flex; - position: relative; - width: 100%; - padding: ${PADDING_XX_SMALL} 0 ${PADDING_SMALL} 0; -`; - -export const StyledInputRange = attachThemeAttrs(styled.input)` - appearance: none; - width: 100%; - margin: 0.5rem 0; - background: transparent; - border-radius: ${BORDER_RADIUS_3}; - box-sizing: border-box; - color: inherit; - font: inherit; - line-height: normal; - - ::-moz-focus-inner { - border: 0; - padding: 0; - } - - &::-webkit-slider-thumb { - appearance: none; - width: 1rem; - height: 1rem; - border-radius: 50%; - background: #fff; - border: 0; - box-shadow: ${props => props.shadows.shadow_1}; - cursor: pointer; - transition: all 0.3s ease 0s; - margin-top: calc(((1rem / 2) - (4px / 2)) * -1); - } - - &::-webkit-slider-runnable-track { - width: 100%; - height: 4px; - cursor: pointer; - background: ${props => props.palette.background.highlight}; - border-radius: ${BORDER_RADIUS_3}; - } - - &::-moz-range-thumb { - appearance: none; - width: 1rem; - height: 1rem; - border-radius: 50%; - background: #fff; - border: 0; - box-shadow: ${props => props.shadows.shadow_1}; - cursor: pointer; - transition: background 0.15s ease-in-out; - } - - &::-moz-range-track { - width: 100%; - height: 4px; - cursor: pointer; - background: ${props => props.palette.background.highlight}; - border-radius: ${BORDER_RADIUS_3}; - } - - &::-ms-track { - width: 100%; - height: 4px; - cursor: pointer; - border-radius: ${BORDER_RADIUS_3}; - background: transparent; - border-color: transparent; - color: transparent; - } - - &::-ms-thumb { - width: 1rem; - height: 1rem; - border-radius: ${BORDER_RADIUS_2}; - background: #fff; - border: 0; - box-shadow: rgba(0, 0, 0, 0.16) 0 2px 3px; - cursor: pointer; - transition: background 0.15s ease-in-out; - } - - &:focus { - outline: 0; - } - - &::-webkit-slider-thumb:hover { - background-color: ${COLOR_GRAY_1}; - } - - &::-moz-range-thumb:hover { - background-color: ${COLOR_GRAY_1}; - } - - &::-ms-thumb:hover { - background-color: ${COLOR_GRAY_1}; - } - - &:focus::-webkit-slider-thumb { - background-color: #fff; - border: 1px solid ${props => props.palette.brand.main}; - box-shadow: ${props => props.shadows.brand}; - } - - &:active::-webkit-slider-thumb { - background-color: #fff; - transition: all 0.3s ease 0s; - transform: scale3d(1.5, 1.5, 1); - } - - &:focus::-moz-range-thumb { - background-color: #fff; - border: 1px solid ${props => props.palette.brand.main}; - box-shadow: ${props => props.shadows.brand}; - } - - &:active::-moz-range-thumb { - background-color: #fff; - } + flex: 0 0 auto; `; diff --git a/src/components/ColorPicker/variants/colorsFixed.js b/src/components/ColorPicker/variants/colorsFixed.js new file mode 100644 index 000000000..cf1812b48 --- /dev/null +++ b/src/components/ColorPicker/variants/colorsFixed.js @@ -0,0 +1,22 @@ +import React, { useRef, useImperativeHandle } from 'react'; +import { DefaultColors } from '../components'; + +const ColorsFixed = React.forwardRef((_props, ref) => { + const colorsRef = useRef(); + + useImperativeHandle(ref, () => ({ + focus: () => { + colorsRef.current.focus(); + }, + click: () => { + colorsRef.current.click(); + }, + blur: () => { + colorsRef.current.blur(); + }, + })); + + return ; +}); + +export default ColorsFixed; diff --git a/src/components/ColorPicker/variants/default.js b/src/components/ColorPicker/variants/default.js new file mode 100644 index 000000000..dc7a22437 --- /dev/null +++ b/src/components/ColorPicker/variants/default.js @@ -0,0 +1,64 @@ +import React, { useRef, useContext, useImperativeHandle } from 'react'; +import { ColorPickerContext } from '../context'; +import { StyledFlexContainer } from '../styled'; +import { + StyledSaturationContainer, + StyledSlidersContainer, + StyledHexColorContainer, + StyledRgbaColorContainer, +} from './styled'; +import { Saturation, Hue, Alpha, Preview, HexColor, RgbaColor, DefaultColors } from '../components'; +import RenderIf from '../../RenderIf'; + +const Default = React.forwardRef((_props, ref) => { + const { colors } = useContext(ColorPickerContext); + const firstRef = useRef(); + const lastRef = useRef(); + + useImperativeHandle(ref, () => ({ + focus: () => { + firstRef.current.focus(); + }, + click: () => { + firstRef.current.click(); + }, + blur: () => { + lastRef.current.blur(); + }, + })); + + const hasColors = colors.length > 0; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}); + +export default Default; diff --git a/src/components/ColorPicker/variants/index.js b/src/components/ColorPicker/variants/index.js new file mode 100644 index 000000000..96be7be72 --- /dev/null +++ b/src/components/ColorPicker/variants/index.js @@ -0,0 +1,2 @@ +export { default as Default } from './default'; +export { default as ColorsFixed } from './colorsFixed'; diff --git a/src/components/ColorPicker/variants/styled.js b/src/components/ColorPicker/variants/styled.js new file mode 100644 index 000000000..2e2fe366a --- /dev/null +++ b/src/components/ColorPicker/variants/styled.js @@ -0,0 +1,24 @@ +import styled from 'styled-components'; +import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; + +export const StyledSaturationContainer = attachThemeAttrs(styled.div)` + position: relative; + flex: 1 0 auto; + border-radius: 12px; + overflow: hidden; + border: 1px solid ${props => props.palette.border.divider}; +`; + +export const StyledSlidersContainer = styled.div` + flex: 1 0 auto; + margin-right: 15px; + margin-top: 5px; +`; + +export const StyledHexColorContainer = styled.div` + flex: 0 3 auto; +`; + +export const StyledRgbaColorContainer = styled.div` + flex: 0 4 auto; +`; diff --git a/src/styles/helpers/color/index.js b/src/styles/helpers/color/index.js index 2d38d08e2..ee59d547a 100644 --- a/src/styles/helpers/color/index.js +++ b/src/styles/helpers/color/index.js @@ -17,3 +17,4 @@ export { default as colorToRgba } from './colorToRgba'; export { default as rgbaToHex } from './rgbaToHex'; export { default as rgbToHsv } from './rgbToHsv'; export { default as hsvToRgb } from './hsvToRgb'; +export { default as isHsvColor } from './isHsvColor'; diff --git a/src/styles/helpers/color/isHsvColor.js b/src/styles/helpers/color/isHsvColor.js new file mode 100644 index 000000000..899fb6e9d --- /dev/null +++ b/src/styles/helpers/color/isHsvColor.js @@ -0,0 +1,16 @@ +import { decomposeColor } from '.'; + +export default function isHsvColor(color) { + if (typeof color === 'string' && color !== '') { + const { type, values } = decomposeColor(color); + if (type === 'hsv' && Array.isArray(values) && values.length === 3) { + return ( + values.filter( + (value, index) => + (index === 0 && value >= 0 && value <= 360) || (value >= 0 && value <= 100), + ).length === 3 + ); + } + } + return false; +} From dd4d7eb69f651140d437413f7aa37884de558ee4 Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Sun, 30 Aug 2020 02:05:18 -0400 Subject: [PATCH 07/11] fix: rearrange the structure of internal components --- .../ColorPicker/commons/alpha/index.js | 30 ++++ .../ColorPicker/commons/alpha/styled.js | 14 ++ .../commons/defaultColors/color.js | 66 +++++++++ .../commons/defaultColors/index.js | 38 ++++++ .../commons/defaultColors/styled.js | 38 ++++++ .../ColorPicker/commons/hexColor/index.js | 46 +++++++ .../ColorPicker/commons/hexColor/styled.js | 26 ++++ .../ColorPicker/commons/hue/index.js | 37 +++++ .../ColorPicker/commons/hue/styled.js | 32 +++++ src/components/ColorPicker/commons/index.js | 7 + src/components/ColorPicker/commons/preview.js | 11 ++ .../ColorPicker/commons/rgbaColor/index.js | 93 +++++++++++++ .../ColorPicker/commons/rgbaColor/styled.js | 29 ++++ .../ColorPicker/commons/saturation/index.js | 114 ++++++++++++++++ .../ColorPicker/commons/saturation/styled.js | 39 ++++++ .../ColorPicker/commons/slider/index.js | 40 ++++++ .../ColorPicker/commons/slider/styled.js | 128 ++++++++++++++++++ src/components/ColorPicker/commons/styled.js | 22 +++ 18 files changed, 810 insertions(+) create mode 100644 src/components/ColorPicker/commons/alpha/index.js create mode 100644 src/components/ColorPicker/commons/alpha/styled.js create mode 100644 src/components/ColorPicker/commons/defaultColors/color.js create mode 100644 src/components/ColorPicker/commons/defaultColors/index.js create mode 100644 src/components/ColorPicker/commons/defaultColors/styled.js create mode 100644 src/components/ColorPicker/commons/hexColor/index.js create mode 100644 src/components/ColorPicker/commons/hexColor/styled.js create mode 100644 src/components/ColorPicker/commons/hue/index.js create mode 100644 src/components/ColorPicker/commons/hue/styled.js create mode 100644 src/components/ColorPicker/commons/index.js create mode 100644 src/components/ColorPicker/commons/preview.js create mode 100644 src/components/ColorPicker/commons/rgbaColor/index.js create mode 100644 src/components/ColorPicker/commons/rgbaColor/styled.js create mode 100644 src/components/ColorPicker/commons/saturation/index.js create mode 100644 src/components/ColorPicker/commons/saturation/styled.js create mode 100644 src/components/ColorPicker/commons/slider/index.js create mode 100644 src/components/ColorPicker/commons/slider/styled.js create mode 100644 src/components/ColorPicker/commons/styled.js diff --git a/src/components/ColorPicker/commons/alpha/index.js b/src/components/ColorPicker/commons/alpha/index.js new file mode 100644 index 000000000..c4fc33faf --- /dev/null +++ b/src/components/ColorPicker/commons/alpha/index.js @@ -0,0 +1,30 @@ +import React, { useContext } from 'react'; +import StyledAlphaSlider from './styled'; +import { ColorPickerContext } from '../../context'; + +export default function Alpha() { + const { hex, rgba, hsv, tabIndex, onChange } = useContext(ColorPickerContext); + + const handleChange = event => { + const value = parseInt(event.target.value, 10); + rgba[3] = isNaN(value) ? 0 : Math.max(0, Math.min(value, 100)) / 100; + + onChange({ + hex, + rgba, + hsv, + }); + }; + + const alpha = Math.round(rgba[3] * 100); + + return ( + + ); +} diff --git a/src/components/ColorPicker/commons/alpha/styled.js b/src/components/ColorPicker/commons/alpha/styled.js new file mode 100644 index 000000000..479523680 --- /dev/null +++ b/src/components/ColorPicker/commons/alpha/styled.js @@ -0,0 +1,14 @@ +import styled from 'styled-components'; +import Slider from '../slider'; + +const StyledAlphaSlider = styled(Slider)` + input::-webkit-slider-runnable-track { + background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%); + } + + input::-moz-range-track { + background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%) !important; + } +`; + +export default StyledAlphaSlider; diff --git a/src/components/ColorPicker/commons/defaultColors/color.js b/src/components/ColorPicker/commons/defaultColors/color.js new file mode 100644 index 000000000..59c6a799b --- /dev/null +++ b/src/components/ColorPicker/commons/defaultColors/color.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledColor, StyledInput, StyledLabel } from './styled'; +import AssistiveText from '../../../AssistiveText'; +import { useUniqueIdentifier } from '../../../../libs/hooks'; +import { + colorToRgba, + isValidColor, + rgbToHsv, + rgbaToHex, + decomposeColor, +} from '../../../../styles/helpers/color'; + +const Color = React.forwardRef((props, ref) => { + const { color, name, tabIndex, isChecked, onChange } = props; + const colorId = useUniqueIdentifier('color-picker-default'); + + const handleChange = () => { + const rgba = colorToRgba(color); + if (isValidColor(rgba)) { + onChange({ + hex: rgbaToHex(rgba), + rgba: decomposeColor(rgba).values, + hsv: decomposeColor(rgbToHsv(rgba)).values, + }); + } + }; + + const style = { backgroundColor: color }; + return ( + + + + {color} + + + ); +}); + +Color.propTypes = { + color: PropTypes.string.isRequired, + name: PropTypes.string, + isChecked: PropTypes.bool, + onChange: PropTypes.func, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), +}; + +Color.defaultProps = { + color: undefined, + name: '', + isChecked: false, + onChange: () => {}, + tapIndex: undefined, +}; + +export default Color; diff --git a/src/components/ColorPicker/commons/defaultColors/index.js b/src/components/ColorPicker/commons/defaultColors/index.js new file mode 100644 index 000000000..e6a0772a3 --- /dev/null +++ b/src/components/ColorPicker/commons/defaultColors/index.js @@ -0,0 +1,38 @@ +import React, { useContext } from 'react'; +import { ColorPickerContext } from '../../context'; +import { StyledContainer, StyledColors } from './styled'; +import { colorToRgba, recomposeColor } from '../../../../styles/helpers/color'; +import { useUniqueIdentifier } from '../../../../libs/hooks'; +import Color from './color'; + +const DefaultColors = React.forwardRef((_props, ref) => { + const { colors, rgba, tabIndex: tabIndexProp, onChange } = useContext(ColorPickerContext); + const rgbaColor = recomposeColor({ type: 'rgba', values: rgba }); + const name = useUniqueIdentifier('color-picker-default'); + + const listColors = colors.map((color, index) => { + const tabIndex = index === 0 ? tabIndexProp : -1; + const isSelected = colorToRgba(color) === rgbaColor; + const isFirstInput = index === 0; + const inputRef = isFirstInput ? ref : undefined; + return ( + + ); + }); + + return ( + + {listColors} + + ); +}); + +export default DefaultColors; diff --git a/src/components/ColorPicker/commons/defaultColors/styled.js b/src/components/ColorPicker/commons/defaultColors/styled.js new file mode 100644 index 000000000..6d6fd9c3b --- /dev/null +++ b/src/components/ColorPicker/commons/defaultColors/styled.js @@ -0,0 +1,38 @@ +import styled from 'styled-components'; +import attachThemeAttrs from '../../../../styles/helpers/attachThemeAttrs'; +import HiddenElement from '../../../Structural/hiddenElement'; +import { CssCircleColor } from '../styled'; + +export const StyledContainer = styled.div` + flex: 0 0 auto; + padding: 0.5rem 0 0.25rem; +`; + +export const StyledColors = styled.div` + text-align: center; +`; + +export const StyledColor = styled.span` + line-height: inherit; + height: inherit; +`; + +export const StyledInput = attachThemeAttrs(styled(HiddenElement))` + &:focus + label { + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; + } +`; + +export const StyledLabel = attachThemeAttrs(styled.label)` + display: inline-block; + margin: 0 0.45rem; + width: 28px; + height: 28px; + padding: 0; + ${CssCircleColor} + + &:hover { + cursor: pointer; + } +`; diff --git a/src/components/ColorPicker/commons/hexColor/index.js b/src/components/ColorPicker/commons/hexColor/index.js new file mode 100644 index 000000000..ed53ee35e --- /dev/null +++ b/src/components/ColorPicker/commons/hexColor/index.js @@ -0,0 +1,46 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { StyledHexColorIcon, StyledHexInput } from './styled'; +import { hexToRgba, rgbToHsv, isValidColor } from '../../../../styles/helpers/color'; +import { ColorPickerContext } from '../../context'; + +export default function HexColor() { + const { hex, tabIndex, onChange } = useContext(ColorPickerContext); + const [color, setColor] = useState(hex.substr(1)); + const [isFocused, setIsFocused] = useState(false); + + useEffect(() => { + if (!isFocused) { + setColor(hex.substr(1)); + } + }, [hex, isFocused]); + + const handleChange = event => { + const newHex = `#${event.target.value}`; + setColor(event.target.value); + const rgba = hexToRgba(newHex); + if (isValidColor(rgba)) { + onChange({ + hex: newHex, + rgba: rgba.values, + hsv: rgbToHsv(rgba).values, + }); + } + }; + + const handleBlur = () => { + setIsFocused(false); + setColor(hex.substr(1)); + }; + + return ( + setIsFocused(true)} + onBlur={handleBlur} + icon={#} + tabIndex={tabIndex} + /> + ); +} diff --git a/src/components/ColorPicker/commons/hexColor/styled.js b/src/components/ColorPicker/commons/hexColor/styled.js new file mode 100644 index 000000000..27fa9694e --- /dev/null +++ b/src/components/ColorPicker/commons/hexColor/styled.js @@ -0,0 +1,26 @@ +import styled from 'styled-components'; +import attachThemeAttrs from '../../../../styles/helpers/attachThemeAttrs'; +import Input from '../../../Input'; +import { CssInput } from '../styled'; + +export const StyledHexInput = styled(Input)` + input { + ${CssInput} + padding: 0 0.7rem 0 1rem; + + :focus, + :active { + padding: 0 0.65625rem 0 0.9375rem; + } + } + + > div > span { + left: 0.2rem; + } +`; + +export const StyledHexColorIcon = attachThemeAttrs(styled.span)` + font: inherit; + line-height: 2.5rem; + font-size: 1rem; +`; diff --git a/src/components/ColorPicker/commons/hue/index.js b/src/components/ColorPicker/commons/hue/index.js new file mode 100644 index 000000000..ca5b4cf35 --- /dev/null +++ b/src/components/ColorPicker/commons/hue/index.js @@ -0,0 +1,37 @@ +import React, { useContext } from 'react'; +import StyledHueSlider from './styled'; +import { + hsvToRgb, + rgbToRgba, + recomposeColor, + rgbaToHex, + decomposeColor, +} from '../../../../styles/helpers/color'; +import { ColorPickerContext } from '../../context'; + +export default function Hue() { + const { hsv, rgba, tabIndex, onChange } = useContext(ColorPickerContext); + + const handleChange = event => { + const value = parseInt(event.target.value, 10); + hsv[0] = isNaN(value) ? 0 : Math.max(0, Math.min(value, 360)); + const rgbColor = hsvToRgb(recomposeColor({ type: 'hsv', values: hsv })); + const rgbaColor = rgbToRgba(rgbColor, rgba[3]); + + onChange({ + hex: `#${rgbaToHex(rgbaColor)}`, + rgba: decomposeColor(rgbaColor).values, + hsv, + }); + }; + + return ( + + ); +} diff --git a/src/components/ColorPicker/commons/hue/styled.js b/src/components/ColorPicker/commons/hue/styled.js new file mode 100644 index 000000000..02279aee4 --- /dev/null +++ b/src/components/ColorPicker/commons/hue/styled.js @@ -0,0 +1,32 @@ +import styled from 'styled-components'; +import Slider from '../slider'; + +const StyledHueSlider = styled(Slider)` + input::-webkit-slider-runnable-track { + background: linear-gradient( + to right, + rgb(255, 0, 0) 0%, + rgb(255, 255, 0) 17%, + rgb(0, 255, 0) 33%, + rgb(0, 255, 255) 50%, + rgb(0, 0, 255) 67%, + rgb(255, 0, 255) 83%, + rgb(255, 0, 0) 100% + ) !important; + } + + input::-moz-range-track { + background: linear-gradient( + to right, + rgb(255, 0, 0) 0%, + rgb(255, 255, 0) 17%, + rgb(0, 255, 0) 33%, + rgb(0, 255, 255) 50%, + rgb(0, 0, 255) 67%, + rgb(255, 0, 255) 83%, + rgb(255, 0, 0) 100% + ) !important; + } +`; + +export default StyledHueSlider; diff --git a/src/components/ColorPicker/commons/index.js b/src/components/ColorPicker/commons/index.js new file mode 100644 index 000000000..28df733db --- /dev/null +++ b/src/components/ColorPicker/commons/index.js @@ -0,0 +1,7 @@ +export { default as DefaultColors } from './defaultColors'; +export { default as Saturation } from './saturation'; +export { default as Alpha } from './alpha'; +export { default as HexColor } from './hexColor'; +export { default as Hue } from './hue'; +export { default as RgbaColor } from './rgbaColor'; +export { default as Preview } from './preview'; diff --git a/src/components/ColorPicker/commons/preview.js b/src/components/ColorPicker/commons/preview.js new file mode 100644 index 000000000..67c58610a --- /dev/null +++ b/src/components/ColorPicker/commons/preview.js @@ -0,0 +1,11 @@ +import React, { useContext } from 'react'; +import { ColorPickerContext } from '../context'; +import { StyledPreview } from './styled'; +import { recomposeColor } from '../../../styles/helpers/color'; + +export default function Preview() { + const { rgba } = useContext(ColorPickerContext); + const style = { backgroundColor: recomposeColor({ type: 'rgba', values: rgba }) }; + + return ; +} diff --git a/src/components/ColorPicker/commons/rgbaColor/index.js b/src/components/ColorPicker/commons/rgbaColor/index.js new file mode 100644 index 000000000..b526771dd --- /dev/null +++ b/src/components/ColorPicker/commons/rgbaColor/index.js @@ -0,0 +1,93 @@ +import React, { useContext, useRef, useImperativeHandle } from 'react'; +import { ColorPickerContext } from '../../context'; +import { StyledFlexContainer } from '../../styled'; +import StyledNumberInput from './styled'; +import { recomposeColor, rgbaToHex, rgbToHsv } from '../../../../styles/helpers/color'; + +const RgbaColor = React.forwardRef((_props, ref) => { + const { rgba, hsv, hex, tabIndex, onChange } = useContext(ColorPickerContext); + + const firstRef = useRef(); + const lastRef = useRef(); + + useImperativeHandle(ref, () => ({ + focus: () => { + firstRef.current.focus(); + }, + click: () => { + firstRef.current.click(); + }, + blur: () => { + lastRef.current.blur(); + }, + })); + + const handleAlphaChange = event => { + const value = parseInt(event.target.value, 10); + const newApha = isNaN(value) ? 0 : Math.max(0, Math.min(value, 100)) / 100; + rgba[3] = newApha; + + onChange({ + hex, + rgba, + hsv, + }); + }; + + const handleChange = (index, event) => { + const value = parseInt(event.target.value, 10); + + if (isNaN(value)) { + rgba[index] = 0; + } else { + rgba[index] = Math.max(0, Math.min(value, 255)); + } + const rgbaColor = recomposeColor({ type: 'rgba', values: rgba }); + + onChange({ + hex: rgbaToHex(rgbaColor), + rgba, + hsv: rgbToHsv(rgbaColor).values, + }); + }; + + const [r, g, b, a] = rgba; + const alpha = Math.round(a * 100); + + return ( + + handleChange(0, event)} + tabIndex={tabIndex} + ref={firstRef} + /> + handleChange(1, event)} + tabIndex={tabIndex} + /> + handleChange(2, event)} + tabIndex={tabIndex} + /> + + + ); +}); + +export default RgbaColor; diff --git a/src/components/ColorPicker/commons/rgbaColor/styled.js b/src/components/ColorPicker/commons/rgbaColor/styled.js new file mode 100644 index 000000000..dfda2fa9e --- /dev/null +++ b/src/components/ColorPicker/commons/rgbaColor/styled.js @@ -0,0 +1,29 @@ +import styled from 'styled-components'; +import Input from '../../../Input'; +import { CssInput } from '../styled'; + +const StyledNumberInput = styled(Input)` + margin-left: 5px; + + input { + ${CssInput} + padding: 0 0.7rem; + + :focus, + :active { + padding: 0 0.65625rem; + } + } + + input::-webkit-inner-spin-button { + appearance: none; + margin: 0; + } + + input::-webkit-outer-spin-button { + appearance: none; + margin: 0; + } +`; + +export default StyledNumberInput; diff --git a/src/components/ColorPicker/commons/saturation/index.js b/src/components/ColorPicker/commons/saturation/index.js new file mode 100644 index 000000000..ba0809e39 --- /dev/null +++ b/src/components/ColorPicker/commons/saturation/index.js @@ -0,0 +1,114 @@ +import React, { useRef, useEffect, useContext } from 'react'; +import { UP_KEY, DOWN_KEY, RIGHT_KEY, LEFT_KEY } from '../../../../libs/constants'; +import { + hsvToRgb, + rgbToRgba, + recomposeColor, + decomposeColor, + rgbaToHex, +} from '../../../../styles/helpers/color'; +import { StyledColor, StyledCircle } from './styled'; +import { ColorPickerContext } from '../../context'; + +const Saturation = React.forwardRef((_props, ref) => { + const { rgba, hsv, tabIndex, onChange } = useContext(ColorPickerContext); + const containerRef = useRef(); + const [h, s, v] = hsv; + const a = rgba[3]; + + const change = ({ saturation, bright }) => { + if (saturation >= 0) { + hsv[1] = saturation; + } + if (bright >= 0) { + hsv[2] = bright; + } + const rgbaColor = rgbToRgba(hsvToRgb(recomposeColor({ type: 'hsv', values: hsv })), a); + + onChange({ + hex: `#${rgbaToHex(rgbaColor)}`, + rgba: decomposeColor(rgbaColor).values, + hsv, + }); + }; + + const handleChange = event => { + const rect = containerRef.current.getBoundingClientRect(); + const { width, height } = rect; + const x = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX; + const y = typeof event.pageY === 'number' ? event.pageY : event.touches[0].pageY; + const left = Math.min(Math.max(0, x - (rect.left + window.pageXOffset)), width); + const top = Math.min(Math.max(0, y - (rect.top + window.pageYOffset)), height); + + const saturation = Math.round((left / width) * 100); + const bright = Math.round((1 - top / height) * 100); + + change({ saturation, bright }); + }; + + const unbindEventListeners = () => { + window.removeEventListener('mousemove', handleChange); + window.removeEventListener('mouseup', unbindEventListeners); + }; + + const handleMouseDown = event => { + handleChange(event); + window.addEventListener('mousemove', handleChange); + window.addEventListener('mouseup', unbindEventListeners); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => unbindEventListeners, []); + + const keyHandlerMap = { + [UP_KEY]: () => { + const bright = Math.min(100, v + 1); + change({ bright }); + }, + [DOWN_KEY]: () => { + const bright = Math.max(0, v - 1); + change({ bright }); + }, + [LEFT_KEY]: () => { + const saturation = Math.max(0, s - 1); + change({ saturation }); + }, + [RIGHT_KEY]: () => { + const saturation = Math.min(100, s + 1); + change({ saturation }); + }, + }; + + const handleKeyDown = event => { + const { keyCode } = event; + if (keyHandlerMap[keyCode]) { + event.preventDefault(); + event.stopPropagation(); + keyHandlerMap[keyCode](); + } + }; + + const handleClick = () => { + ref.current.focus(); + }; + + const hsl = `hsl(${h},100%, 50%)`; + const styleColor = { background: hsl }; + const stylePointer = { top: `${100 - v}%`, left: `${s}%` }; + + return ( + + + + ); +}); + +export default Saturation; diff --git a/src/components/ColorPicker/commons/saturation/styled.js b/src/components/ColorPicker/commons/saturation/styled.js new file mode 100644 index 000000000..e25efe25b --- /dev/null +++ b/src/components/ColorPicker/commons/saturation/styled.js @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import attachThemeAttrs from '../../../../styles/helpers/attachThemeAttrs'; + +export const StyledColor = styled.div` + position: relative; + height: 100%; + overflow: hidden; + + &::before { + content: ''; + overflow: hidden; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: linear-gradient(0deg, #000, rgba(0, 0, 0, 0.9) 1%, transparent 99%), + linear-gradient(90deg, #fff 1%, hsla(0, 0%, 100%, 0)); + } +`; + +export const StyledCircle = attachThemeAttrs(styled.button)` + position: absolute; + width: 12px; + height: 12px; + border-radius: 6px; + box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset; + transform: translate(-6px, -6px); + padding: 0; + margin: 0; + border: 1px solid ${props => props.palette.border.divider}; + + &:focus, + &:active { + outline: 0; + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; + } +`; diff --git a/src/components/ColorPicker/commons/slider/index.js b/src/components/ColorPicker/commons/slider/index.js new file mode 100644 index 000000000..82495122a --- /dev/null +++ b/src/components/ColorPicker/commons/slider/index.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyledSlider, StyledInputRange } from './styled'; + +export default function Slider(props) { + const { value, min, max, tabIndex, onChange, style, className } = props; + + return ( + + + + ); +} + +Slider.propTypes = { + value: PropTypes.number, + min: PropTypes.number, + max: PropTypes.number, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + onChange: PropTypes.func, + className: PropTypes.string, + style: PropTypes.object, +}; + +Slider.defaultProps = { + value: undefined, + min: 0, + max: 100, + tabIndex: undefined, + onChange: () => {}, + className: undefined, + style: undefined, +}; diff --git a/src/components/ColorPicker/commons/slider/styled.js b/src/components/ColorPicker/commons/slider/styled.js new file mode 100644 index 000000000..df89d5fd0 --- /dev/null +++ b/src/components/ColorPicker/commons/slider/styled.js @@ -0,0 +1,128 @@ +import styled from 'styled-components'; +import { BORDER_RADIUS_3, BORDER_RADIUS_2 } from '../../../../styles/borderRadius'; +import { COLOR_GRAY_1 } from '../../../../styles/colors'; +import attachThemeAttrs from '../../../../styles/helpers/attachThemeAttrs'; + +export const StyledSlider = styled.div` + display: flex; + position: relative; + width: 100%; + padding: 2px 0; +`; + +export const StyledInputRange = attachThemeAttrs(styled.input)` + appearance: none; + width: 100%; + margin: 0.5rem 0; + background: transparent; + border-radius: ${BORDER_RADIUS_3}; + box-sizing: border-box; + color: inherit; + font: inherit; + line-height: normal; + + ::-moz-focus-inner { + border: 0; + padding: 0; + } + + &::-webkit-slider-thumb { + appearance: none; + width: 1rem; + height: 1rem; + border-radius: 50%; + background: #fff; + border: 0; + box-shadow: ${props => props.shadows.shadow_1}; + cursor: pointer; + transition: all 0.3s ease 0s; + margin-top: calc(((1rem / 2) - (4px / 2)) * -1); + } + + &::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + background: ${props => props.palette.background.highlight}; + border-radius: ${BORDER_RADIUS_3}; + } + + &::-moz-range-thumb { + appearance: none; + width: 1rem; + height: 1rem; + border-radius: 50%; + background: #fff; + border: 0; + box-shadow: ${props => props.shadows.shadow_1}; + cursor: pointer; + transition: background 0.15s ease-in-out; + } + + &::-moz-range-track { + width: 100%; + height: 4px; + cursor: pointer; + background: ${props => props.palette.background.highlight}; + border-radius: ${BORDER_RADIUS_3}; + } + + &::-ms-track { + width: 100%; + height: 4px; + cursor: pointer; + border-radius: ${BORDER_RADIUS_3}; + background: transparent; + border-color: transparent; + color: transparent; + } + + &::-ms-thumb { + width: 1rem; + height: 1rem; + border-radius: ${BORDER_RADIUS_2}; + background: #fff; + border: 0; + box-shadow: rgba(0, 0, 0, 0.16) 0 2px 3px; + cursor: pointer; + transition: background 0.15s ease-in-out; + } + + &:focus { + outline: 0; + } + + &::-webkit-slider-thumb:hover { + background-color: ${COLOR_GRAY_1}; + } + + &::-moz-range-thumb:hover { + background-color: ${COLOR_GRAY_1}; + } + + &::-ms-thumb:hover { + background-color: ${COLOR_GRAY_1}; + } + + &:focus::-webkit-slider-thumb { + background-color: #fff; + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; + } + + &:active::-webkit-slider-thumb { + background-color: #fff; + transition: all 0.3s ease 0s; + transform: scale3d(1.5, 1.5, 1); + } + + &:focus::-moz-range-thumb { + background-color: #fff; + border: 1px solid ${props => props.palette.brand.main}; + box-shadow: ${props => props.shadows.brand}; + } + + &:active::-moz-range-thumb { + background-color: #fff; + } +`; diff --git a/src/components/ColorPicker/commons/styled.js b/src/components/ColorPicker/commons/styled.js new file mode 100644 index 000000000..df1224526 --- /dev/null +++ b/src/components/ColorPicker/commons/styled.js @@ -0,0 +1,22 @@ +import styled, { css } from 'styled-components'; +import { FONT_SIZE_TEXT_SMALL } from '../../../styles/fontSizes'; +import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; + +export const CssInput = css` + line-height: 1.5rem; + height: 1.5rem; + font-size: ${FONT_SIZE_TEXT_SMALL}; + text-align: center; +`; + +export const CssCircleColor = css` + border-radius: 50%; + border: 1px solid ${props => props.palette.border.divider}; +`; + +export const StyledPreview = attachThemeAttrs(styled.div)` + width: 40px; + height: 40px; + margin: 10px 0; + ${CssCircleColor} +`; From 958b6fddc0a7e41b8824059586d20bee1fc6926c Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Sun, 30 Aug 2020 02:24:49 -0400 Subject: [PATCH 08/11] fix: import error of commons components --- src/components/ColorPicker/variants/colorsFixed.js | 2 +- src/components/ColorPicker/variants/default.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ColorPicker/variants/colorsFixed.js b/src/components/ColorPicker/variants/colorsFixed.js index cf1812b48..3a7aa1946 100644 --- a/src/components/ColorPicker/variants/colorsFixed.js +++ b/src/components/ColorPicker/variants/colorsFixed.js @@ -1,5 +1,5 @@ import React, { useRef, useImperativeHandle } from 'react'; -import { DefaultColors } from '../components'; +import { DefaultColors } from '../commons'; const ColorsFixed = React.forwardRef((_props, ref) => { const colorsRef = useRef(); diff --git a/src/components/ColorPicker/variants/default.js b/src/components/ColorPicker/variants/default.js index dc7a22437..8cb455b7a 100644 --- a/src/components/ColorPicker/variants/default.js +++ b/src/components/ColorPicker/variants/default.js @@ -7,7 +7,7 @@ import { StyledHexColorContainer, StyledRgbaColorContainer, } from './styled'; -import { Saturation, Hue, Alpha, Preview, HexColor, RgbaColor, DefaultColors } from '../components'; +import { Saturation, Hue, Alpha, Preview, HexColor, RgbaColor, DefaultColors } from '../commons'; import RenderIf from '../../RenderIf'; const Default = React.forwardRef((_props, ref) => { From 5ef86a29381b95856a0c6c503ceb4c3c327ce978 Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Mon, 31 Aug 2020 05:02:49 -0400 Subject: [PATCH 09/11] test: add unit test --- .../ColorPicker/__test__/colorPicker.spec.js | 44 ++++++ .../commons/__test__/alpha.spec.js | 56 ++++++++ .../commons/__test__/color.spec.js | 33 +++++ .../commons/__test__/defaultColors.spec.js | 46 ++++++ .../ColorPicker/commons/__test__/hex.spec.js | 133 ++++++++++++++++++ .../ColorPicker/commons/__test__/hue.spec.js | 58 ++++++++ .../ColorPicker/commons/__test__/rgba.spec.js | 88 ++++++++++++ .../commons/__test__/saturation.spec.js | 92 ++++++++++++ .../ColorPicker/commons/alpha/index.js | 2 +- .../commons/defaultColors/color.js | 18 +-- .../commons/{hexColor => hex}/index.js | 13 +- .../commons/{hexColor => hex}/styled.js | 0 src/components/ColorPicker/commons/index.js | 4 +- .../commons/{rgbaColor => rgba}/index.js | 17 ++- .../commons/{rgbaColor => rgba}/styled.js | 0 .../helpers/__test__/calculateBright.spec.js | 17 +++ .../__test__/calculateSaturation.spec.js | 17 +++ .../saturation/helpers/calculateBright.js | 7 + .../saturation/helpers/calculateSaturation.js | 7 + .../commons/saturation/helpers/index.js | 2 + .../ColorPicker/commons/saturation/index.js | 12 +- .../ColorPicker/variants/default.js | 8 +- 22 files changed, 639 insertions(+), 35 deletions(-) create mode 100644 src/components/ColorPicker/__test__/colorPicker.spec.js create mode 100644 src/components/ColorPicker/commons/__test__/alpha.spec.js create mode 100644 src/components/ColorPicker/commons/__test__/color.spec.js create mode 100644 src/components/ColorPicker/commons/__test__/defaultColors.spec.js create mode 100644 src/components/ColorPicker/commons/__test__/hex.spec.js create mode 100644 src/components/ColorPicker/commons/__test__/hue.spec.js create mode 100644 src/components/ColorPicker/commons/__test__/rgba.spec.js create mode 100644 src/components/ColorPicker/commons/__test__/saturation.spec.js rename src/components/ColorPicker/commons/{hexColor => hex}/index.js (82%) rename src/components/ColorPicker/commons/{hexColor => hex}/styled.js (100%) rename src/components/ColorPicker/commons/{rgbaColor => rgba}/index.js (87%) rename src/components/ColorPicker/commons/{rgbaColor => rgba}/styled.js (100%) create mode 100644 src/components/ColorPicker/commons/saturation/helpers/__test__/calculateBright.spec.js create mode 100644 src/components/ColorPicker/commons/saturation/helpers/__test__/calculateSaturation.spec.js create mode 100644 src/components/ColorPicker/commons/saturation/helpers/calculateBright.js create mode 100644 src/components/ColorPicker/commons/saturation/helpers/calculateSaturation.js create mode 100644 src/components/ColorPicker/commons/saturation/helpers/index.js diff --git a/src/components/ColorPicker/__test__/colorPicker.spec.js b/src/components/ColorPicker/__test__/colorPicker.spec.js new file mode 100644 index 000000000..cbae7e318 --- /dev/null +++ b/src/components/ColorPicker/__test__/colorPicker.spec.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import ColorPicker from '../'; +import * as Commons from '../commons'; + +describe('', () => { + it('should render all common component', () => { + const wrapper = mount(); + const componets = ['Saturation', 'Hue', 'Alpha', 'Hex', 'Rgba', 'DefaultColors']; + + componets.forEach(component => { + expect(wrapper.find(Commons[component]).length).toBe(1); + }); + }); + + it('should render all common component variant is not valid', () => { + const wrapper = mount(); + const componets = ['Saturation', 'Hue', 'Alpha', 'Hex', 'Rgba', 'DefaultColors']; + + componets.forEach(component => { + expect(wrapper.find(Commons[component]).length).toBe(1); + }); + }); + + it('should not render DefaultColors common component when defaultColors do not have some valid color', () => { + const wrapper = mount(); + const componets = ['Saturation', 'Hue', 'Alpha', 'Hex', 'Rgba']; + + componets.forEach(component => { + expect(wrapper.find(Commons[component]).length).toBe(1); + }); + expect(wrapper.find(Commons.DefaultColors).length).toBe(0); + }); + + it('should render only the DefaultColors common component when the variant is colors-fixed', () => { + const wrapper = mount(); + const componets = ['Saturation', 'Hue', 'Alpha', 'Hex', 'Rgba']; + + componets.forEach(component => { + expect(wrapper.find(Commons[component]).length).toBe(0); + }); + expect(wrapper.find(Commons.DefaultColors).length).toBe(1); + }); +}); diff --git a/src/components/ColorPicker/commons/__test__/alpha.spec.js b/src/components/ColorPicker/commons/__test__/alpha.spec.js new file mode 100644 index 000000000..22cfd3b42 --- /dev/null +++ b/src/components/ColorPicker/commons/__test__/alpha.spec.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from '../../context'; +import { Alpha } from '../'; + +describe('', () => { + it('should fire onChange with object containing the new alpha', () => { + const onChangeMockFn = jest.fn(); + const context = { + rgba: [0, 0, 0, 1], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="range"]') + .first() + .simulate('change', { target: { value: 1 } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith( + expect.objectContaining({ + rgba: [0, 0, 0, 0.01], + }), + ); + }); + + it('should fire onChange with object containing the default alpha', () => { + const onChangeMockFn = jest.fn(); + const context = { + rgba: [0, 0, 0, 0.5], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="range"]') + .first() + .simulate('change', { target: { value: 'foo' } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith( + expect.objectContaining({ + rgba: [0, 0, 0, 1], + }), + ); + }); +}); diff --git a/src/components/ColorPicker/commons/__test__/color.spec.js b/src/components/ColorPicker/commons/__test__/color.spec.js new file mode 100644 index 000000000..f7dfa732b --- /dev/null +++ b/src/components/ColorPicker/commons/__test__/color.spec.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Color from '../defaultColors/color'; + +describe('', () => { + it('should fire onChange with object containing the new color', () => { + const onChangeMockFn = jest.fn(); + const wrapper = mount(); + + expect( + wrapper + .find('label') + .first() + .prop('style'), + ).toStrictEqual({ backgroundColor: '#000000' }); + wrapper + .find('input[type="radio"]') + .first() + .simulate('change', { target: { checked: true } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith({ + hex: '#000000', + rgba: [0, 0, 0, 1], + hsv: [0, 0, 0], + }); + }); + + it('should return null when the color is not valid', () => { + const wrapper = mount(); + expect(wrapper.text()).toBeNull(); + }); +}); diff --git a/src/components/ColorPicker/commons/__test__/defaultColors.spec.js b/src/components/ColorPicker/commons/__test__/defaultColors.spec.js new file mode 100644 index 000000000..72b35b42c --- /dev/null +++ b/src/components/ColorPicker/commons/__test__/defaultColors.spec.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from '../../context'; +import { DefaultColors } from '../'; +import Color from '../defaultColors/color'; + +describe('', () => { + it('should render first Color component with tabIndex 1 and isChecked true', () => { + const context = { + rgba: [0, 0, 0, 1], + tabIndex: 1, + colors: ['rgba(0, 0, 0, 1)', 'rgba(255, 255, 255, 1)'], + }; + const wrapper = mount( + + + , + ); + + expect( + wrapper + .find(Color) + .at(0) + .props(), + ).toStrictEqual( + expect.objectContaining({ + color: context.colors[0], + tabIndex: 1, + isChecked: true, + }), + ); + + expect( + wrapper + .find(Color) + .at(1) + .props(), + ).toStrictEqual( + expect.objectContaining({ + color: context.colors[1], + tabIndex: -1, + isChecked: false, + }), + ); + }); +}); diff --git a/src/components/ColorPicker/commons/__test__/hex.spec.js b/src/components/ColorPicker/commons/__test__/hex.spec.js new file mode 100644 index 000000000..74bbddfe3 --- /dev/null +++ b/src/components/ColorPicker/commons/__test__/hex.spec.js @@ -0,0 +1,133 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from '../../context'; +import { Hex } from '../'; + +describe('', () => { + it('should fire onChange with object containing the new color', () => { + const onChangeMockFn = jest.fn(); + const context = { + hex: '#ffffff', + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="text"]') + .first() + .simulate('change', { target: { value: '000000' } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith({ + hex: '#000000', + rgba: [0, 0, 0, 1], + hsv: [0, 0, 0], + }); + }); + + it('should not fire onChange when value is not valid color', () => { + const onChangeMockFn = jest.fn(); + const context = { + hex: '#ffffff', + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="text"]') + .first() + .simulate('change', { target: { value: 'foo' } }); + wrapper.update(); + + expect( + wrapper + .find('input[type="text"]') + .first() + .prop('value'), + ).toBe('foo'); + expect(onChangeMockFn).not.toHaveBeenCalled(); + }); + + it('should change value when fire blur event', () => { + const context = { hex: '#ffffff' }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="text"]') + .first() + .simulate('change', { target: { value: 'foo' } }); + wrapper.update(); + + expect( + wrapper + .find('input[type="text"]') + .first() + .prop('value'), + ).toBe('foo'); + + wrapper + .find('input[type="text"]') + .first() + .simulate('blur'); + wrapper.update(); + + expect( + wrapper + .find('input[type="text"]') + .first() + .prop('value'), + ).toBe('ffffff'); + }); + + it('should not change value when component is focused', () => { + const context = { hex: '#ffffff' }; + const wrapper = mount( + + + , + ); + + expect( + wrapper + .find('input[type="text"]') + .first() + .prop('value'), + ).toBe('ffffff'); + + wrapper.setProps({ value: { hex: '#000000' } }); + wrapper.update(); + + expect( + wrapper + .find('input[type="text"]') + .first() + .prop('value'), + ).toBe('000000'); + + wrapper + .find('input[type="text"]') + .first() + .simulate('focus'); + wrapper.setProps({ value: { hex: '#eeeeee' } }); + wrapper.update(); + + expect( + wrapper + .find('input[type="text"]') + .first() + .prop('value'), + ).toBe('000000'); + }); +}); diff --git a/src/components/ColorPicker/commons/__test__/hue.spec.js b/src/components/ColorPicker/commons/__test__/hue.spec.js new file mode 100644 index 000000000..f314a504c --- /dev/null +++ b/src/components/ColorPicker/commons/__test__/hue.spec.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from '../../context'; +import { Hue } from '../'; + +describe('', () => { + it('should fire onChange with object containing the new hue.', () => { + const onChangeMockFn = jest.fn(); + const context = { + hsv: [0, 0, 0], + rgba: [0, 0, 0, 1], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="range"]') + .first() + .simulate('change', { target: { value: 100 } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith( + expect.objectContaining({ + hsv: [100, 0, 0], + }), + ); + }); + + it('should fire onChange with object containing the default hue.', () => { + const onChangeMockFn = jest.fn(); + const context = { + hsv: [360, 0, 0], + rgba: [0, 0, 0, 1], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="range"]') + .first() + .simulate('change', { target: { value: 'foo' } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith( + expect.objectContaining({ + hsv: [0, 0, 0], + }), + ); + }); +}); diff --git a/src/components/ColorPicker/commons/__test__/rgba.spec.js b/src/components/ColorPicker/commons/__test__/rgba.spec.js new file mode 100644 index 000000000..f3e3ffe52 --- /dev/null +++ b/src/components/ColorPicker/commons/__test__/rgba.spec.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from '../../context'; +import { Rgba } from '../'; + +describe('', () => { + it('should fire onChange with object containing the new alpha', () => { + const onChangeMockFn = jest.fn(); + const context = { + rgba: [0, 0, 0, 1], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + const result = [0, 0, 0, 1]; + const values = [255, 255, 255, 0.5]; + + values.forEach((value, index) => { + result[index] = value; + const normalizeValue = index === 3 ? value * 100 : value; + wrapper + .find('input[type="number"]') + .at(index) + .simulate('change', { target: { value: normalizeValue } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith( + expect.objectContaining({ + rgba: result, + }), + ); + }); + }); + + it('should fire onChange with object containing the default alpha', () => { + const onChangeMockFn = jest.fn(); + const context = { + rgba: [0, 0, 0, 0.5], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="number"]') + .at(3) + .simulate('change', { target: { value: 'foo' } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith( + expect.objectContaining({ + rgba: [0, 0, 0, 1], + }), + ); + }); + + it('should fire onChange with object containing the default component color', () => { + const onChangeMockFn = jest.fn(); + const context = { + rgba: [255, 0, 0, 1], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find('input[type="number"]') + .first() + .simulate('change', { target: { value: 'foo' } }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith( + expect.objectContaining({ + rgba: [0, 0, 0, 1], + }), + ); + }); +}); diff --git a/src/components/ColorPicker/commons/__test__/saturation.spec.js b/src/components/ColorPicker/commons/__test__/saturation.spec.js new file mode 100644 index 000000000..c1055aa7e --- /dev/null +++ b/src/components/ColorPicker/commons/__test__/saturation.spec.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from '../../context'; +import Saturation from '../saturation'; +import { StyledColor, StyledCircle } from '../saturation/styled'; +import { RIGHT_KEY, DOWN_KEY, LEFT_KEY, UP_KEY } from '../../../../libs/constants'; + +jest.mock('../saturation/helpers/calculateBright', () => () => 100); +jest.mock('../saturation/helpers/calculateSaturation', () => () => 100); + +describe('', () => { + it('should fire onChange with object containing the new color', () => { + const onChangeMockFn = jest.fn(); + const context = { + rgba: [0, 0, 0, 1], + hsv: [0, 0, 0], + onChange: onChangeMockFn, + }; + const wrapper = mount( + + + , + ); + + wrapper + .find(StyledColor) + .first() + .simulate('mousedown'); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith({ + hex: '#ff0000', + hsv: [0, 100, 100], + rgba: [255, 0, 0, 1], + }); + }); + + it('should render the pointer in the right position', () => { + const context = { + rgba: [0, 0, 0, 1], + hsv: [0, 0, 0], + }; + const wrapper = mount( + + + , + ); + + expect( + wrapper + .find(StyledCircle) + .first() + .prop('style'), + ).toEqual({ top: '100%', left: '0%' }); + }); + + it('should fire onChange with object containing the new color when using keyboard', () => { + const onChangeMockFn = jest.fn(); + const context = { + rgba: [0, 0, 0, 1], + hsv: [0, 0, 0], + onChange: onChangeMockFn, + }; + const values = [ + { key: RIGHT_KEY, result: [0, 1, 0] }, + { key: UP_KEY, result: [0, 1, 1] }, + { key: LEFT_KEY, result: [0, 0, 1] }, + { key: DOWN_KEY, result: [0, 0, 0] }, + ]; + const wrapper = mount( + + + , + ); + + wrapper + .find(StyledCircle) + .first() + .simulate('focus'); + wrapper.update(); + + values.forEach(({ key, result }) => { + wrapper + .find(StyledColor) + .first() + .simulate('keydown', { keyCode: key }); + wrapper.update(); + + expect(onChangeMockFn).toHaveBeenCalledWith(expect.objectContaining({ hsv: result })); + }); + }); +}); diff --git a/src/components/ColorPicker/commons/alpha/index.js b/src/components/ColorPicker/commons/alpha/index.js index c4fc33faf..f68f6ec06 100644 --- a/src/components/ColorPicker/commons/alpha/index.js +++ b/src/components/ColorPicker/commons/alpha/index.js @@ -7,7 +7,7 @@ export default function Alpha() { const handleChange = event => { const value = parseInt(event.target.value, 10); - rgba[3] = isNaN(value) ? 0 : Math.max(0, Math.min(value, 100)) / 100; + rgba[3] = isNaN(value) ? 1 : Math.max(0, Math.min(value, 100)) / 100; onChange({ hex, diff --git a/src/components/ColorPicker/commons/defaultColors/color.js b/src/components/ColorPicker/commons/defaultColors/color.js index 59c6a799b..1bf4fb125 100644 --- a/src/components/ColorPicker/commons/defaultColors/color.js +++ b/src/components/ColorPicker/commons/defaultColors/color.js @@ -13,17 +13,18 @@ import { const Color = React.forwardRef((props, ref) => { const { color, name, tabIndex, isChecked, onChange } = props; + const rgba = colorToRgba(color); + if (!isValidColor(rgba)) { + return null; + } const colorId = useUniqueIdentifier('color-picker-default'); const handleChange = () => { - const rgba = colorToRgba(color); - if (isValidColor(rgba)) { - onChange({ - hex: rgbaToHex(rgba), - rgba: decomposeColor(rgba).values, - hsv: decomposeColor(rgbToHsv(rgba)).values, - }); - } + onChange({ + hex: `#${rgbaToHex(rgba)}`, + rgba: decomposeColor(rgba).values, + hsv: decomposeColor(rgbToHsv(rgba)).values, + }); }; const style = { backgroundColor: color }; @@ -60,7 +61,6 @@ Color.defaultProps = { name: '', isChecked: false, onChange: () => {}, - tapIndex: undefined, }; export default Color; diff --git a/src/components/ColorPicker/commons/hexColor/index.js b/src/components/ColorPicker/commons/hex/index.js similarity index 82% rename from src/components/ColorPicker/commons/hexColor/index.js rename to src/components/ColorPicker/commons/hex/index.js index ed53ee35e..d47ca8568 100644 --- a/src/components/ColorPicker/commons/hexColor/index.js +++ b/src/components/ColorPicker/commons/hex/index.js @@ -1,9 +1,14 @@ import React, { useState, useEffect, useContext } from 'react'; import { StyledHexColorIcon, StyledHexInput } from './styled'; -import { hexToRgba, rgbToHsv, isValidColor } from '../../../../styles/helpers/color'; +import { + hexToRgba, + rgbToHsv, + isValidColor, + decomposeColor, +} from '../../../../styles/helpers/color'; import { ColorPickerContext } from '../../context'; -export default function HexColor() { +export default function Hex() { const { hex, tabIndex, onChange } = useContext(ColorPickerContext); const [color, setColor] = useState(hex.substr(1)); const [isFocused, setIsFocused] = useState(false); @@ -21,8 +26,8 @@ export default function HexColor() { if (isValidColor(rgba)) { onChange({ hex: newHex, - rgba: rgba.values, - hsv: rgbToHsv(rgba).values, + rgba: decomposeColor(rgba).values, + hsv: decomposeColor(rgbToHsv(rgba)).values, }); } }; diff --git a/src/components/ColorPicker/commons/hexColor/styled.js b/src/components/ColorPicker/commons/hex/styled.js similarity index 100% rename from src/components/ColorPicker/commons/hexColor/styled.js rename to src/components/ColorPicker/commons/hex/styled.js diff --git a/src/components/ColorPicker/commons/index.js b/src/components/ColorPicker/commons/index.js index 28df733db..8605f0fd4 100644 --- a/src/components/ColorPicker/commons/index.js +++ b/src/components/ColorPicker/commons/index.js @@ -1,7 +1,7 @@ export { default as DefaultColors } from './defaultColors'; export { default as Saturation } from './saturation'; export { default as Alpha } from './alpha'; -export { default as HexColor } from './hexColor'; +export { default as Hex } from './hex'; export { default as Hue } from './hue'; -export { default as RgbaColor } from './rgbaColor'; +export { default as Rgba } from './rgba'; export { default as Preview } from './preview'; diff --git a/src/components/ColorPicker/commons/rgbaColor/index.js b/src/components/ColorPicker/commons/rgba/index.js similarity index 87% rename from src/components/ColorPicker/commons/rgbaColor/index.js rename to src/components/ColorPicker/commons/rgba/index.js index b526771dd..ed3798dc0 100644 --- a/src/components/ColorPicker/commons/rgbaColor/index.js +++ b/src/components/ColorPicker/commons/rgba/index.js @@ -2,10 +2,15 @@ import React, { useContext, useRef, useImperativeHandle } from 'react'; import { ColorPickerContext } from '../../context'; import { StyledFlexContainer } from '../../styled'; import StyledNumberInput from './styled'; -import { recomposeColor, rgbaToHex, rgbToHsv } from '../../../../styles/helpers/color'; +import { + recomposeColor, + rgbaToHex, + rgbToHsv, + decomposeColor, +} from '../../../../styles/helpers/color'; -const RgbaColor = React.forwardRef((_props, ref) => { - const { rgba, hsv, hex, tabIndex, onChange } = useContext(ColorPickerContext); +const Rgba = React.forwardRef((_props, ref) => { + const { rgba, hsv, hex, tabIndex, onChange } = useContext(ColorPickerContext) || {}; const firstRef = useRef(); const lastRef = useRef(); @@ -24,7 +29,7 @@ const RgbaColor = React.forwardRef((_props, ref) => { const handleAlphaChange = event => { const value = parseInt(event.target.value, 10); - const newApha = isNaN(value) ? 0 : Math.max(0, Math.min(value, 100)) / 100; + const newApha = isNaN(value) ? 1 : Math.max(0, Math.min(value, 100)) / 100; rgba[3] = newApha; onChange({ @@ -47,7 +52,7 @@ const RgbaColor = React.forwardRef((_props, ref) => { onChange({ hex: rgbaToHex(rgbaColor), rgba, - hsv: rgbToHsv(rgbaColor).values, + hsv: decomposeColor(rgbToHsv(rgbaColor)).values, }); }; @@ -90,4 +95,4 @@ const RgbaColor = React.forwardRef((_props, ref) => { ); }); -export default RgbaColor; +export default Rgba; diff --git a/src/components/ColorPicker/commons/rgbaColor/styled.js b/src/components/ColorPicker/commons/rgba/styled.js similarity index 100% rename from src/components/ColorPicker/commons/rgbaColor/styled.js rename to src/components/ColorPicker/commons/rgba/styled.js diff --git a/src/components/ColorPicker/commons/saturation/helpers/__test__/calculateBright.spec.js b/src/components/ColorPicker/commons/saturation/helpers/__test__/calculateBright.spec.js new file mode 100644 index 000000000..6acc1c220 --- /dev/null +++ b/src/components/ColorPicker/commons/saturation/helpers/__test__/calculateBright.spec.js @@ -0,0 +1,17 @@ +import calculateBright from '../calculateBright'; + +describe('calculateBright', () => { + it('should return the right value when pageY is a number', () => { + const event = { pageY: 20 }; + const rect = { height: 100, top: 0 }; + + expect(calculateBright(event, rect)).toBe(80); + }); + + it('should return the right value when pageY is not a number', () => { + const event = { touches: [{ pageY: 0 }] }; + const rect = { height: 100, top: 0 }; + + expect(calculateBright(event, rect)).toBe(100); + }); +}); diff --git a/src/components/ColorPicker/commons/saturation/helpers/__test__/calculateSaturation.spec.js b/src/components/ColorPicker/commons/saturation/helpers/__test__/calculateSaturation.spec.js new file mode 100644 index 000000000..5be89a4e4 --- /dev/null +++ b/src/components/ColorPicker/commons/saturation/helpers/__test__/calculateSaturation.spec.js @@ -0,0 +1,17 @@ +import calculateSaturation from '../calculateSaturation'; + +describe('calculateSaturation', () => { + it('should the right value when pageX is a number', () => { + const event = { pageX: 20 }; + const rect = { width: 100, left: 0 }; + + expect(calculateSaturation(event, rect)).toBe(20); + }); + + it('should the right value when pageX is not a number', () => { + const event = { touches: [{ pageX: 0 }] }; + const rect = { width: 100, left: 0 }; + + expect(calculateSaturation(event, rect)).toBe(0); + }); +}); diff --git a/src/components/ColorPicker/commons/saturation/helpers/calculateBright.js b/src/components/ColorPicker/commons/saturation/helpers/calculateBright.js new file mode 100644 index 000000000..40e019dec --- /dev/null +++ b/src/components/ColorPicker/commons/saturation/helpers/calculateBright.js @@ -0,0 +1,7 @@ +export default function calculateBright(event, rect) { + const { height, top } = rect; + const y = typeof event.pageY === 'number' ? event.pageY : event.touches[0].pageY; + const relativeTop = Math.min(Math.max(0, y - (top + window.pageYOffset)), height); + + return Math.round((1 - relativeTop / height) * 100); +} diff --git a/src/components/ColorPicker/commons/saturation/helpers/calculateSaturation.js b/src/components/ColorPicker/commons/saturation/helpers/calculateSaturation.js new file mode 100644 index 000000000..55ba17915 --- /dev/null +++ b/src/components/ColorPicker/commons/saturation/helpers/calculateSaturation.js @@ -0,0 +1,7 @@ +export default function calculateSaturation(event, rect) { + const { width, left } = rect; + const x = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX; + const relativeLeft = Math.min(Math.max(0, x - (left + window.pageXOffset)), width); + + return Math.round((relativeLeft / width) * 100); +} diff --git a/src/components/ColorPicker/commons/saturation/helpers/index.js b/src/components/ColorPicker/commons/saturation/helpers/index.js new file mode 100644 index 000000000..3aff626ab --- /dev/null +++ b/src/components/ColorPicker/commons/saturation/helpers/index.js @@ -0,0 +1,2 @@ +export { default as calculateBright } from './calculateBright'; +export { default as calculateSaturation } from './calculateSaturation'; diff --git a/src/components/ColorPicker/commons/saturation/index.js b/src/components/ColorPicker/commons/saturation/index.js index ba0809e39..5cb184aff 100644 --- a/src/components/ColorPicker/commons/saturation/index.js +++ b/src/components/ColorPicker/commons/saturation/index.js @@ -9,6 +9,7 @@ import { } from '../../../../styles/helpers/color'; import { StyledColor, StyledCircle } from './styled'; import { ColorPickerContext } from '../../context'; +import { calculateSaturation, calculateBright } from './helpers'; const Saturation = React.forwardRef((_props, ref) => { const { rgba, hsv, tabIndex, onChange } = useContext(ColorPickerContext); @@ -34,14 +35,8 @@ const Saturation = React.forwardRef((_props, ref) => { const handleChange = event => { const rect = containerRef.current.getBoundingClientRect(); - const { width, height } = rect; - const x = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX; - const y = typeof event.pageY === 'number' ? event.pageY : event.touches[0].pageY; - const left = Math.min(Math.max(0, x - (rect.left + window.pageXOffset)), width); - const top = Math.min(Math.max(0, y - (rect.top + window.pageYOffset)), height); - - const saturation = Math.round((left / width) * 100); - const bright = Math.round((1 - top / height) * 100); + const saturation = calculateSaturation(event, rect); + const bright = calculateBright(event, rect); change({ saturation, bright }); }; @@ -83,7 +78,6 @@ const Saturation = React.forwardRef((_props, ref) => { const { keyCode } = event; if (keyHandlerMap[keyCode]) { event.preventDefault(); - event.stopPropagation(); keyHandlerMap[keyCode](); } }; diff --git a/src/components/ColorPicker/variants/default.js b/src/components/ColorPicker/variants/default.js index 8cb455b7a..e1b84174d 100644 --- a/src/components/ColorPicker/variants/default.js +++ b/src/components/ColorPicker/variants/default.js @@ -7,7 +7,7 @@ import { StyledHexColorContainer, StyledRgbaColorContainer, } from './styled'; -import { Saturation, Hue, Alpha, Preview, HexColor, RgbaColor, DefaultColors } from '../commons'; +import { Saturation, Hue, Alpha, Preview, Hex, Rgba, DefaultColors } from '../commons'; import RenderIf from '../../RenderIf'; const Default = React.forwardRef((_props, ref) => { @@ -43,14 +43,14 @@ const Default = React.forwardRef((_props, ref) => { - + - + - + From 59e275744ef4e0764bd2cd3764f1a9f0b3f94efe Mon Sep 17 00:00:00 2001 From: Yuri Victor Munayev Date: Tue, 1 Sep 2020 15:36:45 -0400 Subject: [PATCH 10/11] fix: add min-height to saturation component --- src/components/ColorPicker/commons/saturation/styled.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ColorPicker/commons/saturation/styled.js b/src/components/ColorPicker/commons/saturation/styled.js index e25efe25b..9cddf3d60 100644 --- a/src/components/ColorPicker/commons/saturation/styled.js +++ b/src/components/ColorPicker/commons/saturation/styled.js @@ -3,6 +3,7 @@ import attachThemeAttrs from '../../../../styles/helpers/attachThemeAttrs'; export const StyledColor = styled.div` position: relative; + min-height: 160px; height: 100%; overflow: hidden; From 34ab944b1f6f46745aa748d75f93505246242b73 Mon Sep 17 00:00:00 2001 From: TahimiLeonBravo Date: Thu, 3 Sep 2020 23:31:51 -0500 Subject: [PATCH 11/11] fix: emaple --- src/components/ColorPicker/readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ColorPicker/readme.md b/src/components/ColorPicker/readme.md index 521a5e7bc..a82793be1 100644 --- a/src/components/ColorPicker/readme.md +++ b/src/components/ColorPicker/readme.md @@ -56,12 +56,11 @@ const Container = styled.div` `; const StyledCard = styled(Card)` - padding: 10px; + padding: 20px; `; const StyledContent = styled.div` width: 19rem; - height: 16rem; `; const StyledLabel = styled.span`