Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
- `<ContextOverlay />`
- `paddingSize` property to add easily some white space
- `<RadioButton />`
- `hideIndicator` property: hide the radio inout indicator but click on children can be processed via `onChange` event
- `<ColorField />`
- 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:
Expand Down
3 changes: 2 additions & 1 deletion src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -22,6 +22,7 @@ export const utils = {
setGlobalVar,
getScrollParent,
getEnabledColorsFromPalette,
getEnabledColorPropertiesFromPalette,
textToColorHash,
reduceToText,
decodeHtmlEntities: decode,
Expand Down
54 changes: 36 additions & 18 deletions src/common/utils/colorHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -21,20 +21,43 @@ interface getEnabledColorsProps {
}

const getEnabledColorsFromPaletteCache = new Map<string, Color[]>();
const getEnabledColorPropertiesFromPaletteCache = new Map<string, string[][]>();

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({
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions src/components/ColorField/ColorField.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof ColorField>;

const Template: StoryFn<typeof ColorField> = (args) => <ColorField {...args}></ColorField>;

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<ColorFieldProps, "onChange" | "allowCustomColor" | "colorWeightFilter" | "paletteGroupFilter"> {
stringForColorHashValue: string;
}

const TemplateColorHash: StoryFn<TemplateColorHashProps> = (args: TemplateColorHashProps) => (
<ColorField
allowCustomColor={args.allowCustomColor}
colorWeightFilter={args.colorWeightFilter}
paletteGroupFilter={args.paletteGroupFilter}
value={ColorField.calculateColorHashValue(args.stringForColorHashValue, {
allowCustomColor: args.allowCustomColor,
colorWeightFilter: args.colorWeightFilter,
paletteGroupFilter: args.paletteGroupFilter,
})}
/>
);

/**
* Component provides a helper function to calculate a color hash from a text,
* that can be used as `value` or `defaultValue`.
*
* ```
* <ColorField value={ColorField.calculateColorHashValue("MyText")} />
* ```
*
* 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.",
};
200 changes: 200 additions & 0 deletions src/components/ColorField/ColorField.tsx
Original file line number Diff line number Diff line change
@@ -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<TextFieldProps, "invisibleCharacterWarning"> {
/**
* 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<string>(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<HTMLInputElement>) => {
setColorValue(forwardedEvent.target.value);
if (onChange) {
onChange(forwardedEvent);
}
};

const colorInput = (
<TextField
inputRef={ref}
className={classNames(`${eccgui}-colorfield`, className, {
[`${eccgui}-colorfield--custom-picker`]: disableNativePicker,
})}
// we cannot use `color` type for the custom picker because we do not have control over it then
type={!disableNativePicker ? "color" : "text"}
readOnly={disableNativePicker}
disabled={disabled}
value={colorValue}
fullWidth={fullWidth}
{...otherTextFieldProps}
onChange={
!disableNativePicker
? (e: React.ChangeEvent<HTMLInputElement>) => {
forwardOnChange(e);
}
: undefined
}
style={{ ...otherTextFieldProps.style, [`--eccgui-colorfield-background`]: colorValue } as CSSProperties}
/>
);

return disableNativePicker && !disabled ? (
<ContextOverlay
fill={fullWidth}
content={
<WhiteSpaceContainer
paddingTop={"small"}
paddingRight={"small"}
paddingBottom={"small"}
paddingLeft={"small"}
className={`${eccgui}-colorfield__picker`}
>
{allowCustomColor && (
<>
<TextField
type={"color"}
value={colorValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
forwardOnChange(e);
}}
/>
<Spacing size={"small"} />
</>
)}
<FieldSet>
<TagList
className={`${eccgui}-colorfield__palette ${eccgui}-colorfield__palette--${
colorWeightFilter.length >= 3 ? colorWeightFilter.length * 2 : "8"
}col`}
>
{allowedPaletteColors!.map((color: [string, string], idx: number) => [
<RadioButton
className={`${eccgui}-colorfield__palette__color`}
hideIndicator
value={color[1]}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
forwardOnChange(e);
}}
>
<Tooltip key={idx} content={color[0].replace(`${eccgui}-color-palette-`, "")}>
<Tag
large
style={{ [`--eccgui-colorfield-palette-color`]: color[1] } as CSSProperties}
>
{color[1]}
</Tag>
</Tooltip>
</RadioButton>,
// 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 && (
<>
<br className={`${eccgui}-colorfield__palette-linebreak`} />
</>
),
])}
</TagList>
</FieldSet>
</WhiteSpaceContainer>
}
>
{colorInput}
</ContextOverlay>
) : (
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;
Loading