Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: inline svg component #583

Merged
merged 3 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Icon/mdi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export enum IconName {
mdiHumanFemale = 'M12,2A2,2 0 0,1 14,4A2,2 0 0,1 12,6A2,2 0 0,1 10,4A2,2 0 0,1 12,2M10.5,22V16H7.5L10.09,8.41C10.34,7.59 11.1,7 12,7C12.9,7 13.66,7.59 13.91,8.41L16.5,16H13.5V22H10.5Z',
mdiHumanMale = 'M12,2A2,2 0 0,1 14,4A2,2 0 0,1 12,6A2,2 0 0,1 10,4A2,2 0 0,1 12,2M10.5,7H13.5A2,2 0 0,1 15.5,9V14.5H14V22H10V14.5H8.5V9A2,2 0 0,1 10.5,7Z',
mdiImage = 'M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z',
mdiImageBroken = 'M19 3a2 2 0 0 1 2 2v6h-2v2h-2v2h-2v2h-2v2h-2v2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m2 12v4a2 2 0 0 1-2 2h-4v-2h2v-2h2v-2h2m-2-6.5a.5.5 0 0 0-.5-.5h-13a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .5.5H11v-1h2v-2h2v-2h2V9h2v-.5Z',
mdiImageEdit = 'M22.7 14.3L21.7 15.3L19.7 13.3L20.7 12.3C20.8 12.2 20.9 12.1 21.1 12.1C21.2 12.1 21.4 12.2 21.5 12.3L22.8 13.6C22.9 13.8 22.9 14.1 22.7 14.3M13 19.9V22H15.1L21.2 15.9L19.2 13.9L13 19.9M21 5C21 3.9 20.1 3 19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H11V19.1L12.1 18H5L8.5 13.5L11 16.5L14.5 12L16.1 14.1L21 9.1V5Z',
mdiImageEditOutline = 'M22.7 14.3L21.7 15.3L19.7 13.3L20.7 12.3C20.8 12.2 20.9 12.1 21.1 12.1C21.2 12.1 21.4 12.2 21.5 12.3L22.8 13.6C22.9 13.8 22.9 14.1 22.7 14.3M13 19.9V22H15.1L21.2 15.9L19.2 13.9L13 19.9M11.21 15.83L9.25 13.47L6.5 17H13.12L15.66 14.55L13.96 12.29L11.21 15.83M11 19.9V19.05L11.05 19H5V5H19V11.31L21 9.38V5C21 3.9 20.11 3 19 3H5C3.9 3 3 3.9 3 5V19C3 20.11 3.9 21 5 21H11V19.9Z',
mdiImageFilterNone = 'M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z',
Expand Down
48 changes: 48 additions & 0 deletions src/components/InlineSvg/InlineSvg.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { Stories } from '@storybook/addon-docs';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { InlineSvg } from './';
import { SkeletonVariant } from '../Skeleton/Skeleton.types';

export default {
title: 'InlineSvg',
parameters: {
docs: {
page: (): JSX.Element => (
<main>
<article>
<section>
<h1>InlineSvg</h1>
<p>
InlineSvg is used to render an SVG image inline, allowing it to
be styled using classes and css variables. This enables svgs to
be leveraged in a themable way, reacting to changes to css
variables.
</p>
</section>
<section>
<Stories includePrimary title="" />
</section>
</article>
</main>
),
},
},
argTypes: {},
} as ComponentMeta<typeof InlineSvg>;

const InlineSvg_Story: ComponentStory<typeof InlineSvg> = (args) => (
<InlineSvg {...args} />
);

export const Default = InlineSvg_Story.bind({});

Default.args = {
classNames: 'my-inline-svg',
height: '120px',
hideBrokenIcon: false,
showSkeleton: true,
skeletonVariant: SkeletonVariant.Rounded,
url: 'https://static.vscdn.net/images/learning-opp.svg',
width: '120px',
};
42 changes: 42 additions & 0 deletions src/components/InlineSvg/InlineSvg.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import '@testing-library/jest-dom/extend-expect';

import { render, waitFor } from '@testing-library/react';
import React from 'react';

import { InlineSvg } from './InlineSvg';

describe('InlineSvg', () => {
const url = 'https://static.vscdn.net/images/learning-opp.svg';
const width = '300px';
const height = '200px';

test('renders a skeleton while loading the SVG image if enabled', async () => {
const { container } = render(
<InlineSvg url={url} width={width} height={height} showSkeleton />
);
const skeleton = container.querySelector('.skeleton.wave.rounded');
expect(skeleton).toBeInTheDocument();
});

test('renders the SVG image', async () => {
const { container } = render(
<InlineSvg url={url} width={width} height={height} />
);

await waitFor(() => {
expect(container.querySelector('svg')).toBeInTheDocument();
});
});

test('renders an error icon when the SVG image fails to load', async () => {
const errorUrl = 'https://example.com/broken.svg';
const { container } = render(
<InlineSvg url={errorUrl} width={width} height={height} />
);
await waitFor(() => {
expect(
container.querySelector('.svg-display-error-icon')
).toBeInTheDocument();
});
});
});
96 changes: 96 additions & 0 deletions src/components/InlineSvg/InlineSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState, useEffect, useRef, FC, Ref } from 'react';
import { Icon, IconName } from '../Icon';
import { Skeleton, SkeletonVariant } from '../Skeleton';

import { InlineSvgProps } from './InlineSvg.types';

export const InlineSvg: FC<InlineSvgProps> = React.forwardRef(
(
{
classNames,
height,
hideBrokenIcon = false,
showSkeleton = false,
skeletonVariant = SkeletonVariant.Rounded,
url,
width,
...rest
},
ref: Ref<HTMLDivElement>
) => {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const svgRef = useRef<HTMLDivElement>(null);

useEffect(() => {
setIsLoading(true);
setHasError(false);
svgRef.current.innerHTML = '';

const fetchSvg = async (): Promise<void> => {
try {
const response = await fetch(url);
const text = await response.text();

const parser = new DOMParser();
const xml = parser.parseFromString(text, 'image/svg+xml');
const svg = xml.documentElement;

if (svg.nodeName !== 'svg') {
throw new Error(`Fetched document is not an SVG: ${url}`);
}

svgRef.current.innerHTML = text;
setIsLoading(false);
} catch (error) {
console.error(error);
setHasError(true);
setIsLoading(false);
}
};

fetchSvg();
}, [url]);

/**
* Provides a broken icon size for when the SVG doesn't work out.
* The size is the smaller of the width and height
*
* @returns {string} The size of the broken icon, as a string with the unit
*/
const getBrokenIconSize = () => {
if (!width || !height) {
return '24px';
}
if (!width) {
return height;
}
if (!height) {
return width;
}

const widthInt = parseInt(width, 10);
const heightInt = parseInt(height, 10);
const smaller = Math.min(widthInt, heightInt);
return `${smaller}px`;
};

return (
<div {...rest} ref={ref} className={classNames} style={{ width, height }}>
{isLoading && showSkeleton && (
<Skeleton width={width} height={height} variant={skeletonVariant} />
)}

{hasError && !hideBrokenIcon && (
<Icon
classNames="svg-display-error-icon"
path={IconName.mdiImageBroken}
role="presentation"
size={getBrokenIconSize()}
/>
)}
<div ref={svgRef} />
</div>
);
}
);
41 changes: 41 additions & 0 deletions src/components/InlineSvg/InlineSvg.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { OcBaseProps } from '../OcBase';
import { SkeletonVariant } from '../Skeleton';

export interface InlineSvgProps extends OcBaseProps<HTMLDivElement> {
/**
* Custom classnames of the component
*/
classNames?: string;
/**
* Height for SVG display.
*/
height?: string;
/**
* Indicates if broken icon should be explicitly hidden.
* If not enabled, the broken icon wil be displayed if
* svg loading fails for whatever reason.
* @default false
*/
hideBrokenIcon?: boolean;
/**
* Indicates if loading skeleton should be shown. If true,
* provided width and height will be used to determine size
* of the skeleton.
* @default false
*/
showSkeleton?: boolean;
/**
* Indicates the skeleton variant to be displayed while
* svg is loading.
* @default SkeletonVariant.Rounded
*/
skeletonVariant?: SkeletonVariant;
/**
* Url for the svg to be displayed.
*/
url: string;
/**
* Width for SVG display.
*/
width?: string;
}
2 changes: 2 additions & 0 deletions src/components/InlineSvg/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './InlineSvg.types';
export * from './InlineSvg';
4 changes: 4 additions & 0 deletions src/octuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import Grid, { Col, Row } from './components/Grid';

import { Icon, IconName, IconSize } from './components/Icon';

import { InlineSvgProps, InlineSvg } from './components/InlineSvg';

import { Label, LabelSize } from './components/Label';

import Layout from './components/Layout';
Expand Down Expand Up @@ -264,6 +266,8 @@ export {
IconSize,
InfoBar,
InfoBarType,
InlineSvgProps,
InlineSvg,
Label,
LabelAlign,
LabelPosition,
Expand Down