Skip to content
Merged
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- `paddingSize` property to add easily some white space
- `<CodeEditor />`
- toolbar in `markdown` mode provides a user config menu for the editor appearance
- `<RadioButton />`
- `hideIndicator` property: hide the radio inout indicator but click on children can be processed via `onChange` event
- `<ColorField />`
- input component for colors
- uses a subset from the configured color 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 Expand Up @@ -60,7 +65,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- `<EdgeDefault />`
- reduce stroke width to only 1px
- `<CodeMirror />`
- `wrapLines` and `preventLineNumber` do use `false` default value but if not set then it will be interpreted as `false`
- `wrapLines` and `preventLineNumber` do not use `false` default value but if not set then it will be interpreted as `false`
- in this way it can be overwritten by new user config for the markdown mode
- automatically hide user interaction elements in print view
- all application header components except `<WorkspaceHeader />`
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
8 changes: 5 additions & 3 deletions src/common/utils/CssCustomProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class CssCustomProperties {

// Methods

customProperties = (props: getCustomPropertiesProps = {}): string[][] | Record<string, string> => {
customProperties = (props: getCustomPropertiesProps = {}): [string, string][] | Record<string, string> => {
// FIXME:
// in case of performance issues results should get saved at least into intern variables
// other cache strategies could be also tested
Expand Down Expand Up @@ -104,7 +104,9 @@ export default class CssCustomProperties {
});
};

static listCustomProperties = (props: getCustomPropertiesProps = {}): string[][] | Record<string, string> => {
static listCustomProperties = (
props: getCustomPropertiesProps = {}
): [string, string][] | Record<string, string> => {
const { removeDashPrefix = true, returnObject = true, filterName = () => true, ...filterProps } = props;

const customProperties = CssCustomProperties.listLocalCssStyleRuleProperties({
Expand All @@ -123,6 +125,6 @@ export default class CssCustomProperties {

return returnObject
? (Object.fromEntries(customProperties) as Record<string, string>)
: (customProperties as string[][]);
: (customProperties as [string, string][]);
};
}
58 changes: 38 additions & 20 deletions src/common/utils/colorHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ 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 {
export interface getEnabledColorsProps {
/** Specify the palette groups used to define the set of colors. */
includePaletteGroup?: PaletteGroup[];
/** Use only some weights of a color tint. */
Expand All @@ -21,20 +21,43 @@ interface getEnabledColorsProps {
}

const getEnabledColorsFromPaletteCache = new Map<string, Color[]>();
const getEnabledColorPropertiesFromPaletteCache = new Map<string, [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, 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 Expand Up @@ -148,7 +166,7 @@ export function textToColorHash({
}

function stringToIntegerHash(inputString: string): number {
/* this function is idempotend, meaning it retrieves the same result for the same input
/* this function is idempotent, meaning it retrieves the same result for the same input
no matter how many times it's called */
// Convert the string to a hash code
let hashCode = 0;
Expand Down
72 changes: 72 additions & 0 deletions src/components/ColorField/ColorField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from "react";
import { Meta, StoryFn } from "@storybook/react";

import { getEnabledColorsProps } from "../../common/utils/colorHash";
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,
allowCustomColor: true,
colorPresets: [],
};

type TemplateColorHashProps = { stringForColorHashValue: string } & Pick<
ColorFieldProps,
"onChange" | "allowCustomColor"
> &
Pick<getEnabledColorsProps, "includeColorWeight" | "includePaletteGroup">;

const TemplateColorHash: StoryFn<TemplateColorHashProps> = (args: TemplateColorHashProps) => (
<ColorField
allowCustomColor={args.allowCustomColor}
colorPresets={ColorField.listColorPalettePresets({
includeColorWeight: args.includeColorWeight,
includePaletteGroup: args.includePaletteGroup,
})}
value={ColorField.calculateColorHashValue(args.stringForColorHashValue, {
allowCustomColor: args.allowCustomColor,
includeColorWeight: args.includeColorWeight,
includePaletteGroup: args.includePaletteGroup,
})}
/>
);

/**
* 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,
includeColorWeight: [300, 500, 700],
includePaletteGroup: ["layout", "extra"],
stringForColorHashValue: "My text that will used to create a color hash as initial value.",
};
101 changes: 101 additions & 0 deletions src/components/ColorField/ColorField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import "@testing-library/jest-dom";

import { CLASSPREFIX as eccgui } from "../../configuration/constants";

import { ColorField } from "./ColorField";

describe("ColorField", () => {
describe("rendering", () => {
it("renders without crashing, and correct component CSS class is applied", () => {
const { container } = render(<ColorField />);
expect(container).not.toBeEmptyDOMElement();
expect(container.getElementsByClassName(`${eccgui}-colorfield`).length).toBe(1);
});

it("renders a color input by default (no palette presets)", () => {
const { container } = render(<ColorField colorPresets={[]} allowCustomColor={true} />);
expect(container.querySelector("input[type='color']")).toBeInTheDocument();
});

it("renders a readonly text input when palette colors are configured, and custom picker CSS class is applied", () => {
const { container } = render(
<ColorField
className="my-custom-class"
colorPresets={[
["my-black", "#000000"],
["my-white", "#ffffff"],
]}
/>
);
// With default palette settings, a text input with readOnly is shown
expect(container.querySelector("input[type='text']")).toBeInTheDocument();
expect(container.querySelector("input[readonly]")).toBeInTheDocument();
expect(container.querySelector(`.${eccgui}-colorfield--custom-picker`)).toBeInTheDocument();
});

it("applies additional className", () => {
render(<ColorField className="my-custom-class" colorPresets={[]} allowCustomColor={true} />);
expect(document.querySelector(".my-custom-class")).toBeInTheDocument();
});
});

describe("value handling", () => {
it("uses defaultValue as initial color", () => {
render(<ColorField defaultValue="#ff0000" colorPresets={[]} allowCustomColor={true} />);
const input = document.querySelector("input") as HTMLInputElement;
expect(input.value).toBe("#ff0000");
});

it("uses value prop as initial color", () => {
render(<ColorField value="#00ff00" colorPresets={[]} allowCustomColor={true} />);
const input = document.querySelector("input") as HTMLInputElement;
expect(input.value).toBe("#00ff00");
});

it("falls back to #000000 when no value or defaultValue is provided", () => {
render(<ColorField colorPresets={[]} allowCustomColor={true} />);
const input = document.querySelector("input") as HTMLInputElement;
expect(input.value).toBe("#000000");
});

it("updates displayed value when value prop changes", () => {
const { rerender } = render(<ColorField value="#ff0000" colorPresets={[]} allowCustomColor={true} />);
let input = document.querySelector("input") as HTMLInputElement;
expect(input.value).toBe("#ff0000");

rerender(<ColorField value="#0000ff" colorPresets={[]} allowCustomColor={true} />);
input = document.querySelector("input") as HTMLInputElement;
expect(input.value).toBe("#0000ff");
});
});

describe("disabled state", () => {
it("is disabled when disabled prop is true", () => {
render(<ColorField disabled colorPresets={[]} allowCustomColor={true} />);
const input = document.querySelector("input") as HTMLInputElement;
expect(input).toBeDisabled();
});

it("is disabled when no palette colors and allowCustomColor is false", () => {
render(<ColorField colorPresets={[]} allowCustomColor={false} />);
const input = document.querySelector("input") as HTMLInputElement;
expect(input).toBeDisabled();
});
});

describe("onChange callback", () => {
it("calls onChange when native color input changes", async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(<ColorField onChange={onChange} colorPresets={[]} allowCustomColor={true} />);
const input = document.querySelector("input[type='color']") as HTMLInputElement;
input.type = "text"; // for unknown reasons Jest seems not able to test it on color inputs
await user.type(input, "#123456");
expect(onChange).toHaveBeenCalled();
});
});
});
Loading
Loading