Skip to content
Open
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- component for hiding elements in specific media
- `<InlineText />`
- force children to get displayed as inline content
- `<DecoupledOverlay />`
- `<DecoupledOverlay />`
- similar to `ContextOverlay` component but not directly linked to a React element, it specifies the target in the DOM to get connected lazy
- `<StringPreviewContentBlobToggler />`
- `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[][]>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string[][] is there perhaps a better type for this?


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,
includeColorWeight: [],
includePaletteGroup: [],
allowCustomColor: true,
};

interface TemplateColorHashProps
extends Pick<ColorFieldProps, "onChange" | "allowCustomColor" | "includeColorWeight" | "includePaletteGroup"> {
stringForColorHashValue: string;
}

const TemplateColorHash: StoryFn<TemplateColorHashProps> = (args: TemplateColorHashProps) => (
<ColorField
allowCustomColor={args.allowCustomColor}
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.",
};
125 changes: 125 additions & 0 deletions src/components/ColorField/ColorField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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 includePaletteGroup={[]} includeColorWeight={[]} allowCustomColor={true} />
);
expect(container.querySelector("input[type='color']")).toBeInTheDocument();
});

// Jest cannot test this because it does not (cannot) load Styles where the palette isconfigured
/*
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" />);
// 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"
includePaletteGroup={[]}
includeColorWeight={[]}
allowCustomColor={true}
/>
);
expect(document.querySelector(".my-custom-class")).toBeInTheDocument();
});
});

describe("value handling", () => {
it("uses defaultValue as initial color", () => {
render(
<ColorField
defaultValue="#ff0000"
includePaletteGroup={[]}
includeColorWeight={[]}
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" includePaletteGroup={[]} includeColorWeight={[]} 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 includePaletteGroup={[]} includeColorWeight={[]} 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" includePaletteGroup={[]} includeColorWeight={[]} allowCustomColor={true} />
);
let input = document.querySelector("input") as HTMLInputElement;
expect(input.value).toBe("#ff0000");

rerender(
<ColorField value="#0000ff" includePaletteGroup={[]} includeColorWeight={[]} 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 includePaletteGroup={[]} includeColorWeight={[]} 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 includePaletteGroup={[]} includeColorWeight={[]} 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}
includePaletteGroup={[]}
includeColorWeight={[]}
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