Skip to content

Commit

Permalink
feat: icon component path element array, children support
Browse files Browse the repository at this point in the history
  • Loading branch information
dkonieczek committed Oct 5, 2023
1 parent 717339a commit c8a6c44
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ export default {
description: 'SVG path',
table: {
type: {
summary: 'string',
summary: 'string | SVGPathElement[]',
},
},
control: { type: 'text' },
},
children: {
description: 'SVG elements to be contained within (using children)',
table: {
type: {
summary: 'string, JSX',
},
},
},
color: {
description: 'Icon color',
table: {
Expand All @@ -70,7 +78,7 @@ export default {
description: 'Icon height. Overwrites size.',
table: {
type: {
summary: 'string',
summary: 'string | number',
},
},
control: { type: 'text' },
Expand All @@ -79,7 +87,7 @@ export default {
description: 'Icon width. Overwrites size.',
table: {
type: {
summary: 'string',
summary: 'string | number',
},
},
control: { type: 'text' },
Expand Down Expand Up @@ -118,6 +126,75 @@ CustomPath.args = {
viewBox: '0 0 70 70',
};

export const Children = (props: IconProps): JSX.Element => (
<Icon {...props}>
<line x1="1" y1="10" x2="69" y2="10" stroke="#000000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></line>
<line x1="1" y1="30" x2="69" y2="30" stroke="#000000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></line>
<circle cx="15" cy="10" r="6" fill="#000000" stroke="#000000" stroke-width="3"></circle>
<circle cx="55" cy="30" r="6" fill="#000000" stroke="#000000" stroke-width="3"></circle>
</Icon>
);
Children.args = {
size: '70px',
viewBox: '0 0 70 70',
};

export const ArrayPath = (props: IconProps): JSX.Element => <Icon {...props} />;
ArrayPath.args = {
path: [
{
type: 'line',
attributes: {
x1: '1',
y1: '10',
x2: '69',
y2: '10',
stroke: '#000000',
'stroke-width': '3',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
type: 'line',
attributes: {
x1: '1',
y1: '30',
x2: '69',
y2: '30',
stroke: '#000000',
'stroke-width': '3',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
type: 'circle',
attributes: {
cx: '15',
cy: '10',
r: '6',
fill: '#000000',
stroke: '#000000',
'stroke-width': '3',
},
},
{
type: 'circle',
attributes: {
cx: '55',
cy: '30',
r: '6',
fill: '#000000',
stroke: '#000000',
'stroke-width': '3',
},
},
],
size: '70',
viewBox: '0 0 70 70',
};

export const Gallery = (): JSX.Element => {
return (
<div style='display: flex; flex-wrap: wrap; font-family: "Nunito Sans",-apple-system,".SFNSText-Regular","San Francisco",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif; font-size: 10px;'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,180 @@ describe('Icon Component', () => {
expect(path).toHaveAttribute('d', svgPath);
});

it('renders path array of elements', () => {
const line1 = {
type: 'line',
attributes: {
x1: '1',
y1: '6',
x2: '19',
y2: '6',
stroke: '#000000',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
};
const line2 = {
type: 'line',
attributes: {
x1: '1',
y1: '14',
x2: '19',
y2: '14',
stroke: '#000000',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
};
const circle1 = {
type: 'circle',
attributes: {
cx: '7',
cy: '6',
r: '3',
fill: 'none',
stroke: 'currentColor',
},
};
const circle2 = {
type: 'circle',
attributes: {
cx: '13',
cy: '14',
r: '3',
fill: 'none',
stroke: 'currentColor',
},
};
const path = [{ ...line1 }, { ...line2 }, { ...circle1 }, { ...circle2 }];

const otherProps = {
'stroke-width': '1.25',
fill: 'none',
xmlns: 'http://www.w3.org/2000/svg',
};
const props = {
color: '#ff0000',
className: 'active',
width: '23',
height: '19',
viewBox: '0 0 20 20',
style: {
transition: `transform .4s cubic-bezier(.11,.44,.03,1)`,
'&.active': {
'& circle:nth-child(3)': {
transform: `translate(6px)`,
},
'& circle:nth-child(4)': {
transform: `translate(-6px)`,
},
},
},
...otherProps,
};

const rendered = render(<Icon path={path} {...props} />);

expect.assertions(
Object.keys(line1.attributes).length +
Object.keys(line2.attributes).length +
Object.keys(circle1.attributes).length +
Object.keys(circle2.attributes).length +
Object.keys(otherProps).length +
13
);

const svgLine1 = rendered.container.querySelector(`svg ${line1.type}:nth-of-type(1)`)!;
expect(svgLine1).toBeInTheDocument();
Object.keys(line1.attributes).forEach((key) => {
expect(svgLine1).toHaveAttribute(key, line1.attributes[key as keyof typeof line1.attributes]);
});

const svgLine2 = rendered.container.querySelector(`svg ${line2.type}:nth-of-type(2)`)!;
expect(svgLine2).toBeInTheDocument();
Object.keys(line2.attributes).forEach((key) => {
expect(svgLine2).toHaveAttribute(key, line2.attributes[key as keyof typeof line2.attributes]);
});

const svgCircle1 = rendered.container.querySelector(`svg ${circle1.type}:nth-of-type(1)`)!;
expect(svgCircle1).toBeInTheDocument();
Object.keys(circle1.attributes).forEach((key) => {
expect(svgCircle1).toHaveAttribute(key, circle1.attributes[key as keyof typeof circle1.attributes]);
});

const svgCircle2 = rendered.container.querySelector(`svg ${circle2.type}:nth-of-type(2)`)!;
expect(svgCircle2).toBeInTheDocument();
Object.keys(circle2.attributes).forEach((key) => {
expect(svgCircle2).toHaveAttribute(key, circle2.attributes[key as keyof typeof circle2.attributes]);
});

const svg = rendered.container.querySelector(`svg`)!;
expect(svg).toBeInTheDocument();

// svg attributes that aren't props to be added on the root element
Object.keys(otherProps).forEach((key) => {
expect(svg).toHaveAttribute(key, otherProps[key as keyof typeof otherProps]);
});

// props
expect(svg).toHaveAttribute('viewBox', props.viewBox);
const styles = getComputedStyle(svg);
expect(styles.width).toBe(`${props.width}px`);
expect(styles.height).toBe(`${props.height}px`);
expect(styles.fill).toBe(props.color);

// custom style script
expect(styles.transition).toBe(props.style.transition);
expect(svg).toHaveClass(props.className);
const circle1Styles = getComputedStyle(svgCircle1);
expect(circle1Styles.transform).toBe(props.style['&.active']['& circle:nth-child(3)'].transform);
const circle2Styles = getComputedStyle(svgCircle2);
expect(circle2Styles.transform).toBe(props.style['&.active']['& circle:nth-child(4)'].transform);
});

it('renders children elements', () => {
const lineAttributes = {
x1: '1',
y1: '6',
x2: '19',
y2: '6',
stroke: '#000000',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
};

const cicleAttributes = {
cx: '7',
cy: '6',
r: '3',
fill: 'none',
stroke: 'currentColor',
};

expect.assertions(Object.keys(lineAttributes).length + Object.keys(cicleAttributes).length + 2);

const rendered = render(
<Icon>
<line {...lineAttributes}></line>
<circle {...cicleAttributes}></circle>
</Icon>
);

const svgLine = rendered.container.querySelector('svg line')!;
expect(svgLine).toBeInTheDocument();

Object.keys(lineAttributes).forEach((key) => {
expect(svgLine).toHaveAttribute(key, lineAttributes[key as keyof typeof lineAttributes]);
});

const svgCicle = rendered.container.querySelector('svg circle')!;
expect(svgCicle).toBeInTheDocument();

Object.keys(cicleAttributes).forEach((key) => {
expect(svgCicle).toHaveAttribute(key, cicleAttributes[key as keyof typeof cicleAttributes]);
});
});

it('can disable styles', () => {
const icon = 'cog';
const color = '#3a23ad';
Expand Down
37 changes: 27 additions & 10 deletions packages/snap-preact-components/src/components/Atoms/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jsx jsx */
import { Fragment, h } from 'preact';
import { Fragment, h, ComponentChildren } from 'preact';

import { jsx, css } from '@emotion/react';
import classnames from 'classnames';
Expand All @@ -12,8 +12,8 @@ const CSS = {
icon: ({ color, height, width, size, theme }: IconProps) =>
css({
fill: color || theme?.colors?.primary,
width: width || size,
height: height || size,
width: isNaN(Number(width || size)) ? width || size : `${width || size}px`,
height: isNaN(Number(height || size)) ? height || size : `${height || size}px`,
position: 'relative',
}),
};
Expand All @@ -32,18 +32,18 @@ export function Icon(properties: IconProps): JSX.Element {
...properties,
...properties.theme?.components?.icon,
};
const { color, icon, path, size, width, height, viewBox, disableStyles, className, style } = props;
const { color, icon, path, children, size, width, height, viewBox, disableStyles, className, style, ...otherProps } = props;

const iconPath = iconPaths[icon as keyof typeof iconPaths] || path;

const pathType = typeof iconPath;
const styling: { css?: StylingCSS } = {};
if (!disableStyles) {
styling.css = [CSS.icon({ color, width, height, size, theme }), style];
} else if (style) {
styling.css = [style];
}

return iconPath ? (
return children || (iconPath && pathType === 'string') || (pathType === 'object' && Array.isArray(path)) ? (
<CacheProvider>
<svg
{...styling}
Expand All @@ -52,21 +52,38 @@ export function Icon(properties: IconProps): JSX.Element {
xmlns="http://www.w3.org/2000/svg"
width={disableStyles ? width || size : undefined}
height={disableStyles ? height || size : undefined}
{...otherProps}
>
<path fill={disableStyles ? color : undefined} d={iconPath} />
{(() => {
if (children) {
return children;
} else if (pathType === 'string') {
return <path fill={disableStyles ? color : undefined} d={iconPath as string} />;
} else if (pathType === 'object' && Array.isArray(path)) {
return path.map((p: SVGPathElement, i) => <p.type key={i} {...p.attributes} />);
}
})()}
</svg>
</CacheProvider>
) : (
<Fragment></Fragment>
);
}

type SVGPathElement = {
type: string;
attributes: {
[attribute: string]: string;
};
};

export interface IconProps extends ComponentProps {
color?: string;
icon?: IconType | string;
path?: string;
path?: string | SVGPathElement[];
children?: ComponentChildren;
size?: string;
width?: string;
height?: string;
width?: string | number;
height?: string | number;
viewBox?: string;
}
Loading

0 comments on commit c8a6c44

Please sign in to comment.