Merge pull request #877 from sharetribe/reusable-search-map

Reusable map container
This commit is contained in:
Vesa Luusua 2018-08-01 16:51:31 +03:00 committed by GitHub
commit 31f4dda8bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 165 additions and 29 deletions

View file

@ -13,6 +13,8 @@ way to update this template, but currently, we follow a pattern:
---
## Upcoming version
* [change] Reusable SearchMap.
[#877](https://github.com/sharetribe/flex-template-web/pull/877)
* [change] Use seeded random for client side coordinate obfuscation
[#874](https://github.com/sharetribe/flex-template-web/pull/874)

View file

@ -0,0 +1,90 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { node, string } from 'prop-types';
import css from './SearchMap.css';
class ReusableMapContainer extends React.Component {
constructor(props) {
super(props);
if (typeof window !== 'undefined') {
window.reusableSearchMapElement =
window.reusableSearchMapElement || document.createElement('div');
if (!props.className) {
console.warn('ReusableMapContainer should get className prop which defines its layout');
}
// If no className is given, we use some defaults, which makes it easier to debug loading.
const mapLayoutClassName = props.className || css.defaultMapLayout;
this.el = window.reusableSearchMapElement;
this.el.id = 'search-map';
this.el.classList.add(mapLayoutClassName);
}
this.mountNode = null;
this.renderSearchMap = this.renderSearchMap.bind(this);
}
componentDidMount() {
this.renderSearchMap();
}
componentDidUpdate() {
this.renderSearchMap();
}
componentWillUnmount() {
this.el.classList.add(css.reusableMapHidden);
this.mountNode.removeChild(this.el);
document.body.appendChild(this.el);
}
renderSearchMap() {
const targetDomNode = document.getElementById(this.el.id);
// Check if we have already added map somewhere on the DOM
if (!targetDomNode) {
if (this.mountNode && !this.mountNode.firstChild) {
// If mountable, but not yet mounted, append rendering context inside SPA rendering tree.
this.mountNode.appendChild(this.el);
} else if (!this.mountNode) {
// if no mountNode is found, append this outside SPA rendering tree (to document body)
document.body.appendChild(this.el);
}
} else {
this.el.classList.remove(css.reusableMapHidden);
if (this.mountNode && !this.mountNode.firstChild) {
// Move the map to correct location if we have rendered the map before
// (but it's not yet moved to correct location of rendering tree).
document.body.removeChild(this.el);
this.mountNode.appendChild(this.el);
}
}
ReactDOM.render(this.props.children, this.el);
}
render() {
return (
<div
className={css.reusableMap}
ref={node => {
this.mountNode = node;
}}
/>
);
}
}
ReusableMapContainer.defaultProps = {
className: string,
};
ReusableMapContainer.propTypes = {
children: node.isRequired,
className: string,
};
export default ReusableMapContainer;

View file

@ -10,3 +10,34 @@
width: 100%;
height: 100%;
}
.reusableMap {
width: 100%;
height: 100%;
}
.defaultMapLayout {
position: fixed;
top: 0;
right: 0;
width: 50vw;
height: 100vh;
}
/**
* When reusable map is attached right to the body it's hidden.
* Specificity rule is added to overwrite positioning coming from props.className
*/
body > .reusableMapHidden {
position: absolute;
top: -1000px;
left: -1000px;
visibility: hidden;
opacity: 0;
@media (--viewportMedium) {
top: -1000px;
left: -1000px;
right: auto;
}
}

View file

@ -12,6 +12,7 @@ import { googleBoundsToSDKBounds } from '../../util/googleMaps';
import { SearchMapInfoCard, SearchMapPriceLabel, SearchMapGroupLabel } from '../../components';
import config from '../../config';
import ReusableMapContainer from './ReusableMapContainer';
import css from './SearchMap.css';
const LABEL_HANDLE = 'SearchMapLabel';
@ -240,6 +241,7 @@ export class SearchMapComponent extends Component {
const {
className,
rootClassName,
reuseableContainerClassName,
center,
isOpenOnModal,
listings: originalListings,
@ -263,28 +265,30 @@ export class SearchMapComponent extends Component {
// container element listens clicks so that opened SearchMapInfoCard can be closed
/* eslint-disable jsx-a11y/no-static-element-interactions */
return isMapsLibLoaded ? (
<MapWithGoogleMap
containerElement={<div className={classes} onClick={this.onMapClicked} />}
mapElement={<div className={mapClasses} />}
center={center}
isOpenOnModal={isOpenOnModal}
listings={listings}
activeListingId={activeListingId}
infoCardOpen={this.state.infoCardOpen}
onListingClicked={this.onListingClicked}
onMapLoad={this.onMapLoadHandler}
onIdle={() => {
if (this.googleMap) {
onIdle(this.googleMap);
}
}}
onCloseAsModal={() => {
if (onCloseAsModal) {
onCloseAsModal();
}
}}
zoom={zoom}
/>
<ReusableMapContainer className={reuseableContainerClassName}>
<MapWithGoogleMap
containerElement={<div className={classes} onClick={this.onMapClicked} />}
mapElement={<div className={mapClasses} />}
center={center}
isOpenOnModal={isOpenOnModal}
listings={listings}
activeListingId={activeListingId}
infoCardOpen={this.state.infoCardOpen}
onListingClicked={this.onListingClicked}
onMapLoad={this.onMapLoadHandler}
onIdle={() => {
if (this.googleMap) {
onIdle(this.googleMap);
}
}}
onCloseAsModal={() => {
if (onCloseAsModal) {
onCloseAsModal();
}
}}
zoom={zoom}
/>
</ReusableMapContainer>
) : (
<div className={classes} />
);
@ -293,31 +297,33 @@ export class SearchMapComponent extends Component {
}
SearchMapComponent.defaultProps = {
className: null,
rootClassName: null,
mapRootClassName: null,
reuseableContainerClassName: null,
bounds: null,
center: new sdkTypes.LatLng(0, 0),
activeListingId: null,
className: null,
isOpenOnModal: false,
listings: [],
mapRootClassName: null,
onCloseAsModal: null,
rootClassName: null,
useLocationSearchBounds: true,
zoom: 11,
coordinatesConfig: config.coordinates,
};
SearchMapComponent.propTypes = {
className: string,
rootClassName: string,
mapRootClassName: string,
reuseableContainerClassName: string,
bounds: propTypes.latlngBounds,
center: propTypes.latlng,
activeListingId: propTypes.uuid,
className: string,
isOpenOnModal: bool,
listings: arrayOf(propTypes.listing),
mapRootClassName: string,
onCloseAsModal: func,
onIdle: func.isRequired,
rootClassName: string,
useLocationSearchBounds: bool, // eslint-disable-line react/no-unused-prop-types
zoom: number,
coordinatesConfig: shape({

View file

@ -137,6 +137,11 @@
}
}
.mapWrapper {
width: 100%;
height: 100%;
}
.map {
width: 100vw;
height: 100vh;

View file

@ -251,9 +251,10 @@ export class SearchPageComponent extends Component {
showAsModalMaxWidth={MODAL_BREAKPOINT}
onManageDisableScrolling={onManageDisableScrolling}
>
<div className={css.map}>
<div className={css.mapWrapper}>
{shouldShowSearchMap ? (
<SearchMap
reuseableContainerClassName={css.map}
activeListingId={activeListingId}
bounds={bounds}
center={origin}

View file

@ -116,6 +116,7 @@ exports[`SearchPageComponent matches snapshot 1`] = `
mapRootClassName={null}
onCloseAsModal={[Function]}
onIdle={[Function]}
reuseableContainerClassName={null}
rootClassName={null}
useLocationSearchBounds={true}
zoom={11}