diff --git a/src/components/EditListingWizard/EditListingWizard.css b/src/components/EditListingWizard/EditListingWizard.css index f24ab657..6a00506d 100644 --- a/src/components/EditListingWizard/EditListingWizard.css +++ b/src/components/EditListingWizard/EditListingWizard.css @@ -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; + } } } diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js index b3d123a0..f1aa91bc 100644 --- a/src/components/EditListingWizard/EditListingWizard.js +++ b/src/components/EditListingWizard/EditListingWizard.js @@ -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 ; + // 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 ( - - { - 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); - } - }} - /> - { - 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); - } - }} - /> - { - if (isNew) { - onUpdateListingDraft(values); - const pathParams = tabParams(PHOTOS); - // Redirect to photos tab - history.push( - createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {}) - ); - } else { - update(PRICING, 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} - /> - - ); -}; + // If selectedStep is not active, redirect to the beginning of wizard + if (!stepsStatus[selectedTab]) { + return ; + } + + 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 ( + + { + 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); + } + }} + /> + { + 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); + } + }} + /> + { + if (isNew) { + onUpdateListingDraft(values); + const pathParams = tabParams(PHOTOS); + // Redirect to photos tab + history.push( + createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {}) + ); + } else { + update(PRICING, 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} + /> + + ); + } +} 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); diff --git a/src/components/TabNav/TabNav.js b/src/components/TabNav/TabNav.js index 53262f9e..9ba8fdc5 100644 --- a/src/components/TabNav/TabNav.js +++ b/src/components/TabNav/TabNav.js @@ -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 ( -
+
{text} @@ -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 ( ); diff --git a/src/components/Tabs/Tabs.example.js b/src/components/Tabs/Tabs.example.js index bcf21fad..ed7a85ed 100644 --- a/src/components/Tabs/Tabs.example.js +++ b/src/components/Tabs/Tabs.example.js @@ -20,13 +20,13 @@ const selfLinkProps = { const TabsWrapper = () => { return ( - + Description form stuff - + Location form stuff - + Price form stuff diff --git a/src/components/Tabs/Tabs.js b/src/components/Tabs/Tabs.js index aaf8a1a4..d7b19071 100644 --- a/src/components/Tabs/Tabs.js +++ b/src/components/Tabs/Tabs.js @@ -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, diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js index 070c7ef2..6ca4b854 100644 --- a/src/containers/EditListingPage/EditListingPage.js +++ b/src/containers/EditListingPage/EditListingPage.js @@ -120,6 +120,7 @@ export const EditListingPageComponent = props => { mobileClassName={css.mobileTopbar} /> -