diff --git a/.storybook/public/story-assets/map-mark.svg b/.storybook/public/story-assets/map-mark.svg
new file mode 100644
index 000000000..b5ebf6957
--- /dev/null
+++ b/.storybook/public/story-assets/map-mark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/memory-bank/usage/map.md b/memory-bank/usage/map.md
index 90c037c1e..7dde6f2a2 100644
--- a/memory-bank/usage/map.md
+++ b/memory-bank/usage/map.md
@@ -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
@@ -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
diff --git a/src/blocks/Map/Map.tsx b/src/blocks/Map/Map.tsx
index a7733ec6d..636ef119f 100644
--- a/src/blocks/Map/Map.tsx
+++ b/src/blocks/Map/Map.tsx
@@ -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';
@@ -17,7 +17,7 @@ export const MapBlock = ({map, border, disableShadow, ...props}: MapBlockProps)
return (
-
+
);
diff --git a/src/components/Map/GoogleMap.tsx b/src/components/Map/GoogleMap.tsx
index 5d9b9cab0..6aa322431 100644
--- a/src/components/Map/GoogleMap.tsx
+++ b/src/components/Map/GoogleMap.tsx
@@ -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);
@@ -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)));
@@ -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;
diff --git a/src/components/Map/README.md b/src/components/Map/README.md
index 90865c78a..66d9a751d 100644
--- a/src/components/Map/README.md
+++ b/src/components/Map/README.md
@@ -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
@@ -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` - 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
diff --git a/src/components/Map/YMap/YMap.ts b/src/components/Map/YMap/YMap.ts
index b5f1c0c95..7df3e0abe 100644
--- a/src/components/Map/YMap/YMap.ts
+++ b/src/components/Map/YMap/YMap.ts
@@ -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 {
@@ -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 = {
+ 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,
};
@@ -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);
@@ -58,7 +66,7 @@ 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);
@@ -66,7 +74,7 @@ export class YMap {
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;
@@ -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) {
diff --git a/src/components/Map/YMap/YandexMap.tsx b/src/components/Map/YMap/YandexMap.tsx
index 89c521697..862abe318 100644
--- a/src/components/Map/YMap/YandexMap.tsx
+++ b/src/components/Map/YMap/YandexMap.tsx
@@ -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';
@@ -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);
@@ -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,
),
@@ -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)));
@@ -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;
diff --git a/src/components/Map/__stories__/Map.mdx b/src/components/Map/__stories__/Map.mdx
index f5d7d284a..b9c480cb8 100644
--- a/src/components/Map/__stories__/Map.mdx
+++ b/src/components/Map/__stories__/Map.mdx
@@ -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)
@@ -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
@@ -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` - 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
diff --git a/src/components/Map/__stories__/Map.stories.scss b/src/components/Map/__stories__/Map.stories.scss
new file mode 100644
index 000000000..ddd94ce28
--- /dev/null
+++ b/src/components/Map/__stories__/Map.stories.scss
@@ -0,0 +1,10 @@
+.aspect-ratio-story {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 0 24px;
+
+ &-map {
+ width: 100%;
+ }
+}
diff --git a/src/components/Map/__stories__/Map.stories.tsx b/src/components/Map/__stories__/Map.stories.tsx
index 936671e26..dff9200fc 100644
--- a/src/components/Map/__stories__/Map.stories.tsx
+++ b/src/components/Map/__stories__/Map.stories.tsx
@@ -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,
@@ -42,6 +44,25 @@ const GoogleMapTemplate: StoryFn = (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;
diff --git a/src/components/Map/__stories__/data.json b/src/components/Map/__stories__/data.json
index bfd8bcc6b..9fe4155d8 100644
--- a/src/components/Map/__stories__/data.json
+++ b/src/components/Map/__stories__/data.json
@@ -24,6 +24,38 @@
}
]
},
+ "ymapCustomMarkers": {
+ "id": "common-places",
+ "markers": [
+ {
+ "address": "Moscow Arbat",
+ "label": {
+ "iconLayout": "default#image",
+ "iconImageHref": "/story-assets/map-mark.svg",
+ "iconImageSize": [24, 24],
+ "iconImageOffset": [-12, -24]
+ }
+ },
+ {
+ "address": "Moscow, Red square",
+ "label": {
+ "iconLayout": "default#image",
+ "iconImageHref": "/story-assets/map-mark.svg",
+ "iconImageSize": [24, 24],
+ "iconImageOffset": [-12, -24]
+ }
+ },
+ {
+ "coordinate": [55.733974, 37.587093],
+ "label": {
+ "iconLayout": "default#image",
+ "iconImageHref": "/story-assets/map-mark.svg",
+ "iconImageSize": [24, 24],
+ "iconImageOffset": [-12, -24]
+ }
+ }
+ ]
+ },
"gmap": {
"address": "Anthony Fokkerweg 1, 1059 CM Amsterdam"
}
diff --git a/src/components/index.ts b/src/components/index.ts
index c873ebd11..389ac05e7 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -26,6 +26,7 @@ export {default as ImageBase} from './ImageBase/ImageBase';
export {default as InnerForm} from './InnerForm/InnerForm';
export {default as Link} from './Link/Link';
export {default as Links} from './Links/Links';
+export {default as Map} from './Map/Map';
export {default as Media} from './Media/Media';
export {default as MetaInfo} from './MetaInfo/MetaInfo';
export {default as OutsideClick} from './OutsideClick/OutsideClick';
diff --git a/src/internal-typings/global.d.ts b/src/internal-typings/global.d.ts
index ff2db76e7..ea8c450b6 100644
--- a/src/internal-typings/global.d.ts
+++ b/src/internal-typings/global.d.ts
@@ -45,10 +45,12 @@ declare namespace Ymaps {
export class GeoObject {
properties: {
- set: (objectName: string, value: string) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ set: (objectName: string, value: any) => void;
};
options: {
- set: (objectName: string, value: string) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ set: (objectName: string, value: any) => void;
};
}
diff --git a/src/models/constructor-items/blocks.ts b/src/models/constructor-items/blocks.ts
index f6b2b187e..f7a2ce411 100644
--- a/src/models/constructor-items/blocks.ts
+++ b/src/models/constructor-items/blocks.ts
@@ -292,7 +292,7 @@ export interface MediaBlockProps extends MediaBaseBlockProps, WithBorder {
}
export interface MapBlockProps extends MediaBaseBlockProps, WithBorder {
- map: MapProps;
+ map: Omit;
}
export interface InfoBlockProps {
diff --git a/src/models/constructor-items/common.ts b/src/models/constructor-items/common.ts
index 8fad42078..09bcadd91 100644
--- a/src/models/constructor-items/common.ts
+++ b/src/models/constructor-items/common.ts
@@ -328,6 +328,7 @@ export type Coordinate = number[];
export interface MapBaseProps {
zoom?: number;
className?: string;
+ forceAspectRatio?: boolean;
}
export interface GMapProps extends MapBaseProps {
@@ -336,6 +337,8 @@ export interface GMapProps extends MapBaseProps {
export interface YMapProps extends MapBaseProps {
markers: YMapMarker[];
+ disableControls?: boolean;
+ disableBalloons?: boolean;
id: string;
}
@@ -349,9 +352,25 @@ export interface YMapMarkerLabel {
iconCaption?: string;
iconContent?: string;
iconColor?: string;
+ iconImageHref?: string;
+ iconImageSize?: [number, number];
+ iconImageOffset?: [number, number];
+ iconImageClipRect?: [[number, number], [number, number]];
+ iconLayout?: 'default#image';
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ iconShape?: Record;
preset?: string;
}
+export interface YMapMarkerPrivate extends YMapMarker {
+ label?: YMapMarkerLabelPrivate;
+}
+
+export interface YMapMarkerLabelPrivate extends YMapMarkerLabel {
+ cursor?: string;
+ interactivityModel?: string;
+}
+
export type MapProps = GMapProps | YMapProps;
export type ThemedMediaProps = ThemeSupporting;
diff --git a/src/schema/validators/common.ts b/src/schema/validators/common.ts
index 89a398d21..9a9f326f5 100644
--- a/src/schema/validators/common.ts
+++ b/src/schema/validators/common.ts
@@ -641,6 +641,13 @@ export const MediaProps = {
},
};
+const YMapXY = {
+ type: 'array',
+ items: {type: 'number'},
+ minItems: 2,
+ maxItems: 2,
+};
+
export const YMapMarkerLabel = {
type: 'object',
required: [],
@@ -654,6 +661,24 @@ export const YMapMarkerLabel = {
iconColor: {
type: 'string',
},
+ iconImageHref: {
+ type: 'string',
+ },
+ iconImageSize: YMapXY,
+ iconImageOffset: YMapXY,
+ iconImageClipRect: {
+ type: 'array',
+ items: YMapXY,
+ minItems: 2,
+ maxItems: 2,
+ },
+ iconLayout: {
+ type: 'string',
+ },
+ iconShape: {
+ type: 'object',
+ additionalProperties: true,
+ },
preset: {
type: 'string',
},
@@ -690,6 +715,15 @@ export const MapProps = {
type: 'array',
items: YMapMarker,
},
+ forceAspectRatio: {
+ type: 'boolean',
+ },
+ disableControls: {
+ type: 'boolean',
+ },
+ disableBalloons: {
+ type: 'boolean',
+ },
};
export const BorderProps = {