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

[Icon] render HTML element & tagName prop #2884

Merged
merged 6 commits into from
Aug 30, 2018
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
37 changes: 26 additions & 11 deletions packages/core/src/components/icon/_icon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,26 @@

@import "~@blueprintjs/icons/src/icons";

#{$icon-classes} {
display: inline-block;
// the SVG icon class
.#{$ns}-icon {
// remove any extra inline space and ensure centering with text
display: inline-flex;
// respect dimensions exactly
flex: 0 0 auto;

&:not(:empty)::before {
// clear font icon when there's an <svg> image
content: unset;
}

// inherit text color unless `color` prop is set
> svg:not([fill]) {
fill: currentColor;
}
}

// intent support for both SVG and legacy font icons
#{$icon-classes} {
@each $intent, $color in $pt-intent-text-colors {
&.#{$ns}-intent-#{$intent} {
color: $color;
Expand All @@ -17,15 +34,20 @@
}
}

// legacy font styles
span.#{$ns}-icon-standard {
@include pt-icon($pt-icon-size-standard);
display: inline-block;
}

span.#{$ns}-icon-large {
@include pt-icon($pt-icon-size-large);
display: inline-block;
}

span.#{$ns}-icon {
// only apply icon font styles when <svg> image is not present
span.#{$ns}-icon:empty {
display: inline-block;
line-height: 1;
font-family: $icons20-family;
font-size: inherit;
Expand All @@ -38,15 +60,8 @@ span.#{$ns}-icon {
}

@each $name, $content in $icons {
// only insert font glyph if <svg> image is not present
.#{$ns}-icon-#{$name}::before {
content: $content;
}
}

svg.#{$ns}-icon {
// respect dimensions exactly
flex: 0 0 auto;
vertical-align: top;
// inherit text color by default
fill: currentColor;
}
7 changes: 5 additions & 2 deletions packages/core/src/components/icon/icon.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ Many Blueprint components provide an `icon` prop which accepts an icon name

Use the `<Icon>` component to easily render __SVG icons__ in React. The `icon`
prop is typed such that editors can offer autocomplete for known icon names. The
optional `iconSize` prop determines the expected width and height of the icon
element. The component also accepts all valid HTML props for an `<svg>` element.
optional `iconSize` prop determines the exact width and height of the icon
image; the icon element itself can be sized separately using CSS.

The HTML element rendered by `<Icon>` can be customized with the `tagName` prop
(defaults to `span`), and additional props are passed to this element.

Data files in the __@blueprintjs/icons__ package provide SVG path information
for Blueprint's 300+ icons for 16px and 20px grids. The `icon` prop dictates
Expand Down
54 changes: 30 additions & 24 deletions packages/core/src/components/icon/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export interface IIconProps extends IIntentProps, IProps {
children?: never;

/**
* Color of icon. Equivalent to setting CSS `fill` property.
* Color of icon. This is used as the `fill` attribute on the `<svg>` image
* so it will override any CSS `color` property, including that set by
* `intent`. If this prop is omitted, icon color is inherited from
* surrounding text.
*/
color?: string;

Expand Down Expand Up @@ -46,23 +49,38 @@ export interface IIconProps extends IIntentProps, IProps {
/** CSS style properties. */
style?: React.CSSProperties;

/**
* HTML tag to use for the rendered element.
* @default "span"
*/
tagName?: keyof JSX.IntrinsicElements;

/**
* Description string. This string does not appear in normal browsers, but
* it increases accessibility. For instance, screen readers will use it for
* aural feedback. By default, this is set to the icon's name for
* accessibility.
* aural feedback. By default, this is set to the icon's name. Pass an
* explicit falsy value to disable.
*/
title?: string | false | null;
}

export class Icon extends React.PureComponent<IIconProps & React.SVGAttributes<SVGElement>> {
export class Icon extends React.PureComponent<IIconProps & React.DOMAttributes<HTMLElement>> {
public static displayName = `${DISPLAYNAME_PREFIX}.Icon`;

public static readonly SIZE_STANDARD = 16;
public static readonly SIZE_LARGE = 20;

public render() {
const { className, color, icon, iconSize = Icon.SIZE_STANDARD, intent, title = icon, ...svgProps } = this.props;
const {
className,
color,
icon,
iconSize = Icon.SIZE_STANDARD,
intent,
title = icon,
tagName: TagName = "span",
...htmlprops
} = this.props;

if (icon == null) {
return null;
Expand All @@ -77,28 +95,16 @@ export class Icon extends React.PureComponent<IIconProps & React.SVGAttributes<S
return null;
}

const classes = classNames(Classes.ICON, Classes.intentClass(intent), className);
const classes = classNames(Classes.ICON, Classes.iconClass(icon), Classes.intentClass(intent), className);
const viewBox = `0 0 ${pixelGridSize} ${pixelGridSize}`;

// ICON class will apply a "fill" CSS style, so we need to inject an inline style to override it
let { style = {} } = this.props;
if (color != null) {
style = { ...style, fill: color };
}

return (
<svg
{...svgProps}
className={classes}
style={style}
data-icon={icon}
width={iconSize}
height={iconSize}
viewBox={viewBox}
>
{title && <desc>{title}</desc>}
{paths}
</svg>
<TagName className={classes} {...htmlprops}>
<svg fill={color} data-icon={icon} width={iconSize} height={iconSize} viewBox={viewBox}>
{title && <desc>{title}</desc>}
{paths}
</svg>
</TagName>
);
}

Expand Down
18 changes: 13 additions & 5 deletions packages/core/test/icon/iconTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import { IconName } from "@blueprintjs/icons";
import { Classes, Icon, IIconProps, Intent } from "../../src/index";

describe("<Icon>", () => {
it("tagName dictates HTML tag", () => {
const icon = shallow(<Icon icon="calendar" />);
assert.isTrue(icon.is("span"));
assert.isTrue(icon.setProps({ tagName: "article" }).is("article"));
});

it("iconSize=16 renders standard size", () =>
assertIconSize(<Icon icon="graph" iconSize={Icon.SIZE_STANDARD} />, Icon.SIZE_STANDARD));

Expand All @@ -24,6 +30,7 @@ describe("<Icon>", () => {

it("renders icon name", () => assertIcon(<Icon icon="calendar" />, "calendar"));

it("renders icon without color", () => assertIconColor(<Icon icon="add" />));
it("renders icon color", () => assertIconColor(<Icon icon="add" color="red" />, "red"));

it("prefixed icon renders nothing", () => {
Expand Down Expand Up @@ -63,13 +70,14 @@ describe("<Icon>", () => {

/** Asserts that rendered icon has width/height equal to size. */
function assertIconSize(icon: React.ReactElement<IIconProps>, size: number) {
const wrapper = shallow(icon);
assert.strictEqual(wrapper.prop("width"), size);
assert.strictEqual(wrapper.prop("height"), size);
const svg = shallow(icon).find("svg");
assert.strictEqual(svg.prop("width"), size);
assert.strictEqual(svg.prop("height"), size);
}

/** Asserts that rendered icon has color equal to color. */
function assertIconColor(icon: React.ReactElement<IIconProps>, color: string) {
assert.deepEqual(shallow(icon).prop("style"), { fill: color });
function assertIconColor(icon: React.ReactElement<IIconProps>, color?: string) {
const svg = shallow(icon).find("svg");
assert.deepEqual(svg.prop("fill"), color);
}
});