diff --git a/packages/components/src/dashicon/index.js b/packages/components/src/dashicon/index.js index e2f54deed1f9b2..97f90b70bf4fde 100644 --- a/packages/components/src/dashicon/index.js +++ b/packages/components/src/dashicon/index.js @@ -1,7 +1,7 @@ /** * @typedef OwnProps * - * @property {string} icon Icon name + * @property {import('./types').IconKey} icon Icon name * @property {string} [className] Class name */ /** @typedef {import('react').ComponentPropsWithoutRef<'span'> & OwnProps} Props */ diff --git a/packages/components/src/dashicon/types.ts b/packages/components/src/dashicon/types.ts new file mode 100644 index 00000000000000..a8be986a606a3a --- /dev/null +++ b/packages/components/src/dashicon/types.ts @@ -0,0 +1,345 @@ +export type IconKey = + | 'admin-appearance' + | 'admin-collapse' + | 'admin-comments' + | 'admin-customizer' + | 'admin-generic' + | 'admin-home' + | 'admin-links' + | 'admin-media' + | 'admin-multisite' + | 'admin-network' + | 'admin-page' + | 'admin-plugins' + | 'admin-post' + | 'admin-settings' + | 'admin-site-alt' + | 'admin-site-alt2' + | 'admin-site-alt3' + | 'admin-site' + | 'admin-tools' + | 'admin-users' + | 'airplane' + | 'album' + | 'align-center' + | 'align-full-width' + | 'align-left' + | 'align-none' + | 'align-pull-left' + | 'align-pull-right' + | 'align-right' + | 'align-wide' + | 'amazon' + | 'analytics' + | 'archive' + | 'arrow-down-alt' + | 'arrow-down-alt2' + | 'arrow-down' + | 'arrow-left-alt' + | 'arrow-left-alt2' + | 'arrow-left' + | 'arrow-right-alt' + | 'arrow-right-alt2' + | 'arrow-right' + | 'arrow-up-alt' + | 'arrow-up-alt2' + | 'arrow-up' + | 'arrow-up-duplicate' + | 'art' + | 'awards' + | 'backup' + | 'bank' + | 'beer' + | 'bell' + | 'block-default' + | 'book-alt' + | 'book' + | 'buddicons-activity' + | 'buddicons-bbpress-logo' + | 'buddicons-buddypress-logo' + | 'buddicons-community' + | 'buddicons-forums' + | 'buddicons-friends' + | 'buddicons-groups' + | 'buddicons-pm' + | 'buddicons-replies' + | 'buddicons-topics' + | 'buddicons-tracking' + | 'building' + | 'businessman' + | 'businessperson' + | 'businesswoman' + | 'button' + | 'calculator' + | 'camera-alt' + | 'car' + | 'calendar-alt' + | 'calendar' + | 'camera' + | 'carrot' + | 'cart' + | 'category' + | 'chart-area' + | 'chart-bar' + | 'chart-line' + | 'chart-pie' + | 'clipboard' + | 'clock' + | 'cloud-saved' + | 'cloud-upload' + | 'cloud' + | 'columns' + | 'code-standards' + | 'coffee' + | 'color-picker' + | 'controls-back' + | 'controls-forward' + | 'controls-pause' + | 'controls-play' + | 'controls-repeat' + | 'controls-skipback' + | 'controls-skipforward' + | 'controls-volumeoff' + | 'controls-volumeon' + | 'cover-image' + | 'dashboard' + | 'database' + | 'database-add' + | 'database-export' + | 'database-import' + | 'database-remove' + | 'database-view' + | 'desktop' + | 'dismiss' + | 'download' + | 'drumstick' + | 'edit' + | 'edit-large' + | 'edit-page' + | 'editor-aligncenter' + | 'editor-alignleft' + | 'editor-alignright' + | 'editor-bold' + | 'editor-break' + | 'editor-code' + | 'editor-code-duplicate' + | 'editor-contract' + | 'editor-customchar' + | 'editor-expand' + | 'editor-help' + | 'editor-indent' + | 'editor-insertmore' + | 'editor-italic' + | 'editor-justify' + | 'editor-kitchensink' + | 'editor-ltr' + | 'editor-ol-rtl' + | 'editor-ol' + | 'editor-outdent' + | 'editor-paragraph' + | 'editor-paste-text' + | 'editor-paste-word' + | 'editor-quote' + | 'editor-removeformatting' + | 'editor-rtl' + | 'editor-spellcheck' + | 'editor-strikethrough' + | 'editor-table' + | 'editor-textcolor' + | 'editor-ul' + | 'editor-underline' + | 'editor-unlink' + | 'editor-video' + | 'ellipsis' + | 'email-alt' + | 'email-alt2' + | 'email' + | 'embed-audio' + | 'embed-generic' + | 'embed-photo' + | 'embed-post' + | 'embed-video' + | 'excerpt-view' + | 'exit' + | 'external' + | 'facebook-alt' + | 'facebook' + | 'feedback' + | 'filter' + | 'flag' + | 'food' + | 'format-aside' + | 'format-audio' + | 'format-chat' + | 'format-gallery' + | 'format-image' + | 'format-quote' + | 'format-status' + | 'format-video' + | 'forms' + | 'fullscreen-alt' + | 'fullscreen-exit-alt' + | 'games' + | 'google' + | 'googleplus' + | 'grid-view' + | 'groups' + | 'hammer' + | 'heading' + | 'heart' + | 'hidden' + | 'hourglass' + | 'html' + | 'id-alt' + | 'id' + | 'image-crop' + | 'image-filter' + | 'image-flip-horizontal' + | 'image-flip-vertical' + | 'image-rotate-left' + | 'image-rotate-right' + | 'image-rotate' + | 'images-alt' + | 'images-alt2' + | 'index-card' + | 'info-outline' + | 'info' + | 'insert-after' + | 'insert-before' + | 'insert' + | 'instagram' + | 'keyboard-hide' + | 'laptop' + | 'layout' + | 'leftright' + | 'lightbulb' + | 'list-view' + | 'linkedin' + | 'location-alt' + | 'location' + | 'lock-duplicate' + | 'lock' + | 'marker' + | 'media-archive' + | 'media-audio' + | 'media-code' + | 'media-default' + | 'media-document' + | 'media-interactive' + | 'media-spreadsheet' + | 'media-text' + | 'media-video' + | 'megaphone' + | 'menu-alt' + | 'menu-alt2' + | 'menu-alt3' + | 'menu' + | 'money-alt' + | 'microphone' + | 'migrate' + | 'minus' + | 'money' + | 'move' + | 'nametag' + | 'networking' + | 'no-alt' + | 'no' + | 'open-folder' + | 'palmtree' + | 'paperclip' + | 'performance' + | 'pets' + | 'pdf' + | 'phone' + | 'pinterest' + | 'playlist-audio' + | 'playlist-video' + | 'plus-alt' + | 'plus-light' + | 'plus' + | 'portfolio' + | 'post-status' + | 'pressthis' + | 'products' + | 'plugins-checked' + | 'plus-alt2' + | 'podio' + | 'printer' + | 'privacy' + | 'randomize' + | 'reddit' + | 'redo' + | 'remove' + | 'rest-api' + | 'rss' + | 'saved' + | 'schedule' + | 'screenoptions' + | 'search' + | 'share-alt' + | 'share-alt2' + | 'share' + | 'shield-alt' + | 'shield' + | 'shortcode' + | 'slides' + | 'smartphone' + | 'smiley' + | 'sort' + | 'sos' + | 'star-empty' + | 'star-filled' + | 'star-half' + | 'sticky' + | 'store' + | 'spotify' + | 'superhero' + | 'superhero-alt' + | 'table-col-after' + | 'table-col-before' + | 'table-col-delete' + | 'table-row-after' + | 'table-row-before' + | 'table-row-delete' + | 'tablet' + | 'tag' + | 'tagcloud' + | 'testimonial' + | 'text' + | 'text-page' + | 'thumbs-down' + | 'thumbs-up' + | 'tickets-alt' + | 'tickets' + | 'tide' + | 'translation' + | 'trash' + | 'twitch' + | 'twitter' + | 'twitter-alt' + | 'undo' + | 'universal-access-alt' + | 'universal-access' + | 'unlock' + | 'update-alt' + | 'update' + | 'upload' + | 'vault' + | 'video-alt' + | 'video-alt2' + | 'video-alt3' + | 'visibility' + | 'warning' + | 'welcome-add-page' + | 'welcome-comments' + | 'welcome-learn-more' + | 'welcome-view-site' + | 'welcome-widgets-menus' + | 'welcome-write-blog' + | 'whatsapp' + | 'wordpress-alt' + | 'wordpress' + | 'xing' + | 'yes-alt' + | 'yes' + | 'youtube'; diff --git a/packages/components/src/icon/index.js b/packages/components/src/icon/index.js deleted file mode 100644 index 225964a2e79aee..00000000000000 --- a/packages/components/src/icon/index.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * WordPress dependencies - */ -import { - cloneElement, - createElement, - Component, - isValidElement, -} from '@wordpress/element'; -import { SVG } from '@wordpress/primitives'; - -/** - * Internal dependencies - */ -import Dashicon from '../dashicon'; - -function Icon( { icon = null, size, ...additionalProps } ) { - if ( 'string' === typeof icon ) { - return ; - } - - if ( icon && Dashicon === icon.type ) { - return cloneElement( icon, { - ...additionalProps, - } ); - } - - // Icons should be 24x24 by default. - const iconSize = size || 24; - if ( 'function' === typeof icon ) { - if ( icon.prototype instanceof Component ) { - return createElement( icon, { - size: iconSize, - ...additionalProps, - } ); - } - - return icon( { size: iconSize, ...additionalProps } ); - } - - if ( icon && ( icon.type === 'svg' || icon.type === SVG ) ) { - const appliedProps = { - width: iconSize, - height: iconSize, - ...icon.props, - ...additionalProps, - }; - - return ; - } - - if ( isValidElement( icon ) ) { - return cloneElement( icon, { - size: iconSize, - ...additionalProps, - } ); - } - - return icon; -} - -export default Icon; diff --git a/packages/components/src/icon/index.tsx b/packages/components/src/icon/index.tsx new file mode 100644 index 00000000000000..e4f10d0e7fdbe5 --- /dev/null +++ b/packages/components/src/icon/index.tsx @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import type { ComponentType, HTMLProps, SVGProps } from 'react'; + +/** + * WordPress dependencies + */ +import { + cloneElement, + createElement, + Component, + isValidElement, +} from '@wordpress/element'; +import { SVG } from '@wordpress/primitives'; + +/** + * Internal dependencies + */ +import Dashicon from '../dashicon'; +import type { IconKey as DashiconIconKey } from '../dashicon/types'; + +type IconType< P > = DashiconIconKey | ComponentType< P > | JSX.Element; + +interface BaseProps< P > { + /** + * The icon to render. Supported values are: Dashicons (specified as + * strings), functions, Component instances and `null`. + * + * @default null + */ + icon?: IconType< P > | null; + /** + * The size (width and height) of the icon. + * + * @default 24 + */ + size?: number; +} + +type AdditionalProps< T > = T extends ComponentType< infer U > + ? U + : T extends DashiconIconKey + ? SVGProps< SVGSVGElement > + : {}; + +export type Props< P > = BaseProps< P > & AdditionalProps< IconType< P > >; + +function Icon< P >( { + icon = null, + size = 24, + ...additionalProps +}: Props< P > ) { + if ( 'string' === typeof icon ) { + return ( + ) } + /> + ); + } + + if ( isValidElement( icon ) && Dashicon === icon.type ) { + return cloneElement( icon, { + ...additionalProps, + } ); + } + + if ( 'function' === typeof icon ) { + if ( icon.prototype instanceof Component ) { + return createElement( icon, ( { + size, + ...additionalProps, + } as unknown ) as P ); + } + + return ( icon as ( ...args: any[] ) => JSX.Element )( { + size, + ...additionalProps, + } ); + } + + if ( icon && ( icon.type === 'svg' || icon.type === SVG ) ) { + const appliedProps = { + width: size, + height: size, + ...icon.props, + ...additionalProps, + }; + + return ; + } + + if ( isValidElement( icon ) ) { + return cloneElement( icon, { + // @ts-ignore Just forwarding the size prop along + size, + ...additionalProps, + } ); + } + + return icon; +} + +export default Icon; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index ca00ff32d3db98..e7f194dfb19cbd 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -27,6 +27,7 @@ "src/flex/**/*", "src/form-group/**/*", "src/heading/**/*", + "src/icon/**/*", "src/scroll-lock/**/*", "src/shortcut/**/*", "src/spinner/**/*",