diff --git a/ColorPicker/ColorPicker.js b/ColorPicker/ColorPicker.js index da6d196762..a838431265 100644 --- a/ColorPicker/ColorPicker.js +++ b/ColorPicker/ColorPicker.js @@ -1,181 +1,244 @@ -/* eslint-disable react-hooks/rules-of-hooks */ /** - * Sandstone component to allow the user to choose a color. + * Sandstone component that allows the user to choose a color + * either from a grid, a spectrum, or RGB/HSL color sliders. * * @example * * * @module sandstone/ColorPicker * @exports ColorPicker * @exports ColorPickerBase - * @exports ColorPickerDecorator - * @private + * @public */ - -import kind from '@enact/core/kind'; import Spottable from '@enact/spotlight/Spottable'; -import {Cell, Column, Row} from '@enact/ui/Layout'; -import Toggleable from '@enact/ui/Toggleable'; +import {Cell, Row} from '@enact/ui/Layout'; import PropTypes from 'prop-types'; -import compose from 'ramda/src/compose'; -import {useCallback, useEffect, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; -import BodyText from '../BodyText'; -import Button, {ButtonBase} from '../Button'; +import {ButtonBase} from '../Button'; import Icon from '../Icon'; -import Item from '../Item'; import Popup from '../Popup'; import Skinnable from '../Skinnable'; -import Slider from '../Slider'; +import TabGroup from '../TabLayout/TabGroup'; -import {hexToHSL, HSLToHex} from './utils'; +import ColorPickerGrid from './ColorPickerGrid'; +import ColorPickerSlider from './ColorPickerSlider'; +import ColorPickerSpectrum from './ColorPickerSpectrum'; +import {generateOppositeColor} from './utils'; import componentCss from './ColorPicker.module.less'; const SpottableButton = Spottable(ButtonBase); +const defaultColors = ['#eb4034', '#32a852', '#3455eb']; + /** - * A component that contains the content for the {@link sandstone/ColorPicker|ColorPicker} popup. + * The favorite colors component. + * + * This component is most often not used directly but may be composed within another component as it + * is within {@link sandstone/ColorPicker|ColorPicker}. * - * @class PopupContent + * @class FavoriteColors * @memberof sandstone/ColorPicker * @ui * @private */ -const PopupContent = ({color, colorHandler, css, presetColors}) => { - const [hue, setHue] = useState(0); - const [saturation, setSaturation] = useState(0); - const [lightness, setLightness] = useState(0); - - useEffect(() => { - let {h, s, l} = hexToHSL(color); - - setHue(h); - setSaturation(s); - setLightness(l); - }, [color]); +const FavoriteColors = ({disabled, favoriteColors = [], favoriteColorsHandler, selectedColor = '#3455eb', selectedColorHandler}) => { + const [clickEnabled, setClickEnabled] = useState(true); + const [editEnabled, setEditEnabled] = useState(false); + + const shakeEffectRef = useRef(null); + const timerRef = useRef(null); + + const addNewFavoriteColor = useCallback(() => { + if (disabled) return; + if (favoriteColors.includes(selectedColor)) return; + favoriteColorsHandler(() => { + const colorsState = [...favoriteColors, selectedColor]; + if (colorsState.length > 8) colorsState.shift(); + + return colorsState; + }); + }, [disabled, favoriteColors, favoriteColorsHandler, selectedColor]); + + const onAddNewFavoriteColor = useCallback(() => { + if (disabled) return; + if (editEnabled) { + setEditEnabled(false); + return; + } + addNewFavoriteColor(); + }, [addNewFavoriteColor, disabled, editEnabled]); + + const onSelectFavoriteColor = useCallback((ev) => { + if (disabled) return; + if (!clickEnabled) return; + const targetId = ev.target.offsetParent.id || ev.target.id; + const [buttonColor, buttonIndex] = targetId.split('-'); + + if (editEnabled && clickEnabled) { + const filteredColors = favoriteColors.filter((color, index) => { + return !(color === buttonColor && index === Number(buttonIndex)); + }); + + favoriteColorsHandler(filteredColors); + selectedColorHandler(selectedColor); + return; + } - const changeHue = useCallback((ev) => { - setHue(ev.value); - }, []); + favoriteColorsHandler(favoriteColors); + selectedColorHandler(buttonColor); + }, [clickEnabled, disabled, editEnabled, favoriteColors, favoriteColorsHandler, selectedColor, selectedColorHandler]); + + const onPressHandler = useCallback((ev) => { + if (disabled) return; + if (editEnabled) return; + if (ev.type === 'pointerdown' || (ev.type === 'keydown' && ev.keyCode === 13)) { + const target = ev.target.id ? ev.target : ev.target.offsetParent; + + shakeEffectRef.current = setTimeout(() => { + target.classList.add(componentCss.shakeFavoriteColor); + }, 300); + + timerRef.current = setTimeout(() => { + setEditEnabled(true); + setClickEnabled(false); + target.classList.remove(componentCss.shakeFavoriteColor); + }, 1000); + } + }, [disabled, editEnabled]); - const changeLightness = useCallback((ev) => { - setLightness(ev.value); - }, []); + const onReleaseHandler = useCallback((ev) => { + const target = ev.target.id ? ev.target : ev.target.offsetParent; + target.classList.remove(componentCss.shakeFavoriteColor); - const changeSaturation = useCallback((ev) => { - setSaturation(ev.value); + clearTimeout(shakeEffectRef.current); + clearTimeout(timerRef.current); + setTimeout(() => { + setClickEnabled(true); + }, 100); }, []); - const handleClick = useCallback((ev) => { - colorHandler(ev.target.offsetParent.id); - }, [colorHandler]); - - const onSliderValueChange = useCallback(() => { - colorHandler(HSLToHex(hue, saturation, lightness)); - }, [colorHandler, hue, lightness, saturation]); - return ( - - - {presetColors?.map((presetColor, presetColorIndex) => { - - return ( - +
+ + + {favoriteColors.slice(4, 8).map((color, index) => { + return ( - - ); - })} + onClick={onSelectFavoriteColor} + onKeyDown={onPressHandler} + onKeyUp={onReleaseHandler} + onPointerDown={onPressHandler} + onPointerUp={onReleaseHandler} + size="small" + spotlightDisabled={disabled} + style={{ + backgroundColor: color, + borderColor: generateOppositeColor(color), + color: generateOppositeColor(color) + }} + > + {editEnabled && trash} + + ); + })} + + + {favoriteColors.slice(0, 4).map((color, index) => { + return ( + + {editEnabled && trash} + + ); + })} + -
- - Hue {hue} - - Saturation {saturation}% - - Lightness {lightness}% - - -
-
- + + + {editEnabled ? 'check' : 'plus'} + + +
); }; -PopupContent.propTypes = { +FavoriteColors.displayName = 'FavoriteColors'; + +FavoriteColors.propTypes = { /** - * Indicates the color. + * Applies a disabled style and prevents interacting with the component. * - * @type {String} + * @type {Boolean} + * @default false * @private */ - color: PropTypes.string, + disabled: PropTypes.bool, /** - * Called when color is modified. + * Contains an array with the favorite colors. * - * @type {Function} + * @type {Array} * @private */ - colorHandler: PropTypes.func, + favoriteColors: PropTypes.array, /** - * Customizes the component by mapping the supplied collection of CSS class names to the - * corresponding internal elements and states of this component. - * - * The following classes are supported: + * Called when the favorite colors array is modified. * - * `colorPicker` - The root class name - * `coloredDiv` - A class name used for a single div + * @type {Function} + * @private + */ + favoriteColorsHandler: PropTypes.func, + + /** + * Indicates the selected color. * - * @type {Object} + * @type {String} * @private */ - css: PropTypes.object, + selectedColor: PropTypes.string, /** - * Contains an array with a couple of possible preset colors. + * Called when the selected color is modified. * - * @type {Array} + * @type {Function} * @private */ - presetColors: PropTypes.array + selectedColorHandler: PropTypes.func }; /** @@ -187,161 +250,178 @@ PopupContent.propTypes = { * @class ColorPickerBase * @memberof sandstone/ColorPicker * @ui - * @private + * @public */ -const ColorPickerBase = kind({ - name: 'ColorPicker', - - functional: true, - - propTypes: /** @lends sandstone/ColorPicker.ColorPickerBase.prototype */ { - /** - * Indicates the color. - * - * @type {String} - * @public - */ - color: PropTypes.string, - - /** - * Called when the color is modified. - * - * @type {Function} - * @public - */ - colorHandler: PropTypes.func, - - /** - * Customizes the component by mapping the supplied collection of CSS class names to the - * corresponding internal elements and states of this component. - * - * The following classes are supported: - * - * `colorPicker` - The root class name - * `coloredDiv` - A class name used for a single div - * - * @type {Object} - * @public - */ - css: PropTypes.object, - - /** - * Applies the `disabled` class. - * - * When `true`, the color picker is shown as disabled. - * - * @type {Boolean} - * @default false - * @public - */ - disabled: PropTypes.bool, - - /** - * Called to open or close the color picker. - * - * @type {Function} - * @public - */ - onTogglePopup: PropTypes.func, - - /** - * Indicates if the color picker is open. - * - * When `true`, contextual popup opens. - * - * @type {Boolean} - * @default false - * @private - */ - popupOpen: PropTypes.bool, - - /** - * Contains an array with a couple of possible preset colors. - * - * @type {Array} - * @public - */ - presetColors: PropTypes.array, - - /** - * Contains the text that shows near color picker. - * - * @type {String} - * @public - */ - text: PropTypes.string - }, - - handlers: { - handleClosePopup: (ev, {onTogglePopup}) => { - onTogglePopup(); - }, - handleOpenPopup: (ev, {disabled, onTogglePopup}) => { - if (!disabled) { - onTogglePopup(); - } +const ColorPickerBase = ({color = '#eb4034', colors = defaultColors, disabled, onChangeColor, open, type = 'grid', ...rest}) => { + const [favoriteColors, setFavoriteColors] = useState(colors); + const [selectedColor, setSelectedColor] = useState(color); + const [tabLayoutIndex, setTabLayoutIndex] = useState(0); + + useEffect(() => { + setFavoriteColors(colors); + setSelectedColor(color); + + switch (type) { + case 'grid': + setTabLayoutIndex(0); + return; + case 'spectrum': + setTabLayoutIndex(1); + return; + case 'sliders': + setTabLayoutIndex(2); + return; + default: + setTabLayoutIndex(0); + } + }, [color, colors, type]); + + const handleFavouriteColors = useCallback(favColors => { + const parameterType = typeof (favColors); + let newFavouriteColors = {}; + + switch (parameterType) { + case 'function': + newFavouriteColors = favColors(); + break; + case 'object': + newFavouriteColors = favColors; + break; + default: + break; } - }, - - styles: { - css: componentCss, - publicClassNames: true - }, - - render: ({color, colorHandler, css, disabled = false, handleClosePopup, handleOpenPopup, popupOpen = false, presetColors, text, ...rest}) => { - delete rest.onTogglePopup; - - const CloseIcon = useCallback((props) => , [css]); - const slotAfter = ; - - return ( - - - {text} - - - - - {text} - - -