Onboarding: Add WebPreview component for theme previewing (https://github.com/woocommerce/woocommerce-admin/pull/2681)

* Add WebPreview component

* Add theme preview component

* Add WebPreview example for devdocs

* Update loading content prop name for WebPreview

* Add selected class state for device buttons

* Fix tabbing issue in stylesheet

* Fix loadingContent prop changes

* Add in translators note

* Fix theme details height issue

* Add theme demo track events (https://github.com/woocommerce/woocommerce-admin/pull/2715)

* Add theme demo track events

* Track theme chosen location

* Track theme slug on device switch

* Apply design feedback
This commit is contained in:
Joshua T Flowers 2019-08-02 07:27:38 +08:00 committed by GitHub
parent dca1b07377
commit d41ce76451
11 changed files with 372 additions and 39 deletions

View File

@ -23,6 +23,7 @@ import withSelect from 'wc-api/with-select';
import './style.scss';
import { recordEvent } from 'lib/tracks';
import ThemeUploader from './uploader';
import ThemePreview from './preview';
class Theme extends Component {
constructor() {
@ -30,19 +31,21 @@ class Theme extends Component {
this.state = {
activeTab: 'all',
demo: null,
uploadedThemes: [],
};
this.handleUploadComplete = this.handleUploadComplete.bind( this );
this.onChoose = this.onChoose.bind( this );
this.onClosePreview = this.onClosePreview.bind( this );
this.onSelectTab = this.onSelectTab.bind( this );
this.openDemo = this.openDemo.bind( this );
}
async onChoose( theme ) {
async onChoose( theme, location = '' ) {
const { createNotice, goToNextStep, isError, updateProfileItems } = this.props;
recordEvent( 'storeprofiler_store_theme_choose', { theme } );
recordEvent( 'storeprofiler_store_theme_choose', { theme, location } );
await updateProfileItems( { theme } );
if ( ! isError ) {
@ -56,10 +59,17 @@ class Theme extends Component {
}
}
openDemo( theme ) {
// @todo This should open a theme demo preview.
onClosePreview() {
const { demo } = this.state;
recordEvent( 'storeprofiler_store_theme_demo_close', { theme: demo.slug } );
document.body.classList.remove( 'woocommerce-theme-preview-active' );
this.setState( { demo: null } );
}
recordEvent( 'storeprofiler_store_theme_live_demo', { theme } );
openDemo( theme ) {
recordEvent( 'storeprofiler_store_theme_live_demo', { theme: theme.slug } );
document.body.classList.add( 'woocommerce-theme-preview-active' );
this.setState( { demo: theme } );
}
renderTheme( theme ) {
@ -90,12 +100,12 @@ class Theme extends Component {
<Button
isPrimary={ Boolean( demo_url ) }
isDefault={ ! Boolean( demo_url ) }
onClick={ () => this.onChoose( slug ) }
onClick={ () => this.onChoose( slug, 'card' ) }
>
{ __( 'Choose', 'woocommerce-admin' ) }
</Button>
{ demo_url && (
<Button isDefault onClick={ () => this.openDemo( slug ) }>
<Button isDefault onClick={ () => this.openDemo( theme ) }>
{ __( 'Live Demo', 'woocommerce-admin' ) }
</Button>
) }
@ -152,11 +162,14 @@ class Theme extends Component {
this.setState( {
uploadedThemes: [ ...this.state.uploadedThemes, upload.theme_data ],
} );
recordEvent( 'storeprofiler_store_theme_upload', { theme: upload.theme_data.slug } );
}
}
render() {
const themes = this.getThemes();
const { demo } = this.state;
return (
<Fragment>
@ -192,6 +205,9 @@ class Theme extends Component {
</div>
) }
</TabPanel>
{ demo && (
<ThemePreview theme={ demo } onChoose={ this.onChoose } onClose={ this.onClosePreview } />
) }
</Fragment>
);
}

View File

@ -0,0 +1,99 @@
/** @format */
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Button } from 'newspack-components';
import classnames from 'classnames';
import { Component } from '@wordpress/element';
import interpolateComponents from 'interpolate-components';
/**
* WooCommerce dependencies
*/
import { WebPreview } from '@woocommerce/components';
/**
* Internal depdencies
*/
import { recordEvent } from 'lib/tracks';
const devices = [
{
key: 'mobile',
icon: 'phone_android',
},
{
key: 'tablet',
icon: 'tablet_android',
},
{
key: 'desktop',
icon: 'desktop_windows',
},
];
class ThemePreview extends Component {
constructor() {
super( ...arguments );
this.state = {
device: 'desktop',
};
this.handleDeviceClick = this.handleDeviceClick.bind( this );
}
handleDeviceClick( device ) {
const { theme } = this.props;
recordEvent( 'storeprofiler_store_theme_demo_device', { device, theme: theme.slug } );
this.setState( { device } );
}
render() {
const { onChoose, onClose, theme } = this.props;
const { demo_url, slug, title } = theme;
const { device: currentDevice } = this.state;
return (
<div className="woocommerce-theme-preview">
<div className="woocommerce-theme-preview__toolbar">
<Button className="woocommerce-theme-preview__close" onClick={ onClose }>
<i className="material-icons-outlined">close</i>
</Button>
<div className="woocommerce-theme-preview__theme-name">
{ interpolateComponents( {
/* translators: Describing who a previewed theme is developed by. E.g., Storefront developed by WooCommerce */
mixedString: sprintf(
__( '{{strong}}%s{{/strong}} developed by WooCommerce', 'woocommerce-admin' ),
title
),
components: {
strong: <strong />,
},
} ) }
</div>
<div className="woocommerce-theme-preview__devices">
{ devices.map( device => (
<Button
key={ device.key }
className={ classnames( 'woocommerce-theme-preview__device', {
'is-selected': device.key === currentDevice,
} ) }
onClick={ () => this.handleDeviceClick( device.key ) }
>
<i className="material-icons-outlined">{ device.icon }</i>
</Button>
) ) }
</div>
<Button isPrimary onClick={ () => onChoose( slug, 'preview' ) }>
{ __( 'Choose', 'woocommerce-admin' ) }
</Button>
</div>
<WebPreview src={ demo_url } title={ title } className={ `is-${ currentDevice }` } />
</div>
);
}
}
export default ThemePreview;

View File

@ -87,7 +87,7 @@
padding: $gap;
display: flex;
flex-direction: column;
height: 100%;
margin-top: auto;
}
.woocommerce-profile-wizard__theme-status {
@ -178,3 +178,114 @@
margin: 0;
}
}
.woocommerce-theme-preview {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
max-width: 100% !important;
display: flex;
flex-direction: column;
.woocommerce-theme-preview__toolbar {
background: $white;
flex-direction: row;
display: flex;
height: 56px;
border-bottom: 1px solid $muriel-gray-50;
padding-left: $gap;
padding-right: $gap;
align-items: center;
.muriel-button.is-button.is-primary {
height: 40px;
margin: 0;
@include breakpoint( '<782px' ) {
margin-left: auto;
}
}
}
.woocommerce-theme-preview__theme-name {
padding-left: $gap;
color: #1a1a1a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 50%;
}
.woocommerce-theme-preview__close {
padding: 0 $gap 0 0;
color: $muriel-gray-500;
}
.woocommerce-theme-preview__devices {
margin-left: auto;
margin-right: $gap;
.muriel-button {
padding: $gap-small;
color: $muriel-gray-500;
margin: 0;
border-radius: 50%;
&.is-selected,
&:focus {
background: $muriel-gray-500;
color: $white;
}
}
@include breakpoint( '<782px' ) {
display: none;
}
}
.woocommerce-web-preview {
flex: 1;
padding: $gap-largest $gap;
overflow: scroll;
.woocommerce-web-preview__iframe-wrapper {
height: 100%;
border-radius: 3px;
box-shadow: $muriel-box-shadow-1dp;
overflow: hidden;
margin: 0 auto;
iframe {
display: block;
}
}
&.is-mobile .woocommerce-web-preview__iframe-wrapper {
max-width: 360px;
}
&.is-tablet .woocommerce-web-preview__iframe-wrapper {
max-width: 768px;
}
&.is-desktop {
width: 100%;
padding: 0;
.woocommerce-web-preview__iframe-wrapper {
border-radius: 0;
box-shadow: none;
}
}
}
}
.woocommerce-theme-preview-active {
overflow: hidden;
.woocommerce-profile-wizard__header {
display: none;
}
}

View File

@ -30,5 +30,6 @@
{ "component": "Table" },
{ "component": "Tag" },
{ "component": "TextControlWithAffixes" },
{ "component": "ViewMoreList" }
{ "component": "ViewMoreList" },
{ "component": "WebPreview" }
]

View File

@ -9303,8 +9303,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
@ -9322,13 +9321,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -9341,18 +9338,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@ -9455,8 +9449,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@ -9466,7 +9459,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -9479,20 +9471,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -9509,7 +9498,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -9582,8 +9570,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@ -9593,7 +9580,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -9669,8 +9655,7 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@ -9700,7 +9685,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -9718,7 +9702,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -9757,13 +9740,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"optional": true
"bundled": true
}
}
},

View File

@ -5,6 +5,7 @@
- Add new component `<List />` for displaying interactive list items.
- Fix z-index issue in `<Chart />` empty message.
- Added a new `<SimpleSelectControl />` component.
- Added a new `<WebPreview />` component.
# 3.1.0
- Added support for a countLabel prop on SearchListItem to allow custom counts.

View File

@ -56,3 +56,4 @@ export { default as Tag } from './tag';
export { default as TextControlWithAffixes } from './text-control-with-affixes';
export { default as useFilters } from './higher-order/use-filters';
export { default as ViewMoreList } from './view-more-list';
export { default as WebPreview } from './web-preview';

View File

@ -35,3 +35,4 @@
@import 'tag/style.scss';
@import 'text-control-with-affixes/style.scss';
@import 'view-more-list/style.scss';
@import 'web-preview/style.scss';

View File

@ -0,0 +1,9 @@
```jsx
import { WebPreview } from '@woocommerce/components';
const MyWebPreview = () => (
<div>
<WebPreview src="https://themes.woocommerce.com/?name=galleria" title="My Web Preview" />
</div>
);
```

View File

@ -0,0 +1,90 @@
/** @format */
/**
* External dependencies
*/
import classnames from 'classnames';
import { Component, createRef } from '@wordpress/element';
import { noop } from 'lodash';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import Spinner from '../spinner';
/**
* WebPreview component to display an iframe of another page.
*/
class WebPreview extends Component {
constructor( props ) {
super( props );
this.state = {
isLoading: true,
};
this.iframeRef = createRef();
this.setLoaded = this.setLoaded.bind( this );
}
componentDidMount() {
this.iframeRef.current.addEventListener( 'load', this.setLoaded );
}
setLoaded() {
this.setState( { isLoading: false } );
this.props.onLoad();
}
render() {
const { className, loadingContent, src, title } = this.props;
const { isLoading } = this.state;
const classes = classnames( 'woocommerce-web-preview', className, {
'is-loading': isLoading,
} );
return (
<div className={ classes }>
{ isLoading && loadingContent }
<div className="woocommerce-web-preview__iframe-wrapper">
<iframe
ref={ this.iframeRef }
title={ title }
src={ src }
/>
</div>
</div>
);
}
}
WebPreview.propTypes = {
/**
* Additional class name to style the component.
*/
className: PropTypes.string,
/**
* Content shown when iframe is still loading.
*/
loadingContent: PropTypes.node,
/**
* Function to fire when iframe content is loaded.
*/
onLoad: PropTypes.func,
/**
* Iframe src to load.
*/
src: PropTypes.string.isRequired,
/**
* Iframe title.
*/
title: PropTypes.string.isRequired,
};
WebPreview.defaultProps = {
loadingContent: <Spinner />,
onLoad: noop,
};
export default WebPreview;

View File

@ -0,0 +1,23 @@
.woocommerce-web-preview {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: $muriel-gray-0;
&.is-loading {
.woocommerce-web-preview__iframe-wrapper {
display: none;
}
}
.woocommerce-web-preview__iframe-wrapper {
width: 100%;
}
iframe {
width: 100%;
height: 100%;
min-height: 400px;
}
}