From 95ff16aee9bd4a601480ca67db7b43358ee6c95c Mon Sep 17 00:00:00 2001 From: Vjacheslav Trushkin Date: Fri, 20 May 2022 19:40:17 +0300 Subject: [PATCH] Add viewBox and preserveAspectRatio properties to web component --- iconify-icon/icon/README.md | 4 +- iconify-icon/icon/package-lock.json | 4 +- iconify-icon/icon/package.json | 2 +- .../icon/src/attributes/customisations.ts | 28 +++++++-- iconify-icon/icon/src/attributes/types.ts | 16 ++++- iconify-icon/icon/src/render/icon.ts | 37 +++++++++++- .../icon/tests/customisations-test.ts | 55 +++++++++++++++++ iconify-icon/icon/tests/render-icon-test.ts | 59 +++++++++++++++++++ iconify-icon/solid/src/iconify.tsx | 14 ++++- 9 files changed, 203 insertions(+), 16 deletions(-) diff --git a/iconify-icon/icon/README.md b/iconify-icon/icon/README.md index c5caca8..569fac5 100644 --- a/iconify-icon/icon/README.md +++ b/iconify-icon/icon/README.md @@ -21,13 +21,13 @@ Iconify Icon web component renders icons. Add this line to your page to load IconifyIcon (you can add it to `` section of the page or before ``): ```html - + ``` or ```html - + ``` or, if you are building a project with something like WebPack or Rollup, you can include the script by installing `iconify-icon` as a dependency and importing it in your project: diff --git a/iconify-icon/icon/package-lock.json b/iconify-icon/icon/package-lock.json index 5db950a..b443f60 100644 --- a/iconify-icon/icon/package-lock.json +++ b/iconify-icon/icon/package-lock.json @@ -1,12 +1,12 @@ { "name": "iconify-icon", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "iconify-icon", - "version": "0.0.4", + "version": "0.0.5", "license": "MIT", "dependencies": { "@iconify/types": "^1.1.0" diff --git a/iconify-icon/icon/package.json b/iconify-icon/icon/package.json index a80d00f..03dcdfd 100644 --- a/iconify-icon/icon/package.json +++ b/iconify-icon/icon/package.json @@ -2,7 +2,7 @@ "name": "iconify-icon", "description": "Icon web component that loads icon data on demand. Over 100,000 icons to choose from", "author": "Vjacheslav Trushkin (https://iconify.design)", - "version": "0.0.4", + "version": "0.0.5", "license": "MIT", "main": "./dist/iconify-icon.cjs", "types": "./dist/iconify-icon.d.ts", diff --git a/iconify-icon/icon/src/attributes/customisations.ts b/iconify-icon/icon/src/attributes/customisations.ts index 710e11a..aabc7bb 100644 --- a/iconify-icon/icon/src/attributes/customisations.ts +++ b/iconify-icon/icon/src/attributes/customisations.ts @@ -2,16 +2,25 @@ import type { FullIconCustomisations } from '@iconify/utils/lib/customisations'; import { defaults } from '@iconify/utils/lib/customisations'; import { rotateFromString } from '@iconify/utils/lib/customisations/rotate'; import { flipFromString } from '@iconify/utils/lib/customisations/flip'; - -// Remove 'inline' from defaults -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const { inline, ...defaultCustomisations } = defaults; -export { defaultCustomisations }; +import type { IconifyIconSVGAttributes } from './types'; /** * Customisations that affect rendering */ -export type RenderedIconCustomisations = Omit; +export type RenderedIconCustomisations = Omit< + FullIconCustomisations, + 'inline' +> & + IconifyIconSVGAttributes; + +// Remove 'inline' from defaults +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { inline, ...defaultCustomisations } = { + ...defaults, + viewBox: '', + preserveAspectRatio: '', +} as IconifyIconSVGAttributes & FullIconCustomisations; +export { defaultCustomisations }; /** * Get customisations @@ -34,6 +43,13 @@ export function getCustomisations(node: Element): RenderedIconCustomisations { // Flip flipFromString(customisations, attr('flip', '')); + // SVG attributes + customisations.viewBox = attr('viewBox', attr('viewbox', '')); + customisations.preserveAspectRatio = attr( + 'preserveAspectRatio', + attr('preserveaspectratio', '') + ); + return customisations; } diff --git a/iconify-icon/icon/src/attributes/types.ts b/iconify-icon/icon/src/attributes/types.ts index 8471863..f57988e 100644 --- a/iconify-icon/icon/src/attributes/types.ts +++ b/iconify-icon/icon/src/attributes/types.ts @@ -1,5 +1,13 @@ import type { IconifyIcon } from '@iconify/types'; +/** + * SVG attributes that can be overwritten + */ +export interface IconifyIconSVGAttributes { + viewBox: string; + preserveAspectRatio: string; +} + /** * Icon render modes * @@ -33,7 +41,8 @@ export type IconifyIconCustomisationProperties = { * All properties */ export interface IconifyIconProperties - extends IconifyIconCustomisationProperties { + extends IconifyIconCustomisationProperties, + Partial { // Icon to render: name, object or serialised object icon: string | IconifyIcon; @@ -49,8 +58,9 @@ export interface IconifyIconProperties */ export interface IconifyIconAttributes extends Partial< - Record, string> - > { + Record, string> + >, + Partial { // Icon to render: name or serialised object icon: string; diff --git a/iconify-icon/icon/src/render/icon.ts b/iconify-icon/icon/src/render/icon.ts index 7e5b98e..dedebd6 100644 --- a/iconify-icon/icon/src/render/icon.ts +++ b/iconify-icon/icon/src/render/icon.ts @@ -1,18 +1,53 @@ +import type { IconifyIcon } from '@iconify/types'; import { iconToSVG } from '@iconify/utils/lib/svg/build'; import type { RenderedState } from '../state'; import { renderSPAN } from './span'; import { renderSVG } from './svg'; +// viewBox properties order +const viewBoxProps: (keyof IconifyIcon)[] = ['left', 'top', 'width', 'height']; + /** * Render icon */ export function renderIcon(parent: Element | ShadowRoot, state: RenderedState) { + let iconData = state.icon.data; + const customisations = state.customisations; + + // Custom viewBox + const viewBox = customisations.viewBox; + if (viewBox) { + const parts = viewBox.split(/\s+/); + const customisedViewBox: Partial = {}; + let failed = false; + if (parts.length === 4) { + for (let i = 0; i < 4; i++) { + const num = parseFloat(parts[i]); + if (isNaN(num)) { + failed = true; + } else { + customisedViewBox[viewBoxProps[i] as 'left'] = num; + } + } + + if (!failed) { + iconData = { + ...iconData, + ...customisedViewBox, + }; + } + } + } + // Render icon - const iconData = state.icon.data; const renderData = iconToSVG(iconData, { ...state.customisations, inline: state.inline, }); + if (customisations.preserveAspectRatio) { + renderData.attributes['preserveAspectRatio'] = + customisations.preserveAspectRatio; + } const mode = state.renderedMode; let node: Element; diff --git a/iconify-icon/icon/tests/customisations-test.ts b/iconify-icon/icon/tests/customisations-test.ts index 6462e95..dfff5ac 100644 --- a/iconify-icon/icon/tests/customisations-test.ts +++ b/iconify-icon/icon/tests/customisations-test.ts @@ -66,6 +66,61 @@ describe('Testing customisations', () => { }); expect(haveCustomisationsChanged(test3, test2)).toBe(true); expect(haveCustomisationsChanged(test1, test3)).toBe(true); + expect(haveCustomisationsChanged(test3, emptyCustomisations)).toBe( + true + ); expect(getInline(testNode)).toBe(false); + + // viewBox + node.innerHTML = ''; + testNode = node.lastChild as HTMLSpanElement; + + const test4 = getCustomisations(testNode); + expect(test4).toEqual({ + ...defaultCustomisations, + viewBox: '0 0 24 24', + }); + expect(haveCustomisationsChanged(test4, emptyCustomisations)).toBe( + true + ); + + node.innerHTML = ''; + testNode = node.lastChild as HTMLSpanElement; + + const test5 = getCustomisations(testNode); + expect(test5).toEqual({ + ...defaultCustomisations, + viewBox: '0 0 32 32', + }); + expect(haveCustomisationsChanged(test5, test4)).toBe(true); + expect(haveCustomisationsChanged(test5, emptyCustomisations)).toBe( + true + ); + + // preserveAspectRatio + node.innerHTML = ''; + testNode = node.lastChild as HTMLSpanElement; + + const test6 = getCustomisations(testNode); + expect(test6).toEqual({ + ...defaultCustomisations, + preserveAspectRatio: 'xMidYMid meet', + }); + expect(haveCustomisationsChanged(test6, emptyCustomisations)).toBe( + true + ); + + node.innerHTML = ''; + testNode = node.lastChild as HTMLSpanElement; + + const test7 = getCustomisations(testNode); + expect(test7).toEqual({ + ...defaultCustomisations, + preserveAspectRatio: 'xMidYMin slice', + }); + expect(haveCustomisationsChanged(test7, emptyCustomisations)).toBe( + true + ); + expect(haveCustomisationsChanged(test7, test6)).toBe(true); }); }); diff --git a/iconify-icon/icon/tests/render-icon-test.ts b/iconify-icon/icon/tests/render-icon-test.ts index 13e8e90..da2994d 100644 --- a/iconify-icon/icon/tests/render-icon-test.ts +++ b/iconify-icon/icon/tests/render-icon-test.ts @@ -69,6 +69,65 @@ describe('Testing rendering loaded icon', () => { ); }); + it('SVG with custom attributes', () => { + // Setup DOM + const doc = setupDOM('').window.document; + + // Create container node and add style + const node = doc.createElement('div'); + updateStyle(node, false); + + // Render SVG + renderIcon(node, { + rendered: true, + icon: { + value: 'whatever', + data: { + ...iconDefaults, + body: '', + }, + }, + renderedMode: 'svg', + inline: false, + customisations: { + ...defaultCustomisations, + viewBox: '0 0 48 24', + preserveAspectRatio: 'xMidYMid meet', + }, + }); + + // Test HTML + expect(node.innerHTML).toBe( + `` + ); + + // Replace icon content + renderIcon(node, { + rendered: true, + icon: { + value: 'whatever', + data: { + ...iconDefaults, + width: 24, + height: 24, + body: '', + }, + }, + renderedMode: 'svg', + inline: false, + customisations: { + ...defaultCustomisations, + rotate: 1, + height: 'auto', + }, + }); + + // Test HTML + expect(node.innerHTML).toBe( + `` + ); + }); + it('Render as SPAN', () => { // Setup DOM const doc = setupDOM('').window.document; diff --git a/iconify-icon/solid/src/iconify.tsx b/iconify-icon/solid/src/iconify.tsx index c8f5c26..02c36e5 100644 --- a/iconify-icon/solid/src/iconify.tsx +++ b/iconify-icon/solid/src/iconify.tsx @@ -88,7 +88,17 @@ export interface IconifyIconProps * Solid component */ export function Icon(props: IconifyIconProps): JSX.Element { - let { icon, mode, inline, rotate, flip, width, height } = props; + let { + icon, + mode, + inline, + rotate, + flip, + width, + height, + viewBox, + preserveAspectRatio, + } = props; // Convert icon to string if (typeof icon === 'object') { @@ -105,6 +115,8 @@ export function Icon(props: IconifyIconProps): JSX.Element { attr:flip={flip} attr:width={width} attr:height={height} + attr:viewBox={viewBox} + attr:preserveAspectRatio={preserveAspectRatio} {...props} /> );