diff --git a/assets/stylesheets/_vendor.scss b/assets/stylesheets/_vendor.scss
index 44276c9e566bbd..b2b0a3a722ed1e 100644
--- a/assets/stylesheets/_vendor.scss
+++ b/assets/stylesheets/_vendor.scss
@@ -5,15 +5,18 @@
.gridicon {
fill: currentColor;
- &.needs-offset g {
+ &.needs-offset > use:first-child,
+ &.needs-offset > g:first-child {
transform: translate( 1px, 1px ); /* translates to .5px because it's in a child element */
}
- &.needs-offset-x g {
+ &.needs-offset-x > use:first-child,
+ &.needs-offset-x > g:first-child {
transform: translate( 1px, 0 ); /* only nudges horizontally */
}
- &.needs-offset-y g {
+ &.needs-offset-y > use:first-child,
+ &.needs-offset-y > g:first-child {
transform: translate( 0, 1px ); /* only nudges vertically */
}
}
diff --git a/client/blocks/inline-help/style.scss b/client/blocks/inline-help/style.scss
index 5e471dc7e39993..29ebeb2a752be8 100644
--- a/client/blocks/inline-help/style.scss
+++ b/client/blocks/inline-help/style.scss
@@ -74,7 +74,8 @@
height: 36px;
width: 36px;
- g {
+ > use:first-child,
+ > g:first-child {
transform: none;
}
}
diff --git a/client/components/async-gridicons/fallback.jsx b/client/components/async-gridicons/fallback.jsx
deleted file mode 100644
index 935e1845eb3aa8..00000000000000
--- a/client/components/async-gridicons/fallback.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/** @format */
-
-/**
- * External dependencies
- */
-import React from 'react';
-
-/**
- * Internal dependencies
- */
-
-export default function FallbackIcon() {
- /* eslint-disable wpcalypso/jsx-classname-namespace */
- return (
-
- );
-}
diff --git a/client/components/async-gridicons/index.jsx b/client/components/async-gridicons/index.jsx
deleted file mode 100644
index 1331189d64e4e1..00000000000000
--- a/client/components/async-gridicons/index.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/** @format */
-
-/**
- * External dependencies
- */
-import React, { Component } from 'react';
-
-/**
- * Internal dependencies
- */
-import FallbackIcon from './fallback';
-
-const loadedIcons = new Map();
-
-function loadIcon( icon ) {
- return import(
- /* webpackChunkName: "gridicons", webpackInclude: /\.js$/, webpackExclude: /dist\/(index\.js|example\.js)$/, webpackMode: "lazy-once" */
- `gridicons/dist/${ icon }`
- ).then(
- g => {
- loadedIcons.set( icon, g.default );
- return g.default;
- },
- err => {
- loadedIcons.set( icon, false );
- console.warn( `Error loading icon '${ icon }':`, err.message ); // eslint-disable-line no-console
- }
- );
-}
-
-class AsyncGridicon extends Component {
- constructor( props ) {
- super( props );
- }
- checkAndLoad() {
- if ( this.props.icon && ! loadedIcons.has( this.props.icon ) ) {
- loadIcon( this.props.icon ).then( () => this.update() );
- }
- }
-
- componentDidMount() {
- this.checkAndLoad();
- }
- componentDidUpdate() {
- this.checkAndLoad();
- }
-
- update = () => this.forceUpdate();
-
- componentWillUnmount() {
- this.update = () => {};
- }
-
- render() {
- const { icon = '', ...rest } = this.props;
- if ( loadedIcons.get( icon ) ) {
- const Icon = loadedIcons.get( icon );
- return ;
- }
-
- return ;
- }
-}
-
-export default AsyncGridicon;
diff --git a/client/components/gridicon/docs/example.jsx b/client/components/gridicon/docs/example.jsx
new file mode 100644
index 00000000000000..641def0611099f
--- /dev/null
+++ b/client/components/gridicon/docs/example.jsx
@@ -0,0 +1,233 @@
+/**
+ * External dependencies
+ */
+
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import Gridicon from '../index';
+
+const icons = [
+ 'add-image',
+ 'add-outline',
+ 'add',
+ 'align-center',
+ 'align-image-center',
+ 'align-image-left',
+ 'align-image-none',
+ 'align-image-right',
+ 'align-justify',
+ 'align-left',
+ 'align-right',
+ 'arrow-down',
+ 'arrow-left',
+ 'arrow-right',
+ 'arrow-up',
+ 'aside',
+ 'attachment',
+ 'audio',
+ 'bell',
+ 'block',
+ 'bold',
+ 'book',
+ 'bookmark-outline',
+ 'bookmark',
+ 'briefcase',
+ 'bug',
+ 'calendar',
+ 'camera',
+ 'caption',
+ 'cart',
+ 'chat',
+ 'checkmark-circle',
+ 'checkmark',
+ 'chevron-down',
+ 'chevron-left',
+ 'chevron-right',
+ 'chevron-up',
+ 'clear-formatting',
+ 'clipboard',
+ 'cloud-download',
+ 'cloud-outline',
+ 'cloud-upload',
+ 'cloud',
+ 'code',
+ 'cog',
+ 'comment',
+ 'computer',
+ 'coupon',
+ 'create',
+ 'credit-card',
+ 'crop',
+ 'cross-circle',
+ 'cross-small',
+ 'cross',
+ 'custom-post-type',
+ 'customize',
+ 'domains',
+ 'dropdown',
+ 'ellipsis-circle',
+ 'ellipsis',
+ 'external',
+ 'filter',
+ 'flag',
+ 'flip-horizontal',
+ 'flip-vertical',
+ 'folder-multiple',
+ 'folder',
+ 'fullscreen-exit',
+ 'fullscreen',
+ 'gift',
+ 'globe',
+ 'grid',
+ 'heading-h1',
+ 'heading-h2',
+ 'heading-h3',
+ 'heading-h4',
+ 'heading-h5',
+ 'heading-h6',
+ 'heading',
+ 'heart-outline',
+ 'heart',
+ 'help-outline',
+ 'help',
+ 'history',
+ 'house',
+ 'image-multiple',
+ 'image-remove',
+ 'image',
+ 'indent-left',
+ 'indent-right',
+ 'info-outline',
+ 'info',
+ 'ink',
+ 'institution',
+ 'italic',
+ 'layout-blocks',
+ 'layout',
+ 'line-graph',
+ 'link-break',
+ 'link',
+ 'list-checkmark',
+ 'list-ordered-rtl',
+ 'list-ordered',
+ 'list-unordered',
+ 'location',
+ 'lock',
+ 'mail',
+ 'mention',
+ 'menu',
+ 'menus',
+ 'microphone',
+ 'minus-small',
+ 'minus',
+ 'money',
+ 'multiple-users',
+ 'my-sites-horizon',
+ 'my-sites',
+ 'nametag',
+ 'next-page',
+ 'not-visible',
+ 'notice-outline',
+ 'notice',
+ 'offline',
+ 'pages',
+ 'pause',
+ 'pencil',
+ 'phone',
+ 'pin',
+ 'plans',
+ 'play',
+ 'plugins',
+ 'plus-small',
+ 'plus',
+ 'popout',
+ 'posts',
+ 'print',
+ 'product-downloadable',
+ 'product-external',
+ 'product-virtual',
+ 'product',
+ 'quote',
+ 'read-more',
+ 'reader-follow-conversation',
+ 'reader-follow',
+ 'reader-following-conversation',
+ 'reader-following',
+ 'reader',
+ 'reblog',
+ 'redo',
+ 'refresh',
+ 'refund',
+ 'reply',
+ 'resize',
+ 'rotate',
+ 'scheduled',
+ 'search',
+ 'share-computer',
+ 'share-ios',
+ 'share',
+ 'shipping',
+ 'shutter',
+ 'sign-out',
+ 'site',
+ 'spam',
+ 'speaker',
+ 'special-character',
+ 'star-outline',
+ 'star',
+ 'stats',
+ 'stats-alt',
+ 'stats-down',
+ 'stats-down-alt',
+ 'stats-up',
+ 'stats-up-alt',
+ 'status',
+ 'strikethrough',
+ 'sync',
+ 'tablet',
+ 'tag',
+ 'text-color',
+ 'themes',
+ 'thumbs-up',
+ 'time',
+ 'trash',
+ 'trophy',
+ 'types',
+ 'underline',
+ 'undo',
+ 'user-add',
+ 'user-circle',
+ 'user',
+ 'video-camera',
+ 'video-remove',
+ 'video',
+ 'visible',
+ 'zoom-in',
+ 'zoom-out',
+];
+
+export default function GridiconExample() {
+ function handleClick( icon ) {
+ const toCopy = ``;
+ window.prompt( 'Copy component code:', toCopy );
+ }
+
+ return (
+ // eslint-disable-next-line wpcalypso/jsx-classname-namespace
+
+
+
+ { icons.map( icon => (
+ handleClick( icon ) } />
+ ) ) }
+
+
+ );
+}
+
+GridiconExample.displayName = 'Gridicon';
diff --git a/client/components/gridicon/index.jsx b/client/components/gridicon/index.jsx
new file mode 100644
index 00000000000000..a85b316c28ff9d
--- /dev/null
+++ b/client/components/gridicon/index.jsx
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import {
+ iconsThatNeedOffset,
+ iconsThatNeedOffsetX,
+ iconsThatNeedOffsetY,
+} from 'gridicons/dist/util/icons-offset';
+import spritePath from 'gridicons/svg-sprite/gridicons.svg';
+
+function needsOffset( name, icons ) {
+ return icons.indexOf( name ) >= 0;
+}
+
+function Gridicon( props ) {
+ const { size = 24, icon, onClick, className, ...otherProps } = props;
+ const isModulo18 = size % 18 === 0;
+
+ // Using a missing icon doesn't produce any errors, just a blank icon, which is the exact intended behaviour.
+ // This means we don't need to perform any checks on the icon name.
+ const iconName = `gridicons-${ icon }`;
+ const offsetClasses = isModulo18
+ ? [
+ needsOffset( iconName, iconsThatNeedOffset ) ? 'needs-offset' : false,
+ needsOffset( iconName, iconsThatNeedOffsetX ) ? 'needs-offset-x' : false,
+ needsOffset( iconName, iconsThatNeedOffsetY ) ? 'needs-offset-y' : false,
+ ]
+ : [];
+ const iconClass = classnames( 'gridicon', iconName, className, ...offsetClasses );
+
+ return (
+
+ );
+}
+
+Gridicon.propTypes = {
+ icon: PropTypes.string.isRequired,
+ size: PropTypes.number,
+ onClick: PropTypes.func,
+ className: PropTypes.string,
+};
+
+export default React.memo( Gridicon );
diff --git a/client/components/happiness-support/index.jsx b/client/components/happiness-support/index.jsx
index e0db08e4ff7e8f..c77a9471e92669 100644
--- a/client/components/happiness-support/index.jsx
+++ b/client/components/happiness-support/index.jsx
@@ -14,7 +14,7 @@ import { localize } from 'i18n-calypso';
* Internal dependencies
*/
import Button from 'components/button';
-import GridiconExternal from 'gridicons/dist/external';
+import Gridicon from 'components/gridicon';
import isHappychatAvailable from 'state/happychat/selectors/is-happychat-available';
import {
CALYPSO_CONTACT,
@@ -149,7 +149,7 @@ export class HappinessSupport extends Component {
rel="noopener noreferrer"
className="happiness-support__support-button"
>
-
+
{ this.props.translate( 'Support documentation' ) }
);
diff --git a/client/components/tinymce/plugins/advanced/plugin.jsx b/client/components/tinymce/plugins/advanced/plugin.jsx
index 63e3fb2cc06dfc..38cebeb0f377b7 100644
--- a/client/components/tinymce/plugins/advanced/plugin.jsx
+++ b/client/components/tinymce/plugins/advanced/plugin.jsx
@@ -8,7 +8,6 @@ import React from 'react';
import ReactDomServer from 'react-dom/server';
import tinymce from 'tinymce/tinymce';
import { translate } from 'i18n-calypso';
-import GridiconEllipsis from 'gridicons/dist/ellipsis';
/**
* Internal dependencies
@@ -16,6 +15,7 @@ import GridiconEllipsis from 'gridicons/dist/ellipsis';
import { isWithinBreakpoint } from 'lib/viewport';
import { savePreference, fetchPreferences } from 'state/preferences/actions';
import { getPreference, isFetchingPreferences } from 'state/preferences/selectors';
+import Gridicon from 'components/gridicon';
function advanced( editor ) {
const store = editor.getParam( 'redux_store' );
@@ -72,9 +72,10 @@ function advanced( editor ) {
this.innerHtml(
ReactDomServer.renderToStaticMarkup(
+ // eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
)
diff --git a/client/components/tinymce/plugins/contact-form/plugin.jsx b/client/components/tinymce/plugins/contact-form/plugin.jsx
index 9f4aca1ea67cc7..e5a575571e9de6 100644
--- a/client/components/tinymce/plugins/contact-form/plugin.jsx
+++ b/client/components/tinymce/plugins/contact-form/plugin.jsx
@@ -9,7 +9,6 @@ import i18n from 'i18n-calypso';
import React, { createElement } from 'react';
import { unmountComponentAtNode } from 'react-dom';
import { renderToStaticMarkup } from 'react-dom/server';
-import GridiconMention from 'gridicons/dist/mention';
/**
* Internal Dependencies
@@ -25,6 +24,7 @@ import {
} from 'state/ui/editor/contact-form/actions';
import { serialize, deserialize } from './shortcode-utils';
import { renderWithReduxStore } from 'lib/react-helpers';
+import Gridicon from 'components/gridicon';
function wpcomContactForm( editor ) {
let node;
@@ -102,7 +102,7 @@ function wpcomContactForm( editor ) {
renderToStaticMarkup(
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
)
);
diff --git a/client/components/tinymce/plugins/insert-menu/menu-items.jsx b/client/components/tinymce/plugins/insert-menu/menu-items.jsx
index bfc54d0ad72f87..af8865a565fd62 100644
--- a/client/components/tinymce/plugins/insert-menu/menu-items.jsx
+++ b/client/components/tinymce/plugins/insert-menu/menu-items.jsx
@@ -6,16 +6,12 @@
import React from 'react';
import i18n from 'i18n-calypso';
-import GridiconImage from 'gridicons/dist/image';
-import GridiconShutter from 'gridicons/dist/shutter';
-import GridiconMoney from 'gridicons/dist/money';
-import GridiconImageMultiple from 'gridicons/dist/image-multiple';
-import GridiconMention from 'gridicons/dist/mention';
/**
* Internal dependencies
*/
import config from 'config';
+import Gridicon from 'components/gridicon';
/* eslint-disable wpcalypso/jsx-classname-namespace */
export const GridiconButton = ( { icon, label, e2e } ) => (
@@ -32,7 +28,11 @@ export const menuItems = [
{
name: 'insert_media_item',
item: (
- } label={ i18n.translate( 'Media' ) } e2e="media" />
+ }
+ label={ i18n.translate( 'Media' ) }
+ e2e="media"
+ />
),
cmd: 'wpcomAddMedia',
},
@@ -44,7 +44,7 @@ if ( config.isEnabled( 'external-media' ) ) {
name: 'insert_from_google',
item: (
}
+ icon={ }
label={ i18n.translate( 'Google Photos library' ) }
e2e="google-media"
/>
@@ -57,7 +57,7 @@ if ( config.isEnabled( 'external-media' ) ) {
name: 'insert_from_pexels',
item: (
}
+ icon={ }
label={ i18n.translate( 'Free photo library' ) }
e2e="stock-media-pexels"
/>
@@ -71,7 +71,7 @@ menuItems.push( {
name: 'insert_contact_form',
item: (
}
+ icon={ }
label={ i18n.translate( 'Contact form' ) }
e2e="contact-form"
/>
@@ -83,7 +83,7 @@ menuItems.push( {
name: 'insert_payment_button',
item: (
}
+ icon={ }
label={ i18n.translate( 'Payment button' ) }
e2e="payment-button"
/>
@@ -96,7 +96,7 @@ if ( config.isEnabled( 'memberships' ) ) {
name: 'insert_memberships_button',
item: (
}
+ icon={ }
label={ i18n.translate( 'Recurring Payment' ) }
e2e="memberships"
/>
diff --git a/client/components/tinymce/plugins/insert-menu/plugin.jsx b/client/components/tinymce/plugins/insert-menu/plugin.jsx
index ed1660526052ee..d8cc10d99c5e6f 100644
--- a/client/components/tinymce/plugins/insert-menu/plugin.jsx
+++ b/client/components/tinymce/plugins/insert-menu/plugin.jsx
@@ -6,12 +6,12 @@ import React from 'react';
import tinymce from 'tinymce/tinymce';
import { renderToString } from 'react-dom/server';
import i18n from 'i18n-calypso';
-import GridiconAddOutline from 'gridicons/dist/add-outline';
/**
* Internal dependencies
*/
import { menuItems, GridiconButton } from './menu-items';
+import Gridicon from 'components/gridicon';
function initialize( editor ) {
menuItems.forEach( item =>
@@ -33,7 +33,10 @@ function initialize( editor ) {
const [ insertContentElm ] = this.$el[ 0 ].children;
insertContentElm.innerHTML = renderToString(
- } label={ i18n.translate( 'Add' ) } />
+ }
+ label={ i18n.translate( 'Add' ) }
+ />
);
},
} );
diff --git a/client/components/tinymce/plugins/media/advanced/index.jsx b/client/components/tinymce/plugins/media/advanced/index.jsx
index 600559e281b644..1da316621766b1 100644
--- a/client/components/tinymce/plugins/media/advanced/index.jsx
+++ b/client/components/tinymce/plugins/media/advanced/index.jsx
@@ -6,7 +6,6 @@ import React from 'react';
import ReactDom from 'react-dom';
import ReactDomServer from 'react-dom/server';
import i18n from 'i18n-calypso';
-import GridiconPencil from 'gridicons/dist/pencil';
/**
* Internal dependencies
@@ -15,6 +14,7 @@ import { deserialize } from 'lib/media-serialization';
import config from 'config';
import EditorMediaAdvanced from 'post-editor/editor-media-advanced';
import { renderWithReduxStore } from 'lib/react-helpers';
+import Gridicon from 'components/gridicon';
export default function( editor ) {
const store = editor.getParam( 'redux_store' );
@@ -70,8 +70,8 @@ export default function( editor ) {
onPostRender() {
this.innerHtml(
ReactDomServer.renderToStaticMarkup(
-