Add RichTextEditor component using native block editor toolbar (#34865)

* Add initial rich text editor component

* Use fixed toolbar and add base formatters

* Add link as formatter option

* Fix up references to core/link block

* Add quote transform

* Add changelog entry

* Add text alignment toolbar

* Remove references to checkbox list

* Update toolbar button organization

* Remove unused rtj format types

* Create unique instance for editor writing flow

* Add registry provider for storybook examples

* Update styling for editor

* Rebase and fix lock file

* Add packages and type dependencies

* Move component to experimental

* Fix up formatting

* Update editor to use default GB toolbar

* Prefix names with woocoommerce

* Create block on initialization so toolbar is visible

* Rely on insertBlock to handle selection

* Update text editor to use setting instead of BlockList prop

* Fix up lock file after rebase

* Handle PR feedback

* Move logic for force rerender

* Fix up pnpm lock file

* Use trunk lock file

* Update lock file

* Add missing semicolon

* Use Pauls lock file and package file
This commit is contained in:
Joshua T Flowers 2022-10-12 14:15:55 -07:00 committed by GitHub
parent 0ca3f6d589
commit 618cc54a3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1357 additions and 198 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add rich text editor component

View File

@ -33,6 +33,10 @@
"@automattic/calypso-color-schemes": "^2.1.1",
"@automattic/interpolate-components": "^1.2.0",
"@automattic/tour-kit": "^1.1.1",
"@types/wordpress__block-editor": "^7.0.0",
"@types/wordpress__block-library": "^2.6.1",
"@types/wordpress__blocks": "^11.0.7",
"@types/wordpress__rich-text": "^3.4.6",
"@woocommerce/csv-export": "workspace:*",
"@woocommerce/currency": "workspace:*",
"@woocommerce/data": "workspace:*",
@ -40,8 +44,12 @@
"@woocommerce/navigation": "workspace:*",
"@wordpress/a11y": "3.5.0",
"@wordpress/api-fetch": "^6.0.1",
"@wordpress/block-editor": "^9.8.0",
"@wordpress/block-library": "^7.16.0",
"@wordpress/blocks": "^11.18.0",
"@wordpress/components": "^19.5.0",
"@wordpress/compose": "^5.1.2",
"@wordpress/core-data": "^4.2.1",
"@wordpress/date": "^4.3.1",
"@wordpress/deprecated": "^3.3.1",
"@wordpress/dom": "^3.3.2",
@ -50,8 +58,10 @@
"@wordpress/html-entities": "^3.3.1",
"@wordpress/i18n": "^4.3.1",
"@wordpress/icons": "^8.1.0",
"@wordpress/keyboard-shortcuts": "^3.17.0",
"@wordpress/keycodes": "^3.3.1",
"@wordpress/media-utils": "^4.6.0",
"@wordpress/rich-text": "^5.17.0",
"@wordpress/url": "^3.4.1",
"@wordpress/viewport": "^4.1.2",
"classnames": "^2.3.1",

View File

@ -34,6 +34,7 @@ export { default as ProductRating } from './rating/product';
export { default as Rating } from './rating';
export { default as ReportFilters } from './filters';
export { default as ReviewRating } from './rating/review';
export { RichTextEditor as __experimentalRichTextEditor } from './rich-text-editor';
export { default as Search } from './search';
export { default as SearchListControl } from './search-list-control';
export { default as SearchListItem } from './search-list-control/item';

View File

@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useInstanceId } from '@wordpress/compose';
import { createElement, useEffect } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
import {
BlockList,
ObserveTyping,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
BlockTools,
store as blockEditorStore,
WritingFlow,
} from '@wordpress/block-editor';
export const EditorWritingFlow: React.VFC = () => {
const instanceId = useInstanceId( EditorWritingFlow );
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This action is available in the block editor data store.
const { insertBlock } = useDispatch( blockEditorStore );
const { isEmpty } = useSelect( ( select ) => {
const blocks = select( 'core/block-editor' ).getBlocks();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This selector is available in the block editor data store.
const { getSelectedBlockClientIds } = select( blockEditorStore );
return {
isEmpty: blocks.length
? blocks.length <= 1 &&
blocks[ 0 ].attributes?.content?.trim() === ''
: true,
firstBlock: blocks[ 0 ],
selectedBlockClientIds: getSelectedBlockClientIds(),
};
} );
useEffect( () => {
if ( isEmpty ) {
const initialBlock = createBlock( 'core/paragraph', {
content: '',
} );
insertBlock( initialBlock );
}
}, [] );
return (
/* Gutenberg handles the keyboard events when focusing the content editable area. */
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
<div
className="woocommerce-rich-text-editor__writing-flow"
id={ `woocommerce-rich-text-editor__writing-flow-${ instanceId }` }
style={ {
cursor: isEmpty ? 'text' : 'initial',
} }
>
<BlockTools>
<WritingFlow>
<ObserveTyping>
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore This action is available in the block editor data store. */ }
<BlockList />
</ObserveTyping>
</WritingFlow>
</BlockTools>
</div>
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
);
};

View File

@ -0,0 +1 @@
export * from './rich-text-editor';

View File

@ -0,0 +1,84 @@
/**
* External dependencies
*/
import { BlockEditorProvider } from '@wordpress/block-editor';
import { BlockInstance } from '@wordpress/blocks';
import { SlotFillProvider } from '@wordpress/components';
import { debounce } from 'lodash';
import {
createElement,
useCallback,
useEffect,
useState,
useRef,
} from '@wordpress/element';
import React from 'react';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
/**
* Internal dependencies
*/
import { EditorWritingFlow } from './editor-writing-flow';
import { registerBlocks } from './utils/register-blocks';
registerBlocks();
type RichTextEditorProps = {
blocks: BlockInstance[];
onChange: ( changes: BlockInstance[] ) => void;
entryId?: string;
};
export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
blocks,
onChange,
} ) => {
const blocksRef = useRef( blocks );
const [ , setRefresh ] = useState( 0 );
// If there is a props change we need to update the ref and force re-render.
// Note: Because this component is memoized and because we don't re-render
// when this component initiates a change, a prop change won't force the re-render
// you'd expect. A change to the blocks must come from outside the editor.
const forceRerender = () => {
setRefresh( ( refresh ) => refresh + 1 );
};
useEffect( () => {
blocksRef.current = blocks;
forceRerender();
}, [ blocks ] );
const debounceChange = debounce( ( updatedBlocks ) => {
onChange( updatedBlocks );
blocksRef.current = updatedBlocks;
forceRerender();
}, 200 );
return (
<div className="woocommerce-rich-text-editor">
<SlotFillProvider>
<BlockEditorProvider
value={ blocksRef.current }
settings={ {
bodyPlaceholder: '',
hasFixedToolbar: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This property was recently added in the block editor data store.
__experimentalClearBlockSelection: false,
} }
onInput={ debounceChange }
onChange={ debounceChange }
>
<ShortcutProvider>
<EditorWritingFlow />
</ShortcutProvider>
</BlockEditorProvider>
</SlotFillProvider>
</div>
);
};

View File

@ -0,0 +1,50 @@
/**
* External dependencies
*/
import React from 'react';
import { createElement } from '@wordpress/element';
import { createRegistry, RegistryProvider } from '@wordpress/data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { store as coreDataStore } from '@wordpress/core-data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { store as blockEditorStore } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { RichTextEditor } from '../';
const registry = createRegistry();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
registry.register( coreDataStore );
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
registry.register( blockEditorStore );
export const Basic: React.FC = () => {
return (
<RegistryProvider value={ registry }>
<RichTextEditor blocks={ [] } onChange={ () => null } />
</RegistryProvider>
);
};
export const MultipleEditors: React.FC = () => {
return (
<RegistryProvider value={ registry }>
<RichTextEditor blocks={ [] } onChange={ () => null } />
<br />
<RichTextEditor blocks={ [] } onChange={ () => null } />
</RegistryProvider>
);
};
export default {
title: 'WooCommerce Admin/experimental/RichTextEditor',
component: RichTextEditor,
};

View File

@ -0,0 +1,73 @@
.woocommerce-rich-text-editor {
border: 1px solid $gray-600;
border-radius: 2px;
background: $white;
* {
box-sizing: border-box;
}
.block-editor-inserter {
display: none;
}
.block-editor-block-contextual-toolbar.is-fixed,
.block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar-group,
.block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar {
border-color: $gray-600;
}
/* Hide rich text placeholder text */
.rich-text [data-rich-text-placeholder] {
display: none !important;
}
/* hide block boundary background styling */
.rich-text:focus *[data-rich-text-format-boundary] {
background: none !important;
}
.block-editor-block-list__layout
.block-editor-block-list__block.is-multi-selected::after {
box-shadow: none;
outline: none;
}
.block-editor-block-list__layout
.block-editor-block-list__block:not([contenteditable]):focus::after {
box-shadow: none;
outline: none;
}
.block-editor-block-list__empty-block-inserter,
.block-editor-block-list__insertion-point {
display: none !important;
}
.block-editor-writing-flow {
padding: $gap-small;
}
/* Within the writing flow reset some of the css resets used elsewhere */
.block-editor-writing-flow ul,
.block-editor-writing-flow ol {
list-style: revert;
padding: revert;
}
.components-accessible-toolbar {
width: 100%;
background-color: $white;
border-color: $gray-700;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.wp-block-quote {
border-left: 0.25em solid currentColor;
margin: 0 0 1.75em 0;
padding-left: 1em;
cite {
font-size: 11px;
}
}
}

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { BlockInstance } from '@wordpress/blocks';
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We need this to import the block modules for registration.
__experimentalGetCoreBlocks,
registerCoreBlocks as wpRegisterCoreBlocks,
} from '@wordpress/block-library';
export const PARAGRAPH_BLOCK_ID = 'core/paragraph';
export const HEADING_BLOCK_ID = 'core/heading';
export const LIST_BLOCK_ID = 'core/list';
export const LIST_ITEM_BLOCK_ID = 'core/list-item';
export const QUOTE_BLOCK_ID = 'core/quote';
const ALLOWED_CORE_BLOCKS = [
PARAGRAPH_BLOCK_ID,
HEADING_BLOCK_ID,
LIST_BLOCK_ID,
LIST_ITEM_BLOCK_ID,
QUOTE_BLOCK_ID,
];
const registerCoreBlocks = () => {
const coreBlocks = __experimentalGetCoreBlocks();
const blocks = coreBlocks.filter( ( block: BlockInstance ) =>
ALLOWED_CORE_BLOCKS.includes( block.name )
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore An argument is allowed to specify which blocks to register.
wpRegisterCoreBlocks( blocks );
};
export const registerBlocks = () => {
registerCoreBlocks();
};

View File

@ -32,6 +32,7 @@
@import 'pill/style.scss';
@import 'product-image/style.scss';
@import 'rating/style.scss';
@import 'rich-text-editor/style.scss';
@import 'search/style.scss';
@import 'search-list-control/style.scss';
@import 'section-header/style.scss';

File diff suppressed because it is too large Load Diff