diff --git a/packages/e2e-tests/specs/editor/various/preview.test.js b/packages/e2e-tests/specs/editor/various/preview.test.js
index db92a0ff26e8c2..004c3944e9c86f 100644
--- a/packages/e2e-tests/specs/editor/various/preview.test.js
+++ b/packages/e2e-tests/specs/editor/various/preview.test.js
@@ -23,6 +23,8 @@ async function openPreviewPage( editorPage ) {
let openTabs = await browser.pages();
const expectedTabsCount = openTabs.length + 1;
await editorPage.click( '.editor-post-preview' );
+ await editorPage.waitForSelector( '.editor-post-preview__new-tab' );
+ await editorPage.click( '.editor-post-preview__new-tab' );
// Wait for the new tab to open.
while ( openTabs.length < expectedTabsCount ) {
@@ -31,9 +33,7 @@ async function openPreviewPage( editorPage ) {
}
const previewPage = last( openTabs );
- // Wait for the preview to load. We can't do interstitial detection here,
- // because it might load too quickly for us to pick up, so we wait for
- // the preview to load by waiting for the title to appear.
+ // Wait for the preview to load by waiting for the title to appear.
await previewPage.waitForSelector( '.entry-title' );
return previewPage;
}
@@ -48,6 +48,8 @@ async function openPreviewPage( editorPage ) {
*/
async function waitForPreviewNavigation( previewPage ) {
await page.click( '.editor-post-preview' );
+ await page.waitForSelector( '.editor-post-preview__new-tab' );
+ await page.click( '.editor-post-preview__new-tab' );
return previewPage.waitForNavigation();
}
@@ -117,6 +119,7 @@ describe( 'Preview', () => {
// Return to editor to change title.
await editorPage.bringToFront();
+ await editorPage.click( '[aria-label="Close dialog"' );
await editorPage.type( '.editor-post-title__input', '!' );
await waitForPreviewNavigation( previewPage );
@@ -127,12 +130,14 @@ describe( 'Preview', () => {
// Pressing preview without changes should bring same preview window to
// front and reload, but should not show interstitial.
await editorPage.bringToFront();
+ await editorPage.click( '[aria-label="Close dialog"' );
await waitForPreviewNavigation( previewPage );
previewTitle = await previewPage.$eval( '.entry-title', ( node ) => node.textContent );
expect( previewTitle ).toBe( 'Hello World!' );
// Preview for published post (no unsaved changes) directs to canonical URL for post.
await editorPage.bringToFront();
+ await editorPage.click( '[aria-label="Close dialog"' );
await publishPost();
// Return to editor to change title.
@@ -156,6 +161,7 @@ describe( 'Preview', () => {
//
// See: https://github.com/WordPress/gutenberg/issues/7561
await editorPage.bringToFront();
+ await editorPage.click( '[aria-label="Close dialog"' );
await waitForPreviewNavigation( previewPage );
// Title in preview should match updated input.
@@ -185,6 +191,7 @@ describe( 'Preview', () => {
// Return to editor.
await editorPage.bringToFront();
+ await editorPage.click( '[aria-label="Close dialog"' );
// Append bbbbb to the title, and tab away from the title so blur event is triggered.
await editorPage.type( '.editor-post-title__input', 'bbbbb' );
@@ -238,6 +245,7 @@ describe( 'Preview with Custom Fields enabled', () => {
// Return to editor and modify the title and content.
await editorPage.bringToFront();
+ await editorPage.click( '[aria-label="Close dialog"' );
await editorPage.click( '.editor-post-title__input' );
await pressKeyWithModifier( 'primary', 'a' );
await editorPage.keyboard.press( 'Delete' );
@@ -258,5 +266,6 @@ describe( 'Preview with Custom Fields enabled', () => {
// Make sure the editor is active for the afterEach function.
await editorPage.bringToFront();
+ await editorPage.click( '[aria-label="Close dialog"' );
} );
} );
diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js
index 415d9da9ba070d..6b36ad4312c428 100644
--- a/packages/editor/src/components/post-preview-button/index.js
+++ b/packages/editor/src/components/post-preview-button/index.js
@@ -6,96 +6,35 @@ import { get } from 'lodash';
/**
* WordPress dependencies
*/
-import { Component, renderToString } from '@wordpress/element';
-import { Button, Path, SVG } from '@wordpress/components';
+import { Component } from '@wordpress/element';
+import { Button, IconButton, Modal, Path, SVG } from '@wordpress/components';
import { __, _x } from '@wordpress/i18n';
import { withSelect, withDispatch } from '@wordpress/data';
import { ifCondition, compose } from '@wordpress/compose';
-import { applyFilters } from '@wordpress/hooks';
-
-function writeInterstitialMessage( targetDocument ) {
- let markup = renderToString(
-
-
-
{ __( 'Generating preview…' ) }
-
- );
-
- markup += `
-
- `;
- /**
- * Filters the interstitial message shown when generating previews.
- *
- * @param {string} markup The preview interstitial markup.
- */
- markup = applyFilters( 'editor.PostPreview.interstitialMarkup', markup );
-
- targetDocument.write( markup );
- targetDocument.title = __( 'Generating preview…' );
- targetDocument.close();
-}
+const DesktopIcon = ;
+const MobileIcon = ;
+const TabletIcon = ;
export class PostPreviewButton extends Component {
constructor() {
super( ...arguments );
- this.openPreviewWindow = this.openPreviewWindow.bind( this );
+ this.state = {
+ isPreviewOpen: false,
+ previewSize: {
+ width: '1200px',
+ height: '800px',
+ },
+ };
+ this.openPreviewInNewTab = this.openPreviewInNewTab.bind( this );
+ this.getWindowTarget = this.getWindowTarget.bind( this );
+ this.setPreviewWindowLink = this.setPreviewWindowLink.bind( this );
+ this.openPreviewModal = this.openPreviewModal.bind( this );
+ this.closePreviewModal = this.closePreviewModal.bind( this );
+ this.setDesktopPreview = this.setDesktopPreview.bind( this );
+ this.setTabletPreview = this.setTabletPreview.bind( this );
+ this.setMobilePreview = this.setMobilePreview.bind( this );
}
componentDidUpdate( prevProps ) {
@@ -128,7 +67,24 @@ export class PostPreviewButton extends Component {
return `wp-preview-${ postId }`;
}
- openPreviewWindow( event ) {
+ openPreviewModal() {
+ this.setState( { isPreviewOpen: true } );
+
+ // If we don't need to autosave the post before previewing, do nothing.
+ if ( ! this.props.isAutosaveable ) {
+ return;
+ }
+
+ // Request an autosave. This happens asynchronously and causes the component
+ // to update when finished.
+ if ( this.props.isDraft ) {
+ this.props.savePost( { isPreview: true } );
+ } else {
+ this.props.autosave( { isPreview: true } );
+ }
+ }
+
+ openPreviewInNewTab( event ) {
// Our Preview button has its 'href' and 'target' set correctly for a11y
// purposes. Unfortunately, though, we can't rely on the default 'click'
// handler since sometimes it incorrectly opens a new tab instead of reusing
@@ -138,7 +94,7 @@ export class PostPreviewButton extends Component {
// Open up a Preview tab if needed. This is where we'll show the preview.
if ( ! this.previewWindow || this.previewWindow.closed ) {
- this.previewWindow = window.open( '', this.getWindowTarget() );
+ this.previewWindow = window.open( '', '_blank' );
}
// Focus the Preview tab. This might not do anything, depending on the browser's
@@ -146,51 +102,123 @@ export class PostPreviewButton extends Component {
// https://html.spec.whatwg.org/multipage/interaction.html#dom-window-focus
this.previewWindow.focus();
- // If we don't need to autosave the post before previewing, then we simply
- // load the Preview URL in the Preview tab.
- if ( ! this.props.isAutosaveable ) {
- this.setPreviewWindowLink( event.target.href );
- return;
- }
+ // Load the Preview URL in the Preview tab.
+ this.setPreviewWindowLink( event.target.href );
+ }
- // Request an autosave. This happens asynchronously and causes the component
- // to update when finished.
- if ( this.props.isDraft ) {
- this.props.savePost( { isPreview: true } );
- } else {
- this.props.autosave( { isPreview: true } );
- }
+ setDesktopPreview() {
+ this.setState( {
+ previewSize: {
+ width: '1200px',
+ height: '800px',
+ },
+ } );
+ }
+
+ setTabletPreview() {
+ this.setState( {
+ previewSize: {
+ width: '768px',
+ height: '1024px',
+ },
+ } );
+ }
- // Display a 'Generating preview' message in the Preview tab while we wait for the
- // autosave to finish.
- writeInterstitialMessage( this.previewWindow.document );
+ setMobilePreview() {
+ this.setState( {
+ previewSize: {
+ width: '375px',
+ height: '812px',
+ },
+ } );
+ }
+
+ closePreviewModal() {
+ this.setState( { isPreviewOpen: false } );
}
render() {
const { previewLink, currentPostLink, isSaveable } = this.props;
-
// Link to the `?preview=true` URL if we have it, since this lets us see
// changes that were autosaved since the post was last published. Otherwise,
// just link to the post's URL.
const href = previewLink || currentPostLink;
return (
-
+ { this.state.isPreviewOpen &&
+
+
+
+
+
+
+ { __( 'Open preview in new tab' ) }
+
+
+
+
+
+
+ }
+ >
);
}
}
diff --git a/packages/editor/src/components/post-preview-button/style.scss b/packages/editor/src/components/post-preview-button/style.scss
new file mode 100644
index 00000000000000..507a25dd734176
--- /dev/null
+++ b/packages/editor/src/components/post-preview-button/style.scss
@@ -0,0 +1,33 @@
+.editor-post-preview-button__preview-modal {
+ @include break-small() {
+ top: 60px;
+ right: 60px;
+ bottom: 60px;
+ left: 60px;
+ transform: none;
+ }
+}
+
+.editor-post-preview__controls {
+ display: flex;
+ align-items: center;
+ margin: $grid-size-large 0;
+
+ * + * {
+ margin-left: $grid-size;
+ }
+}
+
+.editor-post-preview__new-tab {
+ margin-left: $grid-size-large;
+}
+
+.editor-post-preview__frame-container {
+ overflow-x: auto;
+}
+
+.editor-post-preview__frame {
+ display: block;
+ margin: 0 auto;
+ max-height: calc(100vh - 280px);
+}
diff --git a/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap
index 587f30bf8a91bb..0f165fcacfb7ae 100644
--- a/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap
+++ b/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap
@@ -1,37 +1,114 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`PostPreviewButton render() should render currentPostLink otherwise 1`] = `
-
- Preview
-
+
- (opens in a new tab)
-
-
-`;
-
-exports[`PostPreviewButton render() should render previewLink if provided 1`] = `
-
- Preview
-
+ (opens in a new tab)
+
+
+
- (opens in a new tab)
-
-
+
+
+
+
+
+ }
+ label="Preview desktop screen"
+ onClick={[Function]}
+ />
+
+
+
+
+ }
+ label="Preview tablet screen"
+ onClick={[Function]}
+ />
+
+
+
+
+ }
+ label="Preview phone screen"
+ onClick={[Function]}
+ />
+
+ Open preview in new tab
+
+
+
+
+
+
+
`;
diff --git a/packages/editor/src/components/post-preview-button/test/index.js b/packages/editor/src/components/post-preview-button/test/index.js
index 7bd9856de05c77..087b8f78d447d5 100644
--- a/packages/editor/src/components/post-preview-button/test/index.js
+++ b/packages/editor/src/components/post-preview-button/test/index.js
@@ -71,7 +71,40 @@ describe( 'PostPreviewButton', () => {
} );
} );
- describe( 'openPreviewWindow()', () => {
+ describe( 'openPreviewModal()', () => {
+ it( 'behaves like a regular link if not autosaveable', () => {
+ const autosave = jest.fn();
+
+ const wrapper = shallow(
+
+ );
+
+ wrapper.instance().openPreviewModal();
+
+ expect( autosave ).not.toHaveBeenCalled();
+ } );
+
+ it( 'autosaves the post if autosaveable', () => {
+ const autosave = jest.fn();
+
+ const wrapper = shallow(
+
+ );
+
+ wrapper.instance().openPreviewModal();
+
+ expect( autosave ).toHaveBeenCalled();
+ } );
+ } );
+
+ describe( 'openPreviewInNewTab()', () => {
let windowOpen;
beforeEach( () => {
windowOpen = window.open;
@@ -80,9 +113,8 @@ describe( 'PostPreviewButton', () => {
window.open = windowOpen;
} );
- it( 'behaves like a regular link if not autosaveable', () => {
+ it( 'opens preview in new tab', () => {
const preventDefault = jest.fn();
- const autosave = jest.fn();
const setLocation = jest.fn();
window.open = jest.fn( () => ( {
focus: jest.fn(),
@@ -94,65 +126,47 @@ describe( 'PostPreviewButton', () => {
const wrapper = shallow(
);
- wrapper.simulate( 'click', {
+ wrapper.instance().openPreviewInNewTab( {
preventDefault,
target: { href: 'https://wordpress.org/?p=1' },
} );
expect( preventDefault ).toHaveBeenCalled();
- expect( window.open ).toHaveBeenCalledWith( '', 'wp-preview-1' );
+ expect( window.open ).toHaveBeenCalledWith( '', '_blank' );
expect( wrapper.instance().previewWindow.focus ).toHaveBeenCalled();
- expect( autosave ).not.toHaveBeenCalled();
expect( setLocation ).toHaveBeenCalledWith( 'https://wordpress.org/?p=1' );
} );
+ } );
- it( 'autosaves the post if autosaveable', () => {
- const preventDefault = jest.fn();
- const autosave = jest.fn();
-
- window.open = jest.fn( () => ( {
- focus: jest.fn(),
- document: {
- write: jest.fn(),
- close: jest.fn(),
- },
- } ) );
-
+ describe( 'render()', () => {
+ const previewLink = 'https://wordpress.org/?p=1&preview=true';
+ const currentPostLink = 'https://wordpress.org/?p=1';
+ it( 'should render overlay when state is open', () => {
const wrapper = shallow(
- );
-
- wrapper.simulate( 'click', { preventDefault } );
+ ).setState( { isPreviewOpen: true } );
- expect( preventDefault ).toHaveBeenCalled();
- expect( window.open ).toHaveBeenCalledWith( '', 'wp-preview-1' );
- expect( wrapper.instance().previewWindow.focus ).toHaveBeenCalled();
- expect( autosave ).toHaveBeenCalled();
- expect( wrapper.instance().previewWindow.document.write.mock.calls[ 0 ][ 0 ] ).toContain( 'Generating preview…' );
- expect( wrapper.instance().previewWindow.document.close ).toHaveBeenCalled();
+ expect( wrapper ).toMatchSnapshot();
} );
- } );
- describe( 'render()', () => {
it( 'should render previewLink if provided', () => {
const wrapper = shallow(
- );
+ ).setState( { isPreviewOpen: true } );
- expect( wrapper ).toMatchSnapshot();
+ const frameSrc = wrapper.find( '.editor-post-preview__frame' ).prop( 'src' );
+ expect( frameSrc ).toEqual( previewLink );
} );
it( 'should render currentPostLink otherwise', () => {
@@ -160,11 +174,12 @@ describe( 'PostPreviewButton', () => {
- );
+ ).setState( { isPreviewOpen: true } );
- expect( wrapper ).toMatchSnapshot();
+ const frameSrc = wrapper.find( '.editor-post-preview__frame' ).prop( 'src' );
+ expect( frameSrc ).toEqual( currentPostLink );
} );
it( 'should be disabled if post is not saveable', () => {
@@ -173,7 +188,7 @@ describe( 'PostPreviewButton', () => {
postId={ 1 }
currentPostLink="https://wordpress.org/?p=1"
/>
- );
+ ).find( '.editor-post-preview' );
expect( wrapper.prop( 'disabled' ) ).toBe( true );
} );
diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss
index 949f751bd72af5..a085ac3217b103 100644
--- a/packages/editor/src/style.scss
+++ b/packages/editor/src/style.scss
@@ -9,6 +9,7 @@
@import "./components/post-last-revision/style.scss";
@import "./components/post-locked-modal/style.scss";
@import "./components/post-permalink/style.scss";
+@import "./components/post-preview-button/style.scss";
@import "./components/post-publish-panel/style.scss";
@import "./components/post-saved-state/style.scss";
@import "./components/post-taxonomies/style.scss";