diff --git a/package-lock.json b/package-lock.json index 8bb461bcc86606..3f572bbabf14e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11119,6 +11119,7 @@ "moment": "^2.22.1", "re-resizable": "^6.4.0", "react-dates": "^17.1.1", + "react-merge-refs": "^1.0.0", "react-resize-aware": "^3.0.1", "react-spring": "^8.0.20", "react-use-gesture": "^7.0.15", @@ -46071,6 +46072,11 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-merge-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.0.0.tgz", + "integrity": "sha512-VkvWuCR5VoTjb+VYUcOjkFo66HDv1Hw8VjKcwQtWr2lJnT8g7epRRyfz8+Zkl2WhwqNeqR0gIe0XYrBa9ePeXg==" + }, "react-moment-proptypes": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.6.0.tgz", diff --git a/packages/block-editor/src/components/panel-color-settings/test/index.js b/packages/block-editor/src/components/panel-color-settings/test/index.js index 4a6c6e5baa3e26..565124f1eb81e4 100644 --- a/packages/block-editor/src/components/panel-color-settings/test/index.js +++ b/packages/block-editor/src/components/panel-color-settings/test/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { create, act } from 'react-test-renderer'; +import { render } from '@testing-library/react'; import { noop } from 'lodash'; /** @@ -11,120 +11,106 @@ import PanelColorSettings from '../'; describe( 'PanelColorSettings', () => { it( 'should not render anything if there are no colors to choose', async () => { - let root; - - await act( async () => { - root = create( - - ); - } ); - - expect( root.toJSON() ).toBe( null ); + const { container } = render( + + ); + expect( container.innerHTML ).toBe( '' ); } ); it( 'should render a color panel if at least one setting supports custom colors', async () => { - let root; - await act( async () => { - root = create( - - ); - } ); - expect( root ).not.toBe( null ); + const { container } = render( + + ); + expect( container.innerHTML ).not.toBe( '' ); } ); it( 'should render a color panel if at least one setting specifies some colors to choose', async () => { - let root; - await act( async () => { - root = create( - - ); - } ); - expect( root ).not.toBe( null ); + const { container } = render( + + ); + expect( container.innerHTML ).not.toBe( '' ); } ); it( 'should not render anything if none of the setting panels has colors to choose', async () => { - let root; - await act( async () => { - root = create( - - ); - } ); - expect( root ).not.toBe( null ); + const { container } = render( + + ); + expect( container.innerHTML ).not.toBe( '' ); } ); } ); diff --git a/packages/block-library/src/more/test/__snapshots__/edit.js.snap b/packages/block-library/src/more/test/__snapshots__/edit.js.snap index 10b47a9f654220..55069a65868395 100644 --- a/packages/block-library/src/more/test/__snapshots__/edit.js.snap +++ b/packages/block-library/src/more/test/__snapshots__/edit.js.snap @@ -3,14 +3,14 @@ exports[`core/more/edit should match snapshot when noTeaser is false 1`] = ` - + - +
- + - +
{ event.preventDefault(); - if ( this.props.opened === undefined ) { - this.setState( ( state ) => ( { - opened: ! state.opened, - } ) ); - } + const next = ! isOpened; + setIsOpened( next ); + onToggle( next ); + }; - if ( this.props.onToggle ) { - this.props.onToggle(); + // Runs after initial render + useUpdateEffect( () => { + if ( isOpened ) { + /* + * Scrolls the content into view when visible. + * This improves the UX when there are multiple stacking + * components in a scrollable container. + */ + if ( nodeRef.current.scrollIntoView ) { + nodeRef.current.scrollIntoView( { + inline: 'nearest', + block: 'nearest', + behavior: scrollBehavior, + } ); + } } - } + }, [ isOpened, scrollBehavior ] ); - render() { - const { - title, - children, - opened, - className, - icon, - forwardedRef, - } = this.props; - const isOpened = opened === undefined ? this.state.opened : opened; - const classes = classnames( 'components-panel__body', className, { - 'is-opened': isOpened, - } ); + const classes = classnames( 'components-panel__body', className, { + 'is-opened': isOpened, + } ); + + return ( +
+ + { isOpened && children } +
+ ); +} + +const PanelBodyTitle = forwardRef( + ( { isOpened, icon, title, ...props }, ref ) => { + if ( ! title ) return null; return ( -
- { !! title && ( -

- -

- ) } - { isOpened && children } -
+

+ +

); } -} +); -const forwardedPanelBody = ( props, ref ) => { - return ; -}; -forwardedPanelBody.displayName = 'PanelBody'; +const ForwardedComponent = forwardRef( PanelBody ); +ForwardedComponent.displayName = 'PanelBody'; -export default forwardRef( forwardedPanelBody ); +export default ForwardedComponent; diff --git a/packages/components/src/panel/stories/index.js b/packages/components/src/panel/stories/index.js index 10c9a163f45494..778140694c9e88 100644 --- a/packages/components/src/panel/stories/index.js +++ b/packages/components/src/panel/stories/index.js @@ -26,21 +26,31 @@ export const _default = () => { }; export const multipleBodies = () => { - const body1Title = text( '1: Body Title', 'First Settings' ); - const body2Title = text( '2: Body Title', 'Second Settings' ); - const body1Open = boolean( '1: Opened', true ); - const body2Open = boolean( '2: Opened', false ); - const row1Text = text( '1: Row Text', 'My Panel Inputs and Labels' ); - const row2Text = text( '2: Row Text', 'My Panel Inputs and Labels' ); return ( - - - { row1Text } - - - { row2Text } - - + + + + + + + + + + + + + + + + + + + + + + + + ); }; @@ -57,3 +67,23 @@ export const withIcon = () => { ); }; + +function ScrollableContainer( { children } ) { + return ( +
+ { children } +
+ ); +} + +function Placeholder( { height = 200 } ) { + return
; +} diff --git a/packages/components/src/panel/test/body.js b/packages/components/src/panel/test/body.js index a3aa620432217f..bf3f2860ec6ceb 100644 --- a/packages/components/src/panel/test/body.js +++ b/packages/components/src/panel/test/body.js @@ -1,97 +1,118 @@ /** * External dependencies */ -import { shallow, mount } from 'enzyme'; +import { render, fireEvent } from '@testing-library/react'; /** * Internal dependencies */ import { PanelBody } from '../body'; +const getPanelBody = ( container ) => + container.querySelector( '.components-panel__body' ); +const getPanelBodyContent = ( container ) => + container.querySelector( '.components-panel__body > div' ); +const getPanelToggle = ( container ) => + container.querySelector( '.components-panel__body-toggle' ); + describe( 'PanelBody', () => { describe( 'basic rendering', () => { it( 'should render an empty div with the matching className', () => { - const panelBody = shallow( ); - expect( panelBody.hasClass( 'components-panel__body' ) ).toBe( - true - ); - expect( panelBody.type() ).toBe( 'div' ); - } ); + const { container } = render( ); + const panelBody = getPanelBody( container ); - it( 'should render an Button matching the following props and state', () => { - const panelBody = shallow( ); - const button = panelBody.find( '.components-panel__body-toggle' ); - expect( panelBody.hasClass( 'is-opened' ) ).toBe( true ); - expect( panelBody.state( 'opened' ) ).toBe( true ); - expect( button.prop( 'onClick' ) ).toBe( - panelBody.instance().toggle - ); - expect( button.childAt( 0 ).name() ).toBe( 'span' ); - expect( button.childAt( 0 ).childAt( 0 ).name() ).toBe( 'Icon' ); - expect( button.childAt( 1 ).text() ).toBe( 'Some Text' ); + expect( panelBody ).toBeTruthy(); + expect( panelBody.tagName ).toBe( 'DIV' ); } ); - it( 'should change state and class when sidebar is closed', () => { - const panelBody = shallow( - + it( 'should render inner content, if opened', () => { + const { container } = render( + +
Content
+
); - expect( panelBody.state( 'opened' ) ).toBe( false ); - expect( panelBody.hasClass( 'is-opened' ) ).toBe( false ); + const panelContent = getPanelBodyContent( container ); + + expect( panelContent ).toBeTruthy(); } ); - it( 'should use the "opened" prop instead of state if provided', () => { - const panelBody = shallow( - + it( 'should be opened by default', () => { + const { container } = render( + +
Content
+
); - expect( panelBody.state( 'opened' ) ).toBe( false ); - expect( panelBody.hasClass( 'is-opened' ) ).toBe( true ); - } ); + const panelContent = getPanelBodyContent( container ); - it( 'should render child elements within PanelBody element', () => { - const panelBody = shallow( ); - expect( panelBody.instance().props.children ).toBe( 'Some Text' ); - expect( panelBody.text() ).toBe( 'Some Text' ); + expect( panelContent ).toBeTruthy(); } ); - it( 'should pass children prop but not render when sidebar is closed', () => { - const panelBody = shallow( - + it( 'should render as initially opened, if specified', () => { + const { container } = render( + +
Content
+
); - expect( panelBody.instance().props.children ).toBe( 'Some Text' ); - // Text should be empty even though props.children is set. - expect( panelBody.text() ).toBe( '' ); + const panelContent = getPanelBodyContent( container ); + + expect( panelContent ).toBeTruthy(); } ); } ); - describe( 'mounting behavior', () => { - it( 'should mount with a default of being opened', () => { - const panelBody = mount( ); - expect( panelBody.state( 'opened' ) ).toBe( true ); - } ); + describe( 'toggling', () => { + it( 'should toggle collapse with opened prop', () => { + const { container, rerender } = render( + +
Content
+
+ ); + let panelContent = getPanelBodyContent( container ); - it( 'should mount with a state of not opened when initialOpen set to false', () => { - const panelBody = mount( ); - expect( panelBody.state( 'opened' ) ).toBe( false ); - } ); - } ); + expect( panelContent ).toBeTruthy(); + + rerender( + +
Content
+
+ ); - describe( 'toggling behavior', () => { - const fakeEvent = { preventDefault: () => undefined }; + panelContent = getPanelBodyContent( container ); - it( 'should set the opened state to false when a toggle fires', () => { - const panelBody = mount( ); - panelBody.instance().toggle( fakeEvent ); - expect( panelBody.state( 'opened' ) ).toBe( false ); + expect( panelContent ).toBeFalsy(); + + rerender( + +
Content
+
+ ); + + panelContent = getPanelBodyContent( container ); + + expect( panelContent ).toBeTruthy(); } ); - it( 'should set the opened state to true when a toggle fires on a closed state', () => { - const panelBody = mount( ); - panelBody.instance().toggle( fakeEvent ); - expect( panelBody.state( 'opened' ) ).toBe( true ); + it( 'should toggle when clicking header', () => { + const { container } = render( + +
Content
+
+ ); + let panelContent = getPanelBodyContent( container ); + const panelToggle = getPanelToggle( container ); + + expect( panelContent ).toBeFalsy(); + + fireEvent.click( panelToggle ); + + panelContent = getPanelBodyContent( container ); + + expect( panelContent ).toBeTruthy(); + + fireEvent.click( panelToggle ); + + panelContent = getPanelBodyContent( container ); + + expect( panelContent ).toBeFalsy(); } ); } ); } ); diff --git a/packages/components/src/utils/index.js b/packages/components/src/utils/index.js new file mode 100644 index 00000000000000..b903874d95f32a --- /dev/null +++ b/packages/components/src/utils/index.js @@ -0,0 +1,3 @@ +export * from './hooks'; +export * from './style-mixins'; +export * from './use-update-effect'; diff --git a/packages/components/src/utils/use-update-effect.js b/packages/components/src/utils/use-update-effect.js new file mode 100644 index 00000000000000..c7dfab9452daef --- /dev/null +++ b/packages/components/src/utils/use-update-effect.js @@ -0,0 +1,21 @@ +/** + * WordPress dependencies + */ +import { useRef, useEffect } from '@wordpress/element'; + +/* + * A `React.useEffect` that will not run on the first render. + * Source: + * https://github.com/reakit/reakit/blob/master/packages/reakit-utils/src/useUpdateEffect.ts + */ +export function useUpdateEffect( effect, deps ) { + const mounted = useRef( false ); + + useEffect( () => { + if ( mounted.current ) { + return effect(); + } + mounted.current = true; + return undefined; + }, deps ); +} diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index f9450581e4418b..741f4d095de894 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -59,6 +59,7 @@ .interface-interface-skeleton__sidebar > div { height: 100%; + padding-bottom: $grid-unit-60; } .edit-post-layout .editor-post-publish-panel__header-publish-button { diff --git a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap index aafa7c918f1795..a7eb099d379a51 100644 --- a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginPostPublishPanel renders fill properly 1`] = `"

My panel content
"`; +exports[`PluginPostPublishPanel renders fill properly 1`] = `"

My panel content
"`; diff --git a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap index 6087ec62497e5c..f065917b0ee8a3 100644 --- a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginPrePublishPanel renders fill properly 1`] = `"

My panel content
"`; +exports[`PluginPrePublishPanel renders fill properly 1`] = `"

My panel content
"`;