mirror of
https://github.com/iconify/iconify.git
synced 2024-11-09 23:00:56 +00:00
fix: better way to detect previously rendered child node in web component
This commit is contained in:
parent
feefd180bc
commit
e82c905ec8
@ -37,16 +37,22 @@ export function renderIcon(parent: Element | ShadowRoot, state: RenderedState) {
|
||||
}
|
||||
|
||||
// Set element
|
||||
// Assumes first node is a style node created with updateStyle()
|
||||
if (parent.childNodes.length > 1) {
|
||||
const lastChild = parent.lastChild as HTMLElement;
|
||||
if (node.tagName === 'SPAN' && lastChild.tagName === node.tagName) {
|
||||
const oldNode = Array.from(parent.childNodes).find((node) => {
|
||||
const tag =
|
||||
(node as HTMLElement).tagName &&
|
||||
(node as HTMLElement).tagName.toUpperCase();
|
||||
return tag === 'SPAN' || tag === 'SVG';
|
||||
}) as HTMLElement | undefined;
|
||||
if (oldNode) {
|
||||
// Replace old element
|
||||
if (node.tagName === 'SPAN' && oldNode.tagName === node.tagName) {
|
||||
// Swap style instead of whole node
|
||||
lastChild.setAttribute('style', node.getAttribute('style'));
|
||||
oldNode.setAttribute('style', node.getAttribute('style'));
|
||||
} else {
|
||||
parent.replaceChild(node, lastChild);
|
||||
parent.replaceChild(node, oldNode);
|
||||
}
|
||||
} else {
|
||||
// Add new element
|
||||
parent.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,27 @@
|
||||
/**
|
||||
* Attribute to add
|
||||
*/
|
||||
const nodeAttr = 'data-style';
|
||||
|
||||
/**
|
||||
* Add/update style node
|
||||
*/
|
||||
export function updateStyle(parent: Element | ShadowRoot, inline: boolean) {
|
||||
// Get node, create if needed
|
||||
let style = parent.firstChild;
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
parent.appendChild(style);
|
||||
let styleNode = Array.from(parent.childNodes).find(
|
||||
(node) =>
|
||||
(node as HTMLElement).hasAttribute &&
|
||||
(node as HTMLElement).hasAttribute(nodeAttr)
|
||||
) as HTMLElement | undefined;
|
||||
|
||||
if (!styleNode) {
|
||||
styleNode = document.createElement('style');
|
||||
styleNode.setAttribute(nodeAttr, nodeAttr);
|
||||
parent.appendChild(styleNode);
|
||||
}
|
||||
|
||||
// Update content
|
||||
style.textContent =
|
||||
styleNode.textContent =
|
||||
':host{display:inline-block;vertical-align:' +
|
||||
(inline ? '-0.125em' : '0') +
|
||||
'}span,svg{display:block}';
|
||||
|
@ -3,6 +3,13 @@ import { mockAPIModule, mockAPIData } from '@iconify/core/lib/api/modules/mock';
|
||||
import { addAPIProvider } from '@iconify/core/lib/api/config';
|
||||
import { setAPIModule } from '@iconify/core/lib/api/modules';
|
||||
|
||||
/**
|
||||
* <style> tag with extra attribute
|
||||
*
|
||||
* Attribute is used to allow developers inject custom styles, so there could be multiple style tags
|
||||
*/
|
||||
export const styleOpeningTag = '<style data-style="data-style">';
|
||||
|
||||
/**
|
||||
* Generate next prefix
|
||||
*/
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
fakeAPI,
|
||||
mockAPIData,
|
||||
awaitUntil,
|
||||
styleOpeningTag,
|
||||
} from '../src/tests/helpers';
|
||||
import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component';
|
||||
import type { IconState } from '../src/state';
|
||||
@ -44,7 +45,7 @@ describe('Testing icon component with API', () => {
|
||||
|
||||
// Should be empty
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.status).toBe('loading');
|
||||
|
||||
@ -78,7 +79,7 @@ describe('Testing icon component with API', () => {
|
||||
// Should not have sent query to API yet
|
||||
expect(sendQuery).toBeUndefined();
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.status).toBe('loading');
|
||||
|
||||
@ -97,7 +98,7 @@ describe('Testing icon component with API', () => {
|
||||
const blankSVG =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>';
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>${blankSVG}`
|
||||
`${styleOpeningTag}${expectedBlock}</style>${blankSVG}`
|
||||
);
|
||||
expect(node.status).toBe('rendered');
|
||||
});
|
||||
@ -121,7 +122,7 @@ describe('Testing icon component with API', () => {
|
||||
|
||||
// Should be empty
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.status).toBe('loading');
|
||||
|
||||
@ -147,7 +148,7 @@ describe('Testing icon component with API', () => {
|
||||
|
||||
// Should not have sent query to API yet
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.status).toBe('loading');
|
||||
|
||||
@ -157,7 +158,7 @@ describe('Testing icon component with API', () => {
|
||||
|
||||
// Should fail to render
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.status).toBe('failed');
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
expectedInline,
|
||||
setupDOM,
|
||||
nextTick,
|
||||
styleOpeningTag,
|
||||
} from '../src/tests/helpers';
|
||||
import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component';
|
||||
import type { IconState } from '../src/state';
|
||||
@ -73,7 +74,7 @@ describe('Testing icon component', () => {
|
||||
|
||||
// Should be empty
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.status).toBe('loading');
|
||||
|
||||
@ -96,7 +97,7 @@ describe('Testing icon component', () => {
|
||||
|
||||
// Should still be empty: waiting for next tick
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.status).toBe('loading');
|
||||
await nextTick();
|
||||
@ -105,7 +106,7 @@ describe('Testing icon component', () => {
|
||||
const blankSVG =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>';
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>${blankSVG}`
|
||||
`${styleOpeningTag}${expectedBlock}</style>${blankSVG}`
|
||||
);
|
||||
expect(node.status).toBe('rendered');
|
||||
|
||||
@ -119,7 +120,7 @@ describe('Testing icon component', () => {
|
||||
expect(node.getAttribute('inline')).toBe('true');
|
||||
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedInline}</style>${blankSVG}`
|
||||
`${styleOpeningTag}${expectedInline}</style>${blankSVG}`
|
||||
);
|
||||
expect(node.status).toBe('rendered');
|
||||
});
|
||||
@ -145,7 +146,7 @@ describe('Testing icon component', () => {
|
||||
|
||||
// Should be empty with block style
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
|
||||
// Check inline
|
||||
@ -157,7 +158,7 @@ describe('Testing icon component', () => {
|
||||
node.inline = true;
|
||||
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedInline}</style>`
|
||||
`${styleOpeningTag}${expectedInline}</style>`
|
||||
);
|
||||
expect(node.inline).toBe(true);
|
||||
expect(node.hasAttribute('inline')).toBe(true);
|
||||
@ -167,7 +168,7 @@ describe('Testing icon component', () => {
|
||||
node.removeAttribute('inline');
|
||||
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style>`
|
||||
`${styleOpeningTag}${expectedBlock}</style>`
|
||||
);
|
||||
expect(node.inline).toBe(false);
|
||||
expect(node.hasAttribute('inline')).toBe(false);
|
||||
@ -177,7 +178,7 @@ describe('Testing icon component', () => {
|
||||
node.setAttribute('inline', 'inline');
|
||||
|
||||
expect(node._shadowRoot.innerHTML).toBe(
|
||||
`<style>${expectedInline}</style>`
|
||||
`${styleOpeningTag}${expectedInline}</style>`
|
||||
);
|
||||
expect(node.inline).toBe(true);
|
||||
expect(node.hasAttribute('inline')).toBe(true);
|
||||
@ -229,7 +230,7 @@ describe('Testing icon component', () => {
|
||||
"<span style=\"--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Crect width='10' height='10'%3E%3Canimate attributeName='width' values='10;5;10' dur='10s' repeatCount='indefinite' /%3E%3C/rect%3E%3C!-- --%3E%3C/svg%3E"); width: 1em; height: 1em; background-color: transparent; background-repeat: no-repeat; background-size: 100% 100%;\"></span>";
|
||||
const html1 = node._shadowRoot.innerHTML;
|
||||
expect(html1.replace(/-- [0-9]+ --/, '-- --')).toBe(
|
||||
`<style>${expectedBlock}</style>${renderedIconWithComment}`
|
||||
`${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
|
||||
);
|
||||
|
||||
// Restart animation, test icon again
|
||||
@ -238,7 +239,7 @@ describe('Testing icon component', () => {
|
||||
const html2 = node._shadowRoot.innerHTML;
|
||||
expect(html2).not.toBe(html1);
|
||||
expect(html2.replace(/-- [0-9]+ --/, '-- --')).toBe(
|
||||
`<style>${expectedBlock}</style>${renderedIconWithComment}`
|
||||
`${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
|
||||
);
|
||||
expect(node.status).toBe('rendered');
|
||||
|
||||
@ -252,7 +253,7 @@ describe('Testing icon component', () => {
|
||||
|
||||
const html3 = node._shadowRoot.innerHTML;
|
||||
expect(html3.replace(/-- [0-9]+ --/, '-- --')).toBe(
|
||||
`<style>${expectedBlock}</style>${renderedIconWithComment}`
|
||||
`${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
|
||||
);
|
||||
expect(html3).not.toBe(html1);
|
||||
expect(html3).not.toBe(html2);
|
||||
@ -306,7 +307,9 @@ describe('Testing icon component', () => {
|
||||
const html1 = node._shadowRoot.innerHTML;
|
||||
const svg1 = node._shadowRoot.lastChild as SVGSVGElement;
|
||||
const setCurrentTimeSupported = !!svg1.setCurrentTime;
|
||||
expect(html1).toBe(`<style>${expectedBlock}</style>${renderedIcon}`);
|
||||
expect(html1).toBe(
|
||||
`${styleOpeningTag}${expectedBlock}</style>${renderedIcon}`
|
||||
);
|
||||
expect(svg1.outerHTML).toBe(renderedIcon);
|
||||
|
||||
// Restart animation, test icon again
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
expectedBlock,
|
||||
expectedInline,
|
||||
setupDOM,
|
||||
styleOpeningTag,
|
||||
} from '../src/tests/helpers';
|
||||
import { updateStyle } from '../src/render/style';
|
||||
import { renderIcon } from '../src/render/icon';
|
||||
@ -39,7 +40,7 @@ describe('Testing rendering loaded icon', () => {
|
||||
|
||||
// Test HTML
|
||||
expect(node.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>`
|
||||
`${styleOpeningTag}${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>`
|
||||
);
|
||||
|
||||
// Replace icon content
|
||||
@ -65,7 +66,7 @@ describe('Testing rendering loaded icon', () => {
|
||||
|
||||
// Test HTML
|
||||
expect(node.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g transform="rotate(90 12 12)"><g><path d=""></path></g></g></svg>`
|
||||
`${styleOpeningTag}${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g transform="rotate(90 12 12)"><g><path d=""></path></g></g></svg>`
|
||||
);
|
||||
});
|
||||
|
||||
@ -97,7 +98,7 @@ describe('Testing rendering loaded icon', () => {
|
||||
|
||||
// Test HTML
|
||||
expect(node.innerHTML).toBe(
|
||||
`<style>${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" preserveAspectRatio="xMidYMid meet"><g></g></svg>`
|
||||
`${styleOpeningTag}${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" preserveAspectRatio="xMidYMid meet"><g></g></svg>`
|
||||
);
|
||||
});
|
||||
|
||||
@ -128,7 +129,7 @@ describe('Testing rendering loaded icon', () => {
|
||||
|
||||
// Test HTML
|
||||
expect(node.innerHTML).toBe(
|
||||
`<style>${expectedInline}</style><span style="--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E"); width: 1em; height: 1em; background-color: currentColor; mask-image: var(--svg); mask-repeat: no-repeat; mask-size: 100% 100%;"></span>`
|
||||
`${styleOpeningTag}${expectedInline}</style><span style="--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E"); width: 1em; height: 1em; background-color: currentColor; mask-image: var(--svg); mask-repeat: no-repeat; mask-size: 100% 100%;"></span>`
|
||||
);
|
||||
|
||||
// Change mode to background, add some customisations
|
||||
@ -151,7 +152,7 @@ describe('Testing rendering loaded icon', () => {
|
||||
|
||||
// Test HTML
|
||||
expect(node.innerHTML).toBe(
|
||||
`<style>${expectedInline}</style><span style="--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E"); width: 24px; height: 24px; background-color: transparent; background-repeat: no-repeat; background-size: 100% 100%;"></span>`
|
||||
`${styleOpeningTag}${expectedInline}</style><span style="--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E"); width: 24px; height: 24px; background-color: transparent; background-repeat: no-repeat; background-size: 100% 100%;"></span>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
expectedBlock,
|
||||
expectedInline,
|
||||
setupDOM,
|
||||
styleOpeningTag,
|
||||
} from '../src/tests/helpers';
|
||||
|
||||
describe('Testing rendering style', () => {
|
||||
@ -18,18 +19,26 @@ describe('Testing rendering style', () => {
|
||||
|
||||
// Add style to empty parent
|
||||
updateStyle(node, false);
|
||||
expect(node.innerHTML).toBe('<style>' + expectedBlock + '</style>');
|
||||
expect(node.innerHTML).toBe(
|
||||
styleOpeningTag + expectedBlock + '</style>'
|
||||
);
|
||||
|
||||
// Change inline mode
|
||||
updateStyle(node, true);
|
||||
expect(node.innerHTML).toBe('<style>' + expectedInline + '</style>');
|
||||
expect(node.innerHTML).toBe(
|
||||
styleOpeningTag + expectedInline + '</style>'
|
||||
);
|
||||
|
||||
// Do not change anything
|
||||
updateStyle(node, true);
|
||||
expect(node.innerHTML).toBe('<style>' + expectedInline + '</style>');
|
||||
expect(node.innerHTML).toBe(
|
||||
styleOpeningTag + expectedInline + '</style>'
|
||||
);
|
||||
|
||||
// Change to block
|
||||
updateStyle(node, false);
|
||||
expect(node.innerHTML).toBe('<style>' + expectedBlock + '</style>');
|
||||
expect(node.innerHTML).toBe(
|
||||
styleOpeningTag + expectedBlock + '</style>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user