diff --git a/packages/svelte/src/Icon.svelte b/packages/svelte/src/Icon.svelte index ddcbfc7..15af2c9 100644 --- a/packages/svelte/src/Icon.svelte +++ b/packages/svelte/src/Icon.svelte @@ -107,8 +107,12 @@ export { }) -{#if data !== null} - - {@html data.body} - +{#if data} + {#if data.svg} + + {@html data.body} + + {:else} + + {/if} {/if} \ No newline at end of file diff --git a/packages/svelte/src/OfflineIcon.svelte b/packages/svelte/src/OfflineIcon.svelte index 0cf82f9..9bb5fb6 100644 --- a/packages/svelte/src/OfflineIcon.svelte +++ b/packages/svelte/src/OfflineIcon.svelte @@ -24,8 +24,12 @@ export { } -{#if data !== null} - - {@html data.body} - +{#if data} + {#if data.svg} + + {@html data.body} + + {:else} + + {/if} {/if} \ No newline at end of file diff --git a/packages/svelte/src/props.ts b/packages/svelte/src/props.ts index 7180f93..f64fe2b 100644 --- a/packages/svelte/src/props.ts +++ b/packages/svelte/src/props.ts @@ -3,6 +3,16 @@ import type { IconifyIconCustomisations as RawIconCustomisations } from '@iconif export { RawIconCustomisations }; +/** + * Icon render mode + * + * 'style' = 'bg' or 'mask', depending on icon content + * 'bg' = inline style using `background` + * 'mask' = inline style using `mask` + * 'inline' = inline SVG. + */ +export type IconifyRenderMode = 'style' | 'bg' | 'mask' | 'inline'; + // Allow rotation to be string /** * Icon customisations @@ -18,6 +28,9 @@ export interface IconifyIconProps extends IconifyIconCustomisations { // Icon object icon: IconifyIcon | string; + // Render mode + mode?: IconifyRenderMode; + // Style color?: string; diff --git a/packages/svelte/src/render.ts b/packages/svelte/src/render.ts index 5f1e163..fa7f4ec 100644 --- a/packages/svelte/src/render.ts +++ b/packages/svelte/src/render.ts @@ -10,7 +10,9 @@ import { import { rotateFromString } from '@iconify/utils/lib/customisations/rotate'; import { iconToSVG } from '@iconify/utils/lib/svg/build'; import { replaceIDs } from '@iconify/utils/lib/svg/id'; -import type { IconProps } from './props'; +import { iconToHTML } from '@iconify/utils/lib/svg/html'; +import { svgToURL } from '@iconify/utils/lib/svg/url'; +import type { IconProps, IconifyRenderMode } from './props'; /** * Default SVG attributes @@ -22,13 +24,53 @@ const svgDefaults = { 'role': 'img', }; +/** + * Style modes + */ + +const commonProps: Record = { + display: 'inline-block', +}; + +const monotoneProps: Record = { + 'background-color': 'currentColor', +}; + +const coloredProps: Record = { + 'background-color': 'transparent', +}; + +// Dynamically add common props to variables above +const propsToAdd: Record = { + image: 'var(--svg)', + repeat: 'no-repeat', + size: '100% 100%', +}; +const propsToAddTo: Record> = { + '-webkit-mask': monotoneProps, + 'mask': monotoneProps, + 'background': coloredProps, +}; +for (const prefix in propsToAddTo) { + const list = propsToAddTo[prefix]; + for (const prop in propsToAdd) { + list[prefix + '-' + prop] = propsToAdd[prop]; + } +} + /** * Result */ -export interface RenderResult { +interface RenderSVGResult { + svg: true; attributes: Record; body: string; } +interface RenderSPANResult { + svg: false; + attributes: Record; +} +export type RenderResult = RenderSVGResult | RenderSPANResult; /** * Generate icon from properties @@ -43,7 +85,12 @@ export function render( defaults, props as typeof defaults ); - const componentProps = { ...svgDefaults } as Record; + + // Check mode + const mode: IconifyRenderMode = props.mode || 'inline'; + const componentProps = ( + mode === 'inline' ? { ...svgDefaults } : {} + ) as Record; // Create style if missing let style = typeof props.style === 'string' ? props.style : ''; @@ -59,6 +106,7 @@ export function render( case 'icon': case 'style': case 'onLoad': + case 'mode': break; // Boolean attributes @@ -126,37 +174,72 @@ export function render( // Generate icon const item = iconToSVG(icon, customisations); + const renderAttribs = item.attributes; - // Add icon stuff - for (let key in item.attributes) { - componentProps[key] = - item.attributes[key as keyof typeof item.attributes]; - } - + // Inline mode if (item.inline) { // Style overrides it style = 'vertical-align: -0.125em; ' + style; } - // Style - if (style !== '') { - componentProps.style = style; + if (mode === 'inline') { + // Add icon stuff + Object.assign(componentProps, renderAttribs); + + // Style + if (style !== '') { + componentProps.style = style; + } + + // Counter for ids based on "id" property to render icons consistently on server and client + let localCounter = 0; + let id = props.id; + if (typeof id === 'string') { + // Convert '-' to '_' to avoid errors in animations + id = id.replace(/-/g, '_'); + } + + // Generate HTML + return { + svg: true, + attributes: componentProps, + body: replaceIDs( + item.body, + id ? () => id + 'ID' + localCounter++ : 'iconifySvelte' + ), + }; } - // Counter for ids based on "id" property to render icons consistently on server and client - let localCounter = 0; - let id = props.id; - if (typeof id === 'string') { - // Convert '-' to '_' to avoid errors in animations - id = id.replace(/-/g, '_'); + const { body, width, height } = icon; + const useMask = + mode === 'mask' || + (mode === 'bg' ? false : body.indexOf('currentColor') !== -1); + + // Generate SVG + const html = iconToHTML(body, { + ...renderAttribs, + width: width + '', + height: height + '', + }); + + // Generate style + const url = svgToURL(html); + const styles: Record = { + '--svg': url, + 'width': renderAttribs.width, + 'height': renderAttribs.height, + ...commonProps, + ...(useMask ? monotoneProps : coloredProps), + }; + + let customStyle = ''; + for (const key in styles) { + customStyle += key + ': ' + styles[key] + ';'; } - // Generate HTML + componentProps.style = customStyle + style; return { + svg: false, attributes: componentProps, - body: replaceIDs( - item.body, - id ? () => id + 'ID' + localCounter++ : 'iconifySvelte' - ), }; } diff --git a/packages/svelte/tests/iconify/10-style-mode.test.ts b/packages/svelte/tests/iconify/10-style-mode.test.ts new file mode 100644 index 0000000..0ffd720 --- /dev/null +++ b/packages/svelte/tests/iconify/10-style-mode.test.ts @@ -0,0 +1,35 @@ +/** + * @jest-environment jsdom + */ +import { render } from '@testing-library/svelte'; +import Icon from '../../dist'; + +const iconData = { + body: '', + width: 24, + height: 24, +}; + +describe('Rendering as span', () => { + test('basic icon', () => { + const component = render(Icon, { + icon: iconData, + mode: 'style', + onLoad: () => { + // Should be called only for icons loaded from API + throw new Error('onLoad called for object!'); + }, + }); + const node = component.container.querySelector( + 'span' + ) as HTMLSpanElement; + expect(node).not.toBeNull(); + expect(node.parentNode).not.toBeNull(); + const html = node.outerHTML; + + // Check HTML + expect(html).toBe( + "" + ); + }); +});