2
0
mirror of https://github.com/iconify/iconify.git synced 2024-11-09 14:50:56 +00:00

React wrapper for web component: working component, tests and demo

This commit is contained in:
Vjacheslav Trushkin 2022-05-14 13:19:46 +03:00
parent 08974d4aa8
commit c03de0fb07
13 changed files with 844 additions and 3477 deletions

View File

@ -52,16 +52,16 @@ p {
color: #afafaf;
display: none;
}
.test-row-icons > .hidden {
.test-row-icons > iconify-icon.hidden {
display: none !important;
}
.test-row-icons > .visible {
.test-row-icons > iconify-icon.visible {
display: inline-block;
}
.test-row-icons > .success {
.test-row-icons > iconify-icon.success {
color: #327335;
}
.test-row-icons > .failed {
.test-row-icons > iconify-icon.failed {
color: #ba3329;
}

View File

@ -1,19 +1,21 @@
import { addIcon, addCollection, disableCache } from '@iconify-icon/react';
import playIcon from '@iconify-icons/mdi-light/play';
import chartIcon from '@iconify-icons/mdi-light/chart-histogram';
import { Checkbox } from './demo-components/Checkbox';
import { InlineDemo } from './demo-components/Inline';
import { OfflineUsageDemo } from './demo-components/UsageOffline';
import { FullUsageDemo } from './demo-components/UsageAPI';
import { TestIcon } from './test-components/TestIcon';
import './App.css';
// Disable cache
disableCache('all');
// Add few custom icons
addIcon('test:demo', playIcon);
addIcon('test:experiment2', {
addIcon('demo', chartIcon);
addIcon('experiment2', {
width: 16,
height: 16,
body: '<mask id="coffee-mask" x="0" y="0" width="16" height="16"><g fill="white"><path d="M5-2c0 2-2 2-2 4s2 2 2 4-2 2-2 4 2 2 2 4M8.5-2c0 2-2 2-2 4s2 2 2 4-2 2-2 4 2 2 2 4M12-2c0 2-2 2-2 4s2 2 2 4-2 2-2 4 2 2 2 4" stroke="white" stroke-width="1" fill="none"><animateMotion path="M0 0v-8" calcMode="linear" dur="3s" repeatCount="indefinite" /></path></g><rect y="4" width="16" height="12" fill="black" /><path d="M2 5H13C14.1046 5 15 5.89543 15 7V8C15 9.10457 14.1046 10 13 10H12V14C12 15.1046 11.1046 16 10 16H4C2.89543 16 2 15.1046 2 14V5Z" fill="white" /><path d="M12 6H13C13.5523 6 14 6.44772 14 7V8C14 8.55228 13.5523 9 13 9H12V6Z" fill="black" /></mask><rect mask="url(#coffee-mask)" width="16" height="16" fill="currentColor" />',
@ -57,6 +59,8 @@ function App() {
</section>
<InlineDemo />
<TestIcon />
</div>
);
}

View File

@ -7,18 +7,18 @@ export function InlineDemo() {
<h1>Inline demo</h1>
<div>
Block icon (behaving like image):
<Icon icon="test:experiment2" />
<Icon icon="experiment2" />
<Icon
icon="test:experiment2"
icon="experiment2"
inline={true}
style={{ verticalAlign: '0' }}
/>
</div>
<div>
Inline icon (behaving line text / icon font):
<Icon icon="test:experiment2" inline={true} />
<Icon icon="experiment2" inline={true} />
<Icon
icon="test:experiment2"
icon="experiment2"
style={{ verticalAlign: '-0.125em' }}
/>
</div>

View File

@ -8,9 +8,8 @@ export function OfflineUsageDemo() {
<section className="icon-24">
<h1>Usage (offline mode: using preloaded icons)</h1>
<div>
Icons referenced by name (as SVG, as SPAN):{' '}
<Icon icon="test:demo" />
<Icon icon="test:demo" mode="style" />
Icons referenced by name (as SVG, as SPAN): <Icon icon="demo" />
<Icon icon="demo" mode="style" />
</div>
<div>
Icons referenced by object (as SVG, as SPAN):{' '}

View File

@ -0,0 +1,146 @@
import React from 'react';
import { Icon } from '@iconify-icon/react';
import { Tests, toggleTest } from './Tests';
export function TestIcon() {
return (
<section className="tests">
<h1>Tests (offline module)</h1>
<h2>References</h2>
<div className="test-row">
<Tests id="offline-ref1" />
Getting reference
<Icon
icon="demo"
inline
ref={(element) => {
const key = 'offline-ref1';
if (element?.tagName.toLowerCase() === 'iconify-icon') {
toggleTest(key, 'success');
} else {
toggleTest(key, 'failed');
}
}}
/>
</div>
<h2>Style</h2>
<div className="test-row">
<Tests id="offline-style" />
Inline style for icon
<Icon
icon="demo"
style={{
color: '#1769aa',
fontSize: '24px',
lineHeight: '1em',
verticalAlign: '-0.25em',
}}
ref={(element) => {
const key = 'offline-style';
if (element) {
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 ? 'failed' : 'success');
} else {
toggleTest(key, 'failed');
}
}}
/>
</div>
<div className="test-row">
<Tests id="offline-color2" />
Green color from style:{' '}
<Icon
icon="demo"
style={{
color: 'green',
}}
ref={(element) => {
const key = 'offline-color2';
if (element) {
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 ? 'failed' : 'success');
} else {
toggleTest(key, 'failed');
}
}}
/>
</div>
<h2>Properties</h2>
<div className="test-row">
<Tests id="icon-rotate2" />
Rotation as number:{' '}
<Icon
icon="demo"
rotate={2}
inline
ref={(element) => {
const key = 'icon-rotate2';
if (element) {
toggleTest(
key,
element.getAttribute('rotate') !== '2'
? 'failed'
: 'success'
);
} else {
toggleTest(key, 'failed');
}
}}
/>
<Icon icon="demo" rotate="180deg" inline />
</div>
</section>
);
}

View File

@ -0,0 +1,64 @@
import React from 'react';
import { Icon } from '@iconify-icon/react';
import successIcon from '@iconify-icons/uil/check-circle';
import pendingIcon from '@iconify-icons/uil/question-circle';
import failedIcon from '@iconify-icons/uil/times-circle';
type StatusIcon = 'success' | 'failed' | 'pending';
interface TestIconsProps {
id: string;
icon?: StatusIcon;
}
export function Tests(props: TestIconsProps) {
const id = 'test-icons-' + props.id;
const icon = props.icon || 'pending';
return (
<span className="test-row-icons" id={id}>
<Icon
icon={successIcon}
inline={true}
className={
'success ' + (icon === 'success' ? 'visible' : 'hidden')
}
/>
<Icon
icon={pendingIcon}
inline={true}
className={
'pending ' + (icon === 'pending' ? 'visible' : 'hidden')
}
/>
<Icon
icon={failedIcon}
inline={true}
className={
'failed ' + (icon === 'failed' ? 'visible' : 'hidden')
}
/>
</span>
);
}
export function toggleTest(id: string, status: StatusIcon) {
const node = document.getElementById('test-icons-' + id);
if (!node) {
return;
}
// 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('.' + status);
if (toggle) {
toggle.classList.remove('hidden');
toggle.classList.add('visible');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
"clean": "rimraf lib dist tsconfig.tsbuildinfo",
"prebuild": "npm run clean",
"build": "tsup",
"test": "jest --runInBand"
"test": "vitest --config vitest.config.mjs"
},
"main": "dist/iconify.js",
"module": "dist/iconify.mjs",
@ -38,16 +38,16 @@
"iconify-icon": "^0.0.4"
},
"devDependencies": {
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@types/react": "^17.0.41",
"babel-jest": "^27.4.6",
"@types/react-test-renderer": "^18.0.0",
"@vitejs/plugin-react": "^1.3.2",
"jest": "^28.0.0-alpha.7",
"react": "^17.0.2",
"react-test-renderer": "^17.0.2",
"react": "^18.1.0",
"react-test-renderer": "^18.1.0",
"rimraf": "^3.0.2",
"tsup": "^5.12.7",
"typescript": "^4.6.2"
"typescript": "^4.6.2",
"vitest": "^0.12.6"
},
"peerDependencies": {
"react": "*"

View File

@ -1,5 +1,9 @@
import React from 'react';
import type { IconifyIcon, IconifyIconAttributes } from 'iconify-icon';
import type {
IconifyIcon,
IconifyIconAttributes,
IconifyIconHTMLElement,
} from 'iconify-icon';
/**
* Export types
@ -39,7 +43,11 @@ export type { IconifyIconBuildResult } from 'iconify-icon';
export type { IconifyBrowserCacheType } from 'iconify-icon';
// Component types
export type { IconifyIconAttributes, IconifyRenderMode } from 'iconify-icon';
export type {
IconifyIconAttributes,
IconifyRenderMode,
IconifyIconHTMLElement,
} from 'iconify-icon';
/**
* Export functions
@ -70,25 +78,37 @@ export interface IconifyIconProps
IconifyIconAttributes {
icon: string | IconifyIcon;
inline?: boolean;
rotate?: string | number;
}
/**
* React component
*/
export function Icon(props: IconifyIconProps) {
const newProps = {
...props,
};
export const Icon = React.forwardRef(
(
props: IconifyIconProps,
ref: React.ForwardedRef<IconifyIconHTMLElement>
) => {
const newProps: Record<string, unknown> = {
...props,
ref,
};
// Stringify icon
if (typeof props.icon === 'object') {
newProps.icon = JSON.stringify(props.icon);
// Stringify icon
if (typeof props.icon === 'object') {
newProps.icon = JSON.stringify(props.icon);
}
// Boolean
if (!props.inline) {
delete newProps.inline;
}
// React cannot handle className for web components
if (props.className) {
newProps['class'] = props.className;
}
return React.createElement('iconify-icon', newProps);
}
// Boolean
if (!props.inline) {
delete newProps.inline;
}
return React.createElement('iconify-icon', newProps);
}
);

View File

@ -0,0 +1,25 @@
import { describe, test, expect } from 'vitest';
import React from 'react';
import { Icon } from '../dist/iconify';
import { create } from 'react-test-renderer';
const iconData = {
body: '<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
width: 24,
height: 24,
};
describe('Creating component', () => {
test('basic icon', () => {
const component = create(<Icon icon={iconData} />);
const tree = component.toJSON();
expect(tree).toMatchObject({
type: 'iconify-icon',
props: {
icon: JSON.stringify(iconData),
},
children: null,
});
});
});

View File

@ -0,0 +1,27 @@
import { describe, test, expect } from 'vitest';
import React from 'react';
import { Icon } from '../dist/iconify';
import { create } from 'react-test-renderer';
const iconData = {
body: '<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
width: 24,
height: 24,
};
describe('Testing references', () => {
test('basic icon reference', () => {
let gotRef = false;
const component = create(
<Icon
icon={iconData}
ref={() => {
gotRef = true;
}}
/>
);
// Ref should have been called by now
expect(gotRef).toEqual(true);
});
});

View File

@ -8,6 +8,7 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "error",
"resolveJsonModule": true
"resolveJsonModule": true,
"jsx": "react-jsx"
}
}

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
watch: false,
include: ['**/tests/*-test.{ts,tsx}'],
transformMode: {
web: [/\.[jt]sx$/],
},
},
});