Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/public/story-assets/map-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions memory-bank/usage/map.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,27 @@ graph TD
- `address`: String address to display on the map (required)
- `zoom`: Optional zoom level (inherited from MapBaseProps)
- `className`: Optional CSS class name (inherited from MapBaseProps)
- `forceAspectRatio`: Optional boolean to force aspect ratio (16:9 for Desktop, 4:3 for Mobile), `true` by default (inherited from MapBaseProps)

#### YMapProps (Yandex Maps)

- **Description**: Props for Yandex Maps implementation
- **File**: `src/components/Map/YMap/YMap.tsx`
- **Properties**:
- `markers`: Array of YMapMarker objects to display on the map (required)
- `id`: Unique identifier for the map instance (required)
- `zoom`: Optional zoom level (inherited from MapBaseProps)
- `className`: Optional CSS class name (inherited from MapBaseProps)
- `disableControls`: Optional boolean to hide map controls (Yandex Maps only), `false` by default
- `disableBalloons`: Optional boolean to disable info balloons (Yandex Maps only), `false` by default

#### MapBaseProps (Common Props)

- **Description**: Common props available for both Google Maps and Yandex Maps
- **Properties**:
- `zoom`: Optional zoom level for the map
- `className`: Optional CSS class name for styling
- `forceAspectRatio`: Optional boolean to force aspect ratio (16:9 for Desktop, 4:3 for Mobile), `true` by default

### YMapMarker Interface

Expand All @@ -86,6 +98,12 @@ graph TD
- `iconCaption`: Optional caption text for the marker
- `iconContent`: Optional content text for the marker
- `iconColor`: Optional color for the marker icon
- `iconImageHref`: Optional URL of geo object's custom icon image file
- `iconImageSize`: Optional dimensions of custom icon image [width, height]
- `iconImageOffset`: Optional custom icon image's offset relative to it's anchor point [x, y]
- `iconImageClipRect`: Optional coordinates of custom icon image's displayed rectangular area [[x1, y1], [x2, y2]]
- `iconLayout`: Optional layout for icon (e.g., 'default#image' for custom icons)
- `iconShape`: Optional icon's active area shape
- `preset`: Optional preset style for the marker

### MapsContext
Expand Down
4 changes: 2 additions & 2 deletions src/blocks/Map/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Map from '../../components/Map/Map';
import MediaBase from '../../components/MediaBase/MediaBase';
import {MapBlockProps} from '../../models';
import {MapBlockProps, MapProps} from '../../models';
import {block} from '../../utils';
import {getMediaBorder} from '../../utils/borderSelector';

Expand All @@ -17,7 +17,7 @@ export const MapBlock = ({map, border, disableShadow, ...props}: MapBlockProps)
return (
<MediaBase {...props}>
<MediaBase.Card>
<Map {...map} className={b({border: borderSelected})} />
<Map {...(map as MapProps)} className={b({border: borderSelected})} />
</MediaBase.Card>
</MediaBase>
);
Expand Down
9 changes: 7 additions & 2 deletions src/components/Map/GoogleMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function getScriptSrc(params: GoogleMapLinkParams) {
}

const GoogleMap = (props: GMapProps) => {
const {address, zoom, className} = props;
const {address, zoom, className, forceAspectRatio = true} = props;
const {apiKey, scriptSrc} = React.useContext(MapsContext);
const {lang = Lang.Ru} = React.useContext(LocaleContext);
const isMobile = React.useContext(MobileContext);
Expand All @@ -43,6 +43,10 @@ const GoogleMap = (props: GMapProps) => {
);

React.useEffect(() => {
if (!forceAspectRatio) {
return;
}

const updateSize = debounce(() => {
if (ref.current) {
setHeight(Math.round(getMapHeight(ref.current.offsetWidth, isMobile)));
Expand All @@ -52,10 +56,11 @@ const GoogleMap = (props: GMapProps) => {
updateSize();
window.addEventListener('resize', updateSize, {passive: true});

// eslint-disable-next-line consistent-return
return () => {
window.removeEventListener('resize', updateSize);
};
}, [isMobile]);
}, [forceAspectRatio, isMobile]);

if (!apiKey || !address) {
return null;
Expand Down
12 changes: 12 additions & 0 deletions src/components/Map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ Map

`address?: string;` - URL-escaped place name, address. You need to use it for `Google maps`

`forceAspectRatio?: boolean` - Determines whether map's aspect ratio is forced autmatically (16:9 for Desktop, 4:3 for Mobile), `true` by default

`id?: string` - map id. You need to use it for `Yandex maps`. As an id, you can specify a short description, for example `offices`, and the full id will be `ymap-offices`

`disableControls?: boolean` - If `true`, hides map's default controls. Only for `Yandex maps`. `false` by default

`disableBalloons?: boolean` - If `true`, disables info ballon opening when clicking on a marker. Only for `Yandex maps`. `false` by default

`markers?: object[]` - Description for placemarkers. You need to use it for `Yandex maps`. Specify the parameters given below.

- `address?: string` — Place name, address
Expand All @@ -17,4 +23,10 @@ Map
- `iconCaption?: string` - Caption for the geo object's icon
- `iconContent?: string` - Content of the geo object's icon
- `iconColor?: string` - The color of the placemark. There are three ways to set the color: using a keyword, in Hex format, or RGB. A red placemark is used by default.
- `iconImageHref?: string` - URL of geo object's custom icon image file
- `iconImageSize?: [number, number]` - Dimensions of custom icon image
- `iconImageOffset?: [number, number]` - Custom icon image's offset relative to it's anchor point
- `iconImageClipRect?: [[number, number], [number, number]]` - Coordinates of custom icon image's displayed rectangular area, in pixels
- `iconLayout?: 'default#image'` - Required to use custom icons for a geo object
- `iconShape?: Record<string, any>` - Icon's active area shape. Refer to documentation [e.g. Circle](https://yandex.ru/dev/jsapi-v2-1/doc/ru/v2-1/ref/reference/shape.Circle)
- `preset?: string` - Key for the placemark's preset options. A `islands#dotIcon` is used by default. The list of available keys is stored in the [presetStorage](https://yandex.com/dev/maps/jsapi/doc/2.1/ref/reference/option.presetStorage.html) description
38 changes: 24 additions & 14 deletions src/components/Map/YMap/YMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {YMapMarker, YMapMarkerLabel, YMapProps} from '../../../models';
import {YMapMarkerLabelPrivate, YMapMarkerPrivate, YMapProps} from '../../../models';
import {Coordinate} from '../../../models/constructor-items/common';

enum GeoObjectTypes {
Expand All @@ -11,10 +11,18 @@ const DEFAULT_PLACEMARKS_COLOR = '#dc534b';
const DEFAULT_PLACEMARKS_PRESET = 'islands#dotIcon';
const DEFAULT_MAP_CONTROL_BUTTON_HEIGHT = 30;

const geoObjectPropsAndOptions = {
const geoObjectPropsAndOptions: Record<keyof YMapMarkerLabelPrivate, GeoObjectTypes> = {
cursor: GeoObjectTypes.Options,
iconCaption: GeoObjectTypes.Properties,
iconContent: GeoObjectTypes.Properties,
iconColor: GeoObjectTypes.Options,
iconImageHref: GeoObjectTypes.Options,
iconImageSize: GeoObjectTypes.Options,
iconImageOffset: GeoObjectTypes.Options,
iconImageClipRect: GeoObjectTypes.Options,
iconLayout: GeoObjectTypes.Options,
iconShape: GeoObjectTypes.Options,
interactivityModel: GeoObjectTypes.Options,
preset: GeoObjectTypes.Options,
};

Expand Down Expand Up @@ -44,7 +52,7 @@ export class YMap {
this.recalcZoomAndCenter(props);
}

async findAddress(marker: YMapMarker) {
async findAddress(marker: YMapMarkerPrivate) {
try {
const res = await window.ymaps.geocode(marker.address, {results: 1});
const geoObject = res.geoObjects.get(0);
Expand All @@ -58,15 +66,15 @@ export class YMap {
} catch {} // If error - placemark will not be displayed
}

findCoordinate(marker: YMapMarker) {
findCoordinate(marker: YMapMarkerPrivate) {
const geoObject = new window.ymaps.Placemark(marker.coordinate, {});

this.coords.push(marker.coordinate as Coordinate);
this.drawPlaceMarkStyle(geoObject, marker);
this.ymap.geoObjects.add(geoObject);
}

private drawPlaceMarkStyle(geoObject: Ymaps.GeoObject, marker: YMapMarker) {
private drawPlaceMarkStyle(geoObject: Ymaps.GeoObject, marker: YMapMarkerPrivate) {
const {iconColor, preset = DEFAULT_PLACEMARKS_PRESET} = marker.label || {};
let localIconColor: string | undefined = iconColor;

Expand All @@ -75,16 +83,18 @@ export class YMap {
localIconColor = DEFAULT_PLACEMARKS_COLOR;
}

Object.entries({...marker.label, iconColor: localIconColor, preset}).forEach(
([key, value]) => {
const geoObjectParamType: GeoObjectTypes | undefined =
geoObjectPropsAndOptions[key as keyof YMapMarkerLabel];
Object.entries({
...marker.label,
iconColor: localIconColor,
preset,
}).forEach(([key, value]) => {
const geoObjectParamType: GeoObjectTypes | undefined =
geoObjectPropsAndOptions[key as keyof YMapMarkerLabelPrivate];

if (value && geoObjectParamType) {
geoObject[geoObjectParamType].set(key, value);
}
},
);
if (value && geoObjectParamType) {
geoObject[geoObjectParamType].set(key, value);
}
});
}

private recalcZoomAndCenter(props: PlacemarksProps) {
Expand Down
54 changes: 47 additions & 7 deletions src/components/Map/YMap/YandexMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import debounce from 'lodash/debounce';
import {LocaleContext} from '../../../context/localeContext/localeContext';
import {MapsContext} from '../../../context/mapsContext/mapsContext';
import {MobileContext} from '../../../context/mobileContext';
import {YMapProps} from '../../../models';
import {YMapMarkerLabelPrivate, YMapMarkerPrivate, YMapProps} from '../../../models';
import {block} from '../../../utils';
import ErrorWrapper from '../../ErrorWrapper/ErrorWrapper';
import {getMapHeight} from '../helpers';
Expand All @@ -23,8 +23,21 @@ const DEFAULT_ZOOM = 9;
// The real center of the map will be calculated later, using the coordinates of the markers
const INITIAL_CENTER = [0, 0];

const BALLOON_DISABLING_MARKER_OPTIONS: YMapMarkerLabelPrivate = {
cursor: 'drag',
interactivityModel: 'default#silent',
};

const YandexMap = (props: YMapProps) => {
const {markers, zoom, id, className} = props;
const {
markers,
zoom,
id,
disableControls = false,
disableBalloons = false,
className,
forceAspectRatio = true,
} = props;
const {apiKey, scriptSrc, nonce} = React.useContext(MapsContext);
const isMobile = React.useContext(MobileContext);

Expand Down Expand Up @@ -56,8 +69,13 @@ const YandexMap = (props: YMapProps) => {
{
center: INITIAL_CENTER,
zoom: zoom || DEFAULT_ZOOM,
controls: disableControls ? [] : undefined,
},
{
autoFitToViewport: 'always',
suppressMapOpenBlock: disableControls,
yandexMapDisablePoiInteractivity: disableControls,
},
{autoFitToViewport: 'always'},
),
ref.current,
),
Expand All @@ -66,9 +84,23 @@ const YandexMap = (props: YMapProps) => {

setLoading(false);
})();
}, [apiKey, lang, scriptSrc, containerId, zoom, nonce, attemptsIndex, setLoading]);
}, [
apiKey,
lang,
scriptSrc,
containerId,
zoom,
nonce,
attemptsIndex,
setLoading,
disableControls,
]);

React.useEffect(() => {
if (!forceAspectRatio) {
return;
}

const updateSize = debounce(() => {
if (ref.current) {
setHeight(Math.round(getMapHeight(ref.current.offsetWidth, isMobile)));
Expand All @@ -78,23 +110,31 @@ const YandexMap = (props: YMapProps) => {
updateSize();
window.addEventListener('resize', updateSize, {passive: true});

// eslint-disable-next-line consistent-return
return () => {
window.removeEventListener('resize', updateSize);
};
}, [markers, ymap, setYmaps, isMobile]);
}, [isMobile, forceAspectRatio]);

React.useEffect(() => {
if (ymap) {
// show with computed center and placemarks
const showPlacemarks = async () => {
await ymap.showPlacemarks({markers, zoom});
const privateMarkers: YMapMarkerPrivate[] = disableBalloons
? markers.map(({label, ...marker}) => ({
...marker,
label: {...label, ...BALLOON_DISABLING_MARKER_OPTIONS},
}))
: markers;

await ymap.showPlacemarks({markers: privateMarkers, zoom});

setReady(true);
};

showPlacemarks();
}
});
}, [ymap, markers, zoom, disableBalloons]);

if (!markers) return null;

Expand Down
18 changes: 18 additions & 0 deletions src/components/Map/__stories__/Map.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ For a detailed usage guide of the Map component, see [Map Usage](https://github.

`className?: string` — Optional CSS class name

`forceAspectRatio?: boolean` — Determines whether map's aspect ratio is forced autmatically (16:9 for Desktop, 4:3 for Mobile) (`true` by default)

### Google Maps (GMapProps)

`address: string` — Target address to display on the map (required)
Expand All @@ -28,6 +30,10 @@ For a detailed usage guide of the Map component, see [Map Usage](https://github.

`id: string` — Unique identifier for the map instance (required)

`disableControls?: boolean` - If `true`, hides map's default controls (`false` by default)

`disableBalloons?: boolean` - If `true`, disables info ballon opening when clicking on a marker (`false` by default)

#### YMapMarker Interface

`address?: string` — Optional string address for the marker
Expand All @@ -44,6 +50,18 @@ For a detailed usage guide of the Map component, see [Map Usage](https://github.

`iconColor?: string` — Optional color for the marker icon

`iconImageHref?: string` - Optional URL for marker's custom icon image file

`iconImageSize?: [number, number]` - Optional dimensions of custom icon

`iconImageOffset?: [number, number]` - Optional custom icon's offset relative to it's anchor point

`iconImageClipRect?: [[number, number], [number, number]]` - Optional coordinates of custom icon's displayed rectangular area, in pixels

`iconLayout?: 'default#image'` - Required to use custom icons for a marker, otherwise optional

`iconShape?: Record<string, any>` - Optional active area shape for custom icon. Refer to documentation [e.g. Circle](https://yandex.ru/dev/jsapi-v2-1/doc/ru/v2-1/ref/reference/shape.Circle)

`preset?: string` — Optional preset style for the marker

</StoryTemplate>
10 changes: 10 additions & 0 deletions src/components/Map/__stories__/Map.stories.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.aspect-ratio-story {
display: flex;
flex-direction: column;
gap: 16px;
padding: 0 24px;

&-map {
width: 100%;
}
}
25 changes: 23 additions & 2 deletions src/components/Map/__stories__/Map.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {ApiKeyInput} from './ApiKeyInput';

import data from './data.json';

const maxMapWidth = '500px';
import './Map.stories.scss';

const maxMapWidth = 500;

export default {
component: Map,
Expand Down Expand Up @@ -42,6 +44,25 @@ const GoogleMapTemplate: StoryFn<MapProps> = (args: MapProps) => (

export const GoogleMap = GoogleMapTemplate.bind({});
export const YMap = YMapTemplate.bind({});
export const YMapHiddenControls = YMapTemplate.bind({});
export const YMapHiddenBalloons = YMapTemplate.bind({});
export const YMapCustomMarkers = YMapTemplate.bind({});

YMapHiddenControls.storyName = 'Y Map (Hidden Controls)';
YMapHiddenBalloons.storyName = 'Y Map (Hidden Balloons)';
YMapCustomMarkers.storyName = 'Y Map (Custom Markers)';

YMap.args = data.ymap;
GoogleMap.args = data.gmap;
YMap.args = data.ymap;

YMapHiddenControls.args = {
...data.ymap,
disableControls: true,
};

YMapHiddenBalloons.args = {
...data.ymap,
disableBalloons: true,
};

YMapCustomMarkers.args = data.ymapCustomMarkers as MapProps;
Loading