Skip to content

Commit

Permalink
SVG Icons! (#2028)
Browse files Browse the repository at this point in the history
* add svgo to node-build-scripts

* add type information and @ts-check to generate-icons-source

* iconContents.tsx contains `export const ICON_NAME = <svg>...</svg>` for each icon!

* generate and export IconSvgs

* refactor existing Icon component to render SVGs

* fix usages (no more iconSize)

* svg.pt-icon style fixes, LegacyIconName extends IconName

* revert documentalist usage

* svgo v1 (0.7 is still used by css-loader :sad:)

* iconSvgs => iconSvgPaths, map of icon name to array of path strings (d="string"). Icon component renders svg and title tags.

* update DocsIcon

* remove style tag from endorsed icon (the only one that had it)

* fix tree test

* IconSvgPaths16 and IconSvgPaths20 objects

* [WIP] choose 16 or 20 based on iconSize + example

really awkward experience. makes me think we want to use 20 whenever iconSize > 20.

* back to iconSize

* one more, InputGroup needs a css fix

* requires, remove tslints

* iconSize <= 16 ? use 16 : use 20

* color + style props, spread all svg props

* fix React prop warnings, remove invalid clipRule (not inside clipPath)

* fix icons in input groups - padding only!

* improve IconExample with IconSuggest

* scripts: dev:core watches labs, icons clean rm's src/generated

* Button uses Icon instead of class, Dialog close button Icon

* IconSuggest new syntax

* MenuItem renders Icon instead of class

* magic negative margin aligns SVG and DOM

* fix a bunch of tests (one offending Popover test)

* fix that popover test

* comments

* revert popoverTests, delete iconSize=inherit test

* ensure itemPredicate has default value in QueryList. fixes Icon component eaxmple.

* fix tree example

* move icon class to data-icon attribute

* update docs

* put short IconName in data-icon attribute

* fix tests

* fix Icon props spread

* examples/core-examples/common/IconSelect

.pt-label supports .pt-popover-wrapper

* docs language, normalizedIconName

* fix MenuItem icon color and dark submenu shadow

* fix input-group button icon size

* Callout renders SVG Icon component (#2060)

* Callout renders SVG Icon component

- added .pt-callout-icon class to adjust padding & layout when icon is present
- existing CSS API is unchanged
- add Callout Example in docs

* fix tests

* 👏 totally non-breaking API! only one use of .pt-callout-icon

* iconName=null overrides intent

* clarify callout icon logic
  • Loading branch information
giladgray authored Feb 1, 2018
1 parent dde2d32 commit db083fa
Show file tree
Hide file tree
Showing 52 changed files with 732 additions and 221 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"copy-landing-app": "cp -rf packages/landing-app/dist/* docs/.",
"deploy": "gh-pages -d docs -b master",
"dev:all": "lerna run dev --parallel --scope '!@blueprintjs/{landing-app,table-dev-app}'",
"dev:core": "lerna run dev --parallel --scope '@blueprintjs/{core,docs-app}'",
"dev:core": "lerna run dev --parallel --scope '@blueprintjs/{core,icons,docs-app}'",
"dev:docs": "lerna run dev --parallel --scope '@blueprintjs/{docs-app,docs-theme}'",
"dev:datetime": "lerna run dev --parallel --scope '@blueprintjs/{core,datetime,docs-app}'",
"dev:labs": "lerna run dev --parallel --scope '@blueprintjs/{core,labs,select,docs-app}'",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const BUTTON = "pt-button";
export const BUTTON_GROUP = "pt-button-group";

export const CALLOUT = "pt-callout";
export const CALLOUT_ICON = "pt-callout-icon";

export const CARD = "pt-card";

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class Alert extends AbstractPureComponent<IAlertProps, {}> {
return (
<Dialog className={classNames(Classes.ALERT, className)} isOpen={isOpen} style={style}>
<div className={Classes.ALERT_BODY}>
<Icon iconName={iconName} iconSize="inherit" intent={Intent.DANGER} />
<Icon iconName={iconName} iconSize={40} intent={Intent.DANGER} />
<div className={Classes.ALERT_CONTENTS}>{children}</div>
</div>
<div className={Classes.ALERT_FOOTER}>
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/components/button/_button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ Styleguide pt-button
color: $pt-icon-color;
}

// icon-only button
.pt-icon:first-child:last-child {
// center icon horizontally. this works for large buttons too.
$icon-margin-offset: -($pt-button-height - $pt-icon-size-standard) / 2;
margin-right: $icon-margin-offset;
margin-left: $icon-margin-offset;
}

/*
Advanced icon usage
Expand Down
18 changes: 10 additions & 8 deletions packages/core/src/components/button/abstractButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export abstract class AbstractButton<T> extends React.Component<React.HTMLProps<
[Classes.DISABLED]: disabled,
[Classes.LOADING]: this.props.loading,
},
Classes.iconClass(this.props.iconName),
Classes.intentClass(this.props.intent),
this.props.className,
);
Expand Down Expand Up @@ -114,7 +113,7 @@ export abstract class AbstractButton<T> extends React.Component<React.HTMLProps<
};

protected renderChildren(): React.ReactNode {
const { loading, rightIconName, text } = this.props;
const { iconName, loading, rightIconName, text } = this.props;

const children = React.Children.map(this.props.children, (child, index) => {
if (child === "") {
Expand All @@ -127,12 +126,15 @@ export abstract class AbstractButton<T> extends React.Component<React.HTMLProps<
return child;
});

return [
loading ? <Spinner className="pt-small pt-button-spinner" key="spinner" /> : undefined,
text != null ? <span key="text">{text}</span> : undefined,
...children,
<Icon className={Classes.ALIGN_RIGHT} iconName={rightIconName} key="icon" />,
];
return (
<>
<Icon iconName={iconName} />
{loading && <Spinner className="pt-small pt-button-spinner" />}
{text != null && <span>{text}</span>}
{children}
<Icon className={Classes.ALIGN_RIGHT} iconName={rightIconName} />
</>
);
}
}

Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/components/callout/_callout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Styleguide pt-callout
background-color: rgba($color, 0.15);

&[class*="pt-icon-"]::before,
.pt-callout-icon,
h5 {
color: map-get($pt-intent-text-colors, $intent);
}
Expand All @@ -67,6 +68,7 @@ Styleguide pt-callout
background-color: rgba($color, 0.25);

&[class*="pt-icon-"]::before,
.pt-callout-icon,
h5 {
color: map-get($pt-dark-intent-text-colors, $intent);
}
Expand All @@ -78,3 +80,12 @@ Styleguide pt-callout
margin: ($pt-grid-size * 2) 0;
}
}

// callout icon fills vertical space at left side, pushing title and content over.
// note that CSS API `.pt-callout.pt-icon-[name]` is still supported.
.pt-callout-icon {
float: left;
margin-right: $pt-grid-size;
height: 100%;
color: $pt-icon-color;
}
12 changes: 10 additions & 2 deletions packages/core/src/components/callout/callout.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Callouts visually highlight important content for the user.

@reactExample CalloutExample

@## CSS API

Callouts use the same visual intent modifier classes as buttons. If you need a
Expand All @@ -18,7 +20,13 @@ heading, use the `<h5>` element.
The `Callout` component is available in the __@blueprintjs/core__ package.
Make sure to review the [general usage docs for JS components](#blueprint.usage).

The component is a simple wrapper around the CSS API that provides props for modifiers and optional title element.
Any additional HTML props will be spread to the rendered `<div>` element.
The component is a simple wrapper around the CSS API that provides props for modifiers and the optional title
element. Any additional HTML props will be spread to the rendered `<div>` element. It provides two additional
useful features:

1. Providing an `intent` will set use a default icon per intent, which can be overridden by supplying
your own `iconName`.
1. The React component renders an SVG `Icon` element for the `iconName` prop, instead of the `.pt-icon-*`
CSS class.

@interface ICalloutProps
52 changes: 42 additions & 10 deletions packages/core/src/components/callout/callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
import * as classNames from "classnames";
import * as React from "react";

import { Classes, IIntentProps, IProps } from "../../common";
import { Classes, IIntentProps, Intent, IProps } from "../../common";
import { Icon } from "../../index";
import { IconName } from "../icon/icon";

/** This component also supports the full range of HTML `<div>` props. */
export interface ICalloutProps extends IIntentProps, IProps {
/** Name of icon to render on left-hand side. */
iconName?: IconName;
/**
* Name of icon to render on left-hand side.
*
* If this prop is omitted or `undefined`, the `intent` prop will determine a default icon.
* If this prop is explicitly `null`, no icon will be displayed (regardless of `intent`).
*/
iconName?: IconName | null;

/**
* String content of optional title element.
Expand All @@ -26,18 +32,44 @@ export interface ICalloutProps extends IIntentProps, IProps {

export class Callout extends React.PureComponent<ICalloutProps & React.HTMLAttributes<HTMLDivElement>, {}> {
public render() {
const { className, children, iconName, intent, title, ...htmlProps } = this.props;
const classes = classNames(
Classes.CALLOUT,
Classes.intentClass(intent),
Classes.iconClass(iconName),
className,
);
const { className, children, iconName: _nospread, intent, title, ...htmlProps } = this.props;
const iconName = this.getIconName();
const classes = classNames(Classes.CALLOUT, Classes.intentClass(intent), className);
return (
<div className={classes} {...htmlProps}>
{iconName && (
<span className={Classes.CALLOUT_ICON}>
<Icon iconName={iconName} iconSize={Icon.SIZE_LARGE} />
</span>
)}
{title && <h5>{title}</h5>}
{children}
</div>
);
}

private getIconName(): IconName | undefined {
const { iconName, intent } = this.props;
// 1. no icon
if (iconName === null) {
return undefined;
}
// 2. defined iconName prop
if (iconName !== undefined) {
return iconName;
}
// 3. default intent icon
switch (intent) {
case Intent.DANGER:
return "error";
case Intent.PRIMARY:
return "info-sign";
case Intent.WARNING:
return "warning-sign";
case Intent.SUCCESS:
return "tick";
default:
return undefined;
}
}
}
2 changes: 2 additions & 0 deletions packages/core/src/components/dialog/_dialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ $dialog-padding: $pt-grid-size * 2 !default;
min-height: $pt-icon-size-large + $dialog-padding;
padding-left: $dialog-padding;

> .pt-icon,
.pt-icon-large {
flex: 0 0 auto;
margin-right: $dialog-padding / 2;
Expand Down Expand Up @@ -143,6 +144,7 @@ $dialog-padding: $pt-grid-size * 2 !default;

cursor: pointer;
padding: $pt-grid-size;
padding-right: $dialog-padding;
}

.pt-dialog-body {
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,14 @@ export class Dialog extends AbstractPureComponent<IDialogProps, {}> {
}

private maybeRenderCloseButton() {
// for now, show close button if prop is undefined or null
// show close button if prop is undefined or null
// this gives us a behavior as if the default value were `true`
if (this.props.isCloseButtonShown !== false) {
const classes = classNames(Classes.DIALOG_CLOSE_BUTTON, Classes.iconClass("small-cross"));
return <button aria-label="Close" className={classes} onClick={this.props.onClose} />;
return (
<button aria-label="Close" className={Classes.DIALOG_CLOSE_BUTTON} onClick={this.props.onClose}>
<Icon iconName="small-cross" iconSize={Icon.SIZE_LARGE} />
</button>
);
} else {
return undefined;
}
Expand All @@ -112,7 +115,7 @@ export class Dialog extends AbstractPureComponent<IDialogProps, {}> {
}
return (
<div className={Classes.DIALOG_HEADER}>
<Icon iconName={iconName} iconSize={20} />
<Icon iconName={iconName} iconSize={Icon.SIZE_LARGE} />
<h5>{title}</h5>
{this.maybeRenderCloseButton()}
</div>
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/components/forms/_input-group.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,20 @@ $input-button-height-large: $pt-button-height !default;
margin: ($pt-input-height - $input-button-height) / 2;
padding-top: 0;
padding-bottom: 0;

.pt-icon {
$small-icon-margin: ($input-button-height - $pt-icon-size-standard) / 2;
margin-top: $small-icon-margin;
margin-bottom: $small-icon-margin;
}
}

.pt-icon {
@include pt-icon($pt-icon-size-standard);

// bump icon up so it sits above input
z-index: 1;
margin: 0 ($pt-input-height - $pt-icon-size-standard) / 2;
line-height: $pt-input-height;
margin: ($pt-input-height - $pt-icon-size-standard) / 2;
color: $pt-icon-color;
}

Expand Down Expand Up @@ -145,8 +150,7 @@ $input-button-height-large: $pt-button-height !default;
}

.pt-icon {
margin: 0 ($pt-input-height-large - $pt-icon-size-standard) / 2;
line-height: $pt-input-height-large;
margin: ($pt-input-height-large - $pt-icon-size-standard) / 2;
}

.pt-input {
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/components/forms/_label.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ label.pt-label {
margin: 0 0 ($pt-grid-size * 1.5);

.pt-input,
.pt-select {
.pt-select,
.pt-popover-wrapper {
display: block;
margin-top: $pt-grid-size / 2;
text-transform: none;
Expand All @@ -49,7 +50,8 @@ label.pt-label {

.pt-input,
.pt-input-group,
.pt-select {
.pt-select,
.pt-popover-wrapper {
display: inline-block;
margin: 0 0 0 ($pt-grid-size / 2);
vertical-align: top;
Expand All @@ -64,6 +66,10 @@ label.pt-label {
}
}

&:not(.pt-inline) .pt-popover-target {
display: block;
}

/*
Disabled labels
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/forms/inputGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class InputGroup extends React.PureComponent<HTMLInputProps & IInputGroup

return (
<div className={classes}>
<Icon iconName={leftIconName} iconSize="inherit" />
<Icon iconName={leftIconName} />
<input
type="text"
{...removeNonHTMLProps(this.props)}
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/components/icon/_icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ span.pt-icon {
content: $content;
}
}

svg.pt-icon {
// respect dimensions exactly
flex: 0 0 auto;
// SVG and DOM elements don't align perfectly - this magic number seems to fix it.
// https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4#6e0c
margin-top: -0.125em;
vertical-align: middle;
// inherit text color by default
fill: currentColor;
}
Loading

1 comment on commit db083fa

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SVG Icons! (#2028)

Preview: documentation | landing | table

Please sign in to comment.