@salesforce/design-system-react
Version:
Salesforce Lightning Design System for React
655 lines (608 loc) • 20.6 kB
JSX
/* eslint-disable max-lines */
/* eslint-disable react/sort-comp */
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import assign from 'lodash.assign';
import checkProps from './check-props';
import CustomColor from './private/custom-color';
import Swatch from './private/swatch';
import SwatchPicker from './private/swatch-picker';
import Button from '../button';
import Input from '../input';
import Tabs from '../tabs';
import TabsPanel from '../tabs/panel';
import Popover from '../popover';
import ColorUtils from '../../utilities/color';
import { COLOR_PICKER } from '../../utilities/constants';
import generateId from '../../utilities/generate-id';
import componentDoc from './component.json';
const propTypes = {
/**
* **Assistive text for accessibility**
* * `label`: Visually hidden label but read out loud by screen readers.
* * `hueSlider`: Instructions for hue selection input
* * `saturationValueGrid`: Instructions for using the grid for saturation
* and value selection
*/
assistiveText: PropTypes.shape({
label: PropTypes.string,
hueSlider: PropTypes.string,
saturationValueGrid: PropTypes.string,
}),
/**
* CSS classes to be added to tag with `.slds-color-picker`. Uses `classNames` [API](https://github.com/JedWatson/classnames). _Tested with snapshot testing._
*/
className: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string,
]),
/**
* CSS classes to be added to tag with `.slds-popover`. Uses `classNames` [API](https://github.com/JedWatson/classnames). _Tested with snapshot testing._
*/
classNameMenu: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string,
]),
/**
* Unique ID for component.
*/
id: PropTypes.string,
/**
* Disables the input and button.
*/
disabled: PropTypes.bool,
/**
* Message to display when the outer input is in an error state. When this is present, also visually highlights the component as in error.
*/
errorText: PropTypes.string,
/**
* Message to display when the custom tab input is in an error state. When this is present, also visually highlights the component as in error.
*/
errorTextWorkingColor: PropTypes.string,
/**
* Event Callbacks
* * `onChange`: This function is triggered when done is clicked. This function returns `{event, { color: [string] }}`, which is a hex representation of the color.
* * `onClose`: This function is triggered when the menu is closed. This function returns `{event, { trigger, componentWillUnmount }}`. Trigger can have the values `cancel`, `clickOutside`, or `newPopover`.
* * `onOpen`: This function is triggered when the color-picker menu is mounted and added to the DOM. The parameters are `event, { portal: }`. `portal` can be used as a React tree root node.
* * `onRequestClose`: This function is triggered when the user clicks outside the menu or clicks the close button. You will want to define this if color-picker is to be a controlled component. Most of the time you will want to set `isOpen` to `false` when this is triggered unless you need to validate something.
* This function returns `{event, {trigger: [string]}}` where `trigger` is either `cancel` or `clickOutside`.
* * `onRequestOpen`: Function called when the color-picker menu would like show.
* * `onValidateColor`: Function that overwrites default color validator and called when validating HEX color on outer input change. If callback returns false, errorText is shown if set.
* * `onValidateWorkingColor`: Function that overwrites default color validator and called when validating HEX color on custom tab inner input change. If callback returns false, errorTextWorkingColor is shown if set.
* * `onWorkingColorChange`: This function is triggered when working color changes (color inside the custom tab). This function returns `{event, { color: [string] }}`, which is a hex representation of the color.
* _Tested with Mocha framework._
*/
events: PropTypes.shape({
onChange: PropTypes.func,
onClose: PropTypes.func,
onOpen: PropTypes.func,
onRequestClose: PropTypes.func,
onRequestOpen: PropTypes.func,
onValidateColor: PropTypes.func,
onValidateWorkingColor: PropTypes.func,
onWorkingColorChange: PropTypes.func,
}),
/**
* By default, dialogs will flip their alignment (such as bottom to top) if they extend beyond a boundary element such as a scrolling parent or a window/viewpoint. `hasStaticAlignment` disables this behavior and allows this component to extend beyond boundary elements. _Not tested._
*/
hasStaticAlignment: PropTypes.bool,
/**
* Hides the text input
*/
hideInput: PropTypes.bool,
/**
* Popover open state
*/
isOpen: PropTypes.bool,
/**
* **Text labels for internationalization**
* * `blueAbbreviated`: One letter abbreviation of blue color component
* * `cancelButton`: Text for cancel button on popover
* * `customTab`: Text for custom tab of popover
* * `customTabActiveWorkingColorSwatch`: Label for custom tab active working color swatch
* * `customTabTransparentSwatch`: Label for custom tab active transparent swatch
* * `greenAbbreviated`: One letter abbreviation of green color component
* * `hexLabel`: Label for input of hexadecimal color
* * `invalidColor`: Error message when hex color input is invalid
* * `invalidComponent`: Error message when a component input is invalid
* * `label`: An `input` label as for a `form`
* * `redAbbreviated`: One letter abbreviation of red color component
* * `swatchTab`: Label for swatch tab of popover
* * `submitButton`: Text for submit/done button of popover
*/
labels: PropTypes.shape({
blueAbbreviated: PropTypes.string,
cancelButton: PropTypes.string,
customTab: PropTypes.string,
customTabActiveWorkingColorSwatch: PropTypes.string,
customTabTransparentSwatch: PropTypes.string,
greenAbbreviated: PropTypes.string,
hexLabel: PropTypes.string,
invalidColor: PropTypes.string,
invalidComponent: PropTypes.string,
label: PropTypes.string,
redAbbreviated: PropTypes.string,
swatchTab: PropTypes.string,
swatchTabTransparentSwatch: PropTypes.string,
submitButton: PropTypes.string,
}),
/**
* Please select one of the following:
* * `absolute` - (default) The dialog will use `position: absolute` and style attributes to position itself. This allows inverted placement or flipping of the dialog.
* * `overflowBoundaryElement` - The dialog will overflow scrolling parents. Use on elements that are aligned to the left or right of their target and don't care about the target being within a scrolling parent. Typically this is a popover or tooltip. Dropdown menus can usually open up and down if no room exists. In order to achieve this a portal element will be created and attached to `body`. This element will render into that detached render tree.
* * `relative` - No styling or portals will be used. Menus will be positioned relative to their triggers. This is a great choice for HTML snapshot testing.
*/
menuPosition: PropTypes.oneOf([
'absolute',
'overflowBoundaryElement',
'relative',
]),
/**
* An array of hex color values which is used to set the options of the
* swatch tab of the colorpicker popover.
* To specify transparent, use empty string as a value.
*/
swatchColors: PropTypes.arrayOf(PropTypes.string),
/**
* Determines which tab is visible when dialog opens. Use this prop with `base` variant only.
* Defaults to `swatch` tab.
*/
defaultSelectedTab: PropTypes.oneOf(['swatches', 'custom']),
/**
* Selects which tabs are present for the colorpicker.
* * `base`: both swatches and custom tabs are present
* * `swatches`: only swatch tab is present
* * `custom`: only custom tab is present
* _Tested with snapshot testing._
*/
variant: PropTypes.oneOf(['base', 'swatches', 'custom']),
/**
* Current color in hexadecimal string, including # sign (eg: "#000000")
*/
value: PropTypes.string,
/**
* Current working color in hexadecimal string, including # sign (eg: "#000000")
*/
valueWorking: PropTypes.string,
};
const defaultProps = {
assistiveText: {
saturationValueGrid:
'Use arrow keys to select a saturation and brightness, on an x and y axis.',
hueSlider: 'Select Hue',
},
events: {},
labels: {
blueAbbreviated: 'B',
cancelButton: 'Cancel',
customTab: 'Custom',
customTabActiveWorkingColorSwatch: 'Working Color',
customTabTransparentSwatch: 'Transparent Swatch',
greenAbbreviated: 'G',
hexLabel: 'Hex',
invalidColor: 'The color entered is invalid',
invalidComponent: 'The value needs to be an integer from 0-255',
redAbbreviated: 'R',
submitButton: 'Done',
swatchTab: 'Default',
swatchTabTransparentSwatch: 'Transparent Swatch',
},
menuPosition: 'absolute',
swatchColors: [
'#e3abec',
'#c2dbf7',
'#9fd6ff',
'#9de7da',
'#9df0c0',
'#fff099',
'#fed49a',
'#d073e0',
'#86baf3',
'#5ebbff',
'#44d8be',
'#3be282',
'#ffe654',
'#ffb758',
'#bd35bd',
'#5779c1',
'#5679c0',
'#00aea9',
'#3cba4c',
'#f5bc25',
'#f99221',
'#580d8c',
'#001970',
'#0a2399',
'#0b7477',
'#0b6b50',
'#b67e11',
'#b85d0d',
'',
],
defaultSelectedTab: 'swatches',
variant: 'base',
};
/**
* The Unified Color Picker component allows for a fully accessible and configurable color picker, allowing the user to pick from a set of predefined colors (swatches), or to pick a custom color using a HSB selection interface. It can be configured to show one or both of those color selection interfaces. View [component blueprint guidelines](https://lightningdesignsystem.com/components/color-picker/).
*/
class ColorPicker extends React.Component {
static displayName = COLOR_PICKER;
static propTypes = propTypes;
static defaultProps = defaultProps;
constructor(props) {
super(props);
this.generatedId = props.id || generateId();
const workingColor = ColorUtils.getNewColor(
{
hex: props.valueWorking || props.value,
},
props.events.onValidateWorkingColor
);
this.state = {
currentColor: props.value != null ? props.value : '',
disabled: props.disabled,
isOpen: props.isOpen,
workingColor,
previousWorkingColor: workingColor,
colorErrorMessage: props.errorText,
};
checkProps(COLOR_PICKER, props, componentDoc);
}
componentDidUpdate(prevProps) {
// The following are only present to allow props to update the state if they get out of sync (for instance, the external store is updated).
const nextState = {};
if (this.props.value !== prevProps.value) {
nextState.currentColor = this.props.value;
}
if (this.props.valueWorking !== prevProps.valueWorking) {
nextState.workingColor = ColorUtils.getNewColor(
{
hex: this.props.valueWorking,
},
this.props.events.onValidateWorkingColor
);
}
if (this.props.disabled !== prevProps.disabled) {
nextState.disabled = this.props.disabled;
}
if (Object.entries(nextState).length !== 0) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState(nextState);
}
}
getInput({ labels }) {
return this.props.hideInput ? null : (
<Input
aria-describedby={
!this.state.isOpen && this.state.colorErrorMessage
? `color-picker-summary-error-${this.generatedId}`
: undefined
}
className={classNames(
'slds-color-picker__summary-input',
'slds-align-top',
{
'slds-has-error': !!this.state.colorErrorMessage,
}
)}
disabled={this.props.disabled}
id={`color-picker-summary-input-${this.generatedId}`}
onChange={(event) => {
this.handleHexInputChange(event, { labels });
}}
value={this.state.currentColor}
/>
);
}
getDefaultTab({ labels }) {
return (
(this.props.variant === 'base' || this.props.variant === 'swatches') && (
<TabsPanel label={labels.swatchTab}>
<SwatchPicker
color={this.state.workingColor}
labels={labels}
onSelect={this.handleSwatchSelect}
swatchColors={this.props.swatchColors}
/>
</TabsPanel>
)
);
}
getCustomTab({ labels }) {
return (
(this.props.variant === 'base' || this.props.variant === 'custom') && (
<TabsPanel label={labels.customTab}>
<CustomColor
assistiveText={this.props.assistiveText}
id={this.generatedId}
color={this.state.workingColor}
errorTextWorkingColor={this.props.errorTextWorkingColor}
previousColor={this.state.previousWorkingColor}
labels={labels}
onBlueChange={this.handleColorChange('blue')}
onGreenChange={this.handleColorChange('green')}
onHexChange={this.handleColorChange('hex')}
onHueChange={this.handleColorChange('hue')}
onRedChange={this.handleColorChange('red')}
onSwatchChange={this.handleSwatchChange}
onSaturationValueChange={this.handleSaturationValueChange}
onSaturationNavigate={this.handleNavigate('saturation')}
onValueNavigate={this.handleNavigate('value')}
/>
</TabsPanel>
)
);
}
getPopover({ labels }) {
const popoverBody = (
<Tabs
id={`color-picker-tabs-${this.generatedId}`}
defaultSelectedIndex={
this.props.defaultSelectedTab === 'custom' ? 1 : 0
}
>
{this.getDefaultTab({ labels })}
{this.getCustomTab({ labels })}
</Tabs>
);
const popoverFooter = (
<div className="slds-color-picker__selector-footer">
<Button
className="slds-color-picker__selector-cancel"
id={`color-picker-footer-cancel-${this.generatedId}`}
label={labels.cancelButton}
onClick={this.handleCancel}
variant="neutral"
/>
<Button
className="slds-color-picker__selector-submit"
disabled={
Object.keys(this.state.workingColor.errors || {}).length > 0
}
id={`color-picker-footer-submit-${this.generatedId}`}
label={labels.submitButton}
onClick={this.handleSubmitButtonClick}
variant="brand"
/>
</div>
);
return (
<Popover
ariaLabelledby={`color-picker-label-${this.generatedId}`}
align="bottom left"
body={popoverBody}
className={classNames(
'slds-color-picker__selector',
this.props.classNameMenu
)}
footer={popoverFooter}
hasNoCloseButton
hasNoNubbin
hasStaticAlignment={this.props.hasStaticAlignment}
id={`slds-color-picker__selector-${this.generatedId}`}
isOpen={this.state.isOpen}
onClose={this.props.onClose}
onOpen={this.props.onOpen}
onRequestClose={this.handleOnRequestClose}
position={this.props.menuPosition}
>
<Button
className="slds-color-picker__summary-button"
disabled={this.props.disabled}
iconClassName="slds-m-left_xx-small"
iconPosition="right"
iconVariant="more"
id={`slds-color-picker__summary-button-${this.generatedId}`}
label={
<div>
<span className="slds-assistive-text">
{this.props.assistiveText.label
? this.props.assistiveText.label
: labels.label}
</span>
<Swatch color={this.state.currentColor} labels={labels} />
</div>
}
onClick={this.handleSwatchButtonClick}
variant="icon"
/>
</Popover>
);
}
setWorkingColor(event, color) {
const newColor = ColorUtils.getNewColor(
color,
this.props.events.onValidateWorkingColor,
// eslint-disable-next-line react/no-access-state-in-setstate
this.state.workingColor
);
this.setState({
workingColor: newColor,
// eslint-disable-next-line react/no-access-state-in-setstate
previousWorkingColor: this.state.workingColor,
});
if (this.props.events.onWorkingColorChange) {
this.props.events.onWorkingColorChange(event, { color: newColor });
}
}
handleSwatchChange = (event) => {
this.setWorkingColor(event, {
hex: event.target.value,
});
};
handleOnRequestClose = (event, { trigger }) => {
if (trigger === 'clickOutside' || trigger === 'cancel') {
this.handleCancelState();
}
if (this.props.onRequestClose) {
this.props.onRequestClose(event, { trigger });
}
};
handleClickOutside = (event) => {
this.handleCancelButtonClick(event);
};
handleCancel = (event) => {
this.handleCancelState();
if (this.props.onRequestClose) {
this.props.onRequestClose(event, { trigger: 'cancel' });
}
};
handleCancelState = () => {
const workingColor = ColorUtils.getNewColor(
{
// eslint-disable-next-line react/no-access-state-in-setstate
hex: this.state.currentColor,
},
this.props.events.onValidateWorkingColor
);
this.setState({
isOpen: false,
workingColor,
previousWorkingColor: workingColor,
});
};
handleColorChange(property) {
return (event) => {
const colorProperties = {};
colorProperties[property] = event.target.value;
this.setWorkingColor(event, colorProperties);
};
}
handleHexInputChange = (event, { labels }) => {
const currentColor = event.target.value;
const namedColorHex = ColorUtils.getHexFromNamedColor(currentColor);
let isValid = false;
if (this.props.events.onValidateColor) {
isValid = this.props.events.onValidateColor(currentColor);
} else {
isValid = namedColorHex ? true : ColorUtils.isValidHex(currentColor);
}
this.setState({
currentColor,
workingColor: ColorUtils.getNewColor(
{
hex: namedColorHex || currentColor,
name: namedColorHex ? currentColor.toLowerCase() : null,
},
this.props.events.onValidateWorkingColor
),
colorErrorMessage: isValid ? '' : labels.invalidColor,
});
if (this.props.events.onChange) {
this.props.events.onChange(event, {
color: currentColor,
isValid,
});
}
};
handleNavigate(property) {
return (event, { delta }) => {
const colorProperties = {};
colorProperties[property] = delta;
const newColor = ColorUtils.getDeltaColor(
colorProperties,
this.props.events.onValidateWorkingColor,
// eslint-disable-next-line react/no-access-state-in-setstate
this.state.workingColor
);
this.setState({
workingColor: newColor,
// eslint-disable-next-line react/no-access-state-in-setstate
previousWorkingColor: this.state.workingColor,
});
if (this.props.events.onWorkingColorChange) {
this.props.events.onWorkingColorChange(event, { color: newColor });
}
};
}
handleSaturationValueChange = (event, { saturation, value }) => {
this.setWorkingColor(event, {
saturation,
value,
});
};
handleSubmitButtonClick = (event) => {
this.setState({
isOpen: false,
// eslint-disable-next-line react/no-access-state-in-setstate
currentColor: this.state.workingColor.hex,
colorErrorMessage: '',
});
if (this.props.events.onChange) {
this.props.events.onChange(event, {
color: this.state.workingColor.hex,
isValid: true,
});
}
};
handleSwatchButtonClick = () => {
const workingColor = ColorUtils.getNewColor(
{
// eslint-disable-next-line react/no-access-state-in-setstate
hex: this.state.workingColor.hex,
},
this.props.events.onValidateWorkingColor
);
this.setState({
// eslint-disable-next-line react/no-access-state-in-setstate
isOpen: !this.state.isOpen,
workingColor,
// eslint-disable-next-line react/no-access-state-in-setstate
previousWorkingColor: this.state.previousWorkingColor,
});
if (this.props.onRequestOpen) {
this.props.onRequestOpen();
}
};
handleSwatchSelect = (event, { hex }) => {
this.setWorkingColor(event, {
hex,
});
};
render() {
const labels = assign({}, defaultProps.labels, this.props.labels);
return (
<div
className={classNames('slds-color-picker', this.props.className)}
ref={(node) => {
this.wrapper = node;
}}
>
<div className="slds-color-picker__summary">
<label
className={classNames(
'slds-color-picker__summary-label',
this.props.assistiveText.label ? 'slds-assistive-text' : ''
)}
htmlFor={
!this.props.hideInput
? `color-picker-summary-input-${this.generatedId}`
: undefined
}
id={`color-picker-label-${this.generatedId}`}
>
{this.props.assistiveText.label
? this.props.assistiveText.label
: labels.label}
</label>
{this.getPopover({ labels })}
{this.getInput({ labels })}
{!this.state.isOpen && this.state.colorErrorMessage ? (
<p
className="slds-form-error"
id={`color-picker-summary-error-${this.generatedId}`}
>
{this.state.colorErrorMessage}
</p>
) : (
''
)}
</div>
</div>
);
}
}
export default ColorPicker;