{
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
"`;