Merge pull request #489 from sharetribe/map-button-placement

Map button placement in mobile search view
This commit is contained in:
Hannu Lyytikäinen 2017-10-13 11:54:06 +03:00 committed by GitHub
commit c4ec09f4c8
8 changed files with 136 additions and 163 deletions

View file

@ -382,6 +382,7 @@ class LocationAutocompleteInput extends Component {
placeholder,
input,
meta,
inputRef,
} = this.props;
const { name, onFocus } = input;
const { search, predictions } = currentValue(this.props);
@ -421,6 +422,7 @@ class LocationAutocompleteInput extends Component {
onBlur={this.handleOnBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
ref={inputRef}
/>
{renderPredictions ? (
<LocationPredictionsList
@ -448,6 +450,7 @@ LocationAutocompleteInput.defaultProps = {
validClassName: null,
placeholder: '',
meta: null,
inputRef: null,
};
LocationAutocompleteInput.propTypes = {
@ -475,6 +478,7 @@ LocationAutocompleteInput.propTypes = {
valid: bool.isRequired,
touched: bool.isRequired,
}),
inputRef: func,
};
export default LocationAutocompleteInput;

View file

@ -1,61 +0,0 @@
import React, { PropTypes } from 'react'; // eslint-disable-line react/no-deprecated
const MapIcon = props => {
const { className } = props;
return (
<svg
className={className}
width="27"
height="27"
viewBox="0 0 27 27"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<filter
x="-3.8%"
y="-10.4%"
width="107.7%"
height="129.2%"
filterUnits="objectBoundingBox"
id="a"
>
<feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1" />
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
in="shadowBlurOuter1"
result="shadowMatrixOuter1"
/>
<feMerge>
<feMergeNode in="shadowMatrixOuter1" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<g
filter="url(#a)"
transform="translate(-41 -12)"
strokeWidth="1.4"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M51.667 32L46 27.952V15l5.667 4.048L57.333 15 63 19.048V32l-5.667-4.048zM51.7 19v13M57.3 28V15" />
</g>
</svg>
);
};
const { string } = PropTypes;
MapIcon.defaultProps = {
className: null,
};
MapIcon.propTypes = {
className: string,
};
export default MapIcon;

View file

@ -46,14 +46,51 @@
}
.searchResultSummary {
/* Layout */
margin: 8px 24px 14px 24px;
display: none;
@media (--viewportMedium) {
display: block;
margin: 8px 24px 14px 24px;
}
@media (--viewportLarge) {
margin: 8px 36px 14px 36px;
}
}
.searchResultSummaryMobile {
display: flex;
justify-content: space-between;
background-color: var(--matterColorBright);
width: 100vw;
padding: 8px 24px 14px 24px;
/* sticky result summary in mobile */
position: sticky;
top: 0;
z-index: 9;
@media (--viewportMedium) {
display: none;
}
}
.mapIcon {
/* background map image */
background-image: url(./images/map_icon@2x.png);
background-size: cover;
border-radius: 3px;
width: 72px;
height: 35px;
margin-top: 23px;
padding: 9px 22px;
/* Font */
@apply --marketplaceTinyFontStyles;
}
.error {
color: var(--failColor);
}
@ -78,50 +115,6 @@
flex-grow: 1;
}
.openMobileMap {
/* Font */
@apply --marketplaceH4FontStyles;
font-weight: var(--fontWeightSemiBold);
/* Positioning */
/* Positioning from top to place button just above Safari's bottom toolbar
/* 120px = button height + toolbar height (68px) */
position: fixed;
top: calc(100vh - 120px);
left: 50vw;
width: 175px;
transform: translateX(-50%);
margin-top: 0;
margin-bottom: 0;
/* Button's layout */
padding: 12px 36px;
background-color: var(--matterColorLight);
border-radius: 30px;
border: 0;
box-shadow: var(--boxShadow);
cursor: pointer;
transition: var(--transitionStyleButton);
& .openMobileMapIcon {
stroke: var(--matterColor);
margin-right: 4px;
}
&:hover {
color: var(--marketplaceColor);
box-shadow: var(--boxShadowPopupLight);
& .openMobileMapIcon {
stroke: var(--marketplaceColor);
}
}
@media (--viewportMedium) {
display: none;
}
}
.mapPanel {
@media (--viewportMedium) {
/**

View file

@ -22,7 +22,6 @@ import { SearchMap, ModalInMobile, Page, SearchResultsPanel } from '../../compon
import { TopbarContainer } from '../../containers';
import { searchListings, searchMapListings } from './SearchPage.duck';
import MapIcon from './MapIcon';
import css from './SearchPage.css';
// Pagination page size might need to be dynamic on responsive page layouts
@ -198,6 +197,12 @@ export class SearchPageComponent extends Component {
);
const resultsFound = address && !mapSearch ? resultsFoundWithAddress : resultsFoundNoAddress;
const resultsFoundMobile = (
<h2>
<FormattedMessage id="SearchPage.foundResultsMobile" values={{ count: totalItems }} />
</h2>
);
const noResults = (
<h2>
<FormattedMessage id="SearchPage.noResults" />
@ -289,6 +294,24 @@ export class SearchPageComponent extends Component {
{listingsAreLoaded && totalItems === 0 ? noResults : null}
{searchInProgress ? loadingResults : null}
</div>
<div className={css.searchResultSummaryMobile}>
<div>
{searchListingsError ? searchError : null}
{listingsAreLoaded && totalItems > 0 ? resultsFoundMobile : null}
{listingsAreLoaded && totalItems === 0 ? noResults : null}
{searchInProgress ? loadingResults : null}
</div>
<div
className={css.mapIcon}
onClick={() => {
this.useLocationSearchBounds = true;
this.modalOpenedBoundsChange = true;
this.setState({ isSearchMapOpenOnMobile: true });
}}
>
<FormattedMessage id="SearchPage.openMapView" className={css.mapIconText} />
</div>
</div>
<div
className={classNames(css.listings, {
[css.newSearchInProgress]: !listingsAreLoaded,
@ -299,19 +322,7 @@ export class SearchPageComponent extends Component {
listings={listings}
pagination={listingsAreLoaded ? pagination : null}
search={searchParamsForPagination}
>
<div
className={css.openMobileMap}
onClick={() => {
this.locationInputSearch = true;
this.modalOpenedBoundsChange = true;
this.setState({ isSearchMapOpenOnMobile: true });
}}
>
<MapIcon className={css.openMobileMapIcon} />
<FormattedMessage id="SearchPage.openMapView" />
</div>
</SearchResultsPanel>
/>
</div>
</div>
<ModalInMobile

View file

@ -32,6 +32,28 @@ exports[`SearchPageComponent matches snapshot 1`] = `
/>
</h2>
</div>
<div>
<div>
<h2>
<FormattedMessage
id="SearchPage.foundResultsMobile"
values={
Object {
"count": 22,
}
}
/>
</h2>
</div>
<div
onClick={[Function]}
>
<FormattedMessage
id="SearchPage.openMapView"
values={Object {}}
/>
</div>
</div>
<div
className=""
>
@ -48,19 +70,7 @@ exports[`SearchPageComponent matches snapshot 1`] = `
}
rootClassName={null}
search={Object {}}
>
<div
onClick={[Function]}
>
<MapIcon
className={null}
/>
<FormattedMessage
id="SearchPage.openMapView"
values={Object {}}
/>
</div>
</SearchResultsPanel>
/>
</div>
</div>
<withViewport(ModalInMobileComponent)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,4 +1,4 @@
import React, { PropTypes } from 'react'; // eslint-disable-line react/no-deprecated
import React, { Component, PropTypes } from 'react'; // eslint-disable-line react/no-deprecated
import { compose } from 'redux';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
@ -7,43 +7,58 @@ import { Form, LocationAutocompleteInput } from '../../components';
import css from './TopbarSearchForm.css';
const TopbarSearchFormComponent = props => {
const { rootClassName, className, desktopInputRoot, intl, onSubmit, isMobile } = props;
class TopbarSearchFormComponent extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.searchInput = null;
}
const onChange = location => {
onChange(location) {
if (location.selectedPlace) {
// Note that we use `onSubmit` instead of the conventional
// `handleSubmit` prop for submitting. We want to autosubmit
// when a place is selected, and don't require any extra
// validations for the form.
onSubmit({ location });
this.props.onSubmit({ location });
// blur search input to hide software keyboard
if (this.searchInput) {
this.searchInput.blur();
}
}
};
}
const classes = classNames(rootClassName, className);
const desktopInputRootClass = desktopInputRoot || css.desktopInputRoot;
render() {
const { rootClassName, className, desktopInputRoot, intl, isMobile } = this.props;
// Allow form submit only when the place has changed
const preventFormSubmit = e => e.preventDefault();
const classes = classNames(rootClassName, className);
const desktopInputRootClass = desktopInputRoot || css.desktopInputRoot;
return (
<Form className={classes} onSubmit={preventFormSubmit}>
<Field
name="location"
label="Location"
className={isMobile ? css.mobileInputRoot : desktopInputRootClass}
iconClassName={isMobile ? css.mobileIcon : css.desktopIcon}
inputClassName={isMobile ? css.mobileInput : css.desktopInput}
predictionsClassName={isMobile ? css.mobilePredictions : css.desktopPredictions}
placeholder={intl.formatMessage({ id: 'TopbarSearchForm.placeholder' })}
format={null}
component={LocationAutocompleteInput}
closeOnBlur={!isMobile}
onChange={onChange}
/>
</Form>
);
};
// Allow form submit only when the place has changed
const preventFormSubmit = e => e.preventDefault();
return (
<Form className={classes} onSubmit={preventFormSubmit}>
<Field
name="location"
label="Location"
className={isMobile ? css.mobileInputRoot : desktopInputRootClass}
iconClassName={isMobile ? css.mobileIcon : css.desktopIcon}
inputClassName={isMobile ? css.mobileInput : css.desktopInput}
predictionsClassName={isMobile ? css.mobilePredictions : css.desktopPredictions}
placeholder={intl.formatMessage({ id: 'TopbarSearchForm.placeholder' })}
format={null}
component={LocationAutocompleteInput}
closeOnBlur={!isMobile}
onChange={this.onChange}
inputRef={node => {
this.searchInput = node;
}}
/>
</Form>
);
}
}
const { func, string, bool } = PropTypes;

View file

@ -448,11 +448,12 @@
"SalePage.title": "Sale details: {title}",
"SearchMapInfoCard.noImage": "No image",
"SearchPage.foundResults": "{count, number} {count, plural, one {sauna} other {saunas}} found",
"SearchPage.foundResultsMobile": "{count, number} {count, plural, one {result} other {results}}",
"SearchPage.foundResultsWithAddress":
"{count, number} {count, plural, one {sauna} other {saunas}} around {address}",
"SearchPage.loadingResults": "Loading search results…",
"SearchPage.noResults": "Could not find any listings.",
"SearchPage.openMapView": "Map view",
"SearchPage.openMapView": "Map",
"SearchPage.schemaMapSearch": "map search",
"SearchPage.schemaDescription": "Showing search results",
"SearchPage.schemaTitle": "Search results for {searchAddress} | {siteTitle}",