@eccenca/gui-elements 25.1.0-rc.1 → 25.1.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
- package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
- package/dist/cjs/common/index.js +1 -0
- package/dist/cjs/common/index.js.map +1 -1
- package/dist/cjs/common/utils/CssCustomProperties.js.map +1 -1
- package/dist/cjs/common/utils/colorHash.js +26 -12
- package/dist/cjs/common/utils/colorHash.js.map +1 -1
- package/dist/cjs/components/ColorField/ColorField.js +114 -0
- package/dist/cjs/components/ColorField/ColorField.js.map +1 -0
- package/dist/cjs/components/RadioButton/RadioButton.js +5 -2
- package/dist/cjs/components/RadioButton/RadioButton.js.map +1 -1
- package/dist/cjs/components/TextField/useTextValidation.js +17 -8
- package/dist/cjs/components/TextField/useTextValidation.js.map +1 -1
- package/dist/cjs/components/Tooltip/Tooltip.js +11 -7
- package/dist/cjs/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/cjs/components/index.js +1 -0
- package/dist/cjs/components/index.js.map +1 -1
- package/dist/cjs/extensions/codemirror/CodeMirror.js +40 -14
- package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
- package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +23 -0
- package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
- package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js +5 -2
- package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
- package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
- package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
- package/dist/esm/common/index.js +2 -1
- package/dist/esm/common/index.js.map +1 -1
- package/dist/esm/common/utils/CssCustomProperties.js.map +1 -1
- package/dist/esm/common/utils/colorHash.js +26 -13
- package/dist/esm/common/utils/colorHash.js.map +1 -1
- package/dist/esm/components/ColorField/ColorField.js +140 -0
- package/dist/esm/components/ColorField/ColorField.js.map +1 -0
- package/dist/esm/components/RadioButton/RadioButton.js +6 -2
- package/dist/esm/components/RadioButton/RadioButton.js.map +1 -1
- package/dist/esm/components/TextField/useTextValidation.js +39 -8
- package/dist/esm/components/TextField/useTextValidation.js.map +1 -1
- package/dist/esm/components/Tooltip/Tooltip.js +11 -7
- package/dist/esm/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/components/index.js.map +1 -1
- package/dist/esm/extensions/codemirror/CodeMirror.js +42 -16
- package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
- package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +47 -0
- package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
- package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js +16 -2
- package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
- package/dist/types/common/index.d.ts +2 -1
- package/dist/types/common/utils/CssCustomProperties.d.ts +2 -2
- package/dist/types/common/utils/colorHash.d.ts +5 -4
- package/dist/types/components/ColorField/ColorField.d.ts +30 -0
- package/dist/types/components/RadioButton/RadioButton.d.ts +8 -2
- package/dist/types/components/index.d.ts +1 -0
- package/dist/types/extensions/codemirror/CodeMirror.d.ts +12 -9
- package/dist/types/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.d.ts +24 -0
- package/dist/types/extensions/codemirror/toolbars/markdown.toolbar.d.ts +2 -0
- package/package.json +1 -1
- package/src/_shame.scss +1 -35
- package/src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx +1 -1
- package/src/common/index.ts +2 -1
- package/src/common/scss/_accessibility-defaults.scss +101 -0
- package/src/common/utils/CssCustomProperties.ts +5 -3
- package/src/common/utils/colorHash.ts +38 -20
- package/src/components/Application/_header.scss +21 -9
- package/src/components/Application/_sidebar.scss +6 -0
- package/src/components/Application/_toolbar.scss +3 -3
- package/src/components/AutoSuggestion/AutoSuggestion.scss +3 -1
- package/src/components/Checkbox/checkbox.scss +9 -1
- package/src/components/ColorField/ColorField.stories.tsx +72 -0
- package/src/components/ColorField/ColorField.test.tsx +101 -0
- package/src/components/ColorField/ColorField.tsx +200 -0
- package/src/components/ColorField/_colorfield.scss +67 -0
- package/src/components/Dialog/dialog.scss +8 -0
- package/src/components/Link/link.scss +5 -6
- package/src/components/MultiSuggestField/_multisuggestfield.scss +18 -0
- package/src/components/RadioButton/RadioButton.tsx +15 -3
- package/src/components/RadioButton/radiobutton.scss +18 -1
- package/src/components/TextField/stories/TextField.stories.tsx +23 -0
- package/src/components/TextField/tests/useTextValidation.test.tsx +83 -0
- package/src/components/TextField/textfield.scss +20 -0
- package/src/components/TextField/useTextValidation.ts +17 -8
- package/src/components/Tooltip/Tooltip.test.tsx +40 -5
- package/src/components/Tooltip/Tooltip.tsx +14 -10
- package/src/components/index.scss +1 -0
- package/src/components/index.ts +1 -0
- package/src/configuration/stories/customproperties.stories.tsx +4 -0
- package/src/extensions/codemirror/CodeMirror.stories.tsx +9 -4
- package/src/extensions/codemirror/CodeMirror.tsx +71 -26
- package/src/extensions/codemirror/_codemirror.scss +18 -28
- package/src/extensions/codemirror/tests/CodeEditor.test.tsx +138 -0
- package/src/extensions/codemirror/tests/EditorAppearanceConfigMenu.test.tsx +131 -0
- package/src/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.tsx +59 -0
- package/src/extensions/codemirror/toolbars/markdown.toolbar.tsx +17 -3
- package/src/index.scss +1 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import "@testing-library/jest-dom";
|
|
6
|
+
|
|
7
|
+
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
|
|
8
|
+
|
|
9
|
+
import { ColorField } from "./ColorField";
|
|
10
|
+
|
|
11
|
+
describe("ColorField", () => {
|
|
12
|
+
describe("rendering", () => {
|
|
13
|
+
it("renders without crashing, and correct component CSS class is applied", () => {
|
|
14
|
+
const { container } = render(<ColorField />);
|
|
15
|
+
expect(container).not.toBeEmptyDOMElement();
|
|
16
|
+
expect(container.getElementsByClassName(`${eccgui}-colorfield`).length).toBe(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("renders a color input by default (no palette presets)", () => {
|
|
20
|
+
const { container } = render(<ColorField colorPresets={[]} allowCustomColor={true} />);
|
|
21
|
+
expect(container.querySelector("input[type='color']")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders a readonly text input when palette colors are configured, and custom picker CSS class is applied", () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<ColorField
|
|
27
|
+
className="my-custom-class"
|
|
28
|
+
colorPresets={[
|
|
29
|
+
["my-black", "#000000"],
|
|
30
|
+
["my-white", "#ffffff"],
|
|
31
|
+
]}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
// With default palette settings, a text input with readOnly is shown
|
|
35
|
+
expect(container.querySelector("input[type='text']")).toBeInTheDocument();
|
|
36
|
+
expect(container.querySelector("input[readonly]")).toBeInTheDocument();
|
|
37
|
+
expect(container.querySelector(`.${eccgui}-colorfield--custom-picker`)).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("applies additional className", () => {
|
|
41
|
+
render(<ColorField className="my-custom-class" colorPresets={[]} allowCustomColor={true} />);
|
|
42
|
+
expect(document.querySelector(".my-custom-class")).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("value handling", () => {
|
|
47
|
+
it("uses defaultValue as initial color", () => {
|
|
48
|
+
render(<ColorField defaultValue="#ff0000" colorPresets={[]} allowCustomColor={true} />);
|
|
49
|
+
const input = document.querySelector("input") as HTMLInputElement;
|
|
50
|
+
expect(input.value).toBe("#ff0000");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("uses value prop as initial color", () => {
|
|
54
|
+
render(<ColorField value="#00ff00" colorPresets={[]} allowCustomColor={true} />);
|
|
55
|
+
const input = document.querySelector("input") as HTMLInputElement;
|
|
56
|
+
expect(input.value).toBe("#00ff00");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("falls back to #000000 when no value or defaultValue is provided", () => {
|
|
60
|
+
render(<ColorField colorPresets={[]} allowCustomColor={true} />);
|
|
61
|
+
const input = document.querySelector("input") as HTMLInputElement;
|
|
62
|
+
expect(input.value).toBe("#000000");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("updates displayed value when value prop changes", () => {
|
|
66
|
+
const { rerender } = render(<ColorField value="#ff0000" colorPresets={[]} allowCustomColor={true} />);
|
|
67
|
+
let input = document.querySelector("input") as HTMLInputElement;
|
|
68
|
+
expect(input.value).toBe("#ff0000");
|
|
69
|
+
|
|
70
|
+
rerender(<ColorField value="#0000ff" colorPresets={[]} allowCustomColor={true} />);
|
|
71
|
+
input = document.querySelector("input") as HTMLInputElement;
|
|
72
|
+
expect(input.value).toBe("#0000ff");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("disabled state", () => {
|
|
77
|
+
it("is disabled when disabled prop is true", () => {
|
|
78
|
+
render(<ColorField disabled colorPresets={[]} allowCustomColor={true} />);
|
|
79
|
+
const input = document.querySelector("input") as HTMLInputElement;
|
|
80
|
+
expect(input).toBeDisabled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("is disabled when no palette colors and allowCustomColor is false", () => {
|
|
84
|
+
render(<ColorField colorPresets={[]} allowCustomColor={false} />);
|
|
85
|
+
const input = document.querySelector("input") as HTMLInputElement;
|
|
86
|
+
expect(input).toBeDisabled();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("onChange callback", () => {
|
|
91
|
+
it("calls onChange when native color input changes", async () => {
|
|
92
|
+
const user = userEvent.setup();
|
|
93
|
+
const onChange = jest.fn();
|
|
94
|
+
render(<ColorField onChange={onChange} colorPresets={[]} allowCustomColor={true} />);
|
|
95
|
+
const input = document.querySelector("input[type='color']") as HTMLInputElement;
|
|
96
|
+
input.type = "text"; // for unknown reasons Jest seems not able to test it on color inputs
|
|
97
|
+
await user.type(input, "#123456");
|
|
98
|
+
expect(onChange).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { CSSProperties } from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import Color from "color";
|
|
4
|
+
|
|
5
|
+
import { utils } from "../../common";
|
|
6
|
+
import { getEnabledColorsProps } from "../../common/utils/colorHash";
|
|
7
|
+
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
|
|
8
|
+
import { ContextOverlay } from "../ContextOverlay";
|
|
9
|
+
import { FieldSet } from "../Form";
|
|
10
|
+
import { RadioButton } from "../RadioButton/RadioButton";
|
|
11
|
+
import { Spacing } from "../Separation/Spacing";
|
|
12
|
+
import { Tag, TagList } from "../Tag";
|
|
13
|
+
import { TextField, TextFieldProps } from "../TextField";
|
|
14
|
+
import { Tooltip } from "../Tooltip/Tooltip";
|
|
15
|
+
import { WhiteSpaceContainer } from "../Typography";
|
|
16
|
+
|
|
17
|
+
type ColorPresets = [string, string][] | [string, Color][];
|
|
18
|
+
type ColorPresetConfiguration = Pick<getEnabledColorsProps, "includeColorWeight" | "includePaletteGroup">;
|
|
19
|
+
|
|
20
|
+
export interface ColorFieldProps extends Omit<TextFieldProps, "invisibleCharacterWarning"> {
|
|
21
|
+
/**
|
|
22
|
+
* Any color can be selected, not only from the color presets.
|
|
23
|
+
*/
|
|
24
|
+
allowCustomColor?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* List of named colors that are used a selectable color options.
|
|
27
|
+
*/
|
|
28
|
+
colorPresets?: ColorPresets;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Color input field that provides resets from the configured color palette.
|
|
33
|
+
* Use `includeColorWeight` and `includePaletteGroup` to filter them.
|
|
34
|
+
*/
|
|
35
|
+
export const ColorField = ({
|
|
36
|
+
className = "",
|
|
37
|
+
allowCustomColor = false,
|
|
38
|
+
colorPresets = listColorPalettePresets(),
|
|
39
|
+
defaultValue,
|
|
40
|
+
value,
|
|
41
|
+
onChange,
|
|
42
|
+
fullWidth = false,
|
|
43
|
+
...otherTextFieldProps
|
|
44
|
+
}: ColorFieldProps) => {
|
|
45
|
+
const ref = React.useRef(null);
|
|
46
|
+
const [colorValue, setColorValue] = React.useState<string>(defaultValue || value || "#000000");
|
|
47
|
+
if (value && value !== colorValue) {
|
|
48
|
+
setColorValue(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const disableNativePicker = colorPresets.length > 0;
|
|
52
|
+
const disabled = (!disableNativePicker && !allowCustomColor) || otherTextFieldProps.disabled;
|
|
53
|
+
|
|
54
|
+
const forwardOnChange = (forwardedEvent: React.ChangeEvent<HTMLInputElement>) => {
|
|
55
|
+
setColorValue(forwardedEvent.target.value);
|
|
56
|
+
if (onChange) {
|
|
57
|
+
onChange(forwardedEvent);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const colorInput = (
|
|
62
|
+
<TextField
|
|
63
|
+
inputRef={ref}
|
|
64
|
+
className={classNames(`${eccgui}-colorfield`, className, {
|
|
65
|
+
[`${eccgui}-colorfield--custom-picker`]: disableNativePicker,
|
|
66
|
+
[`${eccgui}-colorfield--disabled`]: disabled,
|
|
67
|
+
})}
|
|
68
|
+
// we cannot use `color` type for the custom picker because we do not have control over it then
|
|
69
|
+
type={!disableNativePicker ? "color" : "text"}
|
|
70
|
+
readOnly={disableNativePicker}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
value={colorValue}
|
|
73
|
+
fullWidth={fullWidth}
|
|
74
|
+
{...otherTextFieldProps}
|
|
75
|
+
onChange={
|
|
76
|
+
!disableNativePicker
|
|
77
|
+
? (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
78
|
+
forwardOnChange(e);
|
|
79
|
+
}
|
|
80
|
+
: undefined
|
|
81
|
+
}
|
|
82
|
+
style={{ ...otherTextFieldProps.style, [`--eccgui-colorfield-background`]: colorValue } as CSSProperties}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return disableNativePicker && !disabled ? (
|
|
87
|
+
<ContextOverlay
|
|
88
|
+
fill={fullWidth}
|
|
89
|
+
content={
|
|
90
|
+
<WhiteSpaceContainer
|
|
91
|
+
paddingTop={"small"}
|
|
92
|
+
paddingRight={"small"}
|
|
93
|
+
paddingBottom={"small"}
|
|
94
|
+
paddingLeft={"small"}
|
|
95
|
+
className={`${eccgui}-colorfield__picker`}
|
|
96
|
+
>
|
|
97
|
+
{allowCustomColor && (
|
|
98
|
+
<>
|
|
99
|
+
<TextField
|
|
100
|
+
type={"color"}
|
|
101
|
+
value={colorValue}
|
|
102
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
103
|
+
forwardOnChange(e);
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
<Spacing size={"small"} />
|
|
107
|
+
</>
|
|
108
|
+
)}
|
|
109
|
+
<FieldSet>
|
|
110
|
+
<TagList className={`${eccgui}-colorfield__palette`}>
|
|
111
|
+
{colorPresets!.map((color: [string, string | Color], idx: number) => [
|
|
112
|
+
<RadioButton
|
|
113
|
+
key={idx}
|
|
114
|
+
className={`${eccgui}-colorfield__palette__color`}
|
|
115
|
+
hideIndicator
|
|
116
|
+
value={typeof color[1] === "string" ? color[1] : color[1].toString()}
|
|
117
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
118
|
+
forwardOnChange(e);
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<Tooltip content={color[0]}>
|
|
122
|
+
<Tag
|
|
123
|
+
large
|
|
124
|
+
style={{ [`--eccgui-colorfield-palette-color`]: color[1] } as CSSProperties}
|
|
125
|
+
>
|
|
126
|
+
{color[1]}
|
|
127
|
+
</Tag>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
</RadioButton>,
|
|
130
|
+
// Looks like we cannot force some type of line break in the tag list via CSS only
|
|
131
|
+
(idx + 1) % 8 === 0 && (
|
|
132
|
+
<>
|
|
133
|
+
<br className={`${eccgui}-colorfield__palette-linebreak`} />
|
|
134
|
+
</>
|
|
135
|
+
),
|
|
136
|
+
])}
|
|
137
|
+
</TagList>
|
|
138
|
+
</FieldSet>
|
|
139
|
+
</WhiteSpaceContainer>
|
|
140
|
+
}
|
|
141
|
+
>
|
|
142
|
+
{colorInput}
|
|
143
|
+
</ContextOverlay>
|
|
144
|
+
) : (
|
|
145
|
+
colorInput
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const defaultColorPaletteSet: ColorPresetConfiguration = {
|
|
150
|
+
// on default, we only include color weights that can have enough contrasts to black/white
|
|
151
|
+
includeColorWeight: [100, 300, 700, 900],
|
|
152
|
+
// on default, we only include layout colors
|
|
153
|
+
includePaletteGroup: ["layout"],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Simple helper function to get a list of colors defined in the color palette.
|
|
158
|
+
*/
|
|
159
|
+
const listColorPalettePresets = (colorPaletteSet = defaultColorPaletteSet) => {
|
|
160
|
+
return utils
|
|
161
|
+
.getEnabledColorPropertiesFromPalette({
|
|
162
|
+
...colorPaletteSet,
|
|
163
|
+
minimalColorDistance: 0, // we use all allowed colors, and do not check distances between them
|
|
164
|
+
})
|
|
165
|
+
.map((color: [string, string | Color]) => [
|
|
166
|
+
color[0].replace(`${eccgui}-color-palette-`, ""),
|
|
167
|
+
color[1],
|
|
168
|
+
]) as ColorPresets;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
ColorField.listColorPalettePresets = listColorPalettePresets;
|
|
172
|
+
|
|
173
|
+
type calculateColorHashValueProps = Pick<ColorFieldProps, "allowCustomColor"> & ColorPresetConfiguration;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Simple helper function that provide simple access to color hash calculation.
|
|
177
|
+
*/
|
|
178
|
+
ColorField.calculateColorHashValue = (
|
|
179
|
+
text: string,
|
|
180
|
+
options: calculateColorHashValueProps = {
|
|
181
|
+
...defaultColorPaletteSet,
|
|
182
|
+
allowCustomColor: false,
|
|
183
|
+
}
|
|
184
|
+
) => {
|
|
185
|
+
const hash = utils.textToColorHash({
|
|
186
|
+
text,
|
|
187
|
+
options: {
|
|
188
|
+
returnValidColorsDirectly: options.allowCustomColor as boolean,
|
|
189
|
+
enabledColors: utils.getEnabledColorsFromPalette({
|
|
190
|
+
includePaletteGroup: options.includePaletteGroup,
|
|
191
|
+
includeColorWeight: options.includeColorWeight,
|
|
192
|
+
minimalColorDistance: 0,
|
|
193
|
+
}),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return hash ? hash : undefined;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export default ColorField;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.#{$eccgui}-colorfield {
|
|
2
|
+
cursor: default;
|
|
3
|
+
|
|
4
|
+
&:not(.#{$ns}-fill) {
|
|
5
|
+
width: 100%;
|
|
6
|
+
max-width: 4 * $eccgui-size-textfield-height-regular;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.#{$ns}-input {
|
|
10
|
+
color: var(--#{$eccgui}-colorfield-background);
|
|
11
|
+
cursor: inherit;
|
|
12
|
+
background-color: var(--#{$eccgui}-colorfield-background);
|
|
13
|
+
|
|
14
|
+
&[type="color"] {
|
|
15
|
+
&::-webkit-color-swatch-wrapper {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&::-moz-color-swatch {
|
|
20
|
+
display: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&[class*="#{$ns}-intent-"] {
|
|
26
|
+
// we need to remove normal intent indicators like colored bg or blinking
|
|
27
|
+
.#{$ns}-input {
|
|
28
|
+
background-color: var(--#{$eccgui}-colorfield-background);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.#{$ns}-input-left-container {
|
|
33
|
+
top: 1px;
|
|
34
|
+
left: 1px !important;
|
|
35
|
+
height: calc(100% - 2px);
|
|
36
|
+
background-color: $eccgui-color-textfield-background;
|
|
37
|
+
}
|
|
38
|
+
.#{$ns}-input-action {
|
|
39
|
+
top: 1px;
|
|
40
|
+
right: 1px !important;
|
|
41
|
+
height: calc(100% - 2px);
|
|
42
|
+
background-color: $eccgui-color-textfield-background;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.#{$eccgui}-colorfield--disabled {
|
|
47
|
+
opacity: $eccgui-opacity-disabled;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.#{$eccgui}-colorfield__palette {
|
|
51
|
+
& > li:has(.#{$eccgui}-colorfield__palette-linebreak) {
|
|
52
|
+
display: block;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 0;
|
|
55
|
+
margin: 0;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.#{$eccgui}-colorfield__palette__color {
|
|
61
|
+
margin: 0;
|
|
62
|
+
.#{$eccgui}-tag__item {
|
|
63
|
+
width: 3rem;
|
|
64
|
+
color: var(--#{$eccgui}-colorfield-palette-color) !important;
|
|
65
|
+
background-color: var(--#{$eccgui}-colorfield-palette-color) !important;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -22,6 +22,14 @@ $eccgui-color-modal-backdrop: eccgui-color-rgba(
|
|
|
22
22
|
z-index: $eccgui-zindex-modals;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
.#{$ns}-overlay-start-focus-trap,
|
|
26
|
+
.#{$ns}-overlay-end-focus-trap {
|
|
27
|
+
// do not show focus on trap elements, we need to fix keeping focus on modal elements differently later
|
|
28
|
+
&[tabindex]:not([tabindex^="-"]):focus-visible {
|
|
29
|
+
@extend .#{$eccgui}-a11y-focus-by-pointer;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
.#{$eccgui}-dialog__backdrop {
|
|
26
34
|
background-color: $eccgui-color-modal-backdrop;
|
|
27
35
|
}
|
|
@@ -31,13 +31,12 @@
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
&:focus {
|
|
35
|
-
|
|
36
|
-
outline-offset: 0;
|
|
34
|
+
&:focus:not(.#{$prefix}--link--disabled) {
|
|
35
|
+
@include focus-by-pointer;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
&:focus-visible {
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
&:focus-visible:not(.#{$prefix}--link--disabled) {
|
|
39
|
+
@include focus-by-keyboard-static;
|
|
40
|
+
@include focus-by-keyboard-animation;
|
|
42
41
|
}
|
|
43
42
|
}
|
|
@@ -6,3 +6,21 @@
|
|
|
6
6
|
max-height: var(--eccgui-multisuggestfield-max-height, 45vh);
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
.#{$eccgui}-multisuggestfield {
|
|
11
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-accent};
|
|
12
|
+
|
|
13
|
+
&.#{$ns}-intent-success {
|
|
14
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-success-text};
|
|
15
|
+
}
|
|
16
|
+
&.#{$ns}-intent-warning {
|
|
17
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-warning-text};
|
|
18
|
+
}
|
|
19
|
+
&.#{$ns}-intent-danger {
|
|
20
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-danger-text};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&:focus-within {
|
|
24
|
+
@extend .#{$eccgui}-a11y-focus-by-keyboard-static;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Radio as BlueprintRadioButton, RadioProps as BlueprintRadioProps } from "@blueprintjs/core";
|
|
3
|
+
import classNames from "classnames";
|
|
3
4
|
|
|
4
5
|
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
+
export interface RadioButtonProps extends BlueprintRadioProps {
|
|
8
|
+
/**
|
|
9
|
+
* Hide the indicator.
|
|
10
|
+
* The element cannot be identified as radio input then but a click on the children can be easily processed via `onChange` event.
|
|
11
|
+
*/
|
|
12
|
+
hideIndicator?: boolean;
|
|
13
|
+
}
|
|
7
14
|
|
|
8
|
-
export const RadioButton = ({ children, className = "", ...restProps }: RadioButtonProps) => {
|
|
15
|
+
export const RadioButton = ({ children, className = "", hideIndicator = false, ...restProps }: RadioButtonProps) => {
|
|
9
16
|
return (
|
|
10
|
-
<BlueprintRadioButton
|
|
17
|
+
<BlueprintRadioButton
|
|
18
|
+
{...restProps}
|
|
19
|
+
className={classNames(`${eccgui}-radiobutton`, className, {
|
|
20
|
+
[`${eccgui}-radiobutton--hidden-indicator`]: hideIndicator,
|
|
21
|
+
})}
|
|
22
|
+
>
|
|
11
23
|
{children}
|
|
12
24
|
</BlueprintRadioButton>
|
|
13
25
|
);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Checkbox need to be imported before, we won't double it here currently
|
|
2
2
|
|
|
3
3
|
.#{$ns}-control {
|
|
4
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-accent};
|
|
5
|
+
--#{$eccgui}-a11y-outline-width: 2px;
|
|
6
|
+
--#{$eccgui}-a11y-outline-offset: 0;
|
|
7
|
+
|
|
4
8
|
&.#{$ns}-radio {
|
|
5
9
|
input ~ .#{$ns}-control-indicator,
|
|
6
10
|
input:checked ~ .#{$ns}-control-indicator {
|
|
@@ -8,7 +12,7 @@
|
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
input:focus ~ .#{$ns}-control-indicator {
|
|
11
|
-
|
|
15
|
+
@extend .#{$eccgui}-a11y-focus-by-keyboard-static;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
input:disabled ~ .#{$ns}-control-indicator,
|
|
@@ -29,3 +33,16 @@
|
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
}
|
|
36
|
+
|
|
37
|
+
.#{$eccgui}-radiobutton--hidden-indicator {
|
|
38
|
+
&:not(.#{$ns}-align-right) {
|
|
39
|
+
padding-inline-start: 0;
|
|
40
|
+
}
|
|
41
|
+
&:not(.#{$ns}-align-left) {
|
|
42
|
+
padding-inline-end: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
input ~ .#{$ns}-control-indicator {
|
|
46
|
+
visibility: hidden;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -61,3 +61,26 @@ const invisibleCharacterWarningProps: TextFieldProps = {
|
|
|
61
61
|
defaultValue: "Invisible character -><-",
|
|
62
62
|
};
|
|
63
63
|
InvisibleCharacterWarning.args = invisibleCharacterWarningProps;
|
|
64
|
+
|
|
65
|
+
/** Text field showing that emoji (✔️ variation-selector, 👨👩👧👦 ZWJ, #️⃣ keycap)
|
|
66
|
+
* are NOT reported as invisible characters, while a genuine ZWS still is. */
|
|
67
|
+
export const InvisibleCharacterWarningWithEmoji = Template.bind({});
|
|
68
|
+
|
|
69
|
+
const invisibleCharacterWarningWithEmojiProps: TextFieldProps = {
|
|
70
|
+
...Default.args,
|
|
71
|
+
invisibleCharacterWarning: {
|
|
72
|
+
callback: (codePoints) => {
|
|
73
|
+
if (codePoints.size) {
|
|
74
|
+
const codePointsString = [...codePoints]
|
|
75
|
+
.map((n) => characters.invisibleZeroWidthCharacters.codePointMap.get(n)?.fullLabel)
|
|
76
|
+
.join(", ");
|
|
77
|
+
alert("Invisible character detected in input string. Code points: " + codePointsString);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
callbackDelay: 500,
|
|
81
|
+
},
|
|
82
|
+
onChange: () => {},
|
|
83
|
+
// ZWS should be flagged; ✔️ 👨👩👧👦 #️⃣ should NOT be flagged
|
|
84
|
+
defaultValue: "Check\u200B ✔️ 👨👩👧👦 #️⃣",
|
|
85
|
+
};
|
|
86
|
+
InvisibleCharacterWarningWithEmoji.args = invisibleCharacterWarningWithEmojiProps;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { act, render } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { useTextValidation } from "../useTextValidation";
|
|
5
|
+
|
|
6
|
+
const HookWrapper: React.FC<{ value: string; callback: jest.Mock; callbackDelay?: number }> = ({
|
|
7
|
+
value,
|
|
8
|
+
callback,
|
|
9
|
+
callbackDelay = 0,
|
|
10
|
+
}) => {
|
|
11
|
+
useTextValidation({
|
|
12
|
+
value,
|
|
13
|
+
onChange: jest.fn(),
|
|
14
|
+
invisibleCharacterWarning: { callback, callbackDelay },
|
|
15
|
+
});
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("useTextValidation", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.useFakeTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.useRealTimers();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Render the hook with a controlled value and flush the debounce timer. */
|
|
29
|
+
const runWithValue = (value: string, callbackDelay = 0) => {
|
|
30
|
+
const callback = jest.fn();
|
|
31
|
+
render(<HookWrapper value={value} callback={callback} callbackDelay={callbackDelay} />);
|
|
32
|
+
act(() => {
|
|
33
|
+
jest.runAllTimers();
|
|
34
|
+
});
|
|
35
|
+
return callback;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("invisible character detection", () => {
|
|
39
|
+
it("reports empty set for plain text", () => {
|
|
40
|
+
const callback = runWithValue("hello world");
|
|
41
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("detects zero-width space (U+200B)", () => {
|
|
45
|
+
const callback = runWithValue("hello\u200Bworld");
|
|
46
|
+
expect(callback).toHaveBeenCalledWith(new Set([0x200b]));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("detects zero-width non-joiner (U+200C)", () => {
|
|
50
|
+
const callback = runWithValue("hello\u200Cworld");
|
|
51
|
+
expect(callback).toHaveBeenCalledWith(new Set([0x200c]));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("emoji false-positive prevention", () => {
|
|
56
|
+
it("does not flag ✔️ (base char + variation selector U+FE0F)", () => {
|
|
57
|
+
const callback = runWithValue("✔️");
|
|
58
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not flag ZWJ sequence emoji 👨👩👧👦", () => {
|
|
62
|
+
const callback = runWithValue("👨👩👧👦");
|
|
63
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not flag keycap emoji #️⃣", () => {
|
|
67
|
+
const callback = runWithValue("#️⃣");
|
|
68
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("mixed content", () => {
|
|
73
|
+
it("detects ZWS while ignoring surrounding emoji", () => {
|
|
74
|
+
const callback = runWithValue("Check\u200B ✔️👨👩👧#️⃣");
|
|
75
|
+
expect(callback).toHaveBeenCalledWith(new Set([0x200b]));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reports empty set for text with only emoji", () => {
|
|
79
|
+
const callback = runWithValue("✔️ 👨👩👧👦#️⃣");
|
|
80
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -103,6 +103,8 @@ $eccgui-map-intent-bgcolors: (
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
.#{$ns}-input {
|
|
106
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-accent};
|
|
107
|
+
|
|
106
108
|
.#{$ns}-input-group[class*="#{$eccgui}-intent--"] & {
|
|
107
109
|
animation-duration: 1s;
|
|
108
110
|
animation-delay: 0.5s;
|
|
@@ -111,16 +113,23 @@ $eccgui-map-intent-bgcolors: (
|
|
|
111
113
|
@each $each-intent, $each-bgcolor in $eccgui-map-intent-bgcolors {
|
|
112
114
|
.#{$ns}-input-group.#{$ns}-intent-#{$each-intent} & {
|
|
113
115
|
background-color: eccgui-color-var("semantic", $each-intent, "100");
|
|
116
|
+
|
|
117
|
+
--#{$eccgui}-a11y-outline-color: eccgui-color-var("semantic", $each-intent, "900");
|
|
118
|
+
|
|
114
119
|
animation-name: intent-state-flash-#{$each-intent};
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
.#{$ns}-input-group.#{$eccgui}-intent--info & {
|
|
119
124
|
@include pt-input-intent($eccgui-color-info-text);
|
|
125
|
+
|
|
126
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-info-text};
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
.#{$ns}-input-group.#{$eccgui}-intent--accent & {
|
|
123
130
|
@include pt-input-intent($eccgui-color-primary);
|
|
131
|
+
|
|
132
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-primary};
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
.#{$ns}-input-group.#{$eccgui}-intent--neutral & {
|
|
@@ -138,6 +147,10 @@ $eccgui-map-intent-bgcolors: (
|
|
|
138
147
|
|
|
139
148
|
text-decoration: line-through $eccgui-color-danger-text 2px;
|
|
140
149
|
}
|
|
150
|
+
|
|
151
|
+
&:focus-visible {
|
|
152
|
+
@extend .#{$eccgui}-a11y-focus-by-keyboard-static;
|
|
153
|
+
}
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
.#{$eccgui}-textarea {
|
|
@@ -151,16 +164,23 @@ $eccgui-map-intent-bgcolors: (
|
|
|
151
164
|
@each $each-intent, $each-bgcolor in $eccgui-map-intent-bgcolors {
|
|
152
165
|
&.#{$eccgui}-intent--#{$each-intent} {
|
|
153
166
|
background-color: eccgui-color-var("semantic", $each-intent, "100");
|
|
167
|
+
|
|
168
|
+
--#{$eccgui}-a11y-outline-color: eccgui-color-var("semantic", $each-intent, "900");
|
|
169
|
+
|
|
154
170
|
animation-name: intent-state-flash-#{$each-intent};
|
|
155
171
|
}
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
&.#{$eccgui}-intent--info {
|
|
159
175
|
@include pt-input-intent($eccgui-color-info-text);
|
|
176
|
+
|
|
177
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-info-text};
|
|
160
178
|
}
|
|
161
179
|
|
|
162
180
|
&.#{$eccgui}-intent--accent {
|
|
163
181
|
@include pt-input-intent($eccgui-color-primary);
|
|
182
|
+
|
|
183
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-primary};
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
&.#{$eccgui}-intent--neutral {
|