diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index 156240b130fa2..dd69d4a58dfe7 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -2,6 +2,7 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo ## 3.8.0 + - `wp.components.withContext` has been removed. Please use `wp.element.createContext` instead. See: https://reactjs.org/docs/context.html. - `wp.coreBlocks.registerCoreBlocks` has been removed. Please use `wp.blockLibrary.registerCoreBlocks` instead. ## 3.7.0 diff --git a/packages/blocks/src/block-content-provider/index.js b/packages/blocks/src/block-content-provider/index.js index 6829078682858..f7c15d27e1e8a 100644 --- a/packages/blocks/src/block-content-provider/index.js +++ b/packages/blocks/src/block-content-provider/index.js @@ -1,13 +1,16 @@ /** * WordPress dependencies */ -import { Component, RawHTML } from '@wordpress/element'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { createContext, RawHTML } from '@wordpress/element'; /** * Internal dependencies */ import { serialize } from '../api'; +const { Consumer, Provider } = createContext( () => {} ); + /** * An internal block component used in block content serialization to inject * nested block content within the `save` implementation of the ancestor @@ -22,29 +25,42 @@ import { serialize } from '../api'; * { blockSaveElement } * * ``` + * + * @return {WPElement} Element with BlockContent injected via context. */ -class BlockContentProvider extends Component { - getChildContext() { - const { innerBlocks } = this.props; - - return { - BlockContent() { - // Value is an array of blocks, so defer to block serializer - const html = serialize( innerBlocks ); - - // Use special-cased raw HTML tag to avoid default escaping - return { html }; - }, - }; - } +const BlockContentProvider = ( { children, innerBlocks } ) => { + const BlockContent = () => { + // Value is an array of blocks, so defer to block serializer + const html = serialize( innerBlocks ); - render() { - return this.props.children; - } -} + // Use special-cased raw HTML tag to avoid default escaping + return { html }; + }; -BlockContentProvider.childContextTypes = { - BlockContent: () => {}, + return ( + + { children } + + ); }; +/** + * A Higher Order Component used to inject BlockContent using context to the + * wrapped component. + * + * @return {Component} Enhanced component with injected BlockContent as prop. + */ +export const withBlockContentContext = createHigherOrderComponent( ( OriginalComponent ) => { + return ( props ) => ( + + { ( context ) => ( + + ) } + + ); +}, 'withBlockContentContext' ); + export default BlockContentProvider; diff --git a/packages/blocks/src/index.js b/packages/blocks/src/index.js index 2fed22406b3e9..bb7cde035780f 100644 --- a/packages/blocks/src/index.js +++ b/packages/blocks/src/index.js @@ -9,3 +9,4 @@ // and then stored as objects in state, from which it is then rendered for editing. import './store'; export * from './api'; +export { withBlockContentContext } from './block-content-provider'; diff --git a/packages/components/src/higher-order/with-context/index.js b/packages/components/src/higher-order/with-context/index.js index c10b24001358f..2efc7cf90db98 100644 --- a/packages/components/src/higher-order/with-context/index.js +++ b/packages/components/src/higher-order/with-context/index.js @@ -8,9 +8,17 @@ import { noop } from 'lodash'; */ import { Component } from '@wordpress/element'; import { createHigherOrderComponent } from '@wordpress/compose'; +import deprecated from '@wordpress/deprecated'; export default ( contextName ) => ( mapSettingsToProps ) => createHigherOrderComponent( ( OriginalComponent ) => { + deprecated( 'wp.components.withContext', { + version: '3.8', + alternative: 'wp.element.createContext', + plugin: 'Gutenberg', + hint: 'https://reactjs.org/docs/context.html', + } ); + class WrappedComponent extends Component { render() { const extraProps = mapSettingsToProps ? diff --git a/packages/components/src/higher-order/with-context/test/index.js b/packages/components/src/higher-order/with-context/test/index.js index 855270b6b7baf..b787c11dff989 100644 --- a/packages/components/src/higher-order/with-context/test/index.js +++ b/packages/components/src/higher-order/with-context/test/index.js @@ -5,14 +5,17 @@ import renderer from 'react-test-renderer'; import PropTypes from 'prop-types'; /** - * Internal dependencies + * WordPress dependencies */ -import withContext from '../'; +import { Component } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; /** - * WordPress dependencies + * Internal dependencies */ -import { Component } from '@wordpress/element'; +import withContext from '../'; + +jest.mock( '@wordpress/deprecated', () => jest.fn() ); class PassContext extends Component { getChildContext() { @@ -39,6 +42,7 @@ describe( 'withContext', () => { ); expect( wrapper.root.findByType( 'div' ).children[ 0 ] ).toBe( 'ok' ); + expect( deprecated ).toHaveBeenCalled(); } ); it( 'should allow specifying a context getter mapping', () => { diff --git a/packages/editor/src/components/inner-blocks/index.js b/packages/editor/src/components/inner-blocks/index.js index ef0ff90942aef..006787b8dec79 100644 --- a/packages/editor/src/components/inner-blocks/index.js +++ b/packages/editor/src/components/inner-blocks/index.js @@ -7,11 +7,10 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { withContext } from '@wordpress/components'; import { withViewportMatch } from '@wordpress/viewport'; import { Component } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; -import { synchronizeBlocksWithTemplate } from '@wordpress/blocks'; +import { synchronizeBlocksWithTemplate, withBlockContentContext } from '@wordpress/blocks'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { compose } from '@wordpress/compose'; @@ -149,10 +148,8 @@ InnerBlocks = compose( [ } ), ] )( InnerBlocks ); -InnerBlocks.Content = ( { BlockContent } ) => { - return ; -}; - -InnerBlocks.Content = withContext( 'BlockContent' )()( InnerBlocks.Content ); +InnerBlocks.Content = withBlockContentContext( + ( { BlockContent } ) => +); export default InnerBlocks; diff --git a/packages/element/src/serialize.js b/packages/element/src/serialize.js index d05eb753e7f31..b245fab406e65 100644 --- a/packages/element/src/serialize.js +++ b/packages/element/src/serialize.js @@ -41,7 +41,34 @@ import { /** * Internal dependencies */ -import { Fragment, RawHTML } from './'; +import { + Fragment, + StrictMode, +} from './react'; +import RawHTML from './raw-html'; + +/** + * Boolean reflecting whether the current environment supports Symbol. + * + * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol + * + * @type {boolean} + */ +const HAS_SYMBOL = typeof Symbol === 'function' && Symbol.for; + +/** + * Internal React symbol representing Provider type. + * + * @type {Symbol} + */ +const REACT_PROVIDER_TYPE = HAS_SYMBOL ? Symbol.for( 'react.provider' ) : 0xeacd; + +/** + * Internal React symbol representing context (Consumer) type. + * + * @type {Symbol} + */ +const REACT_CONTEXT_TYPE = HAS_SYMBOL ? Symbol.for( 'react.context' ) : 0xeace; /** * Valid attribute types. @@ -430,18 +457,19 @@ function getNormalStylePropertyValue( property, value ) { /** * Serializes a React element to string. * - * @param {WPElement} element Element to serialize. - * @param {?Object} context Context object. + * @param {WPElement} element Element to serialize. + * @param {?Object} context Context object. + * @param {?Object} legacyContext Legacy context object. * * @return {string} Serialized element. */ -export function renderElement( element, context = {} ) { +export function renderElement( element, context, legacyContext = {} ) { if ( null === element || undefined === element || false === element ) { return ''; } if ( Array.isArray( element ) ) { - return renderChildren( element, context ); + return renderChildren( element, context, legacyContext ); } switch ( typeof element ) { @@ -452,11 +480,12 @@ export function renderElement( element, context = {} ) { return element.toString(); } - const { type: tagName, props } = element; + const { type, props } = element; - switch ( tagName ) { + switch ( type ) { + case StrictMode: case Fragment: - return renderChildren( props.children, context ); + return renderChildren( props.children, context, legacyContext ); case RawHTML: const { children, ...wrapperProps } = props; @@ -467,20 +496,28 @@ export function renderElement( element, context = {} ) { ...wrapperProps, dangerouslySetInnerHTML: { __html: children }, }, - context + context, + legacyContext ); } - switch ( typeof tagName ) { + switch ( typeof type ) { case 'string': - return renderNativeComponent( tagName, props, context ); + return renderNativeComponent( type, props, context, legacyContext ); case 'function': - if ( tagName.prototype && typeof tagName.prototype.render === 'function' ) { - return renderComponent( tagName, props, context ); + if ( type.prototype && typeof type.prototype.render === 'function' ) { + return renderComponent( type, props, context, legacyContext ); } - return renderElement( tagName( props, context ), context ); + return renderElement( type( props, legacyContext ), context, legacyContext ); + } + switch ( type && type.$$typeof ) { + case REACT_PROVIDER_TYPE: + return renderChildren( props.children, props.value, legacyContext ); + + case REACT_CONTEXT_TYPE: + return renderElement( props.children( context || type._currentValue ), context, legacyContext ); } return ''; @@ -489,27 +526,28 @@ export function renderElement( element, context = {} ) { /** * Serializes a native component type to string. * - * @param {?string} type Native component type to serialize, or null if - * rendering as fragment of children content. - * @param {Object} props Props object. - * @param {?Object} context Context object. + * @param {?string} type Native component type to serialize, or null if + * rendering as fragment of children content. + * @param {Object} props Props object. + * @param {?Object} context Context object. + * @param {?Object} legacyContext Legacy context object. * * @return {string} Serialized element. */ -export function renderNativeComponent( type, props, context = {} ) { +export function renderNativeComponent( type, props, context, legacyContext = {} ) { let content = ''; if ( type === 'textarea' && props.hasOwnProperty( 'value' ) ) { // Textarea children can be assigned as value prop. If it is, render in // place of children. Ensure to omit so it is not assigned as attribute // as well. - content = renderChildren( props.value, context ); + content = renderChildren( props.value, context, legacyContext ); props = omit( props, 'value' ); } else if ( props.dangerouslySetInnerHTML && typeof props.dangerouslySetInnerHTML.__html === 'string' ) { // Dangerous content is left unescaped. content = props.dangerouslySetInnerHTML.__html; } else if ( typeof props.children !== 'undefined' ) { - content = renderChildren( props.children, context ); + content = renderChildren( props.children, context, legacyContext ); } if ( ! type ) { @@ -528,20 +566,21 @@ export function renderNativeComponent( type, props, context = {} ) { /** * Serializes a non-native component type to string. * - * @param {Function} Component Component type to serialize. - * @param {Object} props Props object. - * @param {?Object} context Context object. + * @param {Function} Component Component type to serialize. + * @param {Object} props Props object. + * @param {?Object} context Context object. + * @param {?Object} legacyContext Legacy context object. * * @return {string} Serialized element */ -export function renderComponent( Component, props, context = {} ) { - const instance = new Component( props, context ); +export function renderComponent( Component, props, context, legacyContext = {} ) { + const instance = new Component( props, legacyContext ); if ( typeof instance.getChildContext === 'function' ) { - Object.assign( context, instance.getChildContext() ); + Object.assign( legacyContext, instance.getChildContext() ); } - const html = renderElement( instance.render(), context ); + const html = renderElement( instance.render(), context, legacyContext ); return html; } @@ -549,12 +588,13 @@ export function renderComponent( Component, props, context = {} ) { /** * Serializes an array of children to string. * - * @param {Array} children Children to serialize. - * @param {?Object} context Context object. + * @param {Array} children Children to serialize. + * @param {?Object} context Context object. + * @param {?Object} legacyContext Legacy context object. * * @return {string} Serialized children. */ -function renderChildren( children, context = {} ) { +function renderChildren( children, context, legacyContext = {} ) { let result = ''; children = castArray( children ); @@ -562,7 +602,7 @@ function renderChildren( children, context = {} ) { for ( let i = 0; i < children.length; i++ ) { const child = children[ i ]; - result += renderElement( child, context ); + result += renderElement( child, context, legacyContext ); } return result; diff --git a/packages/element/src/test/serialize.js b/packages/element/src/test/serialize.js index 44e83734eae8a..d170789a6794c 100644 --- a/packages/element/src/test/serialize.js +++ b/packages/element/src/test/serialize.js @@ -8,10 +8,12 @@ import { noop } from 'lodash'; */ import { Component, + createContext, createElement, Fragment, - RawHTML, -} from '../'; + StrictMode, +} from '../react'; +import RawHTML from '../raw-html'; import serialize, { escapeAmpersand, escapeQuotationMark, @@ -116,7 +118,7 @@ describe( 'serialize()', () => { expect( result ).toBe( '
' ); } ); - it( 'should render with context', () => { + it( 'should render with context (legacy)', () => { class Provider extends Component { getChildContext() { return { @@ -298,12 +300,104 @@ describe( 'renderElement()', () => { expect( result ).toBe( 'Hello' ); } ); + it( 'renders StrictMode with undefined children', () => { + const result = renderElement( ); + + expect( result ).toBe( '' ); + } ); + + it( 'renders StrictMode as its inner children', () => { + const result = renderElement( Hello ); + + expect( result ).toBe( 'Hello' ); + } ); + it( 'renders Fragment with undefined children', () => { const result = renderElement( ); expect( result ).toBe( '' ); } ); + it( 'renders default value from Context API', () => { + const { Consumer } = createContext( { + value: 'default', + } ); + + const result = renderElement( + + { ( context ) => context.value } + + ); + + expect( result ).toBe( 'default' ); + } ); + + it( 'renders provided value through Context API', () => { + const { Consumer, Provider } = createContext( { + value: 'default', + } ); + + const result = renderElement( + + + { ( context ) => context.value } + + + ); + + expect( result ).toBe( 'provided' ); + } ); + + it( 'renders proper value through Context API when multiple providers present', () => { + const { Consumer, Provider } = createContext( { + value: 'default', + } ); + + const result = renderElement( + + + + { ( context ) => context.value } + + + { '|' } + + + { ( context ) => context.value } + + + { '|' } + + { ( context ) => context.value } + + + ); + + expect( result ).toBe( '1st provided|2nd provided|default' ); + } ); + + it( 'renders proper value through Context API when nested providers present', () => { + const { Consumer, Provider } = createContext( { + value: 'default', + } ); + + const result = renderElement( + + + + { ( context ) => context.value } + + + { '|' } + + { ( context ) => context.value } + + + ); + + expect( result ).toBe( 'inner provided|outer provided' ); + } ); + it( 'renders RawHTML as its unescaped children', () => { const result = renderElement( { '' } );