diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6424fba..7101c14e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,20 +6,41 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased]
+## [26.1.0] - 2026-08-20
+
+### Added
+
+- `utils`
+ - `useComputedStyleFallback` option for `CssCustomProperties`: if the CSSOM does not provide any property name for the used selector, e.g. because the declarations are part of a constructed and adopted stylesheet, then the names are read from the computed style of the matching element; disabled by default because the computed style also contains all inherited custom properties
+
### Changed
-- ``:
- - `shouldHaveMinimalSetup`: Even if set to false, the edit history feature will still be explicitly enabled.
-- ``:
- - column display is only enabled if the container is large enough, this way property name containers do not get to small
+- ``
+ - `shouldHaveMinimalSetup`: even if set to `false`, the edit history feature is still enabled explicitly
+- ``
+ - the two-column display is only used if the container is wide enough, this way property name columns do not get too small
+ - in narrower containers, property name and value are displayed as stacked rows
+ - make the breakpoint configurable via SCSS (`$eccgui-propertyvalue-size-column-breakpoint-small`)
+- `utils`
+ - values of CSS custom properties are resolved via the computed style of a matching element now, so they always represent what the browser really applies, e.g. references to other custom properties are already replaced
### Fixed
-- ``:
- - Code markup inside tooltips was hardly readable because of low contrast.
-- ``:
- - fix width if it contains a label with `OverflowText` children
- - tooltip is displayed correctly inside the container
+- ``
+ - inline `code` markup was hardly readable because of low contrast
+- ``
+ - fix width if it contains a `` with `` children
+ - the label tooltip is displayed correctly inside the container
+- ``
+ - default handle class names were removed as soon as an `intent` was given
+ - fix runtime error if the element holding the handle tools is not available
+- `utils`
+ - CSS custom properties are also found if their rule is nested inside a cascade layer (`@layer`) or another grouping rule like `@media`, `@supports` or `@container`; this affects `textToColorHash()`, `getEnabledColorsFromPalette()`, `getEnabledColorPropertiesFromPalette()` and `getColorConfiguration()`
+ - CSS custom properties are also found if the given selector is only one part of the selector list of a rule, e.g. `:root, :host`
+ - stylesheets that are loaded from another origin do not break the collection of CSS custom properties anymore
+ - collecting CSS custom properties does not throw an error anymore in test environments where the style declaration of a CSS rule is not an iterable object, e.g. in jsdom
+ - empty results are not cached anymore, this way they are read again if the stylesheets are loaded later on
+ - `minimalColorDistance` is part of the cache key of `getEnabledColorsFromPalette()` and `getEnabledColorPropertiesFromPalette()` now
## [26.0.0] - 2026-07-08
diff --git a/package.json b/package.json
index 2e116920..51c9caa6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@eccenca/gui-elements",
"description": "GUI elements based on other libraries, usable in React application, written in Typescript.",
- "version": "26.0.0",
+ "version": "26.1.0",
"license": "Apache-2.0",
"homepage": "https://github.com/eccenca/gui-elements",
"bugs": "https://github.com/eccenca/gui-elements/issues",
@@ -180,12 +180,21 @@
},
"resolutions": {
"**/@blueprintjs/core": "6.8.1",
- "node-sass-package-importer/**/postcss": "^8.5.10",
+ "node-sass-package-importer/**/postcss": "^8.5.18",
+ "stylelint-order/**/postcss": "^8.5.12",
+ "stylelint/**/postcss": "^8.5.18",
"hast-util-from-parse5": "8.0.0",
"**/lodash": "^4.18.1",
"**/minimatch": "^3.1.4",
"**/serialize-javascript": "^7.0.5",
- "**/ws": "^8.21.0"
+ "**/ws": "^8.21.0",
+ "babel-jest/**/js-yaml": "^3.15.1",
+ "stylelint/**/js-yaml": "^4.3.1",
+ "@eslint/eslintrc/**/js-yaml": "^4.3.1",
+ "**/nanoid": "^3.3.18",
+ "**/fast-uri": "^3.1.3",
+ "sass/**/immutable": "^5.1.8",
+ "**/brace-expansion": "^1.1.18"
},
"husky": {
"hooks": {
diff --git a/src/common/utils/CssCustomProperties.test.ts b/src/common/utils/CssCustomProperties.test.ts
new file mode 100644
index 00000000..b3df7a92
--- /dev/null
+++ b/src/common/utils/CssCustomProperties.test.ts
@@ -0,0 +1,63 @@
+import CssCustomProperties from "./CssCustomProperties";
+
+describe("CssCustomProperties in jsdom", () => {
+ beforeEach(() => {
+ const style = document.createElement("style");
+ style.textContent = `
+ :root { --eccgui-color-palette-blue-500: #1c6ecb; }
+ .config { --note-yellow: #ffde8f; }
+ `;
+ document.head.appendChild(style);
+ });
+
+ it("reads property names of a style rule without iterating the declaration", () => {
+ expect(
+ CssCustomProperties.listLocalCssStyleRuleProperties({
+ selectorText: ":root",
+ propertyType: "custom",
+ }),
+ ).toEqual([["--eccgui-color-palette-blue-500", "#1c6ecb"]]);
+ });
+
+ it("does not throw for scoped selectors", () => {
+ expect(() =>
+ new CssCustomProperties({ selectorText: ".config", returnObject: false }).customProperties(),
+ ).not.toThrow();
+ });
+
+ describe("useComputedStyleFallback", () => {
+ beforeEach(() => {
+ // the property is not part of any stylesheet, so the CSSOM does not know its name
+ const element = document.createElement("div");
+ element.classList.add("without-stylesheet");
+ element.style.setProperty("--only-computed", "#c0ffee");
+ document.body.appendChild(element);
+ });
+
+ it("is disabled by default", () => {
+ expect(
+ new CssCustomProperties({
+ selectorText: ".without-stylesheet",
+ }).customProperties(),
+ ).toEqual({});
+ });
+
+ it("reads the names from the computed style if the CSSOM does not provide any", () => {
+ expect(
+ new CssCustomProperties({
+ selectorText: ".without-stylesheet",
+ useComputedStyleFallback: true,
+ }).customProperties(),
+ ).toEqual({ "only-computed": "#c0ffee" });
+ });
+
+ it("is not used if the CSSOM provides names", () => {
+ expect(
+ new CssCustomProperties({
+ selectorText: ".config",
+ useComputedStyleFallback: true,
+ }).customProperties(),
+ ).toEqual({ "note-yellow": "#ffde8f" });
+ });
+ });
+});
diff --git a/src/common/utils/CssCustomProperties.ts b/src/common/utils/CssCustomProperties.ts
index 4d0b3c28..1458e784 100644
--- a/src/common/utils/CssCustomProperties.ts
+++ b/src/common/utils/CssCustomProperties.ts
@@ -1,12 +1,38 @@
/**
* Based on CSS Tricks tutorial.
* @see https://css-tricks.com/how-to-get-all-custom-properties-on-a-page-in-javascript/
+ *
+ * The names of the custom properties are collected from the CSSOM, but their values are resolved
+ * via the computed style of a matching element.
+ * The CSSOM is only a reliable source for the names: declarations can be nested inside grouping
+ * rules (`@layer`, `@media`, `@supports`, `@container`), and their document order does not
+ * represent the cascade anymore, e.g. unlayered declarations win over layered ones.
+ *
+ * If the CSSOM does not provide any name, then the names can optionally be read from the computed
+ * style of the element as well, see the `useComputedStyleFallback` option.
*/
type AllowedCSSRule = CSSStyleRule | CSSPageRule; // they have necessary `selectorText` and `style` properties
+/** Rules that contain other rules, e.g. `@layer`, `@media`, `@supports`, `@container` or `@import`. */
+type CssRuleWithChildren = CSSRule & { cssRules?: CSSRuleList; styleSheet?: CSSStyleSheet };
+
+type CustomPropertyEntry = [string, string];
+
+/** Element that supports the CSS typed object model, we only need to iterate over the property names. */
+type TypedOMElement = Element & {
+ computedStyleMap?: () => { forEach: (callback: (value: unknown, propertyName: string) => void) => void };
+};
+
+const rootSelectors = [":root", "html", ":root:root"];
+const classSelectorPattern = /^(?:\.-?[_a-zA-Z][\w-]*)+$/;
+
interface getLocalCssStyleRulesProps {
cssRuleType?: "CSSStyleRule";
+ /**
+ * Selector the rule needs to use, e.g. `:root`.
+ * A rule matches if the selector is part of its selector list, e.g. `:root, :host`.
+ */
selectorText?: string;
}
interface getLocalCssStyleRulePropertiesProps extends getLocalCssStyleRulesProps {
@@ -16,11 +42,21 @@ interface getCustomPropertiesProps extends getLocalCssStyleRulesProps {
filterName?: (name: string) => boolean;
removeDashPrefix?: boolean;
returnObject?: boolean;
+ /**
+ * Read the property names from the computed style of the matching element if the CSSOM does not
+ * provide any name, e.g. because the declarations are part of a stylesheet that cannot be read
+ * or that is not listed by `document.styleSheets`, like constructed and adopted stylesheets.
+ *
+ * Disabled by default because it changes the result set: the computed style of an element also
+ * contains all custom properties it inherits from its ancestors, e.g. everything defined for
+ * `:root`, and it does not tell which rule declared them.
+ */
+ useComputedStyleFallback?: boolean;
}
export default class CssCustomProperties {
getterDefaultProps = {} as getCustomPropertiesProps;
- customprops = {};
+ customprops = {} as CustomPropertyEntry[] | Record;
constructor(props: getCustomPropertiesProps = {}) {
this.getterDefaultProps = props;
@@ -28,13 +64,14 @@ export default class CssCustomProperties {
// Methods
- customProperties = (props: getCustomPropertiesProps = {}): [string, string][] | Record => {
+ customProperties = (props: getCustomPropertiesProps = {}): CustomPropertyEntry[] | Record => {
// FIXME:
// in case of performance issues results should get saved at least into intern variables
// other cache strategies could be also tested
- if (Object.keys(this.customprops).length > 1) {
+ if (Object.keys(this.customprops).length > 0) {
return this.customprops;
}
+ // an empty result is not cached, the stylesheets may be loaded later on
const customprops = CssCustomProperties.listCustomProperties({
...this.getterDefaultProps,
...props,
@@ -44,7 +81,7 @@ export default class CssCustomProperties {
};
static listLocalStylesheets = (): CSSStyleSheet[] => {
- if (document && document.styleSheets) {
+ if (typeof document !== "undefined" && document.styleSheets) {
return (Array.from(document.styleSheets) as CSSStyleSheet[]).filter((stylesheet) => {
// is inline stylesheet or from same domain
if (!stylesheet.href) {
@@ -57,39 +94,109 @@ export default class CssCustomProperties {
return [] as CSSStyleSheet[];
};
+ /** Rules of a stylesheet are not readable if it was loaded from another origin. */
+ static readCssRules = (stylesheet: CSSStyleSheet): CSSRuleList | undefined => {
+ try {
+ return stylesheet.cssRules;
+ } catch {
+ return undefined;
+ }
+ };
+
static listLocalCssRules = (): CSSRule[] => {
+ const readStylesheets = new Set();
+
+ const collectRules = (rules: CSSRuleList | undefined): CSSRule[] => {
+ if (!rules) {
+ return [];
+ }
+
+ return Array.from(rules)
+ .map((rule) => {
+ const ruleWithChildren = rule as CssRuleWithChildren;
+
+ if (ruleWithChildren.styleSheet) {
+ // `@import` rule, e.g. `@import url(theme.css) layer(theme)`
+ if (readStylesheets.has(ruleWithChildren.styleSheet)) {
+ return [];
+ }
+ readStylesheets.add(ruleWithChildren.styleSheet);
+ return collectRules(CssCustomProperties.readCssRules(ruleWithChildren.styleSheet));
+ }
+
+ if (ruleWithChildren.cssRules) {
+ // rule that groups or nests other rules, e.g. `@layer`, `@media` or `@container`
+ return [rule, ...collectRules(ruleWithChildren.cssRules)];
+ }
+
+ return [rule];
+ })
+ .flat();
+ };
+
return CssCustomProperties.listLocalStylesheets()
.map((stylesheet) => {
- return Array.from(stylesheet.cssRules);
+ readStylesheets.add(stylesheet);
+ return collectRules(CssCustomProperties.readCssRules(stylesheet));
})
.flat();
};
+ static isCssStyleRule = (rule: CSSRule): rule is CSSStyleRule => {
+ if (typeof CSSStyleRule !== "undefined") {
+ return rule instanceof CSSStyleRule;
+ }
+ const cssrule = rule as AllowedCSSRule;
+ return !!cssrule.style && cssrule.selectorText !== undefined;
+ };
+
+ static matchesSelectorText = (rule: CSSStyleRule, selectorText: string): boolean => {
+ return (rule.selectorText ?? "")
+ .split(",")
+ .map((selector) => selector.trim())
+ .includes(selectorText.trim());
+ };
+
static listLocalCssStyleRules = (filter: getLocalCssStyleRulesProps = {}): CSSStyleRule[] => {
const { cssRuleType = "CSSStyleRule", selectorText } = filter;
const cssStyleRules = CssCustomProperties.listLocalCssRules().filter((rule) => {
- const cssrule = rule as AllowedCSSRule;
- if (cssrule.style) {
- if (cssrule.constructor.name !== cssRuleType) {
- return false;
- }
- if (!!selectorText && cssrule.selectorText !== selectorText) {
- return false;
- }
- return true;
- } else {
+ if (cssRuleType === "CSSStyleRule" && !CssCustomProperties.isCssStyleRule(rule)) {
+ return false;
+ }
+ if (!!selectorText && !CssCustomProperties.matchesSelectorText(rule as CSSStyleRule, selectorText)) {
return false;
}
+ return true;
});
return cssStyleRules as CSSStyleRule[];
};
+ /**
+ * Return the property names of a style declaration.
+ * The declaration is not iterated directly because it is not always an iterable object, e.g.
+ * the declarations of style rules are not iterable in test environments using jsdom.
+ */
+ static listStyleDeclarationPropertyNames = (style: CSSStyleDeclaration): string[] => {
+ const propertyNames = [] as string[];
+
+ for (let i = 0; i < style.length; i++) {
+ // `item()` is not available everywhere, the indexed getter is the more reliable one
+ const propertyName = style[i] ?? style.item?.(i);
+ if (propertyName) {
+ propertyNames.push(propertyName);
+ }
+ }
+
+ return propertyNames;
+ };
+
static listLocalCssStyleRuleProperties = (filter: getLocalCssStyleRulePropertiesProps = {}): string[][] => {
const { propertyType = "all", ...otherFilters } = filter;
return CssCustomProperties.listLocalCssStyleRules(otherFilters)
.map((cssrule) => {
- return [...(cssrule as CSSStyleRule).style].map((propertyname) => {
- return [propertyname.trim(), (cssrule as CSSStyleRule).style.getPropertyValue(propertyname).trim()];
+ const style = (cssrule as CSSStyleRule).style;
+ return CssCustomProperties.listStyleDeclarationPropertyNames(style).map((propertyname) => {
+ return [propertyname.trim(), style.getPropertyValue(propertyname).trim()];
});
})
.flat()
@@ -104,27 +211,141 @@ export default class CssCustomProperties {
});
};
+ /**
+ * Return the element the values of custom properties can be read from.
+ * `:root` and `html` are mapped to the root element of the document, for any other selector the
+ * first matching element is used.
+ * If nothing matches and the selector consists of class names only, then a temporary hidden
+ * element is created; the second item of the returned tuple removes it again.
+ */
+ static targetElement = (selectorText: string = ":root"): [Element | undefined, (() => void) | undefined] => {
+ if (typeof document === "undefined") {
+ return [undefined, undefined];
+ }
+
+ if (rootSelectors.includes(selectorText.trim().toLowerCase())) {
+ return [document.documentElement, undefined];
+ }
+
+ try {
+ const existingElement = document.querySelector(selectorText);
+ if (existingElement) {
+ return [existingElement, undefined];
+ }
+ } catch {
+ // selector cannot be used by the DOM API, we try to create a placeholder below
+ }
+
+ if (!classSelectorPattern.test(selectorText)) {
+ return [undefined, undefined];
+ }
+
+ // we need an element inside the DOM, otherwise the browser does not calculate the values for us
+ const placeholder = document.createElement("div");
+ placeholder.classList.add(...selectorText.split(".").filter(Boolean));
+ placeholder.setAttribute("style", "display: none");
+ (document.body ?? document.documentElement).appendChild(placeholder);
+
+ return [placeholder, () => placeholder.remove()];
+ };
+
+ /**
+ * Return the names of all custom properties that apply to an element, they are read from its
+ * computed style.
+ * Inherited custom properties are part of the computed style, so the returned list also contains
+ * the names of custom properties that were declared for one of the ancestors of the element.
+ */
+ static listElementCustomPropertyNames = (element: Element): string[] => {
+ const documentView = element.ownerDocument?.defaultView;
+ if (!documentView) {
+ return [];
+ }
+
+ const computedStyle = documentView.getComputedStyle(element);
+ const propertyNames = new Set(
+ CssCustomProperties.listStyleDeclarationPropertyNames(computedStyle).filter((propertyName) =>
+ propertyName.startsWith("--"),
+ ),
+ );
+
+ const typedOMElement = element as TypedOMElement;
+ if (propertyNames.size === 0 && typeof typedOMElement.computedStyleMap === "function") {
+ // Chromium before v141 does not enumerate custom properties in `getComputedStyle()`,
+ // but they are available via the typed object model
+ typedOMElement.computedStyleMap().forEach((_value, propertyName) => {
+ if (propertyName.startsWith("--")) {
+ propertyNames.add(propertyName);
+ }
+ });
+ }
+
+ return [...propertyNames];
+ };
+
+ /**
+ * Resolve the values of custom properties as they are applied to an element.
+ * Properties without a value are removed, they do not apply to the element, e.g. because they
+ * are only defined inside a currently not matching `@media` rule.
+ */
+ static resolveCustomPropertyValues = (element: Element, propertyNames: string[]): CustomPropertyEntry[] => {
+ const documentView = element.ownerDocument?.defaultView;
+ if (!documentView) {
+ return [];
+ }
+
+ const computedStyle = documentView.getComputedStyle(element);
+
+ return propertyNames
+ .map((propertyName): CustomPropertyEntry => {
+ return [propertyName, computedStyle.getPropertyValue(propertyName).trim()];
+ })
+ .filter(([, value]) => value !== "");
+ };
+
static listCustomProperties = (
props: getCustomPropertiesProps = {},
- ): [string, string][] | Record => {
- const { removeDashPrefix = true, returnObject = true, filterName = () => true, ...filterProps } = props;
+ ): CustomPropertyEntry[] | Record => {
+ const {
+ removeDashPrefix = true,
+ returnObject = true,
+ filterName = () => true,
+ useComputedStyleFallback = false,
+ ...filterProps
+ } = props;
- const customProperties = CssCustomProperties.listLocalCssStyleRuleProperties({
- ...filterProps,
- propertyType: "custom",
- })
- .filter((declaration) => {
- return filterName(declaration[0]);
- })
- .map((declaration) => {
- if (removeDashPrefix) {
- return [declaration[0].substr(2), declaration[1]];
- }
- return declaration;
+ // the CSSOM is used to get the names only, the cascade decides about the values
+ const propertyNames = [
+ ...new Set(
+ CssCustomProperties.listLocalCssStyleRuleProperties({
+ ...filterProps,
+ propertyType: "custom",
+ })
+ .map((declaration) => declaration[0])
+ .filter((propertyName) => filterName(propertyName)),
+ ),
+ ];
+
+ const [element, removePlaceholder] = CssCustomProperties.targetElement(filterProps.selectorText);
+
+ try {
+ const namesToResolve =
+ propertyNames.length === 0 && useComputedStyleFallback && element
+ ? CssCustomProperties.listElementCustomPropertyNames(element).filter((propertyName) =>
+ filterName(propertyName),
+ )
+ : propertyNames;
+
+ const customProperties = (
+ element ? CssCustomProperties.resolveCustomPropertyValues(element, namesToResolve) : []
+ ).map(([propertyName, value]): CustomPropertyEntry => {
+ return [removeDashPrefix ? propertyName.slice(2) : propertyName, value];
});
- return returnObject
- ? (Object.fromEntries(customProperties) as Record)
- : (customProperties as [string, string][]);
+ return returnObject
+ ? (Object.fromEntries(customProperties) as Record)
+ : (customProperties as CustomPropertyEntry[]);
+ } finally {
+ removePlaceholder?.();
+ }
};
}
diff --git a/src/common/utils/colorHash.ts b/src/common/utils/colorHash.ts
index 300ec562..b7af083f 100644
--- a/src/common/utils/colorHash.ts
+++ b/src/common/utils/colorHash.ts
@@ -27,22 +27,23 @@ export function getEnabledColorsFromPalette(props: getEnabledColorsProps): Color
const configId = JSON.stringify({
includePaletteGroup: props.includePaletteGroup,
includeColorWeight: props.includeColorWeight,
+ minimalColorDistance: props.minimalColorDistance,
});
if (getEnabledColorsFromPaletteCache.has(configId)) {
return getEnabledColorsFromPaletteCache.get(configId)!;
}
- const colorPropertiesFromPalette = Object.values(getEnabledColorPropertiesFromPalette(props));
+ const colorsFromPalette = getEnabledColorPropertiesFromPalette(props).map((color) => {
+ return Color(color[1]);
+ });
- getEnabledColorsFromPaletteCache.set(
- configId,
- colorPropertiesFromPalette.map((color) => {
- return Color(color[1]);
- }),
- );
+ if (colorsFromPalette.length > 0) {
+ // an empty result is not cached, the stylesheets may be loaded later on
+ getEnabledColorsFromPaletteCache.set(configId, colorsFromPalette);
+ }
- return getEnabledColorsFromPaletteCache.get(configId)!;
+ return colorsFromPalette;
}
export function getEnabledColorPropertiesFromPalette({
@@ -54,6 +55,7 @@ export function getEnabledColorPropertiesFromPalette({
const configId = JSON.stringify({
includePaletteGroup,
includeColorWeight,
+ minimalColorDistance,
});
if (getEnabledColorPropertiesFromPaletteCache.has(configId)) {
@@ -93,9 +95,12 @@ export function getEnabledColorPropertiesFromPalette({
}, colorsFromPaletteValues)
: colorsFromPaletteValues;
- getEnabledColorPropertiesFromPaletteCache.set(configId, colorsFromPaletteWithEnoughDistance);
+ if (colorsFromPaletteWithEnoughDistance.length > 0) {
+ // an empty result is not cached, the stylesheets may be loaded later on
+ getEnabledColorPropertiesFromPaletteCache.set(configId, colorsFromPaletteWithEnoughDistance);
+ }
- return getEnabledColorPropertiesFromPaletteCache.get(configId)!;
+ return colorsFromPaletteWithEnoughDistance;
}
function getColorcode(text: string): ColorOrFalse {
diff --git a/src/common/utils/getColorConfiguration.ts b/src/common/utils/getColorConfiguration.ts
index 11ac0371..8072697f 100644
--- a/src/common/utils/getColorConfiguration.ts
+++ b/src/common/utils/getColorConfiguration.ts
@@ -17,49 +17,30 @@ const colorConfigurationMemo = new Map>();
const getColorConfiguration = (configId: colorconfigs): Record => {
if (!colorConfigurationMemo.has(configId)) {
const selectorClass = `${eccgui}-configuration--colors__${configId}`;
- colorConfigurationMemo.set(
- configId,
- Object.fromEntries(
- (
- new CssCustomProperties({
- selectorText: `.${selectorClass}`,
- removeDashPrefix: true,
- returnObject: false,
- }).customProperties() as string[][]
- ).map((setting) => {
- // check if the value could be a color
-
- let testColorValue = setting[1];
- // check if value itself is a reference to another css custom property
- if (testColorValue.slice(0, 3) === "var") {
- // we currently only extract the first part and ignore any fallbacks
- const customPropertyName = /var\(\s*(--[a-zA-Z0-9_-]+)/g.exec(testColorValue);
- if (customPropertyName && customPropertyName[1]) {
- let selectorElement = document.getElementsByClassName(selectorClass)[0];
- if (!selectorElement) {
- // we need to add an empty element that the JS API can read the value of the custom prop
- selectorElement = document.createElement("div");
- selectorElement.classList.add(selectorClass);
- selectorElement.setAttribute("style", "display: none");
- document.body.appendChild(selectorElement);
- }
- // only check 1 time, not recursive
- testColorValue = getComputedStyle(selectorElement).getPropertyValue(customPropertyName[1]);
- }
- }
-
- try {
- if (Color(testColorValue)) {
- return [setting[0], testColorValue];
- } else {
- return [setting[0], undefined];
- }
- } catch {
- return [setting[0], undefined];
- }
- }),
- ) as Record,
- );
+ const colorConfiguration = Object.fromEntries(
+ (
+ new CssCustomProperties({
+ selectorText: `.${selectorClass}`,
+ removeDashPrefix: true,
+ returnObject: false,
+ }).customProperties() as string[][]
+ ).map((setting) => {
+ // check if the value could be a color, references to other custom properties are already resolved
+ try {
+ Color(setting[1]);
+ return [setting[0], setting[1]];
+ } catch {
+ return [setting[0], undefined];
+ }
+ }),
+ ) as Record;
+
+ if (Object.keys(colorConfiguration).length === 0) {
+ // an empty result is not cached, the stylesheets may be loaded later on
+ return colorConfiguration;
+ }
+
+ colorConfigurationMemo.set(configId, colorConfiguration);
}
return colorConfigurationMemo.get(configId)!;
};
diff --git a/src/components/PropertyValuePair/propertyvalue.scss b/src/components/PropertyValuePair/propertyvalue.scss
index 7031b8e6..4e289259 100644
--- a/src/components/PropertyValuePair/propertyvalue.scss
+++ b/src/components/PropertyValuePair/propertyvalue.scss
@@ -1,6 +1,6 @@
@use "sass:math";
-$eccgui-pagination-size-column-breakpoint-small: 20rem;
+$eccgui-propertyvalue-size-column-breakpoint-small: 20rem !default;
.#{$eccgui}-propertyvalue__list {
display: block;
@@ -35,17 +35,17 @@ $eccgui-pagination-size-column-breakpoint-small: 20rem;
.#{$eccgui}-propertyvalue__property,
.#{$eccgui}-propertyvalue__value {
+ position: relative;
display: flex;
flex-direction: column;
justify-content: center;
- position: relative;
:not(.#{$eccgui}-propertyvalue__pair--singlecolumn) > & {
- @container eccgui-propertyvalue-pair (width >= #{$eccgui-pagination-size-column-breakpoint-small}) {
+ @container eccgui-propertyvalue-pair (width >= #{$eccgui-propertyvalue-size-column-breakpoint-small}) {
min-height: $eccgui-size-textfield-height-regular;
}
- @container eccgui-propertyvalue-pair (width < #{$eccgui-pagination-size-column-breakpoint-small}) {
+ @container eccgui-propertyvalue-pair (width < #{$eccgui-propertyvalue-size-column-breakpoint-small}) {
&.#{$eccgui}-propertyvalue__value {
margin-bottom: $eccgui-size-inline-whitespace * 0.5;
}
@@ -55,11 +55,11 @@ $eccgui-pagination-size-column-breakpoint-small: 20rem;
.#{$eccgui}-propertyvalue__property {
:not(.#{$eccgui}-propertyvalue__pair--singlecolumn) > & {
- @container eccgui-propertyvalue-pair (width >= #{$eccgui-pagination-size-column-breakpoint-small}) {
+ @container eccgui-propertyvalue-pair (width >= #{$eccgui-propertyvalue-size-column-breakpoint-small}) {
+ bottom: -1px;
float: left;
width: math.div(3, 16) * 100%;
overflow: hidden;
- bottom: -1px;
& > div {
margin-right: $eccgui-size-block-whitespace;
@@ -83,7 +83,7 @@ $eccgui-pagination-size-column-breakpoint-small: 20rem;
}
.#{$eccgui}-label__tooltip,
- .#{$eccgui}-label__other{
+ .#{$eccgui}-label__other {
position: relative;
bottom: -1px;
}
@@ -94,7 +94,7 @@ $eccgui-pagination-size-column-breakpoint-small: 20rem;
box-sizing: content-box;
:not(.#{$eccgui}-propertyvalue__pair--singlecolumn) > & {
- @container eccgui-propertyvalue-pair (width >= #{$eccgui-pagination-size-column-breakpoint-small}) {
+ @container eccgui-propertyvalue-pair (width >= #{$eccgui-propertyvalue-size-column-breakpoint-small}) {
margin-left: math.div(3, 16) * 100%;
}
}
@@ -112,7 +112,7 @@ $eccgui-pagination-size-column-breakpoint-small: 20rem;
.#{$eccgui}-propertyvalue__property--small {
:not(.#{$eccgui}-propertyvalue__pair--singlecolumn) > & {
- @container eccgui-propertyvalue-pair (width >= #{$eccgui-pagination-size-column-breakpoint-small}) {
+ @container eccgui-propertyvalue-pair (width >= #{$eccgui-propertyvalue-size-column-breakpoint-small}) {
width: math.div(2, 16) * 100%;
& + .#{$eccgui}-propertyvalue__value {
@@ -124,7 +124,7 @@ $eccgui-pagination-size-column-breakpoint-small: 20rem;
.#{$eccgui}-propertyvalue__property--large {
:not(.#{$eccgui}-propertyvalue__pair--singlecolumn) > & {
- @container eccgui-propertyvalue-pair (width >= #{$eccgui-pagination-size-column-breakpoint-small}) {
+ @container eccgui-propertyvalue-pair (width >= #{$eccgui-propertyvalue-size-column-breakpoint-small}) {
width: math.div(5, 16) * 100%;
& + .#{$eccgui}-propertyvalue__value {
diff --git a/src/components/PropertyValuePair/stories/PropertyName.stories.tsx b/src/components/PropertyValuePair/stories/PropertyName.stories.tsx
index 20c36819..28a27e0b 100644
--- a/src/components/PropertyValuePair/stories/PropertyName.stories.tsx
+++ b/src/components/PropertyValuePair/stories/PropertyName.stories.tsx
@@ -17,6 +17,7 @@ const Template: StoryFn = (args) => ({
...handleProps,
...tooltipTitle,
- className: intent
- ? `${intentClassName(intent)} `
- : "" + ` ${eccgui}-graphviz__handle ${eccgui}-graphviz__handle--${flowVersionCheck}`,
+ className:
+ `${eccgui}-graphviz__handle ${eccgui}-graphviz__handle--${flowVersionCheck}` +
+ (intent ? ` ${intentClassName(intent)}` : ""),
onClick: (e: React.MouseEvent) => {
if (handleProps.onClick) {
handleProps.onClick(e);
}
- if (toolsTarget.length > 0 && e.currentTarget === handleDefaultRef.current) {
+ if (toolsTarget && toolsTarget.length > 0 && e.currentTarget === handleDefaultRef.current) {
setExtendedTooltipDisplayed(false);
(toolsTarget[0] as HTMLElement).click();
}
@@ -129,7 +129,11 @@ export const HandleDefault = memo(
},
onMouseLeave: () => {
if (switchTooltipTimerOn) clearTimeout(switchTooltipTimerOn);
- if (toolsTarget.length > 0 && toolsTarget[0].classList.contains(BlueprintClasses.POPOVER_OPEN)) {
+ if (
+ toolsTarget &&
+ toolsTarget.length > 0 &&
+ toolsTarget[0].classList.contains(BlueprintClasses.POPOVER_OPEN)
+ ) {
switchToolsTimerOff = setTimeout(() => (toolsTarget[0] as HTMLElement).click(), 500);
}
setExtendedTooltipDisplayed(false);
diff --git a/yarn.lock b/yarn.lock
index 9d1e1a2a..c0f579f3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4770,10 +4770,10 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
-brace-expansion@^1.1.7:
- version "1.1.13"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6"
- integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==
+brace-expansion@^1.1.18, brace-expansion@^1.1.7:
+ version "1.1.18"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.18.tgz#3ce74d89885136be1535341f8c3d4425c29a5cab"
+ integrity sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@@ -6406,10 +6406,10 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
-fast-uri@^3.0.1:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
- integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
+fast-uri@^3.0.1, fast-uri@^3.1.3:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0"
+ integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==
fastest-levenshtein@^1.0.16:
version "1.0.16"
@@ -7349,10 +7349,10 @@ ignore@^7.0.5:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9"
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
-immutable@^5.1.5:
- version "5.1.5"
- resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165"
- integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==
+immutable@^5.1.5, immutable@^5.1.8:
+ version "5.1.9"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.9.tgz#ac23c3a01992ab665e14ac9ffff298f28cd74a0c"
+ integrity sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==
import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.1"
@@ -8269,25 +8269,18 @@ jest@^30.4.2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-js-yaml@^3.13.1:
- version "3.14.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
- integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+js-yaml@^3.13.1, js-yaml@^3.15.1:
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.1.tgz#24bc95028f361cdaaa84745b06a109c4486773c0"
+ integrity sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
-js-yaml@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
- integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
- dependencies:
- argparse "^2.0.1"
-
-js-yaml@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
- integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
+js-yaml@^4.1.0, js-yaml@^4.1.1, js-yaml@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
+ integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
dependencies:
argparse "^2.0.1"
@@ -9275,15 +9268,10 @@ n3@^1.26.0:
buffer "^6.0.3"
readable-stream "^4.0.0"
-nanoid@^3.3.11:
- version "3.3.11"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
- integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
-
-nanoid@^3.3.12:
- version "3.3.12"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05"
- integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==
+nanoid@^3.3.11, nanoid@^3.3.17, nanoid@^3.3.18:
+ version "3.3.18"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913"
+ integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==
napi-postinstall@^0.3.0:
version "0.3.2"
@@ -9934,16 +9922,16 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
-postcss@^6.0.14, postcss@^8.2.7, postcss@^8.5.10:
- version "8.5.15"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c"
- integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==
+postcss@^6.0.14, postcss@^8.2.7, postcss@^8.5.12, postcss@^8.5.13, postcss@^8.5.18, postcss@^8.5.8:
+ version "8.5.26"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620"
+ integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==
dependencies:
- nanoid "^3.3.12"
+ nanoid "^3.3.17"
picocolors "^1.1.1"
source-map-js "^1.2.1"
-postcss@^8.4.40, postcss@^8.5.13:
+postcss@^8.4.40:
version "8.5.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
@@ -9952,15 +9940,6 @@ postcss@^8.4.40, postcss@^8.5.13:
picocolors "^1.1.1"
source-map-js "^1.2.1"
-postcss@^8.5.8:
- version "8.5.8"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399"
- integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==
- dependencies:
- nanoid "^3.3.11"
- picocolors "^1.1.1"
- source-map-js "^1.2.1"
-
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"