From 5bb6d4123aee6028c4ee20668f39996ee7a9ab00 Mon Sep 17 00:00:00 2001 From: James Amner Date: Thu, 20 Jun 2024 09:04:26 +0100 Subject: [PATCH] [FEATURE] Add an icon block --- .../includes/IconographyService.php | 12 ++++ .../src/{AddReactionOutlined.svg => Icon.svg} | 0 .../iconography/src/block/Attributes.type.ts | 4 +- packages/iconography/src/block/Edit.tsx | 70 +++++++------------ packages/iconography/src/block/Save.tsx | 7 +- packages/iconography/src/block/block.json | 44 ++++++++---- packages/iconography/src/block/index.ts | 14 ++++ packages/iconography/src/block/style.scss | 3 + packages/iconography/src/index.ts | 14 ++++ ...ndex.tsx => registerInlineIconography.tsx} | 29 ++------ .../src/{ => shared}/IconModal.tsx | 2 +- .../src/{tests => shared}/IconPanel.test.tsx | 2 +- .../src/{ => shared}/IconPanel.tsx | 4 +- .../src/{ => shared}/IconToolbarButton.tsx | 21 +++--- .../src/{ => shared}/PlaceholderIconPanel.tsx | 2 +- .../__snapshots__/IconPanel.test.tsx.snap | 0 packages/iconography/src/shared/index.ts | 6 ++ ...t.ts => registerInlineIconography.test.ts} | 10 ++- packages/iconography/src/tests/utils.test.ts | 2 +- packages/iconography/svg.d.ts | 8 --- .../tests/TestIcononographyService.php | 20 ++++++ packages/iconography/webpack.config.js | 9 +++ 22 files changed, 172 insertions(+), 111 deletions(-) rename packages/iconography/src/{AddReactionOutlined.svg => Icon.svg} (100%) create mode 100644 packages/iconography/src/block/index.ts create mode 100644 packages/iconography/src/block/style.scss create mode 100644 packages/iconography/src/index.ts rename packages/iconography/src/{index.tsx => registerInlineIconography.tsx} (74%) rename packages/iconography/src/{ => shared}/IconModal.tsx (97%) rename packages/iconography/src/{tests => shared}/IconPanel.test.tsx (97%) rename packages/iconography/src/{ => shared}/IconPanel.tsx (93%) rename packages/iconography/src/{ => shared}/IconToolbarButton.tsx (62%) rename packages/iconography/src/{ => shared}/PlaceholderIconPanel.tsx (95%) rename packages/iconography/src/{tests => shared}/__snapshots__/IconPanel.test.tsx.snap (100%) create mode 100644 packages/iconography/src/shared/index.ts rename packages/iconography/src/tests/{index.test.ts => registerInlineIconography.test.ts} (93%) delete mode 100644 packages/iconography/svg.d.ts create mode 100644 packages/iconography/webpack.config.js diff --git a/packages/iconography/includes/IconographyService.php b/packages/iconography/includes/IconographyService.php index dacaeff..c23292b 100644 --- a/packages/iconography/includes/IconographyService.php +++ b/packages/iconography/includes/IconographyService.php @@ -29,6 +29,7 @@ public function __construct( private ConfigurationParser $configuration_parser ) * Init Hooks */ public function init(): void { + add_action( 'init', array( $this, 'register_block' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'register_assets' ) ); add_action( 'wp_footer', array( $this, 'enqueue_assets' ), 1, 0 ); add_action( 'enqueue_block_assets', array( $this, 'register_assets' ), 1, 0 ); @@ -36,6 +37,17 @@ public function init(): void { add_action( 'enqueue_block_assets', array( $this, 'enqueue_all_assets' ) ); } + /** + * Register the block + * + * @return void + */ + public function register_block(): void { + register_block_type_from_metadata( + plugin_dir_path( __DIR__ ) . 'build/block' + ); + } + /** * Register all assets in WP * diff --git a/packages/iconography/src/AddReactionOutlined.svg b/packages/iconography/src/Icon.svg similarity index 100% rename from packages/iconography/src/AddReactionOutlined.svg rename to packages/iconography/src/Icon.svg diff --git a/packages/iconography/src/block/Attributes.type.ts b/packages/iconography/src/block/Attributes.type.ts index e8960e2..09828b7 100644 --- a/packages/iconography/src/block/Attributes.type.ts +++ b/packages/iconography/src/block/Attributes.type.ts @@ -1,7 +1,7 @@ import type { WPFormat } from '@wordpress/rich-text/build-types/register-format-type'; export type Attributes = Partial< { - className: WPFormat[ 'className' ]; - tagName: WPFormat[ 'tagName' ]; + iconClass: WPFormat[ 'className' ]; + iconTag: WPFormat[ 'tagName' ]; iconContent: string; } >; diff --git a/packages/iconography/src/block/Edit.tsx b/packages/iconography/src/block/Edit.tsx index 7aebe25..254de1a 100644 --- a/packages/iconography/src/block/Edit.tsx +++ b/packages/iconography/src/block/Edit.tsx @@ -1,28 +1,27 @@ import React from 'react'; -import { IconToolbarButton } from '../IconToolbarButton'; -import { - BlockControls, - InspectorControls, - useBlockProps, -} from '@wordpress/block-editor'; +/* WordPress Dependencies */ +import { useBlockProps } from '@wordpress/block-editor'; import { store as RichTextStore } from '@wordpress/rich-text'; import { useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { Button, Panel, PanelBody } from '@wordpress/components'; +import { Icon, Spinner } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { replace } from '@wordpress/icons'; + +/* Internal deps */ +import { IconToolbarButton } from '../shared'; +import './style.scss'; /* Types */ import type { Attributes } from './Attributes.type'; import type { BlockEditProps } from '@wordpress/blocks'; import type { RichTextValue } from '@wordpress/rich-text'; import type { WPFormat } from '@wordpress/rich-text/build-types/register-format-type'; -import { IconModal } from '../IconModal'; export const Edit = ( { attributes, setAttributes, }: BlockEditProps< Attributes > ) => { - const [ showIconModal, setShowIconModal ] = useState( false ); const blockProps = useBlockProps(); const { getFormatType } = useSelect( ( select ) => @@ -42,52 +41,35 @@ export const Edit = ( { setAttributes( { iconContent: value.text, - tagName: format.tagName, - className: format.className, + iconTag: format.tagName, + iconClass: format.className, } ); }; const TagName = - ( attributes.tagName as keyof HTMLElementTagNameMap ) ?? 'span'; - - const ShowModalButton = () => ( - - ); + ( attributes.iconTag as keyof HTMLElementTagNameMap ) ?? 'span'; return ( <> - - - - - - - + } + onChange={ handleChange } + value={ { + text: '', + formats: [], + replacements: [], + start: 0, + end: 0, + } } + initialOpen={ ! attributes.iconContent } + />
{ attributes.iconContent && ( - + { attributes.iconContent } ) } - { ! attributes.iconContent && } - { showIconModal && ( - setShowIconModal( false ) } - onChange={ handleChange } - value={ { - text: '', - formats: [], - replacements: [], - start: 0, - end: 0, - } } - /> - ) } + { ! attributes.iconContent && }
); diff --git a/packages/iconography/src/block/Save.tsx b/packages/iconography/src/block/Save.tsx index f514a23..f377fc1 100644 --- a/packages/iconography/src/block/Save.tsx +++ b/packages/iconography/src/block/Save.tsx @@ -1,5 +1,6 @@ import React from 'react'; +/* WordPress Dependencies */ import { useBlockProps } from '@wordpress/block-editor'; /* Types */ @@ -7,12 +8,12 @@ import type { Attributes } from './Attributes.type'; import type { BlockSaveProps } from '@wordpress/blocks'; export const Save = ( { attributes }: BlockSaveProps< Attributes > ) => { - const { className, tagName, iconContent } = attributes; - const TagName = ( tagName as keyof HTMLElementTagNameMap ) ?? 'span'; + const { iconClass, iconTag, iconContent } = attributes; + const TagName = ( iconTag as keyof HTMLElementTagNameMap ) ?? 'span'; return (
- { iconContent } + { iconContent }
); }; diff --git a/packages/iconography/src/block/block.json b/packages/iconography/src/block/block.json index ad0c210..c17ba7e 100644 --- a/packages/iconography/src/block/block.json +++ b/packages/iconography/src/block/block.json @@ -8,24 +8,33 @@ "icon": "star-filled", "keywords": [ "icon", "emoji", "symbol" ], "textdomain": "boxuk", + "style": "file:./style-index.css", + "editorScript": "file:./index.ts", "supports": { - "align": [ "wide", "full" ], - "alignWide": true, + "anchor": false, + "align": [], + "alignWide": false, + "ariaLabel": true, + "background": { + "backgroundImage": false, + "backgroundSize": false + }, + "className": true, "color": { "background": true, "text": true }, - "background": { "backgroundImage": true, "backgroundSize": true }, - "anchor": false, - "ariaLabel": true, - "className": true, "customClassName": true, - "dimensions": { "aspectRatio": false, "minHeight": true }, - "filter": { "duotone": false }, + "dimensions": { + "aspectRatio": false, + "minHeight": false + }, + "filter": { + "duotone": false + }, "html": false, "inserter": true, "interactivity": false, - "layout": true, "lock": true, "multiple": true, "position": { @@ -33,26 +42,33 @@ }, "renaming": true, "reusable": true, - "shadow": false, + "shadow": true, "spacing": { "margin": true, - "padding": true + "padding": true, + "blockGrap": false }, "typography": { "fontSize": true, - "lineHeight": true, + "lineHeight": false, "textAlign": true } }, "attributes": { - "className": { + "iconClass": { "type": "string" }, - "tagName": { + "iconTag": { "type": "string" }, "iconContent": { "type": "string" + }, + "style": { + "type": "object", + "default": { + "textAlign": "center" + } } }, "example": {} diff --git a/packages/iconography/src/block/index.ts b/packages/iconography/src/block/index.ts new file mode 100644 index 0000000..4410280 --- /dev/null +++ b/packages/iconography/src/block/index.ts @@ -0,0 +1,14 @@ +/* WordPress Dependencies */ +import { registerBlockType } from '@wordpress/blocks'; + +/* Internal deps */ +import metadata from './block.json'; +import { Edit } from './Edit'; +import { Save } from './Save'; +import { ReactComponent as AddReactionOutlined } from '../Icon.svg'; + +registerBlockType( metadata, { + icon: AddReactionOutlined, + edit: Edit, + save: Save, +} ); diff --git a/packages/iconography/src/block/style.scss b/packages/iconography/src/block/style.scss new file mode 100644 index 0000000..8adde7c --- /dev/null +++ b/packages/iconography/src/block/style.scss @@ -0,0 +1,3 @@ +.wp-block-boxuk-icon { + text-align: center; +} diff --git a/packages/iconography/src/index.ts b/packages/iconography/src/index.ts new file mode 100644 index 0000000..5ca17b9 --- /dev/null +++ b/packages/iconography/src/index.ts @@ -0,0 +1,14 @@ +/* WordPress Dependencies */ +import domReady from '@wordpress/dom-ready'; + +/* Internal deps */ +import registerInlineIconography from './registerInlineIconography'; + +domReady( () => { + registerInlineIconography(); +} ); + +/* Export for other packages to consume */ +export * from './types'; +export * from './shared'; +export * from './utils'; diff --git a/packages/iconography/src/index.tsx b/packages/iconography/src/registerInlineIconography.tsx similarity index 74% rename from packages/iconography/src/index.tsx rename to packages/iconography/src/registerInlineIconography.tsx index 67c28d0..719b5aa 100644 --- a/packages/iconography/src/index.tsx +++ b/packages/iconography/src/registerInlineIconography.tsx @@ -1,22 +1,16 @@ import React, { ComponentProps } from 'react'; /* WordPress Dependencies */ -import domReady from '@wordpress/dom-ready'; import { registerFormatType } from '@wordpress/rich-text'; -import { registerBlockType } from '@wordpress/blocks'; -import { BlockControls } from '@wordpress/block-editor'; /* Internal deps */ -import metadata from './block/block.json'; -import { Edit } from './block/Edit'; -import { Save } from './block/Save'; -import { IconToolbarButton } from './IconToolbarButton'; +import { IconToolbarButton } from './shared'; import { getIconGroups, selectIconAtCurrentCursor } from './utils'; /* Types */ import type { IconGroup } from './types'; -export const handleKeyDown = +const handleKeyDown = ( iconGroups: IconGroup[] | undefined ) => ( event: KeyboardEvent ) => { switch ( event.key ) { case 'ArrowLeft': @@ -29,7 +23,7 @@ export const handleKeyDown = } }; -export const handleKeyUp = +const handleKeyUp = ( iconGroups: IconGroup[] | undefined ) => ( event: KeyboardEvent ) => { if ( 'ArrowRight' === event.key ) { const { selection, icon } = selectIconAtCurrentCursor( iconGroups ); @@ -55,7 +49,7 @@ export const handleKeyEvent = } }; -export const registerIconography = () => { +export const registerInlineIconography = () => { const iconGroups = getIconGroups(); if ( ! iconGroups ) { return; @@ -65,11 +59,7 @@ export const registerIconography = () => { if ( index === 0 ) { iconGroup.edit = ( props: ComponentProps< typeof IconToolbarButton > - ) => ( - - - - ); + ) => ; } registerFormatType( iconGroup.name, iconGroup ); } ); @@ -80,11 +70,4 @@ export const registerIconography = () => { document.addEventListener( 'keyup', handleKeyEvent( iconGroups ) ); }; -domReady( () => { - registerIconography(); -} ); - -registerBlockType( metadata, { - edit: Edit, - save: Save, -} ); +export default registerInlineIconography; diff --git a/packages/iconography/src/IconModal.tsx b/packages/iconography/src/shared/IconModal.tsx similarity index 97% rename from packages/iconography/src/IconModal.tsx rename to packages/iconography/src/shared/IconModal.tsx index 82ab1e5..3d76c20 100644 --- a/packages/iconography/src/IconModal.tsx +++ b/packages/iconography/src/shared/IconModal.tsx @@ -8,7 +8,7 @@ import { useState, useMemo } from '@wordpress/element'; /* Internal Dependencies */ import { PlaceholderIconPanel } from './PlaceholderIconPanel'; -import { getIconGroups } from './utils'; +import { getIconGroups } from '../utils'; /* Types */ import type { RichTextValue } from '@wordpress/rich-text'; diff --git a/packages/iconography/src/tests/IconPanel.test.tsx b/packages/iconography/src/shared/IconPanel.test.tsx similarity index 97% rename from packages/iconography/src/tests/IconPanel.test.tsx rename to packages/iconography/src/shared/IconPanel.test.tsx index 1e9eee0..e7ce27a 100644 --- a/packages/iconography/src/tests/IconPanel.test.tsx +++ b/packages/iconography/src/shared/IconPanel.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, jest, test } from '@jest/globals'; import { render, screen } from '@testing-library/react'; import { generateRichTextFormat } from '../utils'; -import { IconPanel } from '../IconPanel'; +import { IconPanel } from './IconPanel'; jest.mock( '../utils', () => ( { generateRichTextFormat: jest.fn(), diff --git a/packages/iconography/src/IconPanel.tsx b/packages/iconography/src/shared/IconPanel.tsx similarity index 93% rename from packages/iconography/src/IconPanel.tsx rename to packages/iconography/src/shared/IconPanel.tsx index 9e221ed..2c0b675 100644 --- a/packages/iconography/src/IconPanel.tsx +++ b/packages/iconography/src/shared/IconPanel.tsx @@ -8,10 +8,10 @@ import { } from '@wordpress/components'; /* Internal Dependencies */ -import { generateRichTextFormat } from './utils'; +import { generateRichTextFormat } from '../utils'; /* Types */ -import type { IconGroup } from './types'; +import type { IconGroup } from '../types'; import type { RichTextValue } from '@wordpress/rich-text'; export type IconPanelProps = { diff --git a/packages/iconography/src/IconToolbarButton.tsx b/packages/iconography/src/shared/IconToolbarButton.tsx similarity index 62% rename from packages/iconography/src/IconToolbarButton.tsx rename to packages/iconography/src/shared/IconToolbarButton.tsx index 299486f..6d0826b 100644 --- a/packages/iconography/src/IconToolbarButton.tsx +++ b/packages/iconography/src/shared/IconToolbarButton.tsx @@ -1,33 +1,38 @@ -import React from 'react'; +import React, { ComponentProps } from 'react'; /* WordPress Dependencies */ import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; +import { BlockControls } from '@wordpress/block-editor'; import { ToolbarButton } from '@wordpress/components'; /* Internal Dependencies */ -import { ReactComponent as AddReactionOutlined } from './AddReactionOutlined.svg'; +import { ReactComponent as Icon } from '../Icon.svg'; +import { IconModal } from './IconModal'; /* Types */ import type { RichTextValue } from '@wordpress/rich-text'; -import { IconModal } from './IconModal'; export type IconToolbarButtonProps = { onChange: ( value: RichTextValue ) => void; value: RichTextValue; + icon: ComponentProps< typeof ToolbarButton >[ 'icon' ]; + initialOpen?: boolean; }; export const IconToolbarButton = ( { onChange, value, + icon = , + initialOpen = false, }: IconToolbarButtonProps ) => { - const [ open, setOpen ] = useState( false ); + const [ open, setOpen ] = useState( initialOpen ); return ( - <> + } - label={ __( 'Add an icon', 'boxuk' ) } + icon={ icon } + label={ __( 'Select icon', 'boxuk' ) } onClick={ () => setOpen( ! open ) } /> { open && ( @@ -37,6 +42,6 @@ export const IconToolbarButton = ( { value={ value } /> ) } - + ); }; diff --git a/packages/iconography/src/PlaceholderIconPanel.tsx b/packages/iconography/src/shared/PlaceholderIconPanel.tsx similarity index 95% rename from packages/iconography/src/PlaceholderIconPanel.tsx rename to packages/iconography/src/shared/PlaceholderIconPanel.tsx index ea5f05c..c5566c6 100644 --- a/packages/iconography/src/PlaceholderIconPanel.tsx +++ b/packages/iconography/src/shared/PlaceholderIconPanel.tsx @@ -10,7 +10,7 @@ import { } from '@wordpress/components'; /* Types */ -import type { IconGroup } from './types'; +import type { IconGroup } from '../types'; import type { RichTextValue } from '@wordpress/rich-text'; export type IconPanelProps = { diff --git a/packages/iconography/src/tests/__snapshots__/IconPanel.test.tsx.snap b/packages/iconography/src/shared/__snapshots__/IconPanel.test.tsx.snap similarity index 100% rename from packages/iconography/src/tests/__snapshots__/IconPanel.test.tsx.snap rename to packages/iconography/src/shared/__snapshots__/IconPanel.test.tsx.snap diff --git a/packages/iconography/src/shared/index.ts b/packages/iconography/src/shared/index.ts new file mode 100644 index 0000000..42e0aa4 --- /dev/null +++ b/packages/iconography/src/shared/index.ts @@ -0,0 +1,6 @@ +import { IconModal } from './IconModal'; +import { IconPanel } from './IconPanel'; +import { IconToolbarButton } from './IconToolbarButton'; +import { PlaceholderIconPanel } from './PlaceholderIconPanel'; + +export { IconModal, IconPanel, IconToolbarButton, PlaceholderIconPanel }; diff --git a/packages/iconography/src/tests/index.test.ts b/packages/iconography/src/tests/registerInlineIconography.test.ts similarity index 93% rename from packages/iconography/src/tests/index.test.ts rename to packages/iconography/src/tests/registerInlineIconography.test.ts index 05e4409..487644b 100644 --- a/packages/iconography/src/tests/index.test.ts +++ b/packages/iconography/src/tests/registerInlineIconography.test.ts @@ -3,13 +3,16 @@ import { registerFormatType } from '@wordpress/rich-text'; /* Internal Dependencies */ import { selectIconAtCurrentCursor, getIconGroups } from '../utils'; -import { handleKeyEvent, registerIconography } from '../'; +import { + handleKeyEvent, + registerInlineIconography, +} from '../registerInlineIconography'; jest.mock( '@wordpress/rich-text', () => ( { registerFormatType: jest.fn(), } ) ); -jest.mock( '../IconToolbarButton', () => ( { +jest.mock( '../shared', () => ( { IconToolbarButton: jest.fn(), } ) ); @@ -26,6 +29,7 @@ jest.mock( '../utils', () => ( { describe( 'registering iconography', () => { test( 'should register all 3 types by default', () => { + registerInlineIconography(); expect( getIconGroups ).toBeCalledTimes( 1 ); // just by having imported the file, the registerFormatType should be called 3 times as per the 3 mocked values. expect( registerFormatType ).toBeCalledTimes( 3 ); @@ -35,7 +39,7 @@ describe( 'registering iconography', () => { jest.clearAllMocks(); getIconGroups.mockReturnValue( undefined ); expect( registerFormatType ).toBeCalledTimes( 0 ); - registerIconography(); + registerInlineIconography(); } ); } ); diff --git a/packages/iconography/src/tests/utils.test.ts b/packages/iconography/src/tests/utils.test.ts index 3a0d37d..003501f 100644 --- a/packages/iconography/src/tests/utils.test.ts +++ b/packages/iconography/src/tests/utils.test.ts @@ -9,7 +9,7 @@ import { IconGroup } from '../types'; jest.mock( '@wordpress/rich-text', () => ( {} ) ); -jest.mock( '../IconToolbarButton', () => ( { +jest.mock( '../shared', () => ( { IconToolbarButton: jest.fn(), } ) ); diff --git a/packages/iconography/svg.d.ts b/packages/iconography/svg.d.ts deleted file mode 100644 index 3222202..0000000 --- a/packages/iconography/svg.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare module '*.svg' { - import React = require('react'); - export const ReactComponent: React.FunctionComponent< - React.SVGProps< SVGSVGElement > - >; - const src: string; - export default src; -} diff --git a/packages/iconography/tests/TestIcononographyService.php b/packages/iconography/tests/TestIcononographyService.php index c9b87c6..ec1397c 100644 --- a/packages/iconography/tests/TestIcononographyService.php +++ b/packages/iconography/tests/TestIcononographyService.php @@ -25,6 +25,7 @@ class TestIcononographyService extends TestCase { */ public function testInit(): void { $class_in_test = new IconographyService( new ConfigurationParser() ); + \WP_Mock::expectActionAdded( 'init', array( $class_in_test, 'register_block' ) ); \WP_Mock::expectActionAdded( 'wp_enqueue_scripts', array( $class_in_test, 'register_assets' ) ); \WP_Mock::expectActionAdded( 'wp_footer', array( $class_in_test, 'enqueue_assets' ), 1, 0 ); \WP_Mock::expectActionAdded( 'enqueue_block_assets', array( $class_in_test, 'register_assets' ), 1, 0 ); @@ -35,6 +36,25 @@ public function testInit(): void { $this->assertConditionsMet(); } + /** + * Test Register Block + * + * @return void + */ + public function testRegisterBlock(): void { + \WP_Mock::userFunction( 'plugin_dir_path' ) + ->once() + ->andReturn( 'test/' ); + + \WP_Mock::userFunction( 'register_block_type_from_metadata' ) + ->once() + ->with( 'test/build/block' ); + + $class_in_test = new IconographyService( new ConfigurationParser() ); + $class_in_test->register_block(); + $this->assertConditionsMet(); + } + /** * Test Register Assets * diff --git a/packages/iconography/webpack.config.js b/packages/iconography/webpack.config.js new file mode 100644 index 0000000..d2df2dd --- /dev/null +++ b/packages/iconography/webpack.config.js @@ -0,0 +1,9 @@ +const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); + +module.exports = { + ...defaultConfig, + entry: { + ...defaultConfig.entry(), + index: './src/index.ts', + }, +};