nexxtway/react-rainbow

View on GitHub
src/components/MapMarker/index.js

Summary

Maintainability
A
1 hr
Test Coverage
/* eslint-disable react/sort-comp */
/* eslint-disable react/prop-types */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Consumer } from '../GMap/context';
import { uniqueId } from '../../libs/utils';
import Icon from './icon';
import HiddenElement from '../Structural/hiddenElement';
import StyledButton from './styled/button';
import StyledTextContainer from './styled/textContainer';
import StyledLabel from './styled/label';
import StyledLi from './styled/li';

const marker = Symbol('marker');

class Marker extends Component {
    constructor(props) {
        super(props);
        this.getLocationInfo = this.getLocationInfo.bind(this);
        this.handleClick = this.handleClick.bind(this);
        this.handleMarkerClick = this.handleMarkerClick.bind(this);
        this.startAnimation = this.startAnimation.bind(this);
        this.stopAnimation = this.stopAnimation.bind(this);
        this.name = uniqueId('marker');

        const { label, description } = props;
        this.state = {
            label,
            description,
        };
    }

    componentDidMount() {
        const { map } = this.props;
        if (map) {
            this.setMarker();
        }
    }

    componentDidUpdate() {
        const { map, latitude, longitude } = this.props;

        if (!this[marker] && map && latitude && longitude) {
            this.setMarker();
        }
    }

    componentWillUnmount() {
        window.google.maps.event.removeListener(this.markerListener);
    }

    setMarker() {
        const { map, latitude, longitude, geocoder, label, description } = this.props;
        const { infowindow } = this.state;

        const position = {
            lat: latitude,
            lng: longitude,
        };
        this[marker] = new window.google.maps.Marker({
            position,
            map,
        });
        this.markerListener = this[marker].addListener('click', this.handleMarkerClick);
        if (!label && !description) {
            geocoder.geocode({ location: position }, this.getLocationInfo);
        } else if (!infowindow) {
            this.setInfoWindow();
        }
    }

    getLocationInfo(results, status) {
        if (status === 'OK' && results[0]) {
            const geoLabel = results[0].address_components[0].long_name;
            const geoDescription = results[0].formatted_address;
            const infowindow = new window.google.maps.InfoWindow({
                content: geoDescription,
            });
            this.setState({
                label: geoLabel,
                description: geoDescription,
                infowindow,
            });
        }
    }

    setInfoWindow() {
        const { description, label } = this.props;
        this.setState({
            infowindow: new window.google.maps.InfoWindow({
                content: description || label,
            }),
        });
    }

    getAssistiveAriaLiveText() {
        const { label } = this.state;
        if (this.isSelected()) {
            return `${label} is currently selected`;
        }
        return undefined;
    }

    handleMarkerClick() {
        const { privateMarkerClick } = this.props;
        const { infowindow } = this.state;
        if (this[marker]) {
            privateMarkerClick(infowindow, this[marker]);
        }
    }

    handleClick() {
        const { privateOnClick, latitude, longitude } = this.props;
        const position = {
            lat: latitude,
            lng: longitude,
        };
        if (this[marker]) {
            this[marker].setAnimation(null);
            privateOnClick(this.name, position);
        }
    }

    startAnimation() {
        if (this[marker]) {
            this[marker].setAnimation(window.google.maps.Animation.BOUNCE);
        }
    }

    stopAnimation() {
        if (this[marker]) {
            this[marker].setAnimation(null);
        }
    }

    isSelected() {
        const { selectedMarker } = this.props;
        return this.name === selectedMarker;
    }

    render() {
        const { className, style, latitude, longitude, icon } = this.props;
        const { label, description } = this.state;

        if (latitude && longitude) {
            return (
                <StyledLi className={className} style={style}>
                    <HiddenElement aria-live="polite">
                        {this.getAssistiveAriaLiveText()}
                    </HiddenElement>
                    <StyledButton
                        aria-pressed={this.isSelected()}
                        onClick={this.handleClick}
                        onMouseOver={this.startAnimation}
                        onFocus={this.startAnimation}
                        onMouseLeave={this.stopAnimation}
                        onBlur={this.stopAnimation}
                    >
                        <Icon icon={icon} />
                        <StyledTextContainer>
                            <StyledLabel>{label}</StyledLabel>
                            <span>{description}</span>
                        </StyledTextContainer>
                    </StyledButton>
                </StyledLi>
            );
        }
        return null;
    }
}

/**
 * The MapMarker component is a single section of information that is nested in the GMap component.
 * This component shows you the detailed information of each location that is displayed in the GMap.
 */
export default function MapMarker(props) {
    // eslint-disable-next-line react/jsx-props-no-spreading
    return <Consumer>{context => <Marker {...props} {...context} />}</Consumer>;
}

MapMarker.propTypes = {
    /** The label of the marker. */
    label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
    /** The description of the marker. */
    description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
    /** The angular distance of a place north or south of the earth's equator. */
    latitude: PropTypes.number,
    /** The angular distance of a place east or west of the meridian at Greenwich. */
    longitude: PropTypes.number,
    /** The icon to show if it is passed. If not passed a fallback icon will be showed. */
    icon: PropTypes.node,
    /** A CSS class for the outer element, in addition to the component's base classes. */
    className: PropTypes.string,
    /** An object with custom style applied to the outer element. */
    style: PropTypes.object,
};

MapMarker.defaultProps = {
    label: undefined,
    description: undefined,
    latitude: undefined,
    longitude: undefined,
    icon: null,
    className: undefined,
    style: undefined,
};