Skip to content

Commit 54302b2

Browse files
authored
feat(Map): export component, update YandexMap (#1305)
* feat(Map): export component, update YandexMap * chore: update story
1 parent 0314fee commit 54302b2

File tree

16 files changed

+253
-30
lines changed

16 files changed

+253
-30
lines changed
Lines changed: 1 addition & 0 deletions
Loading

memory-bank/usage/map.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,27 @@ graph TD
6161
- `address`: String address to display on the map (required)
6262
- `zoom`: Optional zoom level (inherited from MapBaseProps)
6363
- `className`: Optional CSS class name (inherited from MapBaseProps)
64+
- `forceAspectRatio`: Optional boolean to force aspect ratio (16:9 for Desktop, 4:3 for Mobile), `true` by default (inherited from MapBaseProps)
6465

6566
#### YMapProps (Yandex Maps)
6667

6768
- **Description**: Props for Yandex Maps implementation
69+
- **File**: `src/components/Map/YMap/YMap.tsx`
6870
- **Properties**:
6971
- `markers`: Array of YMapMarker objects to display on the map (required)
7072
- `id`: Unique identifier for the map instance (required)
7173
- `zoom`: Optional zoom level (inherited from MapBaseProps)
7274
- `className`: Optional CSS class name (inherited from MapBaseProps)
75+
- `disableControls`: Optional boolean to hide map controls (Yandex Maps only), `false` by default
76+
- `disableBalloons`: Optional boolean to disable info balloons (Yandex Maps only), `false` by default
77+
78+
#### MapBaseProps (Common Props)
79+
80+
- **Description**: Common props available for both Google Maps and Yandex Maps
81+
- **Properties**:
82+
- `zoom`: Optional zoom level for the map
83+
- `className`: Optional CSS class name for styling
84+
- `forceAspectRatio`: Optional boolean to force aspect ratio (16:9 for Desktop, 4:3 for Mobile), `true` by default
7385

7486
### YMapMarker Interface
7587

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

91109
### MapsContext

src/blocks/Map/Map.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Map from '../../components/Map/Map';
22
import MediaBase from '../../components/MediaBase/MediaBase';
3-
import {MapBlockProps} from '../../models';
3+
import {MapBlockProps, MapProps} from '../../models';
44
import {block} from '../../utils';
55
import {getMediaBorder} from '../../utils/borderSelector';
66

@@ -17,7 +17,7 @@ export const MapBlock = ({map, border, disableShadow, ...props}: MapBlockProps)
1717
return (
1818
<MediaBase {...props}>
1919
<MediaBase.Card>
20-
<Map {...map} className={b({border: borderSelected})} />
20+
<Map {...(map as MapProps)} className={b({border: borderSelected})} />
2121
</MediaBase.Card>
2222
</MediaBase>
2323
);

src/components/Map/GoogleMap.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function getScriptSrc(params: GoogleMapLinkParams) {
3030
}
3131

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

4545
React.useEffect(() => {
46+
if (!forceAspectRatio) {
47+
return;
48+
}
49+
4650
const updateSize = debounce(() => {
4751
if (ref.current) {
4852
setHeight(Math.round(getMapHeight(ref.current.offsetWidth, isMobile)));
@@ -52,10 +56,11 @@ const GoogleMap = (props: GMapProps) => {
5256
updateSize();
5357
window.addEventListener('resize', updateSize, {passive: true});
5458

59+
// eslint-disable-next-line consistent-return
5560
return () => {
5661
window.removeEventListener('resize', updateSize);
5762
};
58-
}, [isMobile]);
63+
}, [forceAspectRatio, isMobile]);
5964

6065
if (!apiKey || !address) {
6166
return null;

src/components/Map/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ Map
66

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

9+
`forceAspectRatio?: boolean` - Determines whether map's aspect ratio is forced autmatically (16:9 for Desktop, 4:3 for Mobile), `true` by default
10+
911
`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`
1012

13+
`disableControls?: boolean` - If `true`, hides map's default controls. Only for `Yandex maps`. `false` by default
14+
15+
`disableBalloons?: boolean` - If `true`, disables info ballon opening when clicking on a marker. Only for `Yandex maps`. `false` by default
16+
1117
`markers?: object[]` - Description for placemarkers. You need to use it for `Yandex maps`. Specify the parameters given below.
1218

1319
- `address?: string` — Place name, address
@@ -17,4 +23,10 @@ Map
1723
- `iconCaption?: string` - Caption for the geo object's icon
1824
- `iconContent?: string` - Content of the geo object's icon
1925
- `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.
26+
- `iconImageHref?: string` - URL of geo object's custom icon image file
27+
- `iconImageSize?: [number, number]` - Dimensions of custom icon image
28+
- `iconImageOffset?: [number, number]` - Custom icon image's offset relative to it's anchor point
29+
- `iconImageClipRect?: [[number, number], [number, number]]` - Coordinates of custom icon image's displayed rectangular area, in pixels
30+
- `iconLayout?: 'default#image'` - Required to use custom icons for a geo object
31+
- `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)
2032
- `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

src/components/Map/YMap/YMap.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {YMapMarker, YMapMarkerLabel, YMapProps} from '../../../models';
1+
import {YMapMarkerLabelPrivate, YMapMarkerPrivate, YMapProps} from '../../../models';
22
import {Coordinate} from '../../../models/constructor-items/common';
33

44
enum GeoObjectTypes {
@@ -11,10 +11,18 @@ const DEFAULT_PLACEMARKS_COLOR = '#dc534b';
1111
const DEFAULT_PLACEMARKS_PRESET = 'islands#dotIcon';
1212
const DEFAULT_MAP_CONTROL_BUTTON_HEIGHT = 30;
1313

14-
const geoObjectPropsAndOptions = {
14+
const geoObjectPropsAndOptions: Record<keyof YMapMarkerLabelPrivate, GeoObjectTypes> = {
15+
cursor: GeoObjectTypes.Options,
1516
iconCaption: GeoObjectTypes.Properties,
1617
iconContent: GeoObjectTypes.Properties,
1718
iconColor: GeoObjectTypes.Options,
19+
iconImageHref: GeoObjectTypes.Options,
20+
iconImageSize: GeoObjectTypes.Options,
21+
iconImageOffset: GeoObjectTypes.Options,
22+
iconImageClipRect: GeoObjectTypes.Options,
23+
iconLayout: GeoObjectTypes.Options,
24+
iconShape: GeoObjectTypes.Options,
25+
interactivityModel: GeoObjectTypes.Options,
1826
preset: GeoObjectTypes.Options,
1927
};
2028

@@ -44,7 +52,7 @@ export class YMap {
4452
this.recalcZoomAndCenter(props);
4553
}
4654

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

61-
findCoordinate(marker: YMapMarker) {
69+
findCoordinate(marker: YMapMarkerPrivate) {
6270
const geoObject = new window.ymaps.Placemark(marker.coordinate, {});
6371

6472
this.coords.push(marker.coordinate as Coordinate);
6573
this.drawPlaceMarkStyle(geoObject, marker);
6674
this.ymap.geoObjects.add(geoObject);
6775
}
6876

69-
private drawPlaceMarkStyle(geoObject: Ymaps.GeoObject, marker: YMapMarker) {
77+
private drawPlaceMarkStyle(geoObject: Ymaps.GeoObject, marker: YMapMarkerPrivate) {
7078
const {iconColor, preset = DEFAULT_PLACEMARKS_PRESET} = marker.label || {};
7179
let localIconColor: string | undefined = iconColor;
7280

@@ -75,16 +83,18 @@ export class YMap {
7583
localIconColor = DEFAULT_PLACEMARKS_COLOR;
7684
}
7785

78-
Object.entries({...marker.label, iconColor: localIconColor, preset}).forEach(
79-
([key, value]) => {
80-
const geoObjectParamType: GeoObjectTypes | undefined =
81-
geoObjectPropsAndOptions[key as keyof YMapMarkerLabel];
86+
Object.entries({
87+
...marker.label,
88+
iconColor: localIconColor,
89+
preset,
90+
}).forEach(([key, value]) => {
91+
const geoObjectParamType: GeoObjectTypes | undefined =
92+
geoObjectPropsAndOptions[key as keyof YMapMarkerLabelPrivate];
8293

83-
if (value && geoObjectParamType) {
84-
geoObject[geoObjectParamType].set(key, value);
85-
}
86-
},
87-
);
94+
if (value && geoObjectParamType) {
95+
geoObject[geoObjectParamType].set(key, value);
96+
}
97+
});
8898
}
8999

90100
private recalcZoomAndCenter(props: PlacemarksProps) {

src/components/Map/YMap/YandexMap.tsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import debounce from 'lodash/debounce';
66
import {LocaleContext} from '../../../context/localeContext/localeContext';
77
import {MapsContext} from '../../../context/mapsContext/mapsContext';
88
import {MobileContext} from '../../../context/mobileContext';
9-
import {YMapProps} from '../../../models';
9+
import {YMapMarkerLabelPrivate, YMapMarkerPrivate, YMapProps} from '../../../models';
1010
import {block} from '../../../utils';
1111
import ErrorWrapper from '../../ErrorWrapper/ErrorWrapper';
1212
import {getMapHeight} from '../helpers';
@@ -23,8 +23,21 @@ const DEFAULT_ZOOM = 9;
2323
// The real center of the map will be calculated later, using the coordinates of the markers
2424
const INITIAL_CENTER = [0, 0];
2525

26+
const BALLOON_DISABLING_MARKER_OPTIONS: YMapMarkerLabelPrivate = {
27+
cursor: 'drag',
28+
interactivityModel: 'default#silent',
29+
};
30+
2631
const YandexMap = (props: YMapProps) => {
27-
const {markers, zoom, id, className} = props;
32+
const {
33+
markers,
34+
zoom,
35+
id,
36+
disableControls = false,
37+
disableBalloons = false,
38+
className,
39+
forceAspectRatio = true,
40+
} = props;
2841
const {apiKey, scriptSrc, nonce} = React.useContext(MapsContext);
2942
const isMobile = React.useContext(MobileContext);
3043

@@ -56,8 +69,13 @@ const YandexMap = (props: YMapProps) => {
5669
{
5770
center: INITIAL_CENTER,
5871
zoom: zoom || DEFAULT_ZOOM,
72+
controls: disableControls ? [] : undefined,
73+
},
74+
{
75+
autoFitToViewport: 'always',
76+
suppressMapOpenBlock: disableControls,
77+
yandexMapDisablePoiInteractivity: disableControls,
5978
},
60-
{autoFitToViewport: 'always'},
6179
),
6280
ref.current,
6381
),
@@ -66,9 +84,23 @@ const YandexMap = (props: YMapProps) => {
6684

6785
setLoading(false);
6886
})();
69-
}, [apiKey, lang, scriptSrc, containerId, zoom, nonce, attemptsIndex, setLoading]);
87+
}, [
88+
apiKey,
89+
lang,
90+
scriptSrc,
91+
containerId,
92+
zoom,
93+
nonce,
94+
attemptsIndex,
95+
setLoading,
96+
disableControls,
97+
]);
7098

7199
React.useEffect(() => {
100+
if (!forceAspectRatio) {
101+
return;
102+
}
103+
72104
const updateSize = debounce(() => {
73105
if (ref.current) {
74106
setHeight(Math.round(getMapHeight(ref.current.offsetWidth, isMobile)));
@@ -78,23 +110,31 @@ const YandexMap = (props: YMapProps) => {
78110
updateSize();
79111
window.addEventListener('resize', updateSize, {passive: true});
80112

113+
// eslint-disable-next-line consistent-return
81114
return () => {
82115
window.removeEventListener('resize', updateSize);
83116
};
84-
}, [markers, ymap, setYmaps, isMobile]);
117+
}, [isMobile, forceAspectRatio]);
85118

86119
React.useEffect(() => {
87120
if (ymap) {
88121
// show with computed center and placemarks
89122
const showPlacemarks = async () => {
90-
await ymap.showPlacemarks({markers, zoom});
123+
const privateMarkers: YMapMarkerPrivate[] = disableBalloons
124+
? markers.map(({label, ...marker}) => ({
125+
...marker,
126+
label: {...label, ...BALLOON_DISABLING_MARKER_OPTIONS},
127+
}))
128+
: markers;
129+
130+
await ymap.showPlacemarks({markers: privateMarkers, zoom});
91131

92132
setReady(true);
93133
};
94134

95135
showPlacemarks();
96136
}
97-
});
137+
}, [ymap, markers, zoom, disableBalloons]);
98138

99139
if (!markers) return null;
100140

src/components/Map/__stories__/Map.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ For a detailed usage guide of the Map component, see [Map Usage](https://github.
1818

1919
`className?: string` — Optional CSS class name
2020

21+
`forceAspectRatio?: boolean` — Determines whether map's aspect ratio is forced autmatically (16:9 for Desktop, 4:3 for Mobile) (`true` by default)
22+
2123
### Google Maps (GMapProps)
2224

2325
`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.
2830

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

33+
`disableControls?: boolean` - If `true`, hides map's default controls (`false` by default)
34+
35+
`disableBalloons?: boolean` - If `true`, disables info ballon opening when clicking on a marker (`false` by default)
36+
3137
#### YMapMarker Interface
3238

3339
`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.
4450

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

53+
`iconImageHref?: string` - Optional URL for marker's custom icon image file
54+
55+
`iconImageSize?: [number, number]` - Optional dimensions of custom icon
56+
57+
`iconImageOffset?: [number, number]` - Optional custom icon's offset relative to it's anchor point
58+
59+
`iconImageClipRect?: [[number, number], [number, number]]` - Optional coordinates of custom icon's displayed rectangular area, in pixels
60+
61+
`iconLayout?: 'default#image'` - Required to use custom icons for a marker, otherwise optional
62+
63+
`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)
64+
4765
`preset?: string` — Optional preset style for the marker
4866

4967
</StoryTemplate>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.aspect-ratio-story {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 16px;
5+
padding: 0 24px;
6+
7+
&-map {
8+
width: 100%;
9+
}
10+
}

src/components/Map/__stories__/Map.stories.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {ApiKeyInput} from './ApiKeyInput';
1010

1111
import data from './data.json';
1212

13-
const maxMapWidth = '500px';
13+
import './Map.stories.scss';
14+
15+
const maxMapWidth = 500;
1416

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

4345
export const GoogleMap = GoogleMapTemplate.bind({});
4446
export const YMap = YMapTemplate.bind({});
47+
export const YMapHiddenControls = YMapTemplate.bind({});
48+
export const YMapHiddenBalloons = YMapTemplate.bind({});
49+
export const YMapCustomMarkers = YMapTemplate.bind({});
50+
51+
YMapHiddenControls.storyName = 'Y Map (Hidden Controls)';
52+
YMapHiddenBalloons.storyName = 'Y Map (Hidden Balloons)';
53+
YMapCustomMarkers.storyName = 'Y Map (Custom Markers)';
4554

46-
YMap.args = data.ymap;
4755
GoogleMap.args = data.gmap;
56+
YMap.args = data.ymap;
57+
58+
YMapHiddenControls.args = {
59+
...data.ymap,
60+
disableControls: true,
61+
};
62+
63+
YMapHiddenBalloons.args = {
64+
...data.ymap,
65+
disableBalloons: true,
66+
};
67+
68+
YMapCustomMarkers.args = data.ymapCustomMarkers as MapProps;

0 commit comments

Comments
 (0)