Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions __tests__/unit/editor/managers/interaction-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,32 @@ describe('InteractionManager', () => {
expect(manager.isActive()).toBe(false);
});

it('activates when a shadow-root click path contains the svg document', () => {
const manager = new InteractionManager();
const host = document.createElement('div');
document.body.appendChild(host);

const shadowRoot = host.attachShadow({ mode: 'open' });
svg.remove();
shadowRoot.appendChild(svg);
editor = { getDocument: () => svg };

manager.init({
emitter,
editor,
commander,
state,
interactions: [],
});

(manager as any).handleClick({
target: host,
composedPath: () => [svg, shadowRoot, host, document.body, document],
} as unknown as MouseEvent);

expect(manager.isActive()).toBe(true);
});

it('runs exclusive interactions only when active', async () => {
const manager = new InteractionManager();
manager.init({ state, emitter, editor, commander, interactions: [] });
Expand Down
80 changes: 80 additions & 0 deletions __tests__/unit/editor/plugins/components/popover.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { afterEach, describe, expect, it } from 'vitest';

import { Popover } from '../../../../../src/editor/plugins/components/popover';

function createShadowTrigger() {
const host = document.createElement('div');
document.body.appendChild(host);

const shadowRoot = host.attachShadow({ mode: 'open' });
const trigger = document.createElement('button');
shadowRoot.appendChild(trigger);

return { host, shadowRoot, trigger };
}

describe('Popover', () => {
afterEach(() => {
document.body.innerHTML = '';
document.head.querySelector('#infographic-edit-popover-style')?.remove();
});

it('closes when clicking outside an opened portal popover', () => {
const trigger = document.createElement('button');
document.body.appendChild(trigger);

const popover = Popover({
target: trigger,
content: 'Font tools',
closeOnOutsideClick: true,
open: true,
trigger: 'click',
});
document.body.appendChild(popover);

const content = document.body.querySelector(
'.infographic-edit-popover__content',
) as HTMLElement | null;
expect(content?.getAttribute('data-open')).toBe('true');

const outside = document.createElement('div');
document.body.appendChild(outside);
outside.dispatchEvent(
new MouseEvent('click', { bubbles: true, composed: true }),
);

expect(content?.getAttribute('data-open')).toBe('false');

popover.destroy();
});

it('keeps a shadow-root portal popover open when clicking inside content', () => {
const { shadowRoot, trigger } = createShadowTrigger();

const contentButton = document.createElement('button');
contentButton.textContent = 'Keep open';

const popover = Popover({
target: trigger,
content: contentButton,
closeOnOutsideClick: true,
getContainer: shadowRoot,
open: true,
trigger: 'click',
});
shadowRoot.appendChild(popover);

const content = shadowRoot.querySelector(
'.infographic-edit-popover__content',
) as HTMLElement | null;
expect(content?.getAttribute('data-open')).toBe('true');

contentButton.dispatchEvent(
new MouseEvent('click', { bubbles: true, composed: true }),
);

expect(content?.getAttribute('data-open')).toBe('true');

popover.destroy();
});
});
189 changes: 189 additions & 0 deletions __tests__/unit/editor/plugins/overlay-root.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import { ElementTypeEnum } from '../../../../src/constants/element';
import { EditBar } from '../../../../src/editor/plugins/edit-bar/edit-bar';
import { ResetViewBox } from '../../../../src/editor/plugins/reset-viewbox';
import { createTextElement } from '../../../../src/utils/text';

function createSvgInShadowRoot() {
const host = document.createElement('div');
document.body.appendChild(host);

const shadowRoot = host.attachShadow({ mode: 'open' });
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 100 100');
svg.setAttribute('width', '100');
svg.setAttribute('height', '100');
shadowRoot.appendChild(svg);

return { host, shadowRoot, svg };
}

describe('editor overlay roots', () => {
afterEach(() => {
document.body.innerHTML = '';
document.head.querySelector('#infographic-edit-bar-icon-btn-style')?.remove();
document.head.querySelector('#infographic-color-picker-style')?.remove();
document.head.querySelector('#infographic-edit-popover-style')?.remove();
document.head.querySelector('#infographic-font-family-list-style')?.remove();
document.head
.querySelector('#infographic-reset-viewbox-btn-style')
?.remove();
});

it('uses the custom container root for edit-bar child overlays', () => {
const { shadowRoot, svg } = createSvgInShadowRoot();
const overlayHost = document.createElement('div');
document.body.appendChild(overlayHost);

const text = createTextElement('Hello', {
width: '120',
height: '24',
fill: '#000000',
'font-family': 'Arial',
'font-size': '14',
'data-horizontal-align': 'LEFT',
'data-vertical-align': 'TOP',
});
text.setAttribute('data-element-type', ElementTypeEnum.ItemLabel);
svg.appendChild(text);

const plugin = new EditBar({ getContainer: overlayHost });
plugin.init({
emitter: { on: vi.fn(), off: vi.fn(), emit: vi.fn() } as any,
editor: { getDocument: () => svg } as any,
commander: { execute: vi.fn(), executeBatch: vi.fn() } as any,
plugin: {} as any,
state: {} as any,
});

const items = (plugin as any).getTextEditItems(text);

expect(items).toHaveLength(4);
expect(document.getElementById('infographic-edit-bar-icon-btn-style')).toBeTruthy();
expect(document.getElementById('infographic-color-picker-style')).toBeTruthy();
expect(document.getElementById('infographic-edit-popover-style')).toBeTruthy();
expect(document.getElementById('infographic-font-family-list-style')).toBeTruthy();
expect(
shadowRoot.querySelector('#infographic-edit-bar-icon-btn-style'),
).toBeNull();
expect(shadowRoot.querySelector('#infographic-color-picker-style')).toBeNull();
expect(
shadowRoot.querySelector('#infographic-edit-popover-style'),
).toBeNull();
expect(
shadowRoot.querySelector('#infographic-font-family-list-style'),
).toBeNull();

plugin.destroy();
});

it('defaults edit-bar child overlays to the svg shadow root', () => {
const { shadowRoot, svg } = createSvgInShadowRoot();

const text = createTextElement('Hello', {
width: '120',
height: '24',
fill: '#000000',
'font-family': 'Arial',
'font-size': '14',
'data-horizontal-align': 'LEFT',
'data-vertical-align': 'TOP',
});
text.setAttribute('data-element-type', ElementTypeEnum.ItemLabel);
svg.appendChild(text);

const plugin = new EditBar();
plugin.init({
emitter: { on: vi.fn(), off: vi.fn(), emit: vi.fn() } as any,
editor: { getDocument: () => svg } as any,
commander: { execute: vi.fn(), executeBatch: vi.fn() } as any,
plugin: {} as any,
state: {} as any,
});

const items = (plugin as any).getTextEditItems(text);

expect(items).toHaveLength(4);
expect(
shadowRoot.querySelector('#infographic-edit-bar-icon-btn-style'),
).toBeTruthy();
expect(
shadowRoot.querySelector('#infographic-color-picker-style'),
).toBeTruthy();
expect(
shadowRoot.querySelector('#infographic-edit-popover-style'),
).toBeTruthy();
expect(
shadowRoot.querySelector('#infographic-font-family-list-style'),
).toBeTruthy();
expect(
document.getElementById('infographic-edit-bar-icon-btn-style'),
).toBeNull();
expect(document.getElementById('infographic-color-picker-style')).toBeNull();
expect(document.getElementById('infographic-edit-popover-style')).toBeNull();
expect(document.getElementById('infographic-font-family-list-style')).toBeNull();

plugin.destroy();
});

it('injects reset-viewbox styles into the custom container root', () => {
const { shadowRoot, svg } = createSvgInShadowRoot();
const overlayHost = document.createElement('div');
document.body.appendChild(overlayHost);

const plugin = new ResetViewBox({ getContainer: overlayHost });
plugin.init({
emitter: { on: vi.fn(), off: vi.fn(), emit: vi.fn() } as any,
editor: {
getDocument: () => svg,
registerSync: vi.fn(() => vi.fn()),
} as any,
commander: { execute: vi.fn() } as any,
plugin: {} as any,
state: { getOptions: () => ({ padding: [0, 0, 0, 0] }) } as any,
});

(plugin as any).handleViewBoxChange('10 10 50 50');

const button = (plugin as any).resetButton as HTMLButtonElement | undefined;
expect(button).toBeTruthy();
expect(overlayHost.contains(button as HTMLButtonElement)).toBe(true);
expect(document.getElementById('infographic-reset-viewbox-btn-style')).toBeTruthy();
expect(
shadowRoot.querySelector('#infographic-reset-viewbox-btn-style'),
).toBeNull();

plugin.destroy();
});

it('defaults reset-viewbox button and styles to the svg shadow root', () => {
const { shadowRoot, svg } = createSvgInShadowRoot();

const plugin = new ResetViewBox();
plugin.init({
emitter: { on: vi.fn(), off: vi.fn(), emit: vi.fn() } as any,
editor: {
getDocument: () => svg,
registerSync: vi.fn(() => vi.fn()),
} as any,
commander: { execute: vi.fn() } as any,
plugin: {} as any,
state: { getOptions: () => ({ padding: [0, 0, 0, 0] }) } as any,
});

(plugin as any).handleViewBoxChange('10 10 50 50');

const button = (plugin as any).resetButton as HTMLButtonElement | undefined;
expect(button).toBeTruthy();
expect(shadowRoot.contains(button as HTMLButtonElement)).toBe(true);
expect(
shadowRoot.querySelector('#infographic-reset-viewbox-btn-style'),
).toBeTruthy();
expect(
document.getElementById('infographic-reset-viewbox-btn-style'),
).toBeNull();

plugin.destroy();
});
});
42 changes: 40 additions & 2 deletions __tests__/unit/utils/padding.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { parsePadding } from '../../../src/utils/padding';
import { describe, expect, it, vi } from 'vitest';

vi.mock('../../../src/utils/is-node', () => ({
isNode: false,
}));

import { parsePadding, setSVGPadding } from '../../../src/utils/padding';

describe('padding', () => {
describe('parsePadding', () => {
Expand Down Expand Up @@ -34,4 +39,37 @@ describe('padding', () => {
expect(parsePadding([1, 2, 3, 4, 5] as any)).toEqual([0, 0, 0, 0]);
});
});

describe('setSVGPadding', () => {
it('should compute viewBox for svg connected inside a ShadowRoot', () => {
const host = document.createElement('div');
document.body.appendChild(host);

const shadowRoot = host.attachShadow({ mode: 'open' });
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');

Object.defineProperty(svg, 'getBBox', {
value: () =>
({
x: 10,
y: 20,
width: 100,
height: 50,
}) as SVGRect,
});
Object.defineProperty(svg, 'getBoundingClientRect', {
value: () =>
({
width: 100,
height: 50,
}) as DOMRect,
});

shadowRoot.appendChild(svg);

setSVGPadding(svg, [10, 20, 30, 40]);

expect(svg.getAttribute('viewBox')).toBe('-30 10 160 90');
});
});
});
5 changes: 3 additions & 2 deletions src/editor/interactions/dblclick-edit-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function editText(text: TextElement, options?: EditTextOptions) {
const entity = getTextEntity(text);
if (!entity) return;

ensureEditorStyles();
ensureEditorStyles(entity);
new InlineTextEditor(entity, options).start();
}

Expand Down Expand Up @@ -240,7 +240,7 @@ class InlineTextEditor {
}
}

function ensureEditorStyles() {
function ensureEditorStyles(target?: Node) {
injectStyleOnce(
EDITOR_STYLE_ID,
`
Expand All @@ -256,5 +256,6 @@ function ensureEditorStyles() {
background-color: #b3d4fc;
}
`,
target,
);
}
Loading
Loading