diff --git a/packages/web-component-designer/src/elements/controls/NumericStyleInput.ts b/packages/web-component-designer/src/elements/controls/NumericStyleInput.ts
index 85b071faa..629dcae31 100644
--- a/packages/web-component-designer/src/elements/controls/NumericStyleInput.ts
+++ b/packages/web-component-designer/src/elements/controls/NumericStyleInput.ts
@@ -39,7 +39,7 @@ export class NumericStyleInput extends BaseCustomWebComponentConstructorAppend {
#container {
display: grid;
gap: 0;
- grid-template-columns: minmax(0, 1fr) auto 16px;
+ grid-template-columns: minmax(0, 1fr) auto 16px auto;
width: 100%;
height: 24px;
align-items: stretch;
@@ -151,11 +151,12 @@ export class NumericStyleInput extends BaseCustomWebComponentConstructorAppend {
-
-
-
-
-
+
+
+
+
+
+
`;
@@ -251,8 +252,9 @@ export class NumericStyleInput extends BaseCustomWebComponentConstructorAppend {
this._unitValueConverter = value;
}
- private _input: HTMLInputElement;
- private _select: HTMLSelectElement;
+ private _input: HTMLInputElement;
+ private _select: HTMLSelectElement;
+ private _addonContainer: HTMLDivElement;
private _measure: HTMLSpanElement;
private _scrubberButton: HTMLButtonElement;
private _increaseButton: HTMLButtonElement;
@@ -282,19 +284,33 @@ export class NumericStyleInput extends BaseCustomWebComponentConstructorAppend {
constructor() {
super();
this._restoreCachedInititalValues();
- this._input = this._getDomElement('input');
- this._select = this._getDomElement('select');
+ this._input = this._getDomElement('input');
+ this._select = this._getDomElement('select');
+ this._addonContainer = this._getDomElement('addon');
this._measure = this._getDomElement('measure');
this._scrubberButton = this._getDomElement('scrubber');
this._increaseButton = this._getDomElement('increase');
this._decreaseButton = this._getDomElement('decrease');
}
- ready() {
+ ready() {
this._parseAttributesToProperties();
this._wireEvents();
this._updateValue();
- }
+ }
+
+ private _addon: HTMLElement;
+ public get addon() {
+ return this._addon;
+ }
+ public set addon(value: HTMLElement) {
+ if (this._addon === value)
+ return;
+ this._addon?.remove();
+ this._addon = value ?? null;
+ if (this._addon)
+ this._addonContainer.appendChild(this._addon);
+ }
private _wireEvents() {
this._input.addEventListener('change', () => this._applyTypedValue());
@@ -909,4 +925,4 @@ export class NumericStyleInput extends BaseCustomWebComponentConstructorAppend {
}
}
-customElements.define('node-projects-numeric-style-input', NumericStyleInput);
\ No newline at end of file
+customElements.define('node-projects-numeric-style-input', NumericStyleInput);
diff --git a/packages/web-component-designer/src/elements/services/propertiesService/IProperty.ts b/packages/web-component-designer/src/elements/services/propertiesService/IProperty.ts
index d9aa1339d..1fdf36f68 100644
--- a/packages/web-component-designer/src/elements/services/propertiesService/IProperty.ts
+++ b/packages/web-component-designer/src/elements/services/propertiesService/IProperty.ts
@@ -1,7 +1,8 @@
import { IPropertiesService } from './IPropertiesService.js';
import { IPropertyEditor } from './IPropertyEditor.js';
import { PropertyType } from './PropertyType.js';
-import type { IDesignItem } from '../../item/IDesignItem.js';
+import type { IDesignItem } from '../../item/IDesignItem.js';
+import type { UnitEditorAddon } from './propertyEditors/UnitPropertyEditorConfig.js';
export interface IProperty {
name: string;
@@ -22,7 +23,8 @@ export interface IProperty {
units?: string[]; // selectable units for editors that support unit changes
unitSteps?: Record;
numericValueDecimalPlaces?: number; // rounding used by numeric unit conversions
- numericValueConverter?: (value: number, fromUnit: string, toUnit: string, property: IProperty, numericType: string, numberText?: string, rawValue?: string, designItems?: IDesignItem[]) => string | number | null | undefined;
+ numericValueConverter?: (value: number, fromUnit: string, toUnit: string, property: IProperty, numericType: string, numberText?: string, rawValue?: string, designItems?: IDesignItem[]) => string | number | null | undefined;
+ unitEditorAddon?: UnitEditorAddon;
enumValues?: [name: string, value: string | number][]; // list selectable enum values
createEditor?: (property: IProperty) => IPropertyEditor;
value?: any;
diff --git a/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/AngleUnitEditorAddon.ts b/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/AngleUnitEditorAddon.ts
new file mode 100644
index 000000000..1a4e62ee5
--- /dev/null
+++ b/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/AngleUnitEditorAddon.ts
@@ -0,0 +1,103 @@
+import type { UnitEditorAddon } from './UnitPropertyEditorConfig.js';
+
+const popupSize = 108;
+const angleUnitInDegrees: Record = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
+
+function getAngle(event: PointerEvent, circle: HTMLElement) {
+ const rect = circle.getBoundingClientRect();
+ const x = event.clientX - (rect.left + rect.width / 2);
+ const y = event.clientY - (rect.top + rect.height / 2);
+ return (Math.atan2(-y, x) * 180 / Math.PI + 360) % 360;
+}
+
+export function getAngleInDegrees(value?: string | null) {
+ const match = value?.trim().match(/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-z]+)?$/i);
+ if (!match)
+ return 0;
+ const angle = Number(match[1]) * (angleUnitInDegrees[match[2]?.toLowerCase() ?? 'deg'] ?? 1);
+ return Number.isFinite(angle) ? ((angle % 360) + 360) % 360 : 0;
+}
+
+/** Creates the built-in circular picker used by CSS angle properties. */
+export const createAngleUnitEditorAddon: UnitEditorAddon = context => {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.title = 'Pick angle';
+ button.setAttribute('aria-label', 'Pick angle');
+ button.textContent = '◉';
+ button.style.cssText = 'border:0;background:transparent;color:inherit;cursor:pointer;padding:0 3px;height:24px;line-height:1;';
+ if (context.property.readonly) {
+ button.disabled = true;
+ button.style.cursor = 'default';
+ return button;
+ }
+
+ let popup: HTMLDivElement | null = null;
+ let circle: HTMLDivElement | null = null;
+ let dragging = false;
+ let cancelOnBlur: () => void;
+
+ const updateHand = (angle: number) => {
+ const hand = popup?.querySelector('[data-angle-hand]');
+ if (hand)
+ hand.style.transform = `translateX(-50%) rotate(${angle}deg)`;
+ };
+ const selectAngle = async (event: PointerEvent, commit: boolean) => {
+ if (!circle)
+ return;
+ const angle = Math.round(getAngle(event, circle));
+ updateHand(angle);
+ const value = `${angle}deg`;
+ if (commit)
+ await context.setValue(value);
+ else
+ await context.previewValue(value);
+ };
+ const close = async (removePreview = false) => {
+ if (removePreview)
+ await context.removePreviewValue();
+ popup?.remove();
+ popup = null;
+ circle = null;
+ dragging = false;
+ document.removeEventListener('pointerdown', outsidePointerDown, true);
+ window.removeEventListener('blur', cancelOnBlur);
+ };
+ const outsidePointerDown = (event: PointerEvent) => {
+ if (popup && !popup.contains(event.target as Node) && event.target !== button)
+ void close(dragging);
+ };
+ const open = () => {
+ if (popup) {
+ void close(dragging);
+ return;
+ }
+ popup = document.createElement('div');
+ popup.style.cssText = `position:fixed;z-index:100000;width:${popupSize}px;height:${popupSize}px;border:2px solid currentColor;border-radius:50%;background:var(--property-editor-popup-background,#fff);color:var(--property-editor-popup-color,#111);box-sizing:border-box;`;
+ circle = popup;
+ const labels = [['0', 'right'], ['90', 'top'], ['180', 'left'], ['270', 'bottom']];
+ for (const [label, position] of labels) {
+ const item = document.createElement('span');
+ item.textContent = label;
+ item.style.cssText = `position:absolute;font:10px sans-serif;${position}:4px;${position === 'right' || position === 'left' ? 'top:50%;transform:translateY(-50%);' : 'left:50%;transform:translateX(-50%);'}`;
+ popup.appendChild(item);
+ }
+ const hand = document.createElement('span');
+ hand.dataset.angleHand = '';
+ hand.style.cssText = `position:absolute;left:50%;top:50%;width:42%;height:2px;background:#f22;transform-origin:0 50%;transform:rotate(${getAngleInDegrees(context.value)}deg);`;
+ popup.appendChild(hand);
+ document.body.appendChild(popup);
+ const rect = button.getBoundingClientRect();
+ popup.style.left = `${Math.max(4, rect.right - popupSize)}px`;
+ popup.style.top = `${rect.bottom + 4}px`;
+ circle.addEventListener('pointerdown', event => { dragging = true; circle!.setPointerCapture?.(event.pointerId); void selectAngle(event, false); });
+ circle.addEventListener('pointermove', event => { if (dragging) void selectAngle(event, false); });
+ circle.addEventListener('pointerup', event => { if (dragging) { dragging = false; void selectAngle(event, true); } });
+ circle.addEventListener('pointercancel', () => { if (dragging) void close(true); });
+ cancelOnBlur = () => { if (dragging) void close(true); };
+ window.addEventListener('blur', cancelOnBlur);
+ document.addEventListener('pointerdown', outsidePointerDown, true);
+ };
+ button.addEventListener('click', open);
+ return button;
+};
diff --git a/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditor.ts b/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditor.ts
index 77012bc16..3b3186349 100644
--- a/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditor.ts
+++ b/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditor.ts
@@ -28,6 +28,42 @@ export class UnitPropertyEditor extends BasePropertyEditor {
return;
await this._valueChanged(e.newValue === '' ? null : e.newValue);
});
+ if (config?.addon) {
+ const thisEditor = this;
+ let previewStartValue: string | null = null;
+ const context = {
+ property,
+ get value() {
+ return selector.value;
+ },
+ get designItems() {
+ return thisEditor.designItems;
+ },
+ setValue: async (value: string | null) => {
+ if (previewStartValue !== null)
+ await this._removePreviewValue();
+ selector.value = value ?? '';
+ previewStartValue = null;
+ await this._valueChanged(value);
+ },
+ previewValue: async (value: string | null) => {
+ previewStartValue ??= selector.value;
+ selector.value = value ?? '';
+ await this._previewValueChanged(value);
+ },
+ removePreviewValue: async () => {
+ try {
+ await this._removePreviewValue();
+ } finally {
+ if (previewStartValue !== null) {
+ selector.value = previewStartValue;
+ previewStartValue = null;
+ }
+ }
+ }
+ };
+ selector.addon = config.addon(context);
+ }
this.element = selector;
}
@@ -38,4 +74,4 @@ export class UnitPropertyEditor extends BasePropertyEditor {
refreshValue(valueType: ValueType, value: any) {
this.element.value = value == null ? '' : String(value);
}
-}
\ No newline at end of file
+}
diff --git a/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditorConfig.ts b/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditorConfig.ts
index e2c5c9582..4892df59b 100644
--- a/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditorConfig.ts
+++ b/packages/web-component-designer/src/elements/services/propertiesService/propertyEditors/UnitPropertyEditorConfig.ts
@@ -1,10 +1,30 @@
import type { IDesignItem } from '../../../item/IDesignItem.js';
import type { IProperty } from '../IProperty.js';
+import { createAngleUnitEditorAddon } from './AngleUnitEditorAddon.js';
export type UnitPropertyType = 'css-length' | 'css-angle' | 'css-time' | 'css-frequency' | 'css-flex' | 'css-resolution' | 'css-scale' | 'svg-length';
export type UnitConversionResult = string | number | null | undefined;
+/**
+ * Context supplied to an optional control hosted next to a numeric unit editor.
+ * An addon can use the callbacks to commit or preview values selected by its own popup.
+ */
+export type UnitEditorAddonContext = {
+ property: IProperty,
+ readonly value: string,
+ readonly designItems: IDesignItem[],
+ setValue: (value: string | null) => Promise,
+ previewValue: (value: string | null) => Promise,
+ removePreviewValue: () => Promise
+};
+
+/**
+ * Creates a control, such as a button that opens a specialised unit picker,
+ * which is displayed beside the standard numeric unit controls.
+ */
+export type UnitEditorAddon = (context: UnitEditorAddonContext) => HTMLElement;
+
export type UnitConversionContext = {
property: IProperty,
numericType: UnitPropertyType,
@@ -21,7 +41,8 @@ export type UnitEditorConfig = {
units: string[],
fixedValues: string[],
unitSteps: Record,
- convertValue: (context: Omit) => string
+ convertValue: (context: Omit) => string,
+ addon?: UnitEditorAddon
};
const cssNumericKeywordValues = ['initial', 'inherit', 'unset'];
@@ -542,6 +563,7 @@ export function getCssNumericEditorConfig(property: IProperty): UnitEditorConfig
units: property.units?.length ? property.units : defaultCssNumericUnits[numericType],
fixedValues: getCssNumericKeywordValues(property.values),
unitSteps: { ...defaultUnitSteps, ...(property.unitSteps ?? {}) },
- convertValue: context => convertNumericUnitValue({ ...context, property, numericType })
+ convertValue: context => convertNumericUnitValue({ ...context, property, numericType }),
+ addon: property.unitEditorAddon ?? (numericType === 'css-angle' ? createAngleUnitEditorAddon : undefined)
};
-}
\ No newline at end of file
+}
diff --git a/packages/web-component-designer/src/index.ts b/packages/web-component-designer/src/index.ts
index 378cdca11..5c235c39c 100644
--- a/packages/web-component-designer/src/index.ts
+++ b/packages/web-component-designer/src/index.ts
@@ -158,6 +158,8 @@ export * from "./elements/services/propertiesService/propertyEditors/BooleanProp
export * from "./elements/services/propertiesService/propertyEditors/ColorPropertyEditor.js";
export * from "./elements/services/propertiesService/propertyEditors/CssPropertyEditor.js";
export * from "./elements/services/propertiesService/propertyEditors/UnitPropertyEditor.js";
+export type { UnitEditorAddon, UnitEditorAddonContext } from "./elements/services/propertiesService/propertyEditors/UnitPropertyEditorConfig.js";
+export { createAngleUnitEditorAddon, getAngleInDegrees } from "./elements/services/propertiesService/propertyEditors/AngleUnitEditorAddon.js";
export * from "./elements/services/propertiesService/propertyEditors/DatePropertyEditor.js";
export * from "./elements/services/propertiesService/propertyEditors/ImageButtonListPropertyEditor.js";
export * from "./elements/services/propertiesService/propertyEditors/JsonPropertyEditor.js";
diff --git a/packages/web-component-designer/tests/AngleUnitEditorAddon.test.ts b/packages/web-component-designer/tests/AngleUnitEditorAddon.test.ts
new file mode 100644
index 000000000..eb52e00db
--- /dev/null
+++ b/packages/web-component-designer/tests/AngleUnitEditorAddon.test.ts
@@ -0,0 +1,61 @@
+/** @jest-environment jsdom */
+import { expect, jest, test } from '@jest/globals';
+import type { IProperty } from '../src/elements/services/propertiesService/IProperty';
+import { createAngleUnitEditorAddon, getAngleInDegrees } from '../src/elements/services/propertiesService/propertyEditors/AngleUnitEditorAddon';
+
+test.each([
+ ['90deg', 90], ['100grad', 90], ['1.5707963268rad', 90], ['0.25turn', 90]
+])('normalizes %s for the angle picker hand', (value, expected) => {
+ expect(getAngleInDegrees(value)).toBeCloseTo(expected);
+});
+
+test('angle picker removes a preview when pointer interaction is cancelled', async () => {
+ const removePreviewValue = jest.fn<() => Promise>().mockResolvedValue(undefined);
+ const context = {
+ property: {} as IProperty,
+ value: '1rad',
+ designItems: [],
+ setValue: jest.fn<() => Promise>().mockResolvedValue(undefined),
+ previewValue: jest.fn<() => Promise>().mockResolvedValue(undefined),
+ removePreviewValue
+ };
+ const button = createAngleUnitEditorAddon(context);
+ document.body.appendChild(button);
+ button.click();
+ const popup = document.body.lastElementChild as HTMLElement;
+ popup.dispatchEvent(Object.assign(new Event('pointerdown'), { clientX: 10, clientY: 10, pointerId: 1 }));
+ popup.dispatchEvent(new Event('pointercancel'));
+ await Promise.resolve();
+ expect(context.previewValue).toHaveBeenCalled();
+ expect(removePreviewValue).toHaveBeenCalled();
+ expect(document.body.contains(popup)).toBe(false);
+ button.remove();
+});
+
+test.each([
+ ['right', 100, 50, '0deg'],
+ ['top', 50, 0, '90deg'],
+ ['left', 0, 50, '180deg'],
+ ['bottom', 50, 100, '270deg']
+])('dragging to the %s of the dial selects the expected angle', async (_position, clientX, clientY, expected) => {
+ const previewValue = jest.fn<() => Promise>().mockResolvedValue(undefined);
+ const context = {
+ property: {} as IProperty,
+ value: '0deg',
+ designItems: [],
+ setValue: jest.fn<() => Promise>().mockResolvedValue(undefined),
+ previewValue,
+ removePreviewValue: jest.fn<() => Promise>().mockResolvedValue(undefined)
+ };
+ const button = createAngleUnitEditorAddon(context);
+ document.body.appendChild(button);
+ button.click();
+ const popup = document.body.lastElementChild as HTMLElement;
+ Object.defineProperty(popup, 'getBoundingClientRect', { value: () => ({ left: 0, top: 0, width: 100, height: 100 }) });
+ popup.dispatchEvent(Object.assign(new Event('pointerdown'), { clientX, clientY, pointerId: 1 }));
+ popup.dispatchEvent(Object.assign(new Event('pointerup'), { clientX, clientY, pointerId: 1 }));
+ await Promise.resolve();
+ expect(previewValue).toHaveBeenCalledWith(expected);
+ button.remove();
+ popup.remove();
+});
diff --git a/packages/web-component-designer/tests/NumericStyleInput.test.ts b/packages/web-component-designer/tests/NumericStyleInput.test.ts
index 2d0277259..59b1d2d67 100644
--- a/packages/web-component-designer/tests/NumericStyleInput.test.ts
+++ b/packages/web-component-designer/tests/NumericStyleInput.test.ts
@@ -2,7 +2,8 @@ import { expect, test } from '@jest/globals';
import type { IProperty } from '../src/elements/services/propertiesService/IProperty';
import { PropertyType } from '../src/elements/services/propertiesService/PropertyType';
import { combineNumericStyleInputValue, getNumericStyleInputUnitLabel, normalizeNumericStyleInputOptionValues, parseNumericStyleInputValue, resolveNumericStyleInputSelectedUnit, resolveNumericStyleInputStep } from '../src/elements/controls/NumericStyleInputValueHelpers';
-import { applyCssNumericPropertyDefaults, convertNumericUnitValue, defaultCssNumericUnits, defaultCssNumericUnitSteps } from '../src/elements/services/propertiesService/propertyEditors/UnitPropertyEditorConfig';
+import { applyCssNumericPropertyDefaults, convertNumericUnitValue, defaultCssNumericUnits, defaultCssNumericUnitSteps, getCssNumericEditorConfig } from '../src/elements/services/propertiesService/propertyEditors/UnitPropertyEditorConfig';
+import { createAngleUnitEditorAddon } from '../src/elements/services/propertiesService/propertyEditors/AngleUnitEditorAddon';
test('parses numeric, fixed, and custom values', () => {
expect(parseNumericStyleInputValue('12px')).toEqual({ kind: 'numeric', numberText: '12', value: 12, unit: 'px' });
@@ -219,4 +220,22 @@ test('abstract css properties service fills css numeric metadata', () => {
delete (globalThis as any).getComputedStyle;
else
Object.defineProperty(globalThis, 'getComputedStyle', { configurable: true, value: originalGetComputedStyle });
-});
\ No newline at end of file
+});
+
+test('css numeric editor configuration retains a unit editor addon', () => {
+ const addon = () => ({}) as HTMLElement;
+ const property: IProperty = {
+ name: 'rotate',
+ type: 'css-angle',
+ unitEditorAddon: addon,
+ service: {} as any,
+ propertyType: PropertyType.cssValue
+ };
+
+ expect(getCssNumericEditorConfig(property)?.addon).toBe(addon);
+});
+
+test('css angle editor gets the built-in angle addon', () => {
+ const property: IProperty = { name: 'rotate', type: 'css-angle', service: {} as any, propertyType: PropertyType.cssValue };
+ expect(getCssNumericEditorConfig(property)?.addon).toBe(createAngleUnitEditorAddon);
+});
diff --git a/packages/web-component-designer/tests/UnitPropertyEditor.test.ts b/packages/web-component-designer/tests/UnitPropertyEditor.test.ts
new file mode 100644
index 000000000..60adf8b54
--- /dev/null
+++ b/packages/web-component-designer/tests/UnitPropertyEditor.test.ts
@@ -0,0 +1,79 @@
+/** @jest-environment jsdom */
+import { beforeAll, expect, jest, test } from '@jest/globals';
+import { PropertyType } from '../src/elements/services/propertiesService/PropertyType';
+import type { IProperty } from '../src/elements/services/propertiesService/IProperty';
+
+let UnitPropertyEditor: typeof import('../src/elements/services/propertiesService/propertyEditors/UnitPropertyEditor').UnitPropertyEditor;
+
+beforeAll(async () => {
+ if (!CSSStyleSheet.prototype.replaceSync)
+ Object.defineProperty(CSSStyleSheet.prototype, 'replaceSync', { value() { } });
+ ({ UnitPropertyEditor } = await import('../src/elements/services/propertiesService/propertyEditors/UnitPropertyEditor'));
+});
+
+test('restores the numeric editor value when an addon preview is cancelled', async () => {
+ const property: IProperty = {
+ name: 'rotate',
+ type: 'css-angle',
+ service: {} as any,
+ propertyType: PropertyType.cssValue
+ };
+ const editor = new UnitPropertyEditor(property);
+ editor.element.value = '10deg';
+ editor.element.addon.click();
+ const popup = document.body.lastElementChild as HTMLElement;
+ popup.dispatchEvent(Object.assign(new Event('pointerdown'), { clientX: 10, clientY: 10, pointerId: 1 }));
+ popup.dispatchEvent(new Event('pointercancel'));
+ await Promise.resolve();
+ expect(editor.element.value).toBe('10deg');
+ editor.element.remove();
+});
+
+test('clears the addon preview before committing a selected value', async () => {
+ const previewValue = jest.fn<() => Promise>().mockResolvedValue(undefined);
+ const removePreviewValue = jest.fn<() => Promise>().mockResolvedValue(undefined);
+ const setValue = jest.fn<() => Promise>().mockResolvedValue(undefined);
+ const property: IProperty = {
+ name: 'rotate',
+ type: 'css-angle',
+ service: { previewValue, removePreviewValue, setValue } as any,
+ propertyType: PropertyType.cssValue
+ };
+ const editor = new UnitPropertyEditor(property);
+ const designItem = {
+ openGroup: () => ({ commit: jest.fn() })
+ } as any;
+ editor.designItemsChanged([designItem]);
+ editor.element.value = '10deg';
+ editor.element.addon.click();
+ const popup = document.body.lastElementChild as HTMLElement;
+ Object.defineProperty(popup, 'getBoundingClientRect', { value: () => ({ left: 0, top: 0, width: 100, height: 100 }) });
+ popup.dispatchEvent(Object.assign(new Event('pointerdown'), { clientX: 100, clientY: 50, pointerId: 1 }));
+ popup.dispatchEvent(Object.assign(new Event('pointerup'), { clientX: 100, clientY: 50, pointerId: 1 }));
+ await Promise.resolve();
+ await Promise.resolve();
+ expect(previewValue).toHaveBeenCalledWith([designItem], property, '0deg');
+ expect(removePreviewValue).toHaveBeenCalledWith([designItem], property);
+ expect(setValue).toHaveBeenCalledWith([designItem], property, '0deg');
+ editor.element.remove();
+ popup.remove();
+});
+
+test('does not open or change a readonly angle property', async () => {
+ const previewValue = jest.fn<() => Promise>().mockResolvedValue(undefined);
+ const setValue = jest.fn<() => Promise>().mockResolvedValue(undefined);
+ const property: IProperty = {
+ name: 'rotate',
+ type: 'css-angle',
+ readonly: true,
+ service: { previewValue, setValue } as any,
+ propertyType: PropertyType.cssValue
+ };
+ const editor = new UnitPropertyEditor(property);
+ editor.element.addon.click();
+ expect((editor.element.addon as HTMLButtonElement).disabled).toBe(true);
+ expect(document.body.querySelector('[data-angle-hand]')).toBeNull();
+ expect(previewValue).not.toHaveBeenCalled();
+ expect(setValue).not.toHaveBeenCalled();
+ editor.element.remove();
+});