Skip to content

Commit 0c40f19

Browse files
committed
Hackery with Tabs
1 parent be564b2 commit 0c40f19

File tree

3 files changed

+71
-21
lines changed

3 files changed

+71
-21
lines changed

packages/core/src/components/tabs/tabTitle.tsx

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

1717
import classNames from "classnames";
18+
import * as React from "react";
1819

1920
import { AbstractPureComponent, Classes, Intent } from "../../common";
2021
import { DISPLAYNAME_PREFIX, removeNonHTMLProps } from "../../common/props";
@@ -72,7 +73,14 @@ export class TabTitle extends AbstractPureComponent<TabTitleProps> {
7273
role="tab"
7374
tabIndex={disabled ? undefined : selected ? 0 : -1}
7475
>
75-
{icon != null && <Icon icon={icon} intent={intent} className={Classes.TAB_ICON} />}
76+
{icon != null && icon !== false &&
77+
(typeof icon === "string" ? (
78+
<Icon icon={icon} intent={intent} className={Classes.TAB_ICON} />
79+
) : (
80+
React.cloneElement(icon as React.ReactElement, {
81+
className: classNames(Classes.TAB_ICON, (icon as React.ReactElement).props.className),
82+
})
83+
))}
7684
{title}
7785
{children}
7886
{tagContent != null && (

packages/demo-app/src/IconPlayground.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import kebabCase from "lodash/kebabCase";
66
import { useCallback, useEffect, useState } from "react";
77

88
import { Classes, type IconName } from "@blueprintjs/core";
9-
import { SVGIconContainer } from "@blueprintjs/icons";
109
import { Flex } from "@blueprintjs/labs";
1110

1211
import { ButtonSection } from "./components/ButtonSection";
1312
import { CalloutSection } from "./components/CalloutSection";
1413
import { CompoundTagSection } from "./components/CompoundTagSection";
14+
import { CustomIcon, type CustomIconData } from "./components/CustomIcon";
1515
import { FontSection } from "./components/FontSection";
1616
import { IconPreview } from "./components/IconPreview";
1717
import { InputGroupSection } from "./components/InputGroupSection";
@@ -20,13 +20,6 @@ import { Navigation } from "./components/Navigation";
2020
import { TabsSection } from "./components/TabsSection";
2121
import { TagSection } from "./components/TagSection";
2222

23-
interface CustomIconData {
24-
isActive: boolean;
25-
name: string;
26-
originalViewBox: string;
27-
paths: string[];
28-
}
29-
3023
/**
3124
* Parse an SVG file and extract path data for rendering as a Blueprint icon
3225
*/
@@ -110,18 +103,7 @@ export const IconPlayground = () => {
110103

111104
// Compute the effective icon to use (custom or selected)
112105
const effectiveIcon: IconName | React.JSX.Element =
113-
customIconData?.isActive === true ? (
114-
<SVGIconContainer
115-
iconName={customIconData.name as IconName}
116-
svgProps={{ viewBox: customIconData.originalViewBox } as React.SVGAttributes<SVGElement>}
117-
>
118-
{customIconData.paths.map((d, i) => (
119-
<path d={d} fillRule="evenodd" key={i} />
120-
))}
121-
</SVGIconContainer>
122-
) : (
123-
selectedIcon
124-
);
106+
customIconData?.isActive === true ? <CustomIcon customIconData={customIconData} /> : selectedIcon;
125107

126108
return (
127109
<div className={darkTheme ? Classes.DARK : ""}>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* !
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
3+
*/
4+
5+
import { forwardRef } from "react";
6+
7+
import { type IconName, type IntentProps, type Props, removeNonHTMLProps } from "@blueprintjs/core";
8+
import { IconSize, SVGIconContainer, type SVGIconProps } from "@blueprintjs/icons";
9+
10+
export interface CustomIconData {
11+
isActive: boolean;
12+
name: string;
13+
originalViewBox: string;
14+
paths: string[];
15+
}
16+
17+
export type CustomIconProps<T extends Element = Element> = IntentProps &
18+
Props &
19+
SVGIconProps<T> & {
20+
customIconData: CustomIconData;
21+
};
22+
23+
/**
24+
* Custom icon component that wraps SVGIconContainer and properly forwards className
25+
* to ensure it works correctly with Blueprint components like Button, Tab, MenuItem, etc.
26+
*/
27+
export const CustomIcon = forwardRef(<T extends Element>(props: CustomIconProps<T>, ref: React.Ref<T>) => {
28+
const {
29+
className,
30+
color,
31+
customIconData,
32+
intent,
33+
tagName = "span",
34+
svgProps,
35+
title,
36+
htmlTitle,
37+
...htmlProps
38+
} = props;
39+
40+
const size = props.size ?? IconSize.STANDARD;
41+
const pathElements = customIconData.paths.map((d, i) => <path d={d} fillRule="evenodd" key={i} />);
42+
43+
return (
44+
<SVGIconContainer<any>
45+
children={pathElements}
46+
className={className}
47+
color={color}
48+
htmlTitle={htmlTitle}
49+
iconName={customIconData.name as IconName}
50+
ref={ref}
51+
size={size}
52+
svgProps={{ ...svgProps, viewBox: customIconData.originalViewBox } as React.SVGAttributes<SVGElement>}
53+
tagName={tagName}
54+
title={title}
55+
{...removeNonHTMLProps(htmlProps)}
56+
/>
57+
);
58+
}) as any;
59+
60+
CustomIcon.displayName = "CustomIcon";

0 commit comments

Comments
 (0)