From 3f73f4f145babf687364a3de0ba67a8e8158b8f7 Mon Sep 17 00:00:00 2001 From: Vjacheslav Trushkin Date: Sat, 24 Apr 2021 14:49:37 +0300 Subject: [PATCH] Refactoring React: full component but without API support --- packages/react-demo/src/App.css | 18 +- packages/react-demo/src/App.js | 64 ++-- packages/react-demo/src/components/Inline.jsx | 27 -- .../react-demo/src/components/TestIcon.jsx | 29 -- .../src/components/TestsOffline.jsx | 285 -------------- .../Checkbox.jsx | 0 .../react-demo/src/demo-components/Inline.jsx | 36 ++ .../src/demo-components/UsageFullOffline.jsx | 26 ++ .../UsageOffline.jsx | 7 +- .../src/test-components/TestIcons.jsx | 78 ++++ .../src/test-components/TestsFullOffline.jsx | 212 ++++++++++ .../src/test-components/TestsOffline.jsx | 212 ++++++++++ packages/react/package-lock.json | 1 + packages/react/package.json | 1 + packages/react/src/iconify.ts | 361 ++++++++++++++---- packages/react/tests/iconify/10-basic.test.js | 62 +++ packages/react/tests/iconify/10-empty.test.js | 57 +++ .../react/tests/iconify/20-attributes.test.js | 88 +++++ .../react/tests/iconify/20-dimensions.test.js | 42 ++ packages/react/tests/iconify/20-ids.test.js | 57 +++ .../react/tests/iconify/20-inline.test.js | 31 ++ .../tests/iconify/20-transformations.test.js | 135 +++++++ packages/react/tests/offline/10-empty.test.js | 9 +- .../react/tests/offline/20-inline.test.js | 2 +- .../tests/offline/20-transformations.test.js | 2 +- 25 files changed, 1389 insertions(+), 453 deletions(-) delete mode 100644 packages/react-demo/src/components/Inline.jsx delete mode 100644 packages/react-demo/src/components/TestIcon.jsx delete mode 100644 packages/react-demo/src/components/TestsOffline.jsx rename packages/react-demo/src/{components => demo-components}/Checkbox.jsx (100%) create mode 100644 packages/react-demo/src/demo-components/Inline.jsx create mode 100644 packages/react-demo/src/demo-components/UsageFullOffline.jsx rename packages/react-demo/src/{components => demo-components}/UsageOffline.jsx (83%) create mode 100644 packages/react-demo/src/test-components/TestIcons.jsx create mode 100644 packages/react-demo/src/test-components/TestsFullOffline.jsx create mode 100644 packages/react-demo/src/test-components/TestsOffline.jsx create mode 100644 packages/react/tests/iconify/10-basic.test.js create mode 100644 packages/react/tests/iconify/10-empty.test.js create mode 100644 packages/react/tests/iconify/20-attributes.test.js create mode 100644 packages/react/tests/iconify/20-dimensions.test.js create mode 100644 packages/react/tests/iconify/20-ids.test.js create mode 100644 packages/react/tests/iconify/20-inline.test.js create mode 100644 packages/react/tests/iconify/20-transformations.test.js diff --git a/packages/react-demo/src/App.css b/packages/react-demo/src/App.css index 90ea6b3..669f729 100644 --- a/packages/react-demo/src/App.css +++ b/packages/react-demo/src/App.css @@ -45,18 +45,22 @@ p { font-size: 16px; line-height: 1.5; } -.test-row > svg { - color: #afafaf; +.test-row-icons { + padding-right: 4px; } -.test-row > svg.success { +.test-row-icons > svg { + color: #afafaf; + display: none; +} +.test-row-icons > svg.visible { + display: inline-block; +} +.test-row-icons > svg.success { color: #327335; } -.test-row > svg.fail { +.test-row-icons > svg.failed { color: #ba3329; } -.test-row > span { - padding-left: 4px; -} /* 24px icon */ .icon-24 svg { diff --git a/packages/react-demo/src/App.js b/packages/react-demo/src/App.js index 872a55c..a263c4a 100644 --- a/packages/react-demo/src/App.js +++ b/packages/react-demo/src/App.js @@ -1,44 +1,47 @@ import React from 'react'; import { - Icon, - InlineIcon, - addIcon, - addCollection, + addIcon as addOfflineIcon, + addCollection as addOfflineCollection, } from '@iconify/react/dist/offline'; -import alertIcon from '@iconify-icons/mdi-light/alert'; +import { + addIcon as addOnlineIcon, + addCollection as addOnlineCollection, +} from '@iconify/react/dist/iconify'; import presentationPlay from '@iconify-icons/mdi-light/presentation-play'; -import checkedIcon from '@iconify-icons/uil/check-circle'; -import uncheckedIcon from '@iconify-icons/uil/circle'; +import playIcon from '@iconify-icons/mdi-light/play'; -import { Checkbox } from './components/Checkbox'; -import { InlineDemo } from './components/Inline'; -import { OfflineUsageDemo } from './components/UsageOffline'; -import { TestsOffline } from './components/TestsOffline'; +import { Checkbox } from './demo-components/Checkbox'; +import { InlineDemo } from './demo-components/Inline'; +import { OfflineUsageDemo } from './demo-components/UsageOffline'; +import { FullOfflineUsageDemo } from './demo-components/UsageFullOffline'; +import { TestsOffline } from './test-components/TestsOffline'; +import { TestsFullOffline } from './test-components/TestsFullOffline'; import './App.css'; -// Add 'mdi-light:presentation-play' as 'demo' -addIcon('demo', presentationPlay); +// Add 'mdi-light:presentation-play' as 'demo' for offline module +addOfflineIcon('demo', presentationPlay); + +// Add 'mdi-light:play' as 'demo' for full module +addOnlineIcon('demo', playIcon); // Add custom icon as 'experiment' -addIcon('experiment2', { +addOfflineIcon('experiment2', { width: 16, height: 16, body: '', }); - -// Add icon with id: noto:robot -addIcon('noto-robot', { +addOnlineIcon('experiment2', { + width: 16, + height: 16, body: - '', - width: 128, - height: 128, + '', }); // Add few mdi-light: icons -addCollection({ - prefix: 'mdi-light', +addOfflineCollection({ + prefix: 'offline-mdi-light', icons: { 'account-alert': { body: @@ -52,11 +55,27 @@ addCollection({ width: 24, height: 24, }); +addOnlineCollection({ + prefix: '', + icons: { + alert1: { + body: + '', + }, + link1: { + body: + '', + }, + }, + width: 24, + height: 24, +}); function App() { return (
+

Checkbox

@@ -77,6 +96,7 @@ function App() { +
); } diff --git a/packages/react-demo/src/components/Inline.jsx b/packages/react-demo/src/components/Inline.jsx deleted file mode 100644 index fffa0c3..0000000 --- a/packages/react-demo/src/components/Inline.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { Icon } from '@iconify/react/dist/offline'; - -export function InlineDemo() { - return ( -
-

Inline demo

-
- Block icon (behaving like image): - -
-
- Inline icon (behaving line text / icon font): - -
-
- Using "vertical-align: 0" to override inline attribute: - - -
-
- ); -} diff --git a/packages/react-demo/src/components/TestIcon.jsx b/packages/react-demo/src/components/TestIcon.jsx deleted file mode 100644 index 18021ad..0000000 --- a/packages/react-demo/src/components/TestIcon.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { InlineIcon } from '@iconify/react/dist/offline'; -import successIcon from '@iconify-icons/uil/check-circle'; -import pendingIcon from '@iconify-icons/uil/question-circle'; -import failedIcon from '@iconify-icons/uil/times-circle'; - -export function TestIcon(props) { - let icon = pendingIcon; - let className = ''; - - switch (props.status) { - case 'success': - case 'default-success': - case true: - icon = successIcon; - className = 'success'; - break; - - case 'fail': - case false: - icon = failedIcon; - className = 'fail'; - break; - - default: - } - - return ; -} diff --git a/packages/react-demo/src/components/TestsOffline.jsx b/packages/react-demo/src/components/TestsOffline.jsx deleted file mode 100644 index 4f59012..0000000 --- a/packages/react-demo/src/components/TestsOffline.jsx +++ /dev/null @@ -1,285 +0,0 @@ -import React from 'react'; -import { InlineIcon } from '@iconify/react/dist/offline'; -import { TestIcon } from './TestIcon'; - -export class TestsOffline extends React.Component { - constructor(props) { - super(); - - const state = { - ref_missing: 'default-success', - }; - this.state = state; - } - - render() { - const state = this.state; - const success = this._toggle.bind(this, 'success'); - const fail = this._toggle.bind(this, 'fail'); - return ( -
-

Tests (offline module)

- -

References

- -
- - - Getting reference - { - const key = 'ref'; - if (element && element.tagName === 'svg') { - success(key); - } else { - fail(key); - } - }} - /> - -
- -
- - - Getting reference for empty icon - { - // Cannot be called because there is no SVG to render! - fail('ref_missing'); - }} - /> - -
- -
- - - Getting reference for missing icon with fallback text{' '} - { - // Cannot be called because there is no SVG to render! - fail('ref_missing'); - }} - > - 😀 - - -
- -

Style

- -
- - - Inline style for icon - { - const key = 'style'; - if (element && element.tagName === 'svg') { - let errors = false; - - // Get style - const style = element.style; - - switch (style.color.toLowerCase()) { - case 'rgb(23, 105, 170)': - case '#1769aa': - break; - - default: - console.log( - 'Invalid color:', - style.color - ); - errors = true; - } - - if (style.fontSize !== '24px') { - console.log( - 'Invalid font-size:', - style.fontSize - ); - errors = true; - } - - if (style.verticalAlign !== '-0.25em') { - console.log( - 'Invalid vertical-align:', - style.verticalAlign - ); - errors = true; - } - - if (errors) { - fail(key); - } else { - success(key); - } - } else { - fail(key); - } - }} - /> - -
- -
- - - Green color from attribute:{' '} - { - const key = 'color1'; - if (element && element.tagName === 'svg') { - let errors = false; - - // Get style - const style = element.style; - - switch (style.color.toLowerCase()) { - case 'rgb(0, 128, 0)': - case '#008000': - case 'green': - break; - - default: - console.log( - 'Invalid color:', - style.color - ); - errors = true; - } - - if (errors) { - fail(key); - } else { - success(key); - } - } else { - fail(key); - } - }} - /> - -
- -
- - - Green color from style:{' '} - { - const key = 'color2'; - if (element && element.tagName === 'svg') { - let errors = false; - - // Get style - const style = element.style; - - switch (style.color.toLowerCase()) { - case 'rgb(0, 128, 0)': - case '#008000': - case 'green': - break; - - default: - console.log( - 'Invalid color:', - style.color - ); - errors = true; - } - - if (errors) { - fail(key); - } else { - success(key); - } - } else { - fail(key); - } - }} - /> - -
- -
- - - Green color from attribute (overrides style) + red from - style:{' '} - { - const key = 'color3'; - if (element && element.tagName === 'svg') { - let errors = false; - - // Get style - const style = element.style; - - switch (style.color.toLowerCase()) { - case 'rgb(0, 128, 0)': - case '#008000': - case 'green': - break; - - default: - console.log( - 'Invalid color:', - style.color - ); - errors = true; - } - - if (errors) { - fail(key); - } else { - success(key); - } - } else { - fail(key); - } - }} - /> - -
-
- ); - } - - _toggle(value, key) { - setTimeout(() => { - const oldValue = this.state[key]; - if ( - oldValue === value || - oldValue === 'success' || - oldValue === 'fail' - ) { - return; - } - this.setState({ - [key]: value, - }); - }); - } -} diff --git a/packages/react-demo/src/components/Checkbox.jsx b/packages/react-demo/src/demo-components/Checkbox.jsx similarity index 100% rename from packages/react-demo/src/components/Checkbox.jsx rename to packages/react-demo/src/demo-components/Checkbox.jsx diff --git a/packages/react-demo/src/demo-components/Inline.jsx b/packages/react-demo/src/demo-components/Inline.jsx new file mode 100644 index 0000000..d4d2638 --- /dev/null +++ b/packages/react-demo/src/demo-components/Inline.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Icon as OfflineIcon } from '@iconify/react/dist/offline'; +import { Icon as FullIcon } from '@iconify/react/dist/iconify'; + +export function InlineDemo() { + return ( +
+

Inline demo

+
+ Block icon (behaving like image): + + +
+
+ Inline icon (behaving line text / icon font): + + +
+
+ Using "vertical-align: 0" to override inline attribute: + + + + +
+
+ ); +} diff --git a/packages/react-demo/src/demo-components/UsageFullOffline.jsx b/packages/react-demo/src/demo-components/UsageFullOffline.jsx new file mode 100644 index 0000000..238da75 --- /dev/null +++ b/packages/react-demo/src/demo-components/UsageFullOffline.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Icon } from '@iconify/react/dist/iconify'; +import accountIcon from '@iconify-icons/mdi-light/account'; +import alertIcon from '@iconify-icons/mdi-light/alert'; + +export function FullOfflineUsageDemo() { + return ( +
+

Usage (full module, offline mode)

+
+ Icon referenced by name: +
+
+ Icon referenced by object: +
+
+ 2 icons imported from icon set: + +
+
+ + Important notice with alert icon! +
+
+ ); +} diff --git a/packages/react-demo/src/components/UsageOffline.jsx b/packages/react-demo/src/demo-components/UsageOffline.jsx similarity index 83% rename from packages/react-demo/src/components/UsageOffline.jsx rename to packages/react-demo/src/demo-components/UsageOffline.jsx index 3381809..a255ee8 100644 --- a/packages/react-demo/src/components/UsageOffline.jsx +++ b/packages/react-demo/src/demo-components/UsageOffline.jsx @@ -7,9 +7,6 @@ export function OfflineUsageDemo() { return (

Usage (offline module)

-
- Empty icon: -
Icon referenced by name:
@@ -18,8 +15,8 @@ export function OfflineUsageDemo() {
2 icons imported from icon set:{' '} - - + +
diff --git a/packages/react-demo/src/test-components/TestIcons.jsx b/packages/react-demo/src/test-components/TestIcons.jsx new file mode 100644 index 0000000..42922df --- /dev/null +++ b/packages/react-demo/src/test-components/TestIcons.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { InlineIcon } from '@iconify/react/dist/offline'; +import successIcon from '@iconify-icons/uil/check-circle'; +import pendingIcon from '@iconify-icons/uil/question-circle'; +import failedIcon from '@iconify-icons/uil/times-circle'; + +function getStatus(status) { + switch (status) { + case 'success': + case 'default-success': + case true: + return 'success'; + + case 'failed': + case 'fail': + case false: + return 'failed'; + + default: + return 'pending'; + } +} + +export function TestIcons(props) { + if (!props.id) { + return null; + } + + const id = 'test-icons-' + props.id; + const icon = getStatus(props.status); + + return ( + + + + + + ); +} + +export function toggleTest(id, status) { + const node = document.getElementById('test-icons-' + id); + if (!node) { + return; + } + + // Get icon to show + const icon = getStatus(status); + + // Remove previous status + const visible = node.querySelector('.visible'); + if (visible) { + visible.classList.remove('visible'); + visible.classList.add('hidden'); + } + + // Show new icon + const toggle = node.querySelector('.' + icon); + if (toggle) { + toggle.classList.remove('hidden'); + toggle.classList.add('visible'); + } +} diff --git a/packages/react-demo/src/test-components/TestsFullOffline.jsx b/packages/react-demo/src/test-components/TestsFullOffline.jsx new file mode 100644 index 0000000..4019488 --- /dev/null +++ b/packages/react-demo/src/test-components/TestsFullOffline.jsx @@ -0,0 +1,212 @@ +import React from 'react'; +import { InlineIcon } from '@iconify/react/dist/iconify'; +import { TestIcons, toggleTest } from './TestIcons'; + +export function TestsFullOffline() { + return ( +
+

Tests (full module, without API)

+ +

References

+ +
+ + Getting reference + { + const key = 'full-offline-ref1'; + if (element && element.tagName === 'svg') { + toggleTest(key, 'success'); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Getting reference for empty icon + { + // Cannot be called because there is no SVG to render! + toggleTest('full-offline-ref-missing', 'failed'); + }} + /> +
+ +
+ + Getting reference for missing icon with fallback text{' '} + { + // Cannot be called because there is no SVG to render! + toggleTest('full-offline-ref-missing2', 'failed'); + }} + > + 😀 + +
+ +

Style

+ +
+ + Inline style for icon + { + const key = 'full-offline-style'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(23, 105, 170)': + case '#1769aa': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + if (style.fontSize !== '24px') { + console.log( + 'Invalid font-size:', + style.fontSize + ); + errors = true; + } + + if (style.verticalAlign !== '-0.25em') { + console.log( + 'Invalid vertical-align:', + style.verticalAlign + ); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from attribute:{' '} + { + const key = 'full-offline-color1'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from style:{' '} + { + const key = 'full-offline-color2'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from attribute (overrides style) + red from style:{' '} + { + const key = 'full-offline-color3'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+
+ ); +} diff --git a/packages/react-demo/src/test-components/TestsOffline.jsx b/packages/react-demo/src/test-components/TestsOffline.jsx new file mode 100644 index 0000000..3e18a10 --- /dev/null +++ b/packages/react-demo/src/test-components/TestsOffline.jsx @@ -0,0 +1,212 @@ +import React from 'react'; +import { InlineIcon } from '@iconify/react/dist/offline'; +import { TestIcons, toggleTest } from './TestIcons'; + +export function TestsOffline() { + return ( +
+

Tests (offline module)

+ +

References

+ +
+ + Getting reference + { + const key = 'offline-ref1'; + if (element && element.tagName === 'svg') { + toggleTest(key, 'success'); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Getting reference for empty icon + { + // Cannot be called because there is no SVG to render! + toggleTest('offline-ref-missing', 'failed'); + }} + /> +
+ +
+ + Getting reference for missing icon with fallback text{' '} + { + // Cannot be called because there is no SVG to render! + toggleTest('offline-ref-missing2', 'failed'); + }} + > + 😀 + +
+ +

Style

+ +
+ + Inline style for icon + { + const key = 'offline-style'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(23, 105, 170)': + case '#1769aa': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + if (style.fontSize !== '24px') { + console.log( + 'Invalid font-size:', + style.fontSize + ); + errors = true; + } + + if (style.verticalAlign !== '-0.25em') { + console.log( + 'Invalid vertical-align:', + style.verticalAlign + ); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from attribute:{' '} + { + const key = 'offline-color1'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from style:{' '} + { + const key = 'offline-color2'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from attribute (overrides style) + red from style:{' '} + { + const key = 'offline-color3'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+
+ ); +} diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index b6dec3b..f9a56c3 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -12,6 +12,7 @@ "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", "@iconify/core": "^1.0.0-rc.4", + "@iconify/types": "^1.0.6", "@microsoft/api-extractor": "^7.13.5", "@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-commonjs": "^18.0.0", diff --git a/packages/react/package.json b/packages/react/package.json index 4f78c2d..6c31d6b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -27,6 +27,7 @@ "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", "@iconify/core": "^1.0.0-rc.4", + "@iconify/types": "^1.0.6", "@microsoft/api-extractor": "^7.13.5", "@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-commonjs": "^18.0.0", diff --git a/packages/react/src/iconify.ts b/packages/react/src/iconify.ts index 6f17685..06e4c6a 100644 --- a/packages/react/src/iconify.ts +++ b/packages/react/src/iconify.ts @@ -1,40 +1,293 @@ import React from 'react'; -import type { IconifyIcon, IconifyJSON } from '@iconify/types'; +import type { IconifyJSON } from '@iconify/types'; + +// Core +import { stringToIcon } from '@iconify/core/lib/icon/name'; +import type { IconifyIconName } from '@iconify/core/lib/icon/name'; import type { + IconifyIconSize, IconifyHorizontalIconAlignment, IconifyVerticalIconAlignment, - IconifyIconSize, } from '@iconify/core/lib/customisations'; -import { fullIcon } from '@iconify/core/lib/icon'; -import { parseIconSet } from '@iconify/core/lib/icon/icon-set'; +import { + IconifyStorageFunctions, + storageFunctions, + getIconData, + allowSimpleNames, +} from '@iconify/core/lib/storage/functions'; +import { + IconifyBuilderFunctions, + builderFunctions, +} from '@iconify/core/lib/builder/functions'; +import { fullIcon, IconifyIcon } from '@iconify/core/lib/icon'; + +// Modules +import { coreModules } from '@iconify/core/lib/modules'; + +// API +import { API, IconifyAPIInternalStorage } from '@iconify/core/lib/api/'; +import { + IconifyAPIFunctions, + IconifyAPIInternalFunctions, + APIFunctions, + APIInternalFunctions, +} from '@iconify/core/lib/api/functions'; +import { + setAPIModule, + IconifyAPIModule, + IconifyAPISendQuery, + IconifyAPIPrepareQuery, + GetIconifyAPIModule, +} from '@iconify/core/lib/api/modules'; +import { getAPIModule as getJSONPAPIModule } from '@iconify/core/lib/api/modules/jsonp'; +import { + getAPIModule as getFetchAPIModule, + setFetch, +} from '@iconify/core/lib/api/modules/fetch'; +import { + setAPIConfig, + PartialIconifyAPIConfig, + IconifyAPIConfig, + getAPIConfig, + GetAPIConfig, +} from '@iconify/core/lib/api/config'; +import type { + IconifyIconLoaderCallback, + IconifyIconLoaderAbort, +} from '@iconify/core/lib/interfaces/loader'; + +// Cache +import { storeCache, loadCache } from '@iconify/core/lib/browser-storage'; +import { toggleBrowserCache } from '@iconify/core/lib/browser-storage/functions'; +import type { + IconifyBrowserCacheType, + IconifyBrowserCacheFunctions, +} from '@iconify/core/lib/browser-storage/functions'; + +// Properties import type { IconifyIconCustomisations, IconifyIconProps, IconProps, IconRef, } from './props'; + +// Render SVG import { render } from './render'; /** - * Export stuff from props.ts - */ -export { IconifyIconCustomisations, IconifyIconProps, IconProps }; - -/** - * Export types that could be used in component + * Export required types */ +// Function sets export { - IconifyIcon, - IconifyJSON, - IconifyHorizontalIconAlignment, - IconifyVerticalIconAlignment, - IconifyIconSize, + IconifyStorageFunctions, + IconifyBuilderFunctions, + IconifyBrowserCacheFunctions, + IconifyAPIFunctions, + IconifyAPIInternalFunctions, }; +// JSON stuff +export { IconifyIcon, IconifyJSON, IconifyIconName }; + +// Customisations +export { + IconifyIconCustomisations, + IconifyIconSize, + IconifyHorizontalIconAlignment, + IconifyVerticalIconAlignment, + IconifyIconProps, + IconProps, +}; + +// API +export { + IconifyAPIConfig, + IconifyIconLoaderCallback, + IconifyIconLoaderAbort, + IconifyAPIInternalStorage, + IconifyAPIModule, + GetAPIConfig, + IconifyAPIPrepareQuery, + IconifyAPISendQuery, +}; + +/* Browser cache */ +export { IconifyBrowserCacheType }; + /** - * Storage for icons referred by name + * Enable and disable browser cache + */ +export const enableCache = (storage: IconifyBrowserCacheType) => + toggleBrowserCache(storage, true); + +export const disableCache = (storage: IconifyBrowserCacheType) => + toggleBrowserCache(storage, false); + +/* Storage functions */ +/** + * Check if icon exists + */ +export const iconExists = storageFunctions.iconExists; + +/** + * Get icon data + */ +export const getIcon = storageFunctions.getIcon; + +/** + * List available icons + */ +export const listIcons = storageFunctions.listIcons; + +/** + * Add one icon + */ +export const addIcon = storageFunctions.addIcon; + +/** + * Add icon set + */ +export const addCollection = storageFunctions.addCollection; + +/* Builder functions */ +/** + * Calculate icon size + */ +export const calculateSize = builderFunctions.calculateSize; + +/** + * Replace unique ids in content + */ +export const replaceIDs = builderFunctions.replaceIDs; + +/* API functions */ +/** + * Load icons + */ +export const loadIcons = APIFunctions.loadIcons; + +/** + * Add API provider + */ +export const addAPIProvider = APIFunctions.addAPIProvider; + +/** + * Export internal functions that can be used by third party implementations + */ +export const _api = APIInternalFunctions; + +/** + * Initialise stuff + */ +// Enable short names +allowSimpleNames(true); + +// Set API +coreModules.api = API; + +let getAPIModule: GetIconifyAPIModule; +try { + getAPIModule = + typeof fetch === 'function' && typeof Promise === 'function' + ? getFetchAPIModule + : getJSONPAPIModule; +} catch (err) { + getAPIModule = getJSONPAPIModule; +} +setAPIModule('', getAPIModule(getAPIConfig)); + +/** + * Enable node-fetch for getting icons on server side + */ +export function setNodeFetch(nodeFetch: typeof fetch) { + setFetch(nodeFetch); + if (getAPIModule !== getFetchAPIModule) { + getAPIModule = getFetchAPIModule; + setAPIModule('', getAPIModule(getAPIConfig)); + } +} + +/** + * Browser stuff + */ +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + // Set cache and load existing cache + coreModules.cache = storeCache; + loadCache(); + + const _window = window; + + // Load icons from global "IconifyPreload" + interface WindowWithIconifyPreload { + IconifyPreload: IconifyJSON[] | IconifyJSON; + } + if ( + ((_window as unknown) as WindowWithIconifyPreload).IconifyPreload !== + void 0 + ) { + const preload = ((_window as unknown) as WindowWithIconifyPreload) + .IconifyPreload; + const err = 'Invalid IconifyPreload syntax.'; + if (typeof preload === 'object' && preload !== null) { + (preload instanceof Array ? preload : [preload]).forEach((item) => { + try { + if ( + // Check if item is an object and not null/array + typeof item !== 'object' || + item === null || + item instanceof Array || + // Check for 'icons' and 'prefix' + typeof item.icons !== 'object' || + typeof item.prefix !== 'string' || + // Add icon set + !addCollection(item) + ) { + console.error(err); + } + } catch (e) { + console.error(err); + } + }); + } + } + + // Set API from global "IconifyProviders" + interface WindowWithIconifyProviders { + IconifyProviders: Record; + } + if ( + ((_window as unknown) as WindowWithIconifyProviders) + .IconifyProviders !== void 0 + ) { + const providers = ((_window as unknown) as WindowWithIconifyProviders) + .IconifyProviders; + if (typeof providers === 'object' && providers !== null) { + for (let key in providers) { + const err = 'IconifyProviders[' + key + '] is invalid.'; + try { + const value = providers[key]; + if ( + typeof value !== 'object' || + !value || + value.resources === void 0 + ) { + continue; + } + if (!setAPIConfig(key, value)) { + console.error(err); + } + } catch (e) { + console.error(err); + } + } + } + } +} + +/** + * Component */ -const storage: Record> = Object.create(null); /** * Generate icon @@ -44,27 +297,32 @@ function component( inline: boolean, ref?: IconRef ): JSX.Element { - // Split properties - const icon = - typeof props.icon === 'string' - ? storage[props.icon] - : typeof props.icon === 'object' - ? fullIcon(props.icon) - : null; + const icon = props.icon; - // Validate icon object - if ( - icon === null || - typeof icon !== 'object' || - typeof icon.body !== 'string' - ) { - return props.children - ? (props.children as JSX.Element) - : React.createElement('span', {}); + // Check if icon is an object + if (typeof icon === 'object' && typeof icon.body === 'string') { + return render(fullIcon(icon), props, inline, ref); } - // Valid icon: render it - return render(icon, props, inline, ref); + // Check if icon is a string + if (typeof icon === 'string') { + const iconName = stringToIcon(icon, true, true); + if (iconName) { + // Valid icon name + const iconData = getIconData(iconName); + if (iconData) { + // Icon is available + return render(iconData, props, inline, ref); + } + + // TODO: icon is missing + } + } + + // Error + return props.children + ? (props.children as JSX.Element) + : React.createElement('span', {}); } /** @@ -89,36 +347,3 @@ export const Icon: Component = React.forwardRef( export const InlineIcon: Component = React.forwardRef( (props: IconProps, ref?: IconRef) => component(props, true, ref) ); - -/** - * Add icon to storage, allowing to call it by name - * - * @param name - * @param data - */ -export function addIcon(name: string, data: IconifyIcon): void { - storage[name] = fullIcon(data); -} - -/** - * Add collection to storage, allowing to call icons by name - * - * @param data Icon set - * @param prefix Optional prefix to add to icon names, true if prefix from icon set should be used. - */ -export function addCollection( - data: IconifyJSON, - prefix?: string | boolean -): void { - const iconPrefix: string = - typeof prefix === 'string' - ? prefix - : prefix !== false && typeof data.prefix === 'string' - ? data.prefix + ':' - : ''; - parseIconSet(data, (name, icon) => { - if (icon !== null) { - storage[iconPrefix + name] = icon; - } - }); -} diff --git a/packages/react/tests/iconify/10-basic.test.js b/packages/react/tests/iconify/10-basic.test.js new file mode 100644 index 0000000..beaec32 --- /dev/null +++ b/packages/react/tests/iconify/10-basic.test.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { Icon, InlineIcon } from '../../dist/iconify'; +import renderer from 'react-test-renderer'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Creating component using object', () => { + test('basic icon', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': {}, + 'dangerouslySetInnerHTML': { + __html: iconData.body, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': '0 0 ' + iconData.width + ' ' + iconData.height, + }, + children: null, + }); + }); + + test('inline icon', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': { + verticalAlign: '-0.125em', + }, + 'dangerouslySetInnerHTML': { + __html: iconData.body, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': '0 0 ' + iconData.width + ' ' + iconData.height, + }, + children: null, + }); + }); +}); diff --git a/packages/react/tests/iconify/10-empty.test.js b/packages/react/tests/iconify/10-empty.test.js new file mode 100644 index 0000000..6216f70 --- /dev/null +++ b/packages/react/tests/iconify/10-empty.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Icon } from '../../dist/iconify'; +import renderer from 'react-test-renderer'; + +describe('Empty icon', () => { + test('basic test', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'span', + props: {}, + children: null, + }); + }); + + test('with child node', () => { + const component = renderer.create( + + + + ); + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'i', + props: {}, + children: null, + }); + }); + + test('with text child node', () => { + const component = renderer.create(icon); + const tree = component.toJSON(); + + expect(tree).toMatch('icon'); + }); + + test('with multiple childen', () => { + const component = renderer.create( + + + Home + + ); + const tree = component.toJSON(); + + expect(tree).toMatchObject([ + { + type: 'i', + props: {}, + children: null, + }, + 'Home', + ]); + }); +}); diff --git a/packages/react/tests/iconify/20-attributes.test.js b/packages/react/tests/iconify/20-attributes.test.js new file mode 100644 index 0000000..f8fec54 --- /dev/null +++ b/packages/react/tests/iconify/20-attributes.test.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { Icon, InlineIcon } from '../../dist/iconify'; +import renderer from 'react-test-renderer'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Passing attributes', () => { + test('title', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.title).toStrictEqual('Icon!'); + }); + + test('aria-hidden', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props['aria-hidden']).toStrictEqual(void 0); + }); + + test('ariaHidden', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props['aria-hidden']).toStrictEqual(void 0); + }); + + test('style', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.style).toMatchObject({ + verticalAlign: '0', + color: 'red', + }); + }); + + test('color', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree.props.style).toMatchObject({ + color: 'red', + }); + }); + + test('color with style', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.style).toMatchObject({ + color: 'red', + }); + }); + + test('attributes that cannot change', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.viewBox).toStrictEqual('0 0 24 24'); + expect(tree.props.preserveAspectRatio).toStrictEqual('xMidYMid meet'); + }); +}); diff --git a/packages/react/tests/iconify/20-dimensions.test.js b/packages/react/tests/iconify/20-dimensions.test.js new file mode 100644 index 0000000..887ddf9 --- /dev/null +++ b/packages/react/tests/iconify/20-dimensions.test.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { InlineIcon } from '../../dist/iconify'; +import renderer from 'react-test-renderer'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Dimensions', () => { + test('height', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.height).toStrictEqual('48'); + expect(tree.props.width).toStrictEqual('48'); + }); + + test('width and height', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.height).toStrictEqual('48'); + expect(tree.props.width).toStrictEqual('32'); + }); + + test('auto', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.height).toStrictEqual('24'); + expect(tree.props.width).toStrictEqual('24'); + }); +}); diff --git a/packages/react/tests/iconify/20-ids.test.js b/packages/react/tests/iconify/20-ids.test.js new file mode 100644 index 0000000..22100a0 --- /dev/null +++ b/packages/react/tests/iconify/20-ids.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Icon } from '../../dist/iconify'; +import renderer from 'react-test-renderer'; + +const iconDataWithID = { + body: + '', + width: 128, + height: 128, +}; + +describe('Replacing IDs', () => { + test('default behavior', () => { + const component = renderer.create(); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconDataWithID.body); + }); + + test('custom generator', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + // Generate expected body + let expected = iconDataWithID.body; + const replacements = { + 'ssvg-id-1st-place-medala': 'test-0', + 'ssvg-id-1st-place-medald': 'test-1', + 'ssvg-id-1st-place-medalf': 'test-2', + 'ssvg-id-1st-place-medalh': 'test-3', + 'ssvg-id-1st-place-medalj': 'test-4', + 'ssvg-id-1st-place-medalm': 'test-5', + 'ssvg-id-1st-place-medalp': 'test-6', + 'ssvg-id-1st-place-medalb': 'test-7', + 'ssvg-id-1st-place-medalk': 'test-8', + 'ssvg-id-1st-place-medalo': 'test-9', + 'ssvg-id-1st-place-medalc': 'test-10', + 'ssvg-id-1st-place-medale': 'test-11', + 'ssvg-id-1st-place-medalg': 'test-12', + 'ssvg-id-1st-place-medali': 'test-13', + 'ssvg-id-1st-place-medall': 'test-14', + 'ssvg-id-1st-place-medaln': 'test-15', + }; + Object.keys(replacements).forEach((search) => { + expected = expected.replace( + new RegExp(search, 'g'), + replacements[search] + ); + }); + + expect(body).toStrictEqual(expected); + }); +}); diff --git a/packages/react/tests/iconify/20-inline.test.js b/packages/react/tests/iconify/20-inline.test.js new file mode 100644 index 0000000..9d62fd9 --- /dev/null +++ b/packages/react/tests/iconify/20-inline.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { Icon } from '../../dist/iconify'; +import renderer from 'react-test-renderer'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Inline attribute', () => { + test('string', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.style.verticalAlign).toStrictEqual('-0.125em'); + }); + + test('false string', () => { + // "false" = true + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.style.verticalAlign).toStrictEqual('-0.125em'); + }); +}); diff --git a/packages/react/tests/iconify/20-transformations.test.js b/packages/react/tests/iconify/20-transformations.test.js new file mode 100644 index 0000000..020d924 --- /dev/null +++ b/packages/react/tests/iconify/20-transformations.test.js @@ -0,0 +1,135 @@ +import React from 'react'; +import { InlineIcon } from '../../dist/iconify'; +import renderer from 'react-test-renderer'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Rotation', () => { + test('number', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + expect(body).toMatch('rotate(90 '); + }); + + test('string', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + expect(body).toMatch('rotate(180 '); + }); +}); + +describe('Flip', () => { + test('boolean', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + expect(body).toMatch('scale(-1 1)'); + }); + + test('string', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + expect(body).toMatch('scale(1 -1)'); + }); + + test('string and boolean', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + // horizontal + vertical = 180deg rotation + expect(body).toMatch('rotate(180 '); + }); + + test('string for boolean attribute', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + expect(body).toMatch('scale(-1 1)'); + }); + + test('shorthand and boolean', () => { + // 'flip' is processed after 'hFlip', overwriting value + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + expect(body).toMatch('scale(-1 1)'); + }); + + test('shorthand and boolean as string', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toStrictEqual(iconData.body); + // horizontal + vertical = 180deg rotation + expect(body).toMatch('rotate(180 '); + }); + + test('wrong case', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + const body = tree.props.dangerouslySetInnerHTML.__html; + + expect(body).not.toMatch('scale('); + }); +}); + +describe('Alignment and slice', () => { + test('vAlign and slice', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.preserveAspectRatio).toStrictEqual('xMidYMin slice'); + }); + + test('string', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + + expect(tree.props.preserveAspectRatio).toStrictEqual('xMinYMax meet'); + }); +}); diff --git a/packages/react/tests/offline/10-empty.test.js b/packages/react/tests/offline/10-empty.test.js index a683dcc..5dd2b83 100644 --- a/packages/react/tests/offline/10-empty.test.js +++ b/packages/react/tests/offline/10-empty.test.js @@ -1,14 +1,7 @@ import React from 'react'; -import { Icon, InlineIcon } from '../../dist/offline'; +import { Icon } from '../../dist/offline'; import renderer from 'react-test-renderer'; -const iconData = { - body: - '', - width: 24, - height: 24, -}; - describe('Empty icon', () => { test('basic test', () => { const component = renderer.create(); diff --git a/packages/react/tests/offline/20-inline.test.js b/packages/react/tests/offline/20-inline.test.js index add910a..a476fa3 100644 --- a/packages/react/tests/offline/20-inline.test.js +++ b/packages/react/tests/offline/20-inline.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Icon, InlineIcon } from '../../dist/offline'; +import { Icon } from '../../dist/offline'; import renderer from 'react-test-renderer'; const iconData = { diff --git a/packages/react/tests/offline/20-transformations.test.js b/packages/react/tests/offline/20-transformations.test.js index 40e4bda..f55d85f 100644 --- a/packages/react/tests/offline/20-transformations.test.js +++ b/packages/react/tests/offline/20-transformations.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Icon, InlineIcon } from '../../dist/offline'; +import { InlineIcon } from '../../dist/offline'; import renderer from 'react-test-renderer'; const iconData = {