diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3c53c7e..5942975bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `useOnly` property: specify if only parts of the content should be used for the shortened preview, this property replaces `firstNonEmptyLineOnly` - `` - `paddingSize` property to add easily some white space +- `` + - `hideIndicator` property: hide the radio inout indicator but click on children can be processed via `onChange` event +- `` + - input component for colors, uses the configured palette by default but it also allows to enter custom colors - CSS custom properties - beside the color palette we now mirror the most important layout configuration variables as CSS custom properties - new icons: diff --git a/src/common/index.ts b/src/common/index.ts index ab989fa3a..ed8eb78a9 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -3,7 +3,7 @@ import { decode } from "he"; import { invisibleZeroWidthCharacters } from "./utils/characters"; import { colorCalculateDistance } from "./utils/colorCalculateDistance"; import decideContrastColorValue from "./utils/colorDecideContrastvalue"; -import { getEnabledColorsFromPalette, textToColorHash } from "./utils/colorHash"; +import { getEnabledColorPropertiesFromPalette, getEnabledColorsFromPalette, textToColorHash } from "./utils/colorHash"; import getColorConfiguration from "./utils/getColorConfiguration"; import { getScrollParent } from "./utils/getScrollParent"; import { getGlobalVar, setGlobalVar } from "./utils/globalVars"; @@ -22,6 +22,7 @@ export const utils = { setGlobalVar, getScrollParent, getEnabledColorsFromPalette, + getEnabledColorPropertiesFromPalette, textToColorHash, reduceToText, decodeHtmlEntities: decode, diff --git a/src/common/utils/colorHash.ts b/src/common/utils/colorHash.ts index 45dab3daa..87af57844 100644 --- a/src/common/utils/colorHash.ts +++ b/src/common/utils/colorHash.ts @@ -6,8 +6,8 @@ import { colorCalculateDistance } from "./colorCalculateDistance"; import CssCustomProperties from "./CssCustomProperties"; type ColorOrFalse = Color | false; -type ColorWeight = 100 | 300 | 500 | 700 | 900; -type PaletteGroup = "identity" | "semantic" | "layout" | "extra"; +export type ColorWeight = 100 | 300 | 500 | 700 | 900; +export type PaletteGroup = "identity" | "semantic" | "layout" | "extra"; interface getEnabledColorsProps { /** Specify the palette groups used to define the set of colors. */ @@ -21,20 +21,43 @@ interface getEnabledColorsProps { } const getEnabledColorsFromPaletteCache = new Map(); +const getEnabledColorPropertiesFromPaletteCache = new Map(); -export function getEnabledColorsFromPalette({ +export function getEnabledColorsFromPalette(props: getEnabledColorsProps): Color[] { + const configId = JSON.stringify({ + includePaletteGroup: props.includePaletteGroup, + includeColorWeight: props.includeColorWeight, + }); + + if (getEnabledColorsFromPaletteCache.has(configId)) { + return getEnabledColorsFromPaletteCache.get(configId)!; + } + + const colorPropertiesFromPalette = Object.values(getEnabledColorPropertiesFromPalette(props)); + + getEnabledColorsFromPaletteCache.set( + configId, + colorPropertiesFromPalette.map((color) => { + return Color(color[1]); + }) + ); + + return getEnabledColorsFromPaletteCache.get(configId)!; +} + +export function getEnabledColorPropertiesFromPalette({ includePaletteGroup = ["layout"], includeColorWeight = [100, 300, 500, 700, 900], - // TODO (planned for later): includeMixedColors = false, + // (planned for later): includeMixedColors = false, minimalColorDistance = COLORMINDISTANCE, -}: getEnabledColorsProps): Color[] { +}: getEnabledColorsProps): string[][] { const configId = JSON.stringify({ includePaletteGroup, includeColorWeight, }); - if (getEnabledColorsFromPaletteCache.has(configId)) { - return getEnabledColorsFromPaletteCache.get(configId)!; + if (getEnabledColorPropertiesFromPaletteCache.has(configId)) { + return getEnabledColorPropertiesFromPaletteCache.get(configId)!; } const colorsFromPalette = new CssCustomProperties({ @@ -50,18 +73,18 @@ export function getEnabledColorsFromPalette({ const weight = parseInt(tint[2], 10) as ColorWeight; return includePaletteGroup.includes(group) && includeColorWeight.includes(weight); }, - removeDashPrefix: false, + removeDashPrefix: true, returnObject: true, }).customProperties(); - const colorsFromPaletteValues = Object.values(colorsFromPalette) as string[]; + const colorsFromPaletteValues = Object.entries(colorsFromPalette) as [string, string][]; const colorsFromPaletteWithEnoughDistance = minimalColorDistance > 0 - ? colorsFromPaletteValues.reduce((enoughDistance: string[], color: string) => { + ? colorsFromPaletteValues.reduce((enoughDistance: [string, string][], color: [string, string]) => { if (enoughDistance.includes(color)) { return enoughDistance.filter((checkColor) => { - const distance = colorCalculateDistance({ color1: color, color2: checkColor }); + const distance = colorCalculateDistance({ color1: color[1], color2: checkColor[1] }); return checkColor === color || (distance && minimalColorDistance <= distance); }); } else { @@ -70,14 +93,9 @@ export function getEnabledColorsFromPalette({ }, colorsFromPaletteValues) : colorsFromPaletteValues; - getEnabledColorsFromPaletteCache.set( - configId, - colorsFromPaletteWithEnoughDistance.map((color: string) => { - return Color(color); - }) - ); + getEnabledColorPropertiesFromPaletteCache.set(configId, colorsFromPaletteWithEnoughDistance); - return getEnabledColorsFromPaletteCache.get(configId)!; + return getEnabledColorPropertiesFromPaletteCache.get(configId)!; } function getColorcode(text: string): ColorOrFalse { diff --git a/src/components/ColorField/ColorField.stories.tsx b/src/components/ColorField/ColorField.stories.tsx new file mode 100644 index 000000000..5d61b641e --- /dev/null +++ b/src/components/ColorField/ColorField.stories.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; + +import textFieldTest from "../TextField/stories/TextField.stories"; + +import { ColorField, ColorFieldProps } from "./ColorField"; + +export default { + title: "Forms/ColorField", + component: ColorField, + argTypes: { + ...textFieldTest.argTypes, + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + onChange: (e) => { + alert(e.target.value); + }, +}; + +export const NoPalettePresets = Template.bind({}); +NoPalettePresets.args = { + ...Default.args, + colorWeightFilter: [], + paletteGroupFilter: [], + allowCustomColor: true, +}; + +interface TemplateColorHashProps + extends Pick { + stringForColorHashValue: string; +} + +const TemplateColorHash: StoryFn = (args: TemplateColorHashProps) => ( + +); + +/** + * Component provides a helper function to calculate a color hash from a text, + * that can be used as `value` or `defaultValue`. + * + * ``` + * + * ``` + * + * You can add `options` to set the config for the color palette filters. + * The same default values like on `ColorField` are used for them. + */ +export const ColorHashValue = TemplateColorHash.bind({}); +ColorHashValue.args = { + ...Default.args, + allowCustomColor: true, + colorWeightFilter: [300, 500, 700], + paletteGroupFilter: ["layout", "extra"], + stringForColorHashValue: "My text that will used to create a color hash as initial value.", +}; diff --git a/src/components/ColorField/ColorField.tsx b/src/components/ColorField/ColorField.tsx new file mode 100644 index 000000000..9638aaa30 --- /dev/null +++ b/src/components/ColorField/ColorField.tsx @@ -0,0 +1,200 @@ +import React, { CSSProperties } from "react"; +import classNames from "classnames"; + +import { utils } from "../../common"; +import { ColorWeight, PaletteGroup } from "../../common/utils/colorHash"; +import { CLASSPREFIX as eccgui } from "../../configuration/constants"; +import { ContextOverlay } from "../ContextOverlay"; +import { FieldSet } from "../Form"; +import { RadioButton } from "../RadioButton/RadioButton"; +import { Spacing } from "../Separation/Spacing"; +import { Tag, TagList } from "../Tag"; +import { TextField, TextFieldProps } from "../TextField"; +import { Tooltip } from "../Tooltip/Tooltip"; +import { WhiteSpaceContainer } from "../Typography"; + +export interface ColorFieldProps extends Omit { + /** + * Any color can be selected, not only from the configured color palette. + */ + allowCustomColor?: boolean; + /** + * What color weights should be included in the set of allowed colors. + */ + colorWeightFilter?: ColorWeight[]; + /** + * What palette groups should be included in the set of allowed colors. + */ + paletteGroupFilter?: PaletteGroup[]; +} + +/** + * Color input field that provides resets from the configured color palette. + * Use `colorWeightFilter` and `paletteGroupFilter` to filter them. + */ +export const ColorField = ({ + className = "", + allowCustomColor = false, + colorWeightFilter = [100, 300, 700, 900], + paletteGroupFilter = ["layout"], + defaultValue, + value, + onChange, + fullWidth = false, + ...otherTextFieldProps +}: ColorFieldProps) => { + const ref = React.useRef(null); + const [colorValue, setColorValue] = React.useState(defaultValue || value || "#000000"); + + let allowedPaletteColors, disableNativePicker, disabled; + const updateConfig = () => { + allowedPaletteColors = utils.getEnabledColorPropertiesFromPalette({ + includePaletteGroup: paletteGroupFilter, + includeColorWeight: colorWeightFilter, + minimalColorDistance: 0, // we use all allowed colors, and do not check distances between them + }); + + disableNativePicker = + colorWeightFilter.length > 0 && paletteGroupFilter.length > 0 && allowedPaletteColors.length > 0; + disabled = (!disableNativePicker && !allowCustomColor) || otherTextFieldProps.disabled; + }; + updateConfig(); + React.useEffect(() => { + updateConfig(); + }, [allowCustomColor, colorWeightFilter, paletteGroupFilter, otherTextFieldProps]); + + React.useEffect(() => { + setColorValue(defaultValue || value || "#000000"); + }, [defaultValue, value]); + + const forwardOnChange = (forwardedEvent: React.ChangeEvent) => { + setColorValue(forwardedEvent.target.value); + if (onChange) { + onChange(forwardedEvent); + } + }; + + const colorInput = ( + ) => { + forwardOnChange(e); + } + : undefined + } + style={{ ...otherTextFieldProps.style, [`--eccgui-colorfield-background`]: colorValue } as CSSProperties} + /> + ); + + return disableNativePicker && !disabled ? ( + + {allowCustomColor && ( + <> + ) => { + forwardOnChange(e); + }} + /> + + + )} +
+ = 3 ? colorWeightFilter.length * 2 : "8" + }col`} + > + {allowedPaletteColors!.map((color: [string, string], idx: number) => [ + ) => { + forwardOnChange(e); + }} + > + + + {color[1]} + + + , + // Looks like we cannot force some type of line break in the tag list via CSS only + (idx + 1) % (colorWeightFilter.length >= 3 ? colorWeightFilter.length * 2 : 8) === + 0 && ( + <> +
+ + ), + ])} +
+
+ + } + > + {colorInput} +
+ ) : ( + colorInput + ); +}; + +type calculateColorHashValueProps = Pick< + ColorFieldProps, + "allowCustomColor" | "colorWeightFilter" | "paletteGroupFilter" +>; + +/** + * Simple helper function that provide simple access to color hash calculation. + * Using the same default values for the color palette filter. + */ +ColorField.calculateColorHashValue = ( + text: string, + options: calculateColorHashValueProps = { + allowCustomColor: false, + colorWeightFilter: [100, 300, 700, 900], + paletteGroupFilter: ["layout"], + } +) => { + const hash = utils.textToColorHash({ + text, + options: { + returnValidColorsDirectly: options.allowCustomColor as boolean, + enabledColors: utils.getEnabledColorsFromPalette({ + includePaletteGroup: options.paletteGroupFilter, + includeColorWeight: options.colorWeightFilter, + minimalColorDistance: 0, + }), + }, + }); + + return hash ? hash : undefined; +}; + +export default ColorField; diff --git a/src/components/ColorField/_colorfield.scss b/src/components/ColorField/_colorfield.scss new file mode 100644 index 000000000..51e8b929a --- /dev/null +++ b/src/components/ColorField/_colorfield.scss @@ -0,0 +1,56 @@ +.#{$eccgui}-colorfield { + cursor: default; + + &:not(.#{$ns}-fill) { + width: 100%; + max-width: 4 * $eccgui-size-textfield-height-regular; + } + + .#{$ns}-input { + color: var(--#{$eccgui}-colorfield-background); + cursor: inherit; + background-color: var(--#{$eccgui}-colorfield-background); + + &[type="color"] { + &::-webkit-color-swatch-wrapper { + display: none; + } + + &::-moz-color-swatch { + display: none; + } + } + } + + .#{$ns}-input-left-container { + top: 1px; + left: 1px !important; + height: calc(100% - 2px); + background-color: $eccgui-color-textfield-background; + } + .#{$ns}-input-action { + top: 1px; + right: 1px !important; + height: calc(100% - 2px); + background-color: $eccgui-color-textfield-background; + } +} + +.#{$eccgui}-colorfield__palette { + & > li:has(.#{$eccgui}-colorfield__palette-linebreak) { + display: block; + width: 100%; + height: 0; + margin: 0; + overflow: hidden; + } +} + +.#{$eccgui}-colorfield__palette__color { + margin: 0; + .#{$eccgui}-tag__item { + width: 3rem; + color: var(--#{$eccgui}-colorfield-palette-color) !important; + background-color: var(--#{$eccgui}-colorfield-palette-color) !important; + } +} diff --git a/src/components/RadioButton/RadioButton.tsx b/src/components/RadioButton/RadioButton.tsx index 477d10030..e4e3f5272 100644 --- a/src/components/RadioButton/RadioButton.tsx +++ b/src/components/RadioButton/RadioButton.tsx @@ -1,13 +1,25 @@ import React from "react"; import { Radio as BlueprintRadioButton, RadioProps as BlueprintRadioProps } from "@blueprintjs/core"; +import classNames from "classnames"; import { CLASSPREFIX as eccgui } from "../../configuration/constants"; -export type RadioButtonProps = BlueprintRadioProps; +export interface RadioButtonProps extends BlueprintRadioProps { + /** + * Hide the indicator. + * The element cannot be identified as radio input then but a click on the children can be easily processed via `onChange` event. + */ + hideIndicator?: boolean; +} -export const RadioButton = ({ children, className = "", ...restProps }: RadioButtonProps) => { +export const RadioButton = ({ children, className = "", hideIndicator = false, ...restProps }: RadioButtonProps) => { return ( - + {children} ); diff --git a/src/components/RadioButton/radiobutton.scss b/src/components/RadioButton/radiobutton.scss index ba6648323..0744acf3f 100644 --- a/src/components/RadioButton/radiobutton.scss +++ b/src/components/RadioButton/radiobutton.scss @@ -29,3 +29,16 @@ } } } + +.#{$eccgui}-radiobutton--hidden-indicator { + &:not(.#{$ns}-align-right) { + padding-inline-start: 0; + } + &:not(.#{$ns}-align-left) { + padding-inline-end: 0; + } + + input ~ .#{$ns}-control-indicator { + visibility: hidden; + } +} diff --git a/src/components/index.scss b/src/components/index.scss index f8cbf4fae..7d0bc7352 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -33,6 +33,7 @@ @import "./Tabs/tabs"; @import "./Tag/tag"; @import "./TextField/textfield"; +@import "./ColorField/colorfield"; @import "./TagInput/taginput"; @import "./Toolbar/toolbar"; @import "./Tooltip/tooltip"; diff --git a/src/components/index.ts b/src/components/index.ts index 3ee70457e..372973482 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,6 +7,7 @@ export * from "./Card"; export * from "./Chat"; export * from "./Checkbox/Checkbox"; export * from "./CodeAutocompleteField"; +export * from "./ColorField/ColorField"; export * from "./ContentGroup/ContentGroup"; export * from "./ContextOverlay"; export * from "./DecoupledOverlay/DecoupledOverlay";