Skip to content

Commit 409774c

Browse files
authored
Merge className and intent onto element icons in <Icon> (#8080)
1 parent 9c757cd commit 409774c

2 files changed

Lines changed: 71 additions & 25 deletions

File tree

packages/core/src/components/icon/Icon.stories.tsx

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import type { Meta, StoryObj } from "@storybook/react-vite";
66
import { storybookLayoutDecorator, StoryLabel } from "@storybook-common";
77

8+
import { Buggy, IconNames } from "@blueprintjs/icons";
89
import { Flex } from "@blueprintjs/labs";
910

10-
import { Intent } from "../../common";
11+
import { Colors, Intent } from "../../common";
1112

1213
import { Icon, IconSize } from "./icon";
1314

@@ -16,28 +17,38 @@ const meta: Meta<typeof Icon> = {
1617
component: Icon,
1718
decorators: [storybookLayoutDecorator],
1819
args: {
19-
icon: "buggy",
20+
icon: IconNames.BUGGY,
21+
intent: Intent.NONE,
2022
size: IconSize.STANDARD,
21-
intent: "none",
2223
color: undefined,
24+
title: undefined,
25+
tagName: "span",
26+
autoLoad: true,
2327
},
2428
argTypes: {
29+
icon: {
30+
control: "text",
31+
},
2532
intent: {
2633
control: "select",
2734
options: Object.values(Intent),
2835
},
2936
size: {
30-
control: "select",
31-
options: [IconSize.STANDARD, IconSize.LARGE],
37+
control: "number",
3238
},
33-
icon: {
39+
color: {
40+
control: "color",
41+
},
42+
title: {
3443
control: "text",
3544
},
36-
color: {
45+
tagName: {
3746
control: "text",
3847
},
48+
autoLoad: {
49+
control: "boolean",
50+
},
3951
className: { table: { disable: true } },
40-
autoLoad: { table: { disable: true } },
4152
svgProps: { table: { disable: true } },
4253
},
4354
} satisfies Meta<typeof Icon>;
@@ -48,11 +59,7 @@ type Story = StoryObj<typeof meta>;
4859
/**
4960
* A basic icon with default styling.
5061
*/
51-
export const Default: Story = {
52-
args: {
53-
icon: "buggy",
54-
},
55-
};
62+
export const Default: Story = {};
5663

5764
/**
5865
* Use the `intent` prop to apply a semantic color that conveys the purpose or status of the icon.
@@ -80,15 +87,48 @@ export const SizeExample: Story = {
8087
size: { table: { disable: true } },
8188
},
8289
render: args => (
83-
<Flex flexDirection="column" gap={10}>
84-
<Flex flexDirection="column" gap={2} alignItems="center">
85-
<StoryLabel title="standard size - 16px" />
86-
<Icon {...args} size={IconSize.STANDARD} />
87-
</Flex>
90+
<Flex gap={5}>
91+
<Icon {...args} size={IconSize.STANDARD} />
92+
<Icon {...args} size={IconSize.LARGE} />
93+
<Icon {...args} size={48} />
94+
</Flex>
95+
),
96+
};
97+
98+
/**
99+
* Use the `color` prop to override the icon fill with a custom CSS color.
100+
* This takes precedence over `intent`.
101+
*/
102+
export const ColorExample: Story = {
103+
name: "Color",
104+
render: args => (
105+
<Flex gap={4} alignItems="center">
106+
{[Colors.BLUE3, Colors.FOREST3, Colors.GOLD3, Colors.RED3, Colors.INDIGO4].map(color => (
107+
<Flex key={color} flexDirection="column" gap={1} alignItems="center">
108+
<Icon {...args} color={color} />
109+
<StoryLabel title={color} />
110+
</Flex>
111+
))}
112+
</Flex>
113+
),
114+
};
88115

89-
<Flex flexDirection="column" gap={2} alignItems="center">
90-
<StoryLabel title="large size - 20px" />
91-
<Icon {...args} size={IconSize.LARGE} />
116+
/**
117+
* The `icon` prop accepts either a string icon name or a React element (typically an icon
118+
* component from `@blueprintjs/icons`). When an element is provided, `<Icon>` clones it and
119+
* merges the parent-provided `className` and intent class onto its root.
120+
*/
121+
export const ElementIcon: Story = {
122+
name: "Element icon",
123+
render: args => (
124+
<Flex gap={4} alignItems="center">
125+
<Flex flexDirection="column" gap={1} alignItems="center">
126+
<Icon {...args} icon={IconNames.BUGGY} />
127+
<StoryLabel title='icon="buggy"' />
128+
</Flex>
129+
<Flex flexDirection="column" gap={1} alignItems="center">
130+
<Icon {...args} icon={<Buggy size={args.size} />} />
131+
<StoryLabel title="icon={<Buggy />}" />
92132
</Flex>
93133
</Flex>
94134
),
@@ -99,7 +139,7 @@ export const SizeExample: Story = {
99139
*/
100140
export const Playground: Story = {
101141
args: {
102-
icon: "buggy",
142+
icon: IconNames.BUGGY,
103143
size: IconSize.STANDARD,
104144
intent: "none",
105145
color: undefined,

packages/core/src/components/icon/icon.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import classNames from "classnames";
18-
import { createElement, forwardRef, useEffect, useState } from "react";
18+
import { cloneElement, createElement, forwardRef, isValidElement, useEffect, useState } from "react";
1919

2020
import {
2121
type DefaultSVGIconProps,
@@ -56,8 +56,9 @@ export interface IconOwnProps {
5656
* - If given an `IconName` (a string literal union of all icon names), that
5757
* icon will be rendered as an `<svg>` with `<path>` tags. Unknown strings
5858
* will render a blank icon to occupy space.
59-
* - If given a `React.JSX.Element`, that element will be rendered and _all other
60-
* props on this component are ignored._ This type is supported to
59+
* - If given a `React.JSX.Element`, that element will be rendered with the
60+
* parent-provided `className` and intent class merged onto its root. All
61+
* other props on this component are ignored. This type is supported to
6162
* simplify icon support in other Blueprint components. As a consumer, you
6263
* should avoid using `<Icon icon={<Element />}` directly; simply render
6364
* `<Element />` instead.
@@ -157,6 +158,11 @@ export const Icon: IconComponent = forwardRef(<T extends Element>(props: IconPro
157158
if (icon == null || typeof icon === "boolean") {
158159
return null;
159160
} else if (typeof icon !== "string") {
161+
if (isValidElement<{ className?: string }>(icon)) {
162+
return cloneElement(icon, {
163+
className: classNames(icon.props.className, className, Classes.intentClass(intent)),
164+
});
165+
}
160166
return icon;
161167
}
162168

0 commit comments

Comments
 (0)