Skip to content

Commit 17ac3e2

Browse files
authored
Components: Make local copy of Icon (#103562)
* Copy code from `@wordpress/components` * Apply changes
1 parent 74f2e09 commit 17ac3e2

File tree

3 files changed

+266
-0
lines changed

3 files changed

+266
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { wordpress } from '@wordpress/icons';
2+
import { SVG, Path } from '@wordpress/primitives';
3+
import { Icon } from '.';
4+
import type { Meta, StoryFn } from '@storybook/react';
5+
6+
const meta: Meta< typeof Icon > = {
7+
title: 'Still Internal/Icon',
8+
component: Icon,
9+
parameters: {
10+
docs: { canvas: { sourceState: 'shown' } },
11+
},
12+
};
13+
export default meta;
14+
15+
const Template: StoryFn< typeof Icon > = ( args ) => <Icon { ...args } />;
16+
17+
export const Default = Template.bind( {} );
18+
Default.args = {
19+
icon: wordpress,
20+
};
21+
22+
export const FillColor: StoryFn< typeof Icon > = ( args ) => {
23+
return (
24+
<div
25+
style={ {
26+
fill: 'blue',
27+
} }
28+
>
29+
<Icon { ...args } />
30+
</div>
31+
);
32+
};
33+
FillColor.args = {
34+
...Default.args,
35+
};
36+
37+
/**
38+
* When `icon` is a function, it will be passed the `size` prop and any other additional props.
39+
*/
40+
export const WithAFunction = Template.bind( {} );
41+
WithAFunction.args = {
42+
...Default.args,
43+
icon: ( { size }: { size?: number } ) => (
44+
<img
45+
width={ size }
46+
height={ size }
47+
src="https://s.w.org/style/images/about/WordPress-logotype-wmark.png"
48+
alt="WordPress"
49+
/>
50+
),
51+
};
52+
WithAFunction.parameters = {
53+
docs: {
54+
source: {
55+
code: `
56+
<Icon
57+
icon={ ( { size } ) => (
58+
<img
59+
width={ size }
60+
height={ size }
61+
src="https://s.w.org/style/images/about/WordPress-logotype-wmark.png"
62+
alt="WordPress"
63+
/>
64+
) }
65+
/>
66+
`,
67+
},
68+
},
69+
};
70+
71+
const MyIconComponent = ( { size }: { size?: number } ) => (
72+
<SVG width={ size } height={ size }>
73+
<Path d="M5 4v3h5.5v12h3V7H19V4z" />
74+
</SVG>
75+
);
76+
77+
/**
78+
* When `icon` is a component, it will be passed the `size` prop and any other additional props.
79+
*/
80+
export const WithAComponent = Template.bind( {} );
81+
WithAComponent.args = {
82+
...Default.args,
83+
icon: <MyIconComponent />,
84+
};
85+
WithAComponent.parameters = {
86+
docs: {
87+
source: {
88+
code: `
89+
const MyIconComponent = ( { size } ) => (
90+
<SVG width={ size } height={ size }>
91+
<Path d="M5 4v3h5.5v12h3V7H19V4z" />
92+
</SVG>
93+
);
94+
95+
<Icon icon={ <MyIconComponent /> } />
96+
`,
97+
},
98+
},
99+
};
100+
101+
export const WithAnSVG = Template.bind( {} );
102+
WithAnSVG.args = {
103+
...Default.args,
104+
icon: (
105+
<SVG>
106+
<Path d="M5 4v3h5.5v12h3V7H19V4z" />
107+
</SVG>
108+
),
109+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Forked from `@wordpress/components`
3+
*
4+
* Only forked for CSS collision safety, in case styles or classnames are added upstream.
5+
*
6+
* - Removed Gridicons support (non-critical).
7+
*/
8+
9+
import { cloneElement, createElement, isValidElement } from '@wordpress/element';
10+
import { SVG } from '@wordpress/primitives';
11+
import type { ComponentType } from 'react';
12+
13+
type IconType =
14+
| ComponentType< { size?: number } >
15+
| ( ( props: { size?: number } ) => JSX.Element )
16+
| JSX.Element;
17+
18+
type AdditionalProps< T > = T extends ComponentType< infer U > ? U : Record< string, unknown >;
19+
20+
type Props = {
21+
/**
22+
* The icon to render. In most cases, you should use an icon from
23+
* [the `@wordpress/icons` package](https://wordpress.github.io/gutenberg/?path=/story/icons-icon--library).
24+
*
25+
* Other supported values are: component instances, functions, and `null`.
26+
*
27+
* The `size` value, as well as any other additional props, will be passed through.
28+
* @default null
29+
*/
30+
icon?: IconType | null;
31+
/**
32+
* The size (width and height) of the icon.
33+
* @default 24
34+
*/
35+
size?: number;
36+
} & AdditionalProps< IconType >;
37+
38+
/**
39+
* Renders a raw icon without any initial styling or wrappers.
40+
*
41+
* ```jsx
42+
* import { Icon } from '@automattic/ui';
43+
* import { wordpress } from '@wordpress/icons';
44+
*
45+
* <Icon icon={ wordpress } />
46+
* ```
47+
*/
48+
export function Icon( { icon = null, size = 24, ...additionalProps }: Props ) {
49+
if ( 'function' === typeof icon ) {
50+
return createElement( icon, {
51+
size,
52+
...additionalProps,
53+
} );
54+
}
55+
56+
if ( icon && ( icon.type === 'svg' || icon.type === SVG ) ) {
57+
const appliedProps = {
58+
...icon.props,
59+
width: size,
60+
height: size,
61+
...additionalProps,
62+
};
63+
64+
return <SVG { ...appliedProps } />;
65+
}
66+
67+
if ( isValidElement( icon ) ) {
68+
return cloneElement( icon, {
69+
// @ts-ignore Just forwarding the size prop along
70+
size,
71+
...additionalProps,
72+
} );
73+
}
74+
75+
return icon;
76+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { render, screen } from '@testing-library/react';
5+
import { Path, SVG } from '@wordpress/primitives';
6+
import { Icon } from '..';
7+
8+
describe( 'Icon', () => {
9+
const testId = 'icon';
10+
const className = 'example-class';
11+
const svg = (
12+
<SVG>
13+
<Path d="M5 4v3h5.5v12h3V7H19V4z" />
14+
</SVG>
15+
);
16+
17+
it( 'renders nothing when icon omitted', () => {
18+
render( <Icon data-testid={ testId } /> );
19+
20+
expect( screen.queryByTestId( testId ) ).not.toBeInTheDocument();
21+
} );
22+
23+
it( 'renders a function', () => {
24+
render( <Icon icon={ () => <span data-testid={ testId } /> } /> );
25+
26+
expect( screen.getByTestId( testId ) ).toBeVisible();
27+
} );
28+
29+
it( 'renders an element', () => {
30+
render( <Icon icon={ <span data-testid={ testId } /> } /> );
31+
32+
expect( screen.getByTestId( testId ) ).toBeVisible();
33+
} );
34+
35+
it( 'renders an svg element', () => {
36+
render( <Icon data-testid={ testId } icon={ svg } /> );
37+
38+
expect( screen.getByTestId( testId ) ).toBeVisible();
39+
} );
40+
41+
it( 'renders an svg element with a default width and height of 24', () => {
42+
render( <Icon data-testid={ testId } icon={ svg } /> );
43+
const icon = screen.getByTestId( testId );
44+
45+
expect( icon ).toHaveAttribute( 'width', '24' );
46+
expect( icon ).toHaveAttribute( 'height', '24' );
47+
} );
48+
49+
it( 'renders an svg element and override its width and height', () => {
50+
render(
51+
<Icon
52+
data-testid={ testId }
53+
icon={
54+
<SVG width={ 64 } height={ 64 }>
55+
<Path d="M5 4v3h5.5v12h3V7H19V4z" />
56+
</SVG>
57+
}
58+
size={ 32 }
59+
/>
60+
);
61+
const icon = screen.getByTestId( testId );
62+
63+
expect( icon ).toHaveAttribute( 'width', '32' );
64+
expect( icon ).toHaveAttribute( 'height', '32' );
65+
} );
66+
67+
it( 'renders an svg element and does not override width and height if already specified', () => {
68+
render( <Icon data-testid={ testId } icon={ svg } size={ 32 } /> );
69+
const icon = screen.getByTestId( testId );
70+
71+
expect( icon ).toHaveAttribute( 'width', '32' );
72+
expect( icon ).toHaveAttribute( 'height', '32' );
73+
} );
74+
75+
it( 'renders a component', () => {
76+
const MyComponent = () => <span data-testid={ testId } className={ className } />;
77+
render( <Icon icon={ MyComponent } /> );
78+
79+
expect( screen.getByTestId( testId ) ).toHaveClass( className );
80+
} );
81+
} );

0 commit comments

Comments
 (0)