Merge pull request #664 from sharetribe/mobile-wizard-tabs-srollable

EditListingWizard: tabs should be srollable when their layout is horizontal
This commit is contained in:
Vesa Luusua 2018-01-23 16:47:48 +02:00 committed by GitHub
commit effae8fb25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 267 additions and 190 deletions

View file

@ -18,15 +18,17 @@
/* Layout */
display: flex;
flex-direction: row;
padding: 0 24px 0 24px;
padding: 0;
flex-shrink: 0;
background-color: var(--matterColorLight);
box-shadow: var(--boxShadowLight);
border-top: 1px solid var(--matterColorNegative);
overflow-x: scroll;
@media (--viewportLarge) {
padding: 126px 0 82px 15vw;
flex-direction: column;
overflow-x: auto;
background-color: var(--matterColorBright);
box-shadow: none;
@ -43,10 +45,26 @@
&:first-child {
margin-left: 0;
/* Padding added to tab so that it will be visible after call to scrollToTab */
padding-left: 24px;
}
&:last-child {
/* Padding added to tab so that it will be visible after call to scrollToTab */
padding-right: 24px;
}
@media (--viewportLarge) {
margin-left: 0;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
}

View file

@ -1,9 +1,11 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import routeConfiguration from '../../routeConfiguration';
import { propTypes } from '../../util/types';
import { withViewport } from '../../util/contextHelpers';
import { ensureListing } from '../../util/data';
import { createResourceLocatorString } from '../../util/routes';
import {
@ -23,6 +25,9 @@ const PRICING = 'pricing';
const PHOTOS = 'photos';
const STEPS = [DESCRIPTION, LOCATION, PRICING, PHOTOS];
// Tabs are horizontal in small screens
const MAX_HORIZONTAL_NAV_SCREEN_WIDTH = 1023;
const submitText = (intl, isNew, step) => {
let key = null;
if (step === DESCRIPTION) {
@ -66,189 +71,231 @@ const stepsActive = listing => {
};
};
const scrollToTab = (tabPrefix, tabId) => {
const el = document.querySelector(`#${tabPrefix}_${tabId}`);
if (el) {
el.scrollIntoView({
block: 'start',
behavior: 'smooth',
});
}
};
// Create a new or edit listing through EditListingWizard
const EditListingWizard = props => {
const {
className,
params,
errors,
fetchInProgress,
newListingCreated,
history,
images,
listing,
onCreateListing,
onUpdateListing,
onCreateListingDraft,
onImageUpload,
onPayoutDetailsFormChange,
onPayoutDetailsSubmit,
onUpdateImageOrder,
onRemoveImage,
onUpdateListingDraft,
onChange,
rootClassName,
currentUser,
onManageDisableScrolling,
updatedTab,
updateInProgress,
intl,
} = props;
class EditListingWizard extends Component {
constructor(props) {
super(props);
const isNew = params.type === 'new';
const selectedTab = params.tab;
const rootClasses = rootClassName || css.root;
const classes = classNames(rootClasses, className);
const currentListing = ensureListing(listing);
const stepsStatus = stepsActive(currentListing);
const tabParams = tab => {
return { ...params, tab };
};
const tabLink = tab => {
return { name: 'EditListingPage', params: tabParams(tab) };
};
// If selectedStep is not active, redirect to the beginning of wizard
if (!stepsStatus[selectedTab]) {
return <NamedRedirect name="EditListingPage" params={tabParams(DESCRIPTION)} />;
// Having this info in state would trigger unnecessary rerendering
this.hasScrolledToTab = false;
}
const onUpsertListingDraft = currentListing.id ? onUpdateListingDraft : onCreateListingDraft;
const update = (tab, values) => {
onUpdateListing(tab, { ...values, id: currentListing.id });
};
render() {
const {
id,
className,
params,
errors,
fetchInProgress,
newListingCreated,
history,
images,
listing,
onCreateListing,
onUpdateListing,
onCreateListingDraft,
onImageUpload,
onPayoutDetailsFormChange,
onPayoutDetailsSubmit,
onUpdateImageOrder,
onRemoveImage,
onUpdateListingDraft,
onChange,
rootClassName,
currentUser,
onManageDisableScrolling,
updatedTab,
updateInProgress,
intl,
viewport,
} = this.props;
return (
<Tabs rootClassName={classes} navRootClassName={css.nav} tabRootClassName={css.tab}>
<EditListingDescriptionPanel
className={css.panel}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelDescription' })}
tabLinkProps={tabLink(DESCRIPTION)}
selected={selectedTab === DESCRIPTION}
panelUpdated={updatedTab === DESCRIPTION}
updateInProgress={updateInProgress}
disabled={!stepsStatus[DESCRIPTION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, DESCRIPTION)}
onChange={onChange}
onSubmit={values => {
const { title, description, category } = values;
const updateValues = {
title,
description,
customAttributes: { category },
publicData: { category },
};
const isNew = params.type === 'new';
const selectedTab = params.tab;
const rootClasses = rootClassName || css.root;
const classes = classNames(rootClasses, className);
const currentListing = ensureListing(listing);
const stepsStatus = stepsActive(currentListing);
if (isNew) {
onUpsertListingDraft(updateValues);
const pathParams = tabParams(LOCATION);
// Redirect to location tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(DESCRIPTION, updateValues);
}
}}
/>
<EditListingLocationPanel
className={css.panel}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelLocation' })}
tabLinkProps={tabLink(LOCATION)}
selected={selectedTab === LOCATION}
panelUpdated={updatedTab === LOCATION}
updateInProgress={updateInProgress}
disabled={!stepsStatus[LOCATION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, LOCATION)}
onChange={onChange}
onSubmit={values => {
const { building = '', location } = values;
const { selectedPlace: { address, origin } } = location;
const updateValues = {
geolocation: origin,
publicData: {
location: { address, building },
},
};
const tabParams = tab => {
return { ...params, tab };
};
const tabLink = tab => {
return { name: 'EditListingPage', params: tabParams(tab) };
};
if (isNew) {
onUpdateListingDraft(updateValues);
const pathParams = tabParams(PRICING);
// Redirect to pricing tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(LOCATION, updateValues);
}
}}
/>
<EditListingPricingPanel
className={css.panel}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelPricing' })}
tabLinkProps={tabLink(PRICING)}
selected={selectedTab === PRICING}
panelUpdated={updatedTab === PRICING}
updateInProgress={updateInProgress}
disabled={!stepsStatus[PRICING]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, PRICING)}
onChange={onChange}
onSubmit={values => {
if (isNew) {
onUpdateListingDraft(values);
const pathParams = tabParams(PHOTOS);
// Redirect to photos tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(PRICING, values);
}
}}
/>
<EditListingPhotosPanel
className={css.panel}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelPhotos' })}
tabLinkProps={tabLink(PHOTOS)}
selected={selectedTab === PHOTOS}
disabled={!stepsStatus[PHOTOS]}
panelUpdated={updatedTab === PHOTOS}
newListingCreated={newListingCreated}
updateInProgress={updateInProgress}
errors={errors}
fetchInProgress={fetchInProgress}
listing={listing}
images={images}
onImageUpload={onImageUpload}
onRemoveImage={onRemoveImage}
onPayoutDetailsFormChange={onPayoutDetailsFormChange}
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
submitButtonText={submitText(intl, isNew, PHOTOS)}
onChange={onChange}
onSubmit={values => {
const { images: updatedImages } = values;
const updateValues = { ...listing.attributes, images: updatedImages };
if (isNew) {
onCreateListing(updateValues);
} else {
const imageIds = updatedImages.map(img => img.imageId || img.id);
update(PHOTOS, { images: imageIds });
}
}}
onUpdateImageOrder={onUpdateImageOrder}
currentUser={currentUser}
onManageDisableScrolling={onManageDisableScrolling}
/>
</Tabs>
);
};
// If selectedStep is not active, redirect to the beginning of wizard
if (!stepsStatus[selectedTab]) {
return <NamedRedirect name="EditListingPage" params={tabParams(DESCRIPTION)} />;
}
const onUpsertListingDraft = currentListing.id ? onUpdateListingDraft : onCreateListingDraft;
const update = (tab, values) => {
onUpdateListing(tab, { ...values, id: currentListing.id });
};
const { width } = viewport;
const hasViewport = width > 0;
const hasHorizontalTabLayout = hasViewport && width <= MAX_HORIZONTAL_NAV_SCREEN_WIDTH;
const hasVerticalTabLayout = hasViewport && width > MAX_HORIZONTAL_NAV_SCREEN_WIDTH;
const hasFontsLoaded =
hasViewport && document.documentElement.classList.contains('fontsLoaded');
// Check if scrollToTab call is needed (tab is not visible on mobile)
if (hasVerticalTabLayout) {
this.hasScrolledToTab = true;
} else if (hasHorizontalTabLayout && !this.hasScrolledToTab && hasFontsLoaded) {
const tabPrefix = id;
scrollToTab(tabPrefix, selectedTab);
this.hasScrolledToTab = true;
}
return (
<Tabs rootClassName={classes} navRootClassName={css.nav} tabRootClassName={css.tab}>
<EditListingDescriptionPanel
className={css.panel}
tabId={`${id}_a${DESCRIPTION}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelDescription' })}
tabLinkProps={tabLink(DESCRIPTION)}
selected={selectedTab === DESCRIPTION}
panelUpdated={updatedTab === DESCRIPTION}
updateInProgress={updateInProgress}
disabled={!stepsStatus[DESCRIPTION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, DESCRIPTION)}
onChange={onChange}
onSubmit={values => {
const { title, description, category } = values;
const updateValues = {
title,
description,
customAttributes: { category },
publicData: { category },
};
if (isNew) {
onUpsertListingDraft(updateValues);
const pathParams = tabParams(LOCATION);
// Redirect to location tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(DESCRIPTION, updateValues);
}
}}
/>
<EditListingLocationPanel
className={css.panel}
tabId={`${id}_${LOCATION}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelLocation' })}
tabLinkProps={tabLink(LOCATION)}
selected={selectedTab === LOCATION}
panelUpdated={updatedTab === LOCATION}
updateInProgress={updateInProgress}
disabled={!stepsStatus[LOCATION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, LOCATION)}
onChange={onChange}
onSubmit={values => {
const { building = '', location } = values;
const { selectedPlace: { address, origin } } = location;
const updateValues = {
geolocation: origin,
publicData: {
location: { address, building },
},
};
if (isNew) {
// TODO When API supports building number, etc. change this to use those fields instead.
onUpdateListingDraft(updateValues);
const pathParams = tabParams(PRICING);
// Redirect to pricing tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(LOCATION, updateValues);
}
}}
/>
<EditListingPricingPanel
className={css.panel}
tabId={`${id}_${PRICING}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelPricing' })}
tabLinkProps={tabLink(PRICING)}
selected={selectedTab === PRICING}
panelUpdated={updatedTab === PRICING}
updateInProgress={updateInProgress}
disabled={!stepsStatus[PRICING]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, PRICING)}
onChange={onChange}
onSubmit={values => {
if (isNew) {
onUpdateListingDraft(values);
const pathParams = tabParams(PHOTOS);
// Redirect to photos tab
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(PRICING, values);
}
}}
/>
<EditListingPhotosPanel
className={css.panel}
tabId={`${id}_${PHOTOS}`}
tabLabel={intl.formatMessage({ id: 'EditListingWizard.tabLabelPhotos' })}
tabLinkProps={tabLink(PHOTOS)}
selected={selectedTab === PHOTOS}
disabled={!stepsStatus[PHOTOS]}
panelUpdated={updatedTab === PHOTOS}
newListingCreated={newListingCreated}
updateInProgress={updateInProgress}
errors={errors}
fetchInProgress={fetchInProgress}
listing={listing}
images={images}
onImageUpload={onImageUpload}
onRemoveImage={onRemoveImage}
onPayoutDetailsFormChange={onPayoutDetailsFormChange}
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
submitButtonText={submitText(intl, isNew, PHOTOS)}
onChange={onChange}
onSubmit={values => {
const { country, images: updatedImages } = values;
const updateValues = { ...listing.attributes, country, images: updatedImages };
if (isNew) {
onCreateListing(updateValues);
} else {
const imageIds = updatedImages.map(img => img.imageId || img.id);
update(PHOTOS, { images: imageIds });
}
}}
onUpdateImageOrder={onUpdateImageOrder}
currentUser={currentUser}
onManageDisableScrolling={onManageDisableScrolling}
/>
</Tabs>
);
}
}
EditListingWizard.defaultProps = {
className: null,
@ -259,9 +306,10 @@ EditListingWizard.defaultProps = {
updatedTab: null,
};
const { array, bool, func, object, oneOf, shape, string } = PropTypes;
const { array, bool, func, number, object, oneOf, shape, string } = PropTypes;
EditListingWizard.propTypes = {
id: string.isRequired,
className: string,
params: shape({
id: string.isRequired,
@ -311,8 +359,14 @@ EditListingWizard.propTypes = {
updatedTab: string,
updateInProgress: bool.isRequired,
// from withViewport
viewport: shape({
width: number.isRequired,
height: number.isRequired,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
export default injectIntl(EditListingWizard);
export default compose(withViewport, injectIntl)(EditListingWizard);

View file

@ -6,14 +6,14 @@ import { NamedLink } from '../../components';
import css from './TabNav.css';
const Tab = props => {
const { className, disabled, text, selected, linkProps } = props;
const { className, id, disabled, text, selected, linkProps } = props;
const linkClasses = classNames(css.link, {
[css.selectedLink]: selected,
[css.disabled]: disabled,
});
return (
<div className={className}>
<div id={id} className={className}>
<NamedLink className={linkClasses} {...linkProps}>
{text}
</NamedLink>
@ -26,6 +26,7 @@ Tab.defaultProps = { className: null, disabled: false, selected: false };
const { arrayOf, bool, node, object, string } = PropTypes;
Tab.propTypes = {
id: string.isRequired,
className: string,
text: node.isRequired,
disabled: bool,
@ -40,8 +41,8 @@ const TabNav = props => {
return (
<nav className={classes}>
{tabs.map((tab, index) => {
const key = typeof tab.text === 'string' ? tab.text : index;
return <Tab key={key} className={tabClasses} {...tab} />;
const id = typeof tab.id === 'string' ? tab.id : `${index}`;
return <Tab key={id} id={id} className={tabClasses} {...tab} />;
})}
</nav>
);

View file

@ -20,13 +20,13 @@ const selfLinkProps = {
const TabsWrapper = () => {
return (
<Tabs>
<TestPanel tabLabel="Description" tabLinkProps={selfLinkProps}>
<TestPanel tabId="Description" tabLabel="Description" tabLinkProps={selfLinkProps}>
Description form stuff
</TestPanel>
<TestPanel selected tabLabel="Location" tabLinkProps={selfLinkProps}>
<TestPanel selected tabId="Location" tabLabel="Location" tabLinkProps={selfLinkProps}>
Location form stuff
</TestPanel>
<TestPanel tabLabel="Price" tabLinkProps={selfLinkProps} disabled>
<TestPanel tabId="Price" tabLabel="Price" tabLinkProps={selfLinkProps} disabled>
Price form stuff
</TestPanel>
</Tabs>

View file

@ -28,18 +28,20 @@ const Tabs = props => {
const classes = classNames(rootClasses, className);
const tabNavTabs = React.Children.map(children, child => {
const { tabLabel, tabLinkProps } = child.props;
const { tabId, tabLabel, tabLinkProps } = child.props;
// Child components need to have TabNav props included
if (!tabLabel || !tabLinkProps) {
if (!tabId || !tabLabel || !tabLinkProps) {
throw new Error(
`Tabs component: a child component is missing required props.
tabId: (${tabId})
tabLabel: (${tabLabel})
tabLinkProps: (${tabLinkProps})`
);
}
return {
id: tabId,
text: child.props.tabLabel,
linkProps: child.props.tabLinkProps,
disabled: child.props.disabled,

View file

@ -120,6 +120,7 @@ export const EditListingPageComponent = props => {
mobileClassName={css.mobileTopbar}
/>
<EditListingWizard
id="EditListingWizard"
className={css.wizard}
params={params}
disabled={disableForm}

View file

@ -6,7 +6,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
title="EditListingPage.titleCreateListing"
>
<withRouter(Connect(TopbarContainerComponent)) />
<InjectIntl(EditListingWizard)
<withViewport(InjectIntl(EditListingWizard))
currentUser={null}
errors={
Object {
@ -23,6 +23,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
"push": [Function],
}
}
id="EditListingWizard"
images={Array []}
newListingCreated={false}
onChange={[Function]}