diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 639c4e4a191d9..71dcadd3ca951 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -575,8 +575,7 @@ function RichTextWrapper( value, onChange, onFocus, - editableProps, - editableTagName: TagName, + Editable, } ) => ( <> { children && children( { value, onChange, onFocus } ) } @@ -595,8 +594,7 @@ function RichTextWrapper( isSelected={ nestedIsSelected } > { ( { listBoxId, activeId, onKeyDown } ) => ( - { - onKeyDown( event ); - editableProps.onKeyDown( event ); - } } + onKeyDown={ onKeyDown } /> ) } diff --git a/packages/e2e-tests/specs/editor/various/undo.test.js b/packages/e2e-tests/specs/editor/various/undo.test.js index ac9864b981493..3f2f5d3b752d1 100644 --- a/packages/e2e-tests/specs/editor/various/undo.test.js +++ b/packages/e2e-tests/specs/editor/various/undo.test.js @@ -225,8 +225,8 @@ describe( 'undo', () => { expect( await getSelection() ).toEqual( { blockIndex: 2, editableIndex: 0, - startOffset: 'is'.length, - endOffset: 'is'.length, + startOffset: 0, + endOffset: 0, } ); await pressKeyWithModifier( 'primary', 'z' ); // Undo 2nd paragraph text. @@ -245,8 +245,8 @@ describe( 'undo', () => { expect( await getSelection() ).toEqual( { blockIndex: 1, editableIndex: 0, - startOffset: 'This'.length, - endOffset: 'This'.length, + startOffset: 0, + endOffset: 0, } ); await pressKeyWithModifier( 'primary', 'z' ); // Undo 1st paragraph text. diff --git a/packages/rich-text/src/component/use-boundary-style.js b/packages/rich-text/src/component/boundary-style.js similarity index 89% rename from packages/rich-text/src/component/use-boundary-style.js rename to packages/rich-text/src/component/boundary-style.js index 2636007521b1e..3c17da5369704 100644 --- a/packages/rich-text/src/component/use-boundary-style.js +++ b/packages/rich-text/src/component/boundary-style.js @@ -7,7 +7,7 @@ import { useEffect } from '@wordpress/element'; * Calculates and renders the format boundary style when the active formats * change. */ -export function useBoundaryStyle( { activeFormats, ref } ) { +export function BoundaryStyle( { activeFormats, forwardedRef } ) { useEffect( () => { // There's no need to recalculate the boundary styles if no formats are // active, because no boundary styles will be visible. @@ -16,7 +16,7 @@ export function useBoundaryStyle( { activeFormats, ref } ) { } const boundarySelector = '*[data-rich-text-format-boundary]'; - const element = ref.current.querySelector( boundarySelector ); + const element = forwardedRef.current.querySelector( boundarySelector ); if ( ! element ) { return; @@ -45,4 +45,5 @@ export function useBoundaryStyle( { activeFormats, ref } ) { globalStyle.innerHTML = style; } }, [ activeFormats ] ); + return null; } diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 7a392dcc956ef..cd174b2baaabb 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -7,14 +7,7 @@ import { find, isNil, pickBy, startsWith } from 'lodash'; /** * WordPress dependencies */ -import { - forwardRef, - useEffect, - useRef, - useState, - useMemo, - useLayoutEffect, -} from '@wordpress/element'; +import { Component, forwardRef } from '@wordpress/element'; import { BACKSPACE, DELETE, @@ -24,6 +17,8 @@ import { SPACE, ESCAPE, } from '@wordpress/keycodes'; +import { withSafeTimeout, compose } from '@wordpress/compose'; +import isShallowEqual from '@wordpress/is-shallow-equal'; import deprecated from '@wordpress/deprecated'; /** @@ -43,8 +38,8 @@ import { updateFormats } from '../update-formats'; import { removeLineSeparator } from '../remove-line-separator'; import { isEmptyLine } from '../is-empty'; import withFormatTypes from './with-format-types'; -import { useBoundaryStyle } from './use-boundary-style'; -import { useInlineWarning } from './use-inline-warning'; +import { BoundaryStyle } from './boundary-style'; +import { InlineWarning } from './inline-warning'; import { insert } from '../insert'; /** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ @@ -137,153 +132,90 @@ function fixPlaceholderSelection( defaultView ) { selection.collapseToStart(); } -function RichText( { - tagName: TagName = 'div', - value = '', - selectionStart, - selectionEnd, - children, - allowedFormats, - withoutInteractiveFormatting, - formatTypes, - style, - className, - placeholder, - disabled, - preserveWhiteSpace, - onPaste, - format = 'string', - onDelete, - onEnter, - onSelectionChange, - onChange, - unstableOnFocus: onFocus, - setFocusedElement, - instanceId, - __unstableMultilineTag: multilineTag, - __unstableMultilineRootTag: multilineRootTag, - __unstableDisableFormats: disableFormats, - __unstableDidAutomaticChange: didAutomaticChange, - __unstableInputRule: inputRule, - __unstableMarkAutomaticChange: markAutomaticChange, - __unstableAllowPrefixTransformations: allowPrefixTransformations, - __unstableUndo: undo, - __unstableIsCaretWithinFormattedText: isCaretWithinFormattedText, - __unstableOnEnterFormattedText: onEnterFormattedText, - __unstableOnExitFormattedText: onExitFormattedText, - __unstableOnCreateUndoLevel: onCreateUndoLevel, - __unstableIsSelected: isSelected, - forwardedRef: ref, - ...remainingProps -} ) { - const [ activeFormats = [], setActiveFormats ] = useState(); - - function getDoc() { - return ref.current.ownerDocument; - } - - function getWin() { - return getDoc().defaultView; - } - - /** - * Converts the outside data structure to our internal representation. - * - * @param {*} string The outside value, data type depends on props. - * - * @return {Object} An internal rich-text value. - */ - function formatToValue( string ) { - if ( disableFormats ) { - return { - text: string, - formats: Array( string.length ), - replacements: Array( string.length ), - }; - } - - if ( format !== 'string' ) { - return string; - } - - const prepare = createPrepareEditableTree( - remainingProps, - 'format_value_functions' +/** + * See export statement below. + */ +class RichText extends Component { + constructor( { value, selectionStart, selectionEnd } ) { + super( ...arguments ); + + this.getDocument = this.getDocument.bind( this ); + this.getWindow = this.getWindow.bind( this ); + this.onFocus = this.onFocus.bind( this ); + this.onBlur = this.onBlur.bind( this ); + this.onChange = this.onChange.bind( this ); + this.handleDelete = this.handleDelete.bind( this ); + this.handleEnter = this.handleEnter.bind( this ); + this.handleSpace = this.handleSpace.bind( this ); + this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( + this ); + this.onPaste = this.onPaste.bind( this ); + this.onCreateUndoLevel = this.onCreateUndoLevel.bind( this ); + this.onInput = this.onInput.bind( this ); + this.onCompositionStart = this.onCompositionStart.bind( this ); + this.onCompositionEnd = this.onCompositionEnd.bind( this ); + this.onSelectionChange = this.onSelectionChange.bind( this ); + this.createRecord = this.createRecord.bind( this ); + this.applyRecord = this.applyRecord.bind( this ); + this.valueToFormat = this.valueToFormat.bind( this ); + this.onPointerDown = this.onPointerDown.bind( this ); + this.formatToValue = this.formatToValue.bind( this ); + this.Editable = this.Editable.bind( this ); + + this.onKeyDown = ( event ) => { + if ( event.defaultPrevented ) { + return; + } - const result = create( { - html: string, - multilineTag, - multilineWrapperTags: - multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, - preserveWhiteSpace, - } ); + this.handleDelete( event ); + this.handleEnter( event ); + this.handleSpace( event ); + this.handleHorizontalNavigation( event ); + }; - result.formats = prepare( result ); + this.state = {}; + this.lastHistoryValue = value; - return result; + // Internal values are updated synchronously, unlike props and state. + this.value = value; + this.record = this.formatToValue( value ); + this.record.start = selectionStart; + this.record.end = selectionEnd; } - /** - * Removes editor only formats from the value. - * - * Editor only formats are applied using `prepareEditableTree`, so we need to - * remove them before converting the internal state - * - * @param {Object} val The internal rich-text value. - * - * @return {Object} A new rich-text value. - */ - function removeEditorOnlyFormats( val ) { - formatTypes.forEach( ( formatType ) => { - // Remove formats created by prepareEditableTree, because they are editor only. - if ( formatType.__experimentalCreatePrepareEditableTree ) { - val = removeFormat( val, formatType.name, 0, val.text.length ); - } - } ); - - return val; + componentWillUnmount() { + this.getDocument().removeEventListener( + 'selectionchange', + this.onSelectionChange + ); + this.getWindow().cancelAnimationFrame( this.rafId ); } - /** - * Converts the internal value to the external data format. - * - * @param {Object} val The internal rich-text value. - * - * @return {*} The external data format, data type depends on props. - */ - function valueToFormat( val ) { - if ( disableFormats ) { - return val.text; - } - - val = removeEditorOnlyFormats( val ); + componentDidMount() { + this.applyRecord( this.record, { domOnly: true } ); + } - if ( format !== 'string' ) { - return; - } + getDocument() { + return this.props.forwardedRef.current.ownerDocument; + } - return toHTMLString( { value: val, multilineTag, preserveWhiteSpace } ); + getWindow() { + return this.getDocument().defaultView; } - // Internal values are updated synchronously, unlike props and state. - const _value = useRef( value ); - const record = useRef( - useMemo( () => { - const initialRecord = formatToValue( value ); - initialRecord.start = selectionStart; - initialRecord.end = selectionEnd; - return initialRecord; - }, [] ) - ); - - function createRecord() { - const selection = getWin().getSelection(); + createRecord() { + const { + __unstableMultilineTag: multilineTag, + forwardedRef, + preserveWhiteSpace, + } = this.props; + const selection = this.getWindow().getSelection(); const range = selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null; return create( { - element: ref.current, + element: forwardedRef.current, range, multilineTag, multilineWrapperTags: @@ -293,19 +225,24 @@ function RichText( { } ); } - function applyRecord( newRecord, { domOnly } = {} ) { + applyRecord( record, { domOnly } = {} ) { + const { + __unstableMultilineTag: multilineTag, + forwardedRef, + } = this.props; + apply( { - value: newRecord, - current: ref.current, + value: record, + current: forwardedRef.current, multilineTag, multilineWrapperTags: multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, prepareEditableTree: createPrepareEditableTree( - remainingProps, + this.props, 'format_prepare_functions' ), __unstableDomOnly: domOnly, - placeholder, + placeholder: this.props.placeholder, } ); } @@ -316,7 +253,15 @@ function RichText( { * * @param {ClipboardEvent} event The paste event. */ - function handlePaste( event ) { + onPaste( event ) { + const { + formatTypes, + onPaste, + __unstableIsSelected: isSelected, + __unstableDisableFormats, + } = this.props; + const { activeFormats = [] } = this.state; + if ( ! isSelected ) { event.preventDefault(); return; @@ -356,16 +301,17 @@ function RichText( { window.console.log( 'Received HTML:\n\n', html ); window.console.log( 'Received plain text:\n\n', plainText ); - if ( disableFormats ) { - handleChange( insert( record.current, plainText ) ); + if ( __unstableDisableFormats ) { + this.onChange( insert( this.record, plainText ) ); return; } + const record = this.record; const transformed = formatTypes.reduce( ( accumlator, { __unstablePasteRule } ) => { // Only allow one transform. - if ( __unstablePasteRule && accumlator === record.current ) { - accumlator = __unstablePasteRule( record.current, { + if ( __unstablePasteRule && accumlator === record ) { + accumlator = __unstablePasteRule( record, { html, plainText, } ); @@ -373,11 +319,11 @@ function RichText( { return accumlator; }, - record.current + record ); - if ( transformed !== record.current ) { - handleChange( transformed ); + if ( transformed !== record ) { + this.onChange( transformed ); return; } @@ -403,8 +349,8 @@ function RichText( { } ); onPaste( { - value: removeEditorOnlyFormats( record.current ), - onChange: handleChange, + value: this.removeEditorOnlyFormats( record ), + onChange: this.onChange, html, plainText, files, @@ -414,519 +360,579 @@ function RichText( { } /** - * Handles delete on keydown: - * - outdent list items, - * - delete content if everything is selected, - * - trigger the onDelete prop when selection is uncollapsed and at an edge. + * Handles a focus event on the contenteditable field, calling the + * `unstableOnFocus` prop callback if one is defined. The callback does not + * receive any arguments. * - * @param {WPSyntheticEvent} event A synthetic keyboard event. + * This is marked as a private API and the `unstableOnFocus` prop is not + * documented, as the current requirements where it is used are subject to + * future refactoring following `isSelected` handling. + * + * In contrast with `setFocusedElement`, this is only triggered in response + * to focus within the contenteditable field, whereas `setFocusedElement` + * is triggered on focus within any `RichText` descendent element. + * + * @see setFocusedElement + * + * @private */ - function handleDelete( event ) { - const { keyCode } = event; + onFocus() { + const { unstableOnFocus } = this.props; - if ( - keyCode !== DELETE && - keyCode !== BACKSPACE && - keyCode !== ESCAPE - ) { - return; + if ( unstableOnFocus ) { + unstableOnFocus(); } - if ( didAutomaticChange ) { - event.preventDefault(); - undo(); - return; + if ( ! this.props.__unstableIsSelected ) { + // We know for certain that on focus, the old selection is invalid. It + // will be recalculated on the next mouseup, keyup, or touchend event. + const index = undefined; + const activeFormats = EMPTY_ACTIVE_FORMATS; + + this.record = { + ...this.record, + start: index, + end: index, + activeFormats, + }; + this.props.onSelectionChange( index, index ); + this.setState( { activeFormats } ); + } else { + this.props.onSelectionChange( this.record.start, this.record.end ); + this.setState( { + activeFormats: getActiveFormats( + { + ...this.record, + activeFormats: undefined, + }, + EMPTY_ACTIVE_FORMATS + ), + } ); } - if ( keyCode === ESCAPE ) { - return; + // Update selection as soon as possible, which is at the next animation + // frame. The event listener for selection changes may be added too late + // at this point, but this focus event is still too early to calculate + // the selection. + this.rafId = this.getWindow().requestAnimationFrame( + this.onSelectionChange + ); + + this.getDocument().addEventListener( + 'selectionchange', + this.onSelectionChange + ); + + if ( this.props.setFocusedElement ) { + deprecated( 'wp.blockEditor.RichText setFocusedElement prop', { + alternative: 'selection state from the block editor store.', + } ); + this.props.setFocusedElement( this.props.instanceId ); } + } - const currentValue = createRecord(); - const { start, end, text } = currentValue; - const isReverse = keyCode === BACKSPACE; + onBlur() { + this.getDocument().removeEventListener( + 'selectionchange', + this.onSelectionChange + ); + } - // Always handle full content deletion ourselves. - if ( start === 0 && end !== 0 && end === text.length ) { - handleChange( remove( currentValue ) ); - event.preventDefault(); + /** + * Handle input on the next selection change event. + * + * @param {WPSyntheticEvent} event Synthetic input event. + */ + onInput( event ) { + // Do not trigger a change if characters are being composed. Browsers + // will usually emit a final `input` event when the characters are + // composed. + // As of December 2019, Safari doesn't support nativeEvent.isComposing. + if ( this.isComposing ) { return; } - if ( multilineTag ) { - let newValue; + let inputType; - // Check to see if we should remove the first item if empty. - if ( - isReverse && - currentValue.start === 0 && - currentValue.end === 0 && - isEmptyLine( currentValue ) - ) { - newValue = removeLineSeparator( currentValue, ! isReverse ); - } else { - newValue = removeLineSeparator( currentValue, isReverse ); - } + if ( event ) { + inputType = event.inputType; + } - if ( newValue ) { - handleChange( newValue ); - event.preventDefault(); - return; - } + if ( ! inputType && event && event.nativeEvent ) { + inputType = event.nativeEvent.inputType; } - // Only process delete if the key press occurs at an uncollapsed edge. + // The browser formatted something or tried to insert HTML. + // Overwrite it. It will be handled later by the format library if + // needed. if ( - ! onDelete || - ! isCollapsed( currentValue ) || - activeFormats.length || - ( isReverse && start !== 0 ) || - ( ! isReverse && end !== text.length ) + inputType && + ( inputType.indexOf( 'format' ) === 0 || + INSERTION_INPUT_TYPES_TO_IGNORE.has( inputType ) ) ) { + this.applyRecord( this.record ); return; } - onDelete( { isReverse, value: currentValue } ); - event.preventDefault(); - } + const value = this.createRecord(); + const { start, activeFormats = [] } = this.record; - /** - * Triggers the `onEnter` prop on keydown. - * - * @param {WPSyntheticEvent} event A synthetic keyboard event. - */ - function handleEnter( event ) { - if ( event.keyCode !== ENTER ) { + // Update the formats between the last and new caret position. + const change = updateFormats( { + value, + start, + end: value.start, + formats: activeFormats, + } ); + + this.onChange( change, { withoutHistory: true } ); + + const { + __unstableInputRule: inputRule, + __unstableMarkAutomaticChange: markAutomaticChange, + __unstableAllowPrefixTransformations: allowPrefixTransformations, + formatTypes, + setTimeout, + clearTimeout, + } = this.props; + + // Create an undo level when input stops for over a second. + clearTimeout( this.onInput.timeout ); + this.onInput.timeout = setTimeout( this.onCreateUndoLevel, 1000 ); + + // Only run input rules when inserting text. + if ( inputType !== 'insertText' ) { return; } - event.preventDefault(); + if ( allowPrefixTransformations && inputRule ) { + inputRule( change, this.valueToFormat ); + } - if ( ! onEnter ) { - return; + const transformed = formatTypes.reduce( + ( accumlator, { __unstableInputRule } ) => { + if ( __unstableInputRule ) { + accumlator = __unstableInputRule( accumlator ); + } + + return accumlator; + }, + change + ); + + if ( transformed !== change ) { + this.onCreateUndoLevel(); + this.onChange( { ...transformed, activeFormats } ); + markAutomaticChange(); } + } - onEnter( { - value: removeEditorOnlyFormats( createRecord() ), - onChange: handleChange, - shiftKey: event.shiftKey, - } ); + onCompositionStart() { + this.isComposing = true; + // Do not update the selection when characters are being composed as + // this rerenders the component and might distroy internal browser + // editing state. + this.getDocument().removeEventListener( + 'selectionchange', + this.onSelectionChange + ); + } + + onCompositionEnd() { + this.isComposing = false; + // Ensure the value is up-to-date for browsers that don't emit a final + // input event after composition. + this.onInput( { inputType: 'insertText' } ); + // Tracking selection changes can be resumed. + this.getDocument().addEventListener( + 'selectionchange', + this.onSelectionChange + ); } /** - * Indents list items on space keydown. + * Syncs the selection to local state. A callback for the `selectionchange` + * native events, `keyup`, `mouseup` and `touchend` synthetic events, and + * animation frames after the `focus` event. * - * @param {WPSyntheticEvent} event A synthetic keyboard event. + * @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event */ - function handleSpace( event ) { - const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; - + onSelectionChange( event ) { if ( - // Only override when no modifiers are pressed. - shiftKey || - altKey || - metaKey || - ctrlKey || - keyCode !== SPACE || - multilineTag !== 'li' + event.type !== 'selectionchange' && + ! this.props.__unstableIsSelected ) { return; } - const currentValue = createRecord(); + if ( this.props.disabled ) { + return; + } - if ( ! isCollapsed( currentValue ) ) { + // In case of a keyboard event, ignore selection changes during + // composition. + if ( this.isComposing ) { return; } - const { text, start } = currentValue; - const characterBefore = text[ start - 1 ]; + const { start, end, text } = this.createRecord(); + const value = this.record; - // The caret must be at the start of a line. - if ( characterBefore && characterBefore !== LINE_SEPARATOR ) { + // Fallback mechanism for IE11, which doesn't support the input event. + // Any input results in a selection change. + if ( text !== value.text ) { + this.onInput(); return; } - handleChange( - indentListItems( currentValue, { type: multilineRootTag } ) - ); - event.preventDefault(); - } + if ( start === value.start && end === value.end ) { + // Sometimes the browser may set the selection on the placeholder + // element, in which case the caret is not visible. We need to set + // the caret before the placeholder if that's the case. + if ( value.text.length === 0 && start === 0 ) { + fixPlaceholderSelection( this.getWindow() ); + } - /** - * Handles horizontal keyboard navigation when no modifiers are pressed. The - * navigation is handled separately to move correctly around format - * boundaries. - * - * @param {WPSyntheticEvent} event A synthetic keyboard event. - */ - function handleHorizontalNavigation( event ) { - const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; - - if ( - // Only override left and right keys without modifiers pressed. - shiftKey || - altKey || - metaKey || - ctrlKey || - ( keyCode !== LEFT && keyCode !== RIGHT ) - ) { return; } const { - text, - formats, + __unstableIsCaretWithinFormattedText: isCaretWithinFormattedText, + __unstableOnEnterFormattedText: onEnterFormattedText, + __unstableOnExitFormattedText: onExitFormattedText, + } = this.props; + const newValue = { + ...value, start, end, - activeFormats: currentActiveFormats = [], - } = record.current; - const collapsed = isCollapsed( record.current ); - // To do: ideally, we should look at visual position instead. - const { direction } = getWin().getComputedStyle( ref.current ); - const reverseKey = direction === 'rtl' ? RIGHT : LEFT; - const isReverse = event.keyCode === reverseKey; + // Allow `getActiveFormats` to get new `activeFormats`. + activeFormats: undefined, + }; - // If the selection is collapsed and at the very start, do nothing if - // navigating backward. - // If the selection is collapsed and at the very end, do nothing if - // navigating forward. - if ( collapsed && currentActiveFormats.length === 0 ) { - if ( start === 0 && isReverse ) { - return; - } + const activeFormats = getActiveFormats( + newValue, + EMPTY_ACTIVE_FORMATS + ); - if ( end === text.length && ! isReverse ) { - return; - } - } + // Update the value with the new active formats. + newValue.activeFormats = activeFormats; - // If the selection is not collapsed, let the browser handle collapsing - // the selection for now. Later we could expand this logic to set - // boundary positions if needed. - if ( ! collapsed ) { - return; + if ( ! isCaretWithinFormattedText && activeFormats.length ) { + onEnterFormattedText(); + } else if ( isCaretWithinFormattedText && ! activeFormats.length ) { + onExitFormattedText(); } - // In all other cases, prevent default behaviour. - event.preventDefault(); - - const formatsBefore = formats[ start - 1 ] || EMPTY_ACTIVE_FORMATS; - const formatsAfter = formats[ start ] || EMPTY_ACTIVE_FORMATS; - - let newActiveFormatsLength = currentActiveFormats.length; - let source = formatsAfter; + // It is important that the internal value is updated first, + // otherwise the value will be wrong on render! + this.record = newValue; + this.applyRecord( newValue, { domOnly: true } ); + this.props.onSelectionChange( start, end ); + this.setState( { activeFormats } ); + } - if ( formatsBefore.length > formatsAfter.length ) { - source = formatsBefore; + /** + * Sync the value to global state. The node tree and selection will also be + * updated if differences are found. + * + * @param {Object} record The record to sync and apply. + * @param {Object} $2 Named options. + * @param {boolean} $2.withoutHistory If true, no undo level will be + * created. + */ + onChange( record, { withoutHistory } = {} ) { + if ( this.props.__unstableDisableFormats ) { + record.formats = Array( record.text.length ); + record.replacements = Array( record.text.length ); } - // If the amount of formats before the caret and after the caret is - // different, the caret is at a format boundary. - if ( formatsBefore.length < formatsAfter.length ) { - if ( - ! isReverse && - currentActiveFormats.length < formatsAfter.length - ) { - newActiveFormatsLength++; - } - - if ( - isReverse && - currentActiveFormats.length > formatsBefore.length - ) { - newActiveFormatsLength--; - } - } else if ( formatsBefore.length > formatsAfter.length ) { - if ( - ! isReverse && - currentActiveFormats.length > formatsAfter.length - ) { - newActiveFormatsLength--; - } - - if ( - isReverse && - currentActiveFormats.length < formatsBefore.length - ) { - newActiveFormatsLength++; - } - } + this.applyRecord( record ); - if ( newActiveFormatsLength !== currentActiveFormats.length ) { - const newActiveFormats = source.slice( 0, newActiveFormatsLength ); - const newValue = { - ...record.current, - activeFormats: newActiveFormats, - }; - record.current = newValue; - applyRecord( newValue ); - setActiveFormats( newActiveFormats ); - return; - } + const { start, end, activeFormats = [] } = record; + const changeHandlers = pickBy( this.props, ( v, key ) => + key.startsWith( 'format_on_change_functions_' ) + ); - const newPos = start + ( isReverse ? -1 : 1 ); - const newActiveFormats = isReverse ? formatsBefore : formatsAfter; - const newValue = { - ...record.current, - start: newPos, - end: newPos, - activeFormats: newActiveFormats, - }; + Object.values( changeHandlers ).forEach( ( changeHandler ) => { + changeHandler( record.formats, record.text ); + } ); - record.current = newValue; - applyRecord( newValue ); - onSelectionChange( newPos, newPos ); - setActiveFormats( newActiveFormats ); - } + this.value = this.valueToFormat( record ); + this.record = record; + // Selection must be updated first, so it is recorded in history when + // the content change happens. + this.props.onSelectionChange( start, end ); + this.props.onChange( this.value ); + this.setState( { activeFormats } ); - function handleKeyDown( event ) { - if ( event.defaultPrevented ) { - return; + if ( ! withoutHistory ) { + this.onCreateUndoLevel(); } - - handleDelete( event ); - handleEnter( event ); - handleSpace( event ); - handleHorizontalNavigation( event ); } - const lastHistoryValue = useRef( value ); - - function createUndoLevel() { + onCreateUndoLevel() { // If the content is the same, no level needs to be created. - if ( lastHistoryValue.current === _value.current ) { + if ( this.lastHistoryValue === this.value ) { return; } - onCreateUndoLevel(); - lastHistoryValue.current = _value.current; + this.props.__unstableOnCreateUndoLevel(); + this.lastHistoryValue = this.value; } - const isComposing = useRef( false ); - const timeout = useRef(); - /** - * Handle input on the next selection change event. + * Handles delete on keydown: + * - outdent list items, + * - delete content if everything is selected, + * - trigger the onDelete prop when selection is uncollapsed and at an edge. * - * @param {WPSyntheticEvent} event Synthetic input event. + * @param {WPSyntheticEvent} event A synthetic keyboard event. */ - function handleInput( event ) { - // Do not trigger a change if characters are being composed. Browsers - // will usually emit a final `input` event when the characters are - // composed. - // As of December 2019, Safari doesn't support nativeEvent.isComposing. - if ( isComposing.current ) { + handleDelete( event ) { + const { keyCode } = event; + + if ( + keyCode !== DELETE && + keyCode !== BACKSPACE && + keyCode !== ESCAPE + ) { return; } - let inputType; - - if ( event ) { - inputType = event.inputType; + if ( this.props.__unstableDidAutomaticChange ) { + event.preventDefault(); + this.props.__unstableUndo(); + return; } - if ( ! inputType && event && event.nativeEvent ) { - inputType = event.nativeEvent.inputType; + if ( keyCode === ESCAPE ) { + return; } - // The browser formatted something or tried to insert HTML. - // Overwrite it. It will be handled later by the format library if - // needed. - if ( - inputType && - ( inputType.indexOf( 'format' ) === 0 || - INSERTION_INPUT_TYPES_TO_IGNORE.has( inputType ) ) - ) { - applyRecord( record.current ); + const { onDelete, __unstableMultilineTag: multilineTag } = this.props; + const { activeFormats = [] } = this.state; + const value = this.createRecord(); + const { start, end, text } = value; + const isReverse = keyCode === BACKSPACE; + + // Always handle full content deletion ourselves. + if ( start === 0 && end !== 0 && end === text.length ) { + this.onChange( remove( value ) ); + event.preventDefault(); return; } - const currentValue = createRecord(); - const { start, activeFormats: oldActiveFormats = [] } = record.current; - - // Update the formats between the last and new caret position. - const change = updateFormats( { - value: currentValue, - start, - end: currentValue.start, - formats: oldActiveFormats, - } ); + if ( multilineTag ) { + let newValue; - handleChange( change, { withoutHistory: true } ); + // Check to see if we should remove the first item if empty. + if ( + isReverse && + value.start === 0 && + value.end === 0 && + isEmptyLine( value ) + ) { + newValue = removeLineSeparator( value, ! isReverse ); + } else { + newValue = removeLineSeparator( value, isReverse ); + } - // Create an undo level when input stops for over a second. - getWin().clearTimeout( timeout.current ); - timeout.current = getWin().setTimeout( createUndoLevel, 1000 ); + if ( newValue ) { + this.onChange( newValue ); + event.preventDefault(); + return; + } + } - // Only run input rules when inserting text. - if ( inputType !== 'insertText' ) { + // Only process delete if the key press occurs at an uncollapsed edge. + if ( + ! onDelete || + ! isCollapsed( value ) || + activeFormats.length || + ( isReverse && start !== 0 ) || + ( ! isReverse && end !== text.length ) + ) { return; } - if ( allowPrefixTransformations && inputRule ) { - inputRule( change, valueToFormat ); + onDelete( { isReverse, value } ); + event.preventDefault(); + } + + /** + * Triggers the `onEnter` prop on keydown. + * + * @param {WPSyntheticEvent} event A synthetic keyboard event. + */ + handleEnter( event ) { + if ( event.keyCode !== ENTER ) { + return; } - const transformed = formatTypes.reduce( - ( accumlator, { __unstableInputRule } ) => { - if ( __unstableInputRule ) { - accumlator = __unstableInputRule( accumlator ); - } + event.preventDefault(); - return accumlator; - }, - change - ); + const { onEnter } = this.props; - if ( transformed !== change ) { - createUndoLevel(); - handleChange( { ...transformed, activeFormats: oldActiveFormats } ); - markAutomaticChange(); + if ( ! onEnter ) { + return; } - } - function handleCompositionStart() { - isComposing.current = true; - // Do not update the selection when characters are being composed as - // this rerenders the component and might distroy internal browser - // editing state. - getDoc().removeEventListener( - 'selectionchange', - handleSelectionChange - ); - } - - function handleCompositionEnd() { - isComposing.current = false; - // Ensure the value is up-to-date for browsers that don't emit a final - // input event after composition. - handleInput( { inputType: 'insertText' } ); - // Tracking selection changes can be resumed. - getDoc().addEventListener( 'selectionchange', handleSelectionChange ); + onEnter( { + value: this.removeEditorOnlyFormats( this.createRecord() ), + onChange: this.onChange, + shiftKey: event.shiftKey, + } ); } - const didMount = useRef( false ); - /** - * Syncs the selection to local state. A callback for the `selectionchange` - * native events, `keyup`, `mouseup` and `touchend` synthetic events, and - * animation frames after the `focus` event. + * Indents list items on space keydown. * - * @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event + * @param {WPSyntheticEvent} event A synthetic keyboard event. */ - function handleSelectionChange( event ) { - if ( ! ref.current ) { - return; - } + handleSpace( event ) { + const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; + const { + __unstableMultilineRootTag: multilineRootTag, + __unstableMultilineTag: multilineTag, + } = this.props; - if ( document.activeElement !== ref.current ) { + if ( + // Only override when no modifiers are pressed. + shiftKey || + altKey || + metaKey || + ctrlKey || + keyCode !== SPACE || + multilineTag !== 'li' + ) { return; } - if ( event.type !== 'selectionchange' && ! isSelected ) { - return; - } + const value = this.createRecord(); - if ( disabled ) { + if ( ! isCollapsed( value ) ) { return; } - // In case of a keyboard event, ignore selection changes during - // composition. - if ( isComposing.current ) { + const { text, start } = value; + const characterBefore = text[ start - 1 ]; + + // The caret must be at the start of a line. + if ( characterBefore && characterBefore !== LINE_SEPARATOR ) { return; } - const { start, end, text } = createRecord(); - const oldRecord = record.current; + this.onChange( indentListItems( value, { type: multilineRootTag } ) ); + event.preventDefault(); + } - // Fallback mechanism for IE11, which doesn't support the input event. - // Any input results in a selection change. - if ( text !== oldRecord.text ) { - handleInput(); + /** + * Handles horizontal keyboard navigation when no modifiers are pressed. The + * navigation is handled separately to move correctly around format + * boundaries. + * + * @param {WPSyntheticEvent} event A synthetic keyboard event. + */ + handleHorizontalNavigation( event ) { + const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; + + if ( + // Only override left and right keys without modifiers pressed. + shiftKey || + altKey || + metaKey || + ctrlKey || + ( keyCode !== LEFT && keyCode !== RIGHT ) + ) { return; } - if ( start === oldRecord.start && end === oldRecord.end ) { - // Sometimes the browser may set the selection on the placeholder - // element, in which case the caret is not visible. We need to set - // the caret before the placeholder if that's the case. - if ( oldRecord.text.length === 0 && start === 0 ) { - fixPlaceholderSelection( getWin() ); + const value = this.record; + const { text, formats, start, end, activeFormats = [] } = value; + const collapsed = isCollapsed( value ); + // To do: ideally, we should look at visual position instead. + const { direction } = this.getWindow().getComputedStyle( + this.props.forwardedRef.current + ); + const reverseKey = direction === 'rtl' ? RIGHT : LEFT; + const isReverse = event.keyCode === reverseKey; + + // If the selection is collapsed and at the very start, do nothing if + // navigating backward. + // If the selection is collapsed and at the very end, do nothing if + // navigating forward. + if ( collapsed && activeFormats.length === 0 ) { + if ( start === 0 && isReverse ) { + return; } - return; + if ( end === text.length && ! isReverse ) { + return; + } } - const newValue = { - ...oldRecord, - start, - end, - // Allow `getActiveFormats` to get new `activeFormats`. - activeFormats: undefined, - }; - - const newActiveFormats = getActiveFormats( - newValue, - EMPTY_ACTIVE_FORMATS - ); + // If the selection is not collapsed, let the browser handle collapsing + // the selection for now. Later we could expand this logic to set + // boundary positions if needed. + if ( ! collapsed ) { + return; + } - // Update the value with the new active formats. - newValue.activeFormats = newActiveFormats; + // In all other cases, prevent default behaviour. + event.preventDefault(); - if ( ! isCaretWithinFormattedText && newActiveFormats.length ) { - onEnterFormattedText(); - } else if ( isCaretWithinFormattedText && ! newActiveFormats.length ) { - onExitFormattedText(); - } + const formatsBefore = formats[ start - 1 ] || EMPTY_ACTIVE_FORMATS; + const formatsAfter = formats[ start ] || EMPTY_ACTIVE_FORMATS; - // It is important that the internal value is updated first, - // otherwise the value will be wrong on render! - record.current = newValue; - applyRecord( newValue, { domOnly: true } ); - onSelectionChange( start, end ); - setActiveFormats( newActiveFormats ); - } + let newActiveFormatsLength = activeFormats.length; + let source = formatsAfter; - /** - * Sync the value to global state. The node tree and selection will also be - * updated if differences are found. - * - * @param {Object} newRecord The record to sync and apply. - * @param {Object} $2 Named options. - * @param {boolean} $2.withoutHistory If true, no undo level will be - * created. - */ - function handleChange( newRecord, { withoutHistory } = {} ) { - if ( disableFormats ) { - newRecord.formats = Array( newRecord.text.length ); - newRecord.replacements = Array( newRecord.text.length ); + if ( formatsBefore.length > formatsAfter.length ) { + source = formatsBefore; } - applyRecord( newRecord ); + // If the amount of formats before the caret and after the caret is + // different, the caret is at a format boundary. + if ( formatsBefore.length < formatsAfter.length ) { + if ( ! isReverse && activeFormats.length < formatsAfter.length ) { + newActiveFormatsLength++; + } - const { start, end, activeFormats: newActiveFormats = [] } = newRecord; - const changeHandlers = pickBy( remainingProps, ( v, key ) => - key.startsWith( 'format_on_change_functions_' ) - ); + if ( isReverse && activeFormats.length > formatsBefore.length ) { + newActiveFormatsLength--; + } + } else if ( formatsBefore.length > formatsAfter.length ) { + if ( ! isReverse && activeFormats.length > formatsAfter.length ) { + newActiveFormatsLength--; + } - Object.values( changeHandlers ).forEach( ( changeHandler ) => { - changeHandler( newRecord.formats, newRecord.text ); - } ); + if ( isReverse && activeFormats.length < formatsBefore.length ) { + newActiveFormatsLength++; + } + } - _value.current = valueToFormat( newRecord ); - record.current = newRecord; + if ( newActiveFormatsLength !== activeFormats.length ) { + const newActiveFormats = source.slice( 0, newActiveFormatsLength ); + const newValue = { ...value, activeFormats: newActiveFormats }; + this.record = newValue; + this.applyRecord( newValue ); + this.setState( { activeFormats: newActiveFormats } ); + return; + } - // Selection must be updated first, so it is recorded in history when - // the content change happens. - onSelectionChange( start, end ); - onChange( _value.current ); - setActiveFormats( newActiveFormats ); + const newPos = start + ( isReverse ? -1 : 1 ); + const newActiveFormats = isReverse ? formatsBefore : formatsAfter; + const newValue = { + ...value, + start: newPos, + end: newPos, + activeFormats: newActiveFormats, + }; - if ( ! withoutHistory ) { - createUndoLevel(); - } + this.record = newValue; + this.applyRecord( newValue ); + this.props.onSelectionChange( newPos, newPos ); + this.setState( { activeFormats: newActiveFormats } ); } /** @@ -935,18 +941,21 @@ function RichText( { * * @param {WPSyntheticEvent} event Synthetic mousedown or touchstart event. */ - function handlePointerDown( event ) { + onPointerDown( event ) { const { target } = event; // If the child element has no text content, it must be an object. - if ( target === ref.current || target.textContent ) { + if ( + target === this.props.forwardedRef.current || + target.textContent + ) { return; } const { parentNode } = target; const index = Array.from( parentNode.childNodes ).indexOf( target ); - const range = getDoc().createRange(); - const selection = getWin().getSelection(); + const range = this.getDocument().createRange(); + const selection = this.getWindow().getSelection(); range.setStart( target.parentNode, index ); range.setEnd( target.parentNode, index + 1 ); @@ -955,216 +964,268 @@ function RichText( { selection.addRange( range ); } - const rafId = useRef(); + componentDidUpdate( prevProps ) { + const { + tagName, + value, + selectionStart, + selectionEnd, + placeholder, + __unstableIsSelected: isSelected, + } = this.props; + + // Check if tag name changed. + let shouldReapply = tagName !== prevProps.tagName; + + // Check if the content changed. + shouldReapply = + shouldReapply || + ( value !== prevProps.value && value !== this.value ); + + const selectionChanged = + ( selectionStart !== prevProps.selectionStart && + selectionStart !== this.record.start ) || + ( selectionEnd !== prevProps.selectionEnd && + selectionEnd !== this.record.end ); + + // Check if the selection changed. + shouldReapply = + shouldReapply || + ( isSelected && ! prevProps.isSelected && selectionChanged ); + + const prefix = 'format_prepare_props_'; + const predicate = ( v, key ) => key.startsWith( prefix ); + const prepareProps = pickBy( this.props, predicate ); + const prevPrepareProps = pickBy( prevProps, predicate ); + + // Check if any format props changed. + shouldReapply = + shouldReapply || ! isShallowEqual( prepareProps, prevPrepareProps ); + + // Rerender if the placeholder changed. + shouldReapply = shouldReapply || placeholder !== prevProps.placeholder; + + if ( shouldReapply ) { + this.value = value; + this.record = this.formatToValue( value ); + this.record.start = selectionStart; + this.record.end = selectionEnd; + this.applyRecord( this.record ); + } else if ( selectionChanged ) { + this.record = { + ...this.record, + start: selectionStart, + end: selectionEnd, + }; + } + } /** - * Handles a focus event on the contenteditable field, calling the - * `unstableOnFocus` prop callback if one is defined. The callback does not - * receive any arguments. - * - * This is marked as a private API and the `unstableOnFocus` prop is not - * documented, as the current requirements where it is used are subject to - * future refactoring following `isSelected` handling. - * - * In contrast with `setFocusedElement`, this is only triggered in response - * to focus within the contenteditable field, whereas `setFocusedElement` - * is triggered on focus within any `RichText` descendent element. - * - * @see setFocusedElement + * Converts the outside data structure to our internal representation. * - * @private + * @param {*} value The outside value, data type depends on props. + * @return {Object} An internal rich-text value. */ - function handleFocus() { - if ( onFocus ) { - onFocus(); - } - - if ( ! isSelected ) { - // We know for certain that on focus, the old selection is invalid. - // It will be recalculated on the next mouseup, keyup, or touchend - // event. - const index = undefined; + formatToValue( value ) { + const { + format, + __unstableMultilineTag: multilineTag, + preserveWhiteSpace, + __unstableDisableFormats: disableFormats, + } = this.props; - record.current = { - ...record.current, - start: index, - end: index, - activeFormats: EMPTY_ACTIVE_FORMATS, + if ( disableFormats ) { + return { + text: value, + formats: Array( value.length ), + replacements: Array( value.length ), }; - onSelectionChange( index, index ); - setActiveFormats( EMPTY_ACTIVE_FORMATS ); - } else { - onSelectionChange( record.current.start, record.current.end ); - setActiveFormats( - getActiveFormats( - { - ...record.current, - activeFormats: undefined, - }, - EMPTY_ACTIVE_FORMATS - ) - ); } - // Update selection as soon as possible, which is at the next animation - // frame. The event listener for selection changes may be added too late - // at this point, but this focus event is still too early to calculate - // the selection. - rafId.current = getWin().requestAnimationFrame( handleSelectionChange ); - - getDoc().addEventListener( 'selectionchange', handleSelectionChange ); - - if ( setFocusedElement ) { - deprecated( 'wp.blockEditor.RichText setFocusedElement prop', { - alternative: 'selection state from the block editor store.', - } ); - setFocusedElement( instanceId ); + if ( format !== 'string' ) { + return value; } - } - function handleBlur() { - getDoc().removeEventListener( - 'selectionchange', - handleSelectionChange + const prepare = createPrepareEditableTree( + this.props, + 'format_value_functions' ); - } - function applyFromProps() { - _value.current = value; - record.current = formatToValue( value ); - record.current.start = selectionStart; - record.current.end = selectionEnd; - applyRecord( record.current ); + value = create( { + html: value, + multilineTag, + multilineWrapperTags: + multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, + preserveWhiteSpace, + } ); + value.formats = prepare( value ); + + return value; } - useEffect( () => { - if ( didMount.current ) { - applyFromProps(); - } - }, [ TagName, placeholder ] ); + /** + * Removes editor only formats from the value. + * + * Editor only formats are applied using `prepareEditableTree`, so we need to + * remove them before converting the internal state + * + * @param {Object} value The internal rich-text value. + * @return {Object} A new rich-text value. + */ + removeEditorOnlyFormats( value ) { + this.props.formatTypes.forEach( ( formatType ) => { + // Remove formats created by prepareEditableTree, because they are editor only. + if ( formatType.__experimentalCreatePrepareEditableTree ) { + value = removeFormat( + value, + formatType.name, + 0, + value.text.length + ); + } + } ); - useEffect( () => { - if ( didMount.current && value !== _value.current ) { - applyFromProps(); - } - }, [ value ] ); + return value; + } - useEffect( () => { - if ( ! didMount.current ) { - return; - } + /** + * Converts the internal value to the external data format. + * + * @param {Object} value The internal rich-text value. + * @return {*} The external data format, data type depends on props. + */ + valueToFormat( value ) { + const { + format, + __unstableMultilineTag: multilineTag, + preserveWhiteSpace, + __unstableDisableFormats: disableFormats, + } = this.props; - if ( - isSelected && - ( selectionStart !== record.current.start || - selectionEnd !== record.current.end ) - ) { - applyFromProps(); - } else { - record.current = { - ...record.current, - start: selectionStart, - end: selectionEnd, - }; + if ( disableFormats ) { + return value.text; } - }, [ selectionStart, selectionEnd, isSelected ] ); - const prefix = 'format_prepare_props_'; - const predicate = ( v, key ) => key.startsWith( prefix ); - const prepareProps = pickBy( remainingProps, predicate ); + value = this.removeEditorOnlyFormats( value ); - useEffect( () => { - if ( didMount.current ) { - applyFromProps(); + if ( format !== 'string' ) { + return; } - }, Object.values( prepareProps ) ); - - useLayoutEffect( () => { - applyRecord( record.current, { domOnly: true } ); - didMount.current = true; + return toHTMLString( { value, multilineTag, preserveWhiteSpace } ); + } - return () => { - getDoc().removeEventListener( - 'selectionchange', - handleSelectionChange - ); - getWin().cancelAnimationFrame( rafId.current ); - getWin().clearTimeout( timeout.current ); - }; - }, [] ); + Editable( props ) { + const { + tagName: TagName = 'div', + style, + className, + placeholder, + forwardedRef, + disabled, + } = this.props; + const ariaProps = pickBy( this.props, ( value, key ) => + startsWith( key, 'aria-' ) + ); - function focus() { - ref.current.focus(); - applyRecord( record.current ); + return ( + { + props.onKeyDown( event ); + this.onKeyDown( event ); + } + : this.onKeyDown + } + onFocus={ this.onFocus } + onBlur={ this.onBlur } + onMouseDown={ this.onPointerDown } + onTouchStart={ this.onPointerDown } + // Selection updates must be done at these events as they + // happen before the `selectionchange` event. In some cases, + // the `selectionchange` event may not even fire, for + // example when the window receives focus again on click. + onKeyUp={ this.onSelectionChange } + onMouseUp={ this.onSelectionChange } + onTouchEnd={ this.onSelectionChange } + // Do not set the attribute if disabled. + contentEditable={ disabled ? undefined : true } + suppressContentEditableWarning={ ! disabled } + /> + ); } - const ariaProps = pickBy( remainingProps, ( v, key ) => - startsWith( key, 'aria-' ) - ); - - const editableProps = { - // Overridable props. - role: 'textbox', - 'aria-multiline': '', - 'aria-label': placeholder, - ...ariaProps, - ref, - style: style ? { ...style, whiteSpace } : defaultStyle, - className: classnames( 'rich-text', className ), - onPaste: handlePaste, - onInput: handleInput, - onCompositionStart: handleCompositionStart, - onCompositionEnd: handleCompositionEnd, - onKeyDown: handleKeyDown, - onFocus: handleFocus, - onBlur: handleBlur, - onMouseDown: handlePointerDown, - onTouchStart: handlePointerDown, - // Selection updates must be done at these events as they - // happen before the `selectionchange` event. In some cases, - // the `selectionchange` event may not even fire, for - // example when the window receives focus again on click. - onKeyUp: handleSelectionChange, - onMouseUp: handleSelectionChange, - onTouchEnd: handleSelectionChange, - // Do not set the attribute if disabled. - contentEditable: disabled ? undefined : true, - suppressContentEditableWarning: ! disabled, - }; - - useBoundaryStyle( { ref, activeFormats } ); - useInlineWarning( { ref } ); - - return ( - <> - { isSelected && ( - { + forwardedRef.current.focus(); + this.applyRecord( this.record ); + }; + + return ( + <> + - ) } - { children && - children( { - isSelected, - value: record.current, - onChange: handleChange, - onFocus: focus, - editableProps, - editableTagName: TagName, - } ) } - { ! children && } - - ); + + { isSelected && ( + + ) } + { children && + children( { + isSelected, + value: this.record, + onChange: this.onChange, + onFocus, + Editable: this.Editable, + } ) } + { ! children && } + + ); + } } -const RichTextWrapper = withFormatTypes( RichText ); +RichText.defaultProps = { + format: 'string', + value: '', +}; + +const RichTextWrapper = compose( [ withSafeTimeout, withFormatTypes ] )( + RichText +); /** * Renders a rich content input, providing users with the option to format the diff --git a/packages/rich-text/src/component/use-inline-warning.js b/packages/rich-text/src/component/inline-warning.js similarity index 82% rename from packages/rich-text/src/component/use-inline-warning.js rename to packages/rich-text/src/component/inline-warning.js index dfd3ba8de8515..00598b4d7c544 100644 --- a/packages/rich-text/src/component/use-inline-warning.js +++ b/packages/rich-text/src/component/inline-warning.js @@ -3,10 +3,10 @@ */ import { useEffect } from '@wordpress/element'; -export function useInlineWarning( { ref } ) { +export function InlineWarning( { forwardedRef } ) { useEffect( () => { if ( process.env.NODE_ENV === 'development' ) { - const target = ref.current; + const target = forwardedRef.current; const { defaultView } = target.ownerDocument; const computedStyle = defaultView.getComputedStyle( target ); @@ -18,4 +18,5 @@ export function useInlineWarning( { ref } ) { } } }, [] ); + return null; }