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);
+ }}
+ />
+
+ >
+ )}
+
+
+ }
+ >
+ {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";