Skip to content

Commit 76becce

Browse files
committed
Handle class name change
1 parent 057d67a commit 76becce

File tree

10 files changed

+224
-46
lines changed

10 files changed

+224
-46
lines changed

modules/react-mapbox/src/components/marker.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {MarkerEvent, MarkerDragEvent} from '../types/events';
99

1010
import {MapContext} from './map';
1111
import {arePointsEqual} from '../utils/deep-equal';
12+
import {compareClassNames} from '../utils/compare-class-names';
1213

1314
export type MarkerProps = MarkerOptions & {
1415
/** Longitude of the anchor location */
@@ -32,7 +33,6 @@ export const Marker = memo(
3233
forwardRef((props: MarkerProps, ref: React.Ref<MarkerInstance>) => {
3334
const {map, mapLib} = useContext(MapContext);
3435
const thisRef = useRef({props});
35-
thisRef.current.props = props;
3636

3737
const marker: MarkerInstance = useMemo(() => {
3838
let hasChildren = false;
@@ -102,6 +102,7 @@ export const Marker = memo(
102102

103103
useImperativeHandle(ref, () => marker, []);
104104

105+
const oldProps = thisRef.current.props;
105106
if (marker.getLngLat().lng !== longitude || marker.getLngLat().lat !== latitude) {
106107
marker.setLngLat([longitude, latitude]);
107108
}
@@ -123,7 +124,17 @@ export const Marker = memo(
123124
if (marker.getPopup() !== popup) {
124125
marker.setPopup(popup);
125126
}
127+
const classNameDiff = compareClassNames(oldProps.className, props.className);
128+
if (classNameDiff) {
129+
for (const c of classNameDiff[0]) {
130+
marker.addClassName(c);
131+
}
132+
for (const c of classNameDiff[1]) {
133+
marker.removeClassName(c);
134+
}
135+
}
126136

137+
thisRef.current.props = props;
127138
return createPortal(props.children, marker.getElement());
128139
})
129140
);

modules/react-mapbox/src/components/popup.ts

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {PopupEvent} from '../types/events';
99

1010
import {MapContext} from './map';
1111
import {deepEqual} from '../utils/deep-equal';
12+
import {compareClassNames} from '../utils/compare-class-names';
1213

1314
export type PopupProps = PopupOptions & {
1415
/** Longitude of the anchor location */
@@ -24,11 +25,6 @@ export type PopupProps = PopupOptions & {
2425
children?: React.ReactNode;
2526
};
2627

27-
// Adapted from https://github.com/mapbox/mapbox-gl-js/blob/v1.13.0/src/ui/popup.js
28-
function getClassList(className: string) {
29-
return new Set(className ? className.trim().split(/\s+/) : []);
30-
}
31-
3228
/* eslint-disable complexity,max-statements */
3329
export const Popup = memo(
3430
forwardRef((props: PopupProps, ref: React.Ref<PopupInstance>) => {
@@ -37,7 +33,6 @@ export const Popup = memo(
3733
return document.createElement('div');
3834
}, []);
3935
const thisRef = useRef({props});
40-
thisRef.current.props = props;
4136

4237
const popup: PopupInstance = useMemo(() => {
4338
const options = {...props};
@@ -75,32 +70,26 @@ export const Popup = memo(
7570
useImperativeHandle(ref, () => popup, []);
7671

7772
if (popup.isOpen()) {
73+
const oldProps = thisRef.current.props;
7874
if (popup.getLngLat().lng !== props.longitude || popup.getLngLat().lat !== props.latitude) {
7975
popup.setLngLat([props.longitude, props.latitude]);
8076
}
81-
if (props.offset && !deepEqual(popup.options.offset, props.offset)) {
77+
if (props.offset && !deepEqual(oldProps.offset, props.offset)) {
8278
popup.setOffset(props.offset);
8379
}
84-
if (popup.options.anchor !== props.anchor || popup.options.maxWidth !== props.maxWidth) {
85-
popup.options.anchor = props.anchor;
80+
if (oldProps.anchor !== props.anchor || oldProps.maxWidth !== props.maxWidth) {
8681
popup.setMaxWidth(props.maxWidth);
8782
}
88-
if (popup.options.className !== props.className) {
89-
const prevClassList = getClassList(popup.options.className);
90-
const nextClassList = getClassList(props.className);
91-
92-
for (const c of prevClassList) {
93-
if (!nextClassList.has(c)) {
94-
popup.removeClassName(c);
95-
}
83+
const classNameDiff = compareClassNames(oldProps.className, props.className);
84+
if (classNameDiff) {
85+
for (const c of classNameDiff[0]) {
86+
popup.addClassName(c);
9687
}
97-
for (const c of nextClassList) {
98-
if (!prevClassList.has(c)) {
99-
popup.addClassName(c);
100-
}
88+
for (const c of classNameDiff[1]) {
89+
popup.removeClassName(c);
10190
}
102-
popup.options.className = props.className;
10391
}
92+
thisRef.current.props = props;
10493
}
10594

10695
return createPortal(props.children, container);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/** Compare two classNames string and return the difference */
2+
export function compareClassNames(
3+
prevClassName: string | undefined,
4+
nextClassName: string | undefined
5+
): [added: string[], removed: string[]] | null {
6+
if (prevClassName === nextClassName) {
7+
return null;
8+
}
9+
10+
const prevClassList = getClassList(prevClassName);
11+
const nextClassList = getClassList(nextClassName);
12+
const added: string[] = [];
13+
const removed: string[] = [];
14+
15+
for (const c of prevClassList) {
16+
if (!nextClassList.has(c)) {
17+
removed.push(c);
18+
}
19+
}
20+
21+
if (removed.length === 0 && prevClassList.size === nextClassList.size) {
22+
return null;
23+
}
24+
25+
for (const c of nextClassList) {
26+
if (!prevClassList.has(c)) {
27+
added.push(c);
28+
}
29+
}
30+
31+
return [added, removed];
32+
}
33+
34+
function getClassList(className: string | undefined) {
35+
return new Set(className ? className.trim().split(/\s+/) : []);
36+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import test from 'tape-promise/tape';
2+
import {compareClassNames} from '@vis.gl/react-mapbox/utils/compare-class-names';
3+
4+
test('compareClassNames', t => {
5+
const TEST_CASES = [
6+
{
7+
title: 'Empty class names',
8+
prevClassName: '',
9+
nextClassName: '',
10+
output: null
11+
},
12+
{
13+
title: 'Identical class names',
14+
prevClassName: 'marker active',
15+
nextClassName: 'active marker ',
16+
output: null
17+
},
18+
{
19+
title: 'Addition',
20+
prevClassName: undefined,
21+
nextClassName: 'marker',
22+
output: [['marker'], []]
23+
},
24+
{
25+
title: 'Addition',
26+
prevClassName: 'marker',
27+
nextClassName: 'marker active',
28+
output: [['active'], []]
29+
},
30+
{
31+
title: 'Removal',
32+
prevClassName: 'marker active',
33+
nextClassName: 'marker',
34+
output: [[], ['active']]
35+
},
36+
{
37+
title: 'Multiple addition & removal',
38+
prevClassName: 'marker active',
39+
nextClassName: 'marker hovered hidden',
40+
output: [['hovered', 'hidden'], ['active']]
41+
}
42+
];
43+
44+
for (const testCase of TEST_CASES) {
45+
t.deepEqual(
46+
compareClassNames(testCase.prevClassName, testCase.nextClassName),
47+
testCase.output,
48+
testCase.title
49+
);
50+
}
51+
t.end();
52+
});

modules/react-mapbox/test/utils/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import './deep-equal.spec';
22
import './transform.spec';
33
import './style-utils.spec';
44
import './apply-react-style.spec';
5+
import './compare-class-names.spec';

modules/react-maplibre/src/components/marker.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {MarkerEvent, MarkerDragEvent} from '../types/events';
99

1010
import {MapContext} from './map';
1111
import {arePointsEqual} from '../utils/deep-equal';
12+
import {compareClassNames} from '../utils/compare-class-names';
1213

1314
export type MarkerProps = MarkerOptions & {
1415
/** Longitude of the anchor location */
@@ -32,7 +33,6 @@ export const Marker = memo(
3233
forwardRef((props: MarkerProps, ref: React.Ref<MarkerInstance>) => {
3334
const {map, mapLib} = useContext(MapContext);
3435
const thisRef = useRef({props});
35-
thisRef.current.props = props;
3636

3737
const marker: MarkerInstance = useMemo(() => {
3838
let hasChildren = false;
@@ -102,6 +102,7 @@ export const Marker = memo(
102102

103103
useImperativeHandle(ref, () => marker, []);
104104

105+
const oldProps = thisRef.current.props;
105106
if (marker.getLngLat().lng !== longitude || marker.getLngLat().lat !== latitude) {
106107
marker.setLngLat([longitude, latitude]);
107108
}
@@ -123,7 +124,17 @@ export const Marker = memo(
123124
if (marker.getPopup() !== popup) {
124125
marker.setPopup(popup);
125126
}
127+
const classNameDiff = compareClassNames(oldProps.className, props.className);
128+
if (classNameDiff) {
129+
for (const c of classNameDiff[0]) {
130+
marker.addClassName(c);
131+
}
132+
for (const c of classNameDiff[1]) {
133+
marker.removeClassName(c);
134+
}
135+
}
126136

137+
thisRef.current.props = props;
127138
return createPortal(props.children, marker.getElement());
128139
})
129140
);

modules/react-maplibre/src/components/popup.ts

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {PopupEvent} from '../types/events';
99

1010
import {MapContext} from './map';
1111
import {deepEqual} from '../utils/deep-equal';
12+
import {compareClassNames} from '../utils/compare-class-names';
1213

1314
export type PopupProps = PopupOptions & {
1415
/** Longitude of the anchor location */
@@ -24,11 +25,6 @@ export type PopupProps = PopupOptions & {
2425
children?: React.ReactNode;
2526
};
2627

27-
// Adapted from https://github.com/mapbox/mapbox-gl-js/blob/v1.13.0/src/ui/popup.js
28-
function getClassList(className: string) {
29-
return new Set(className ? className.trim().split(/\s+/) : []);
30-
}
31-
3228
/* eslint-disable complexity,max-statements */
3329
export const Popup = memo(
3430
forwardRef((props: PopupProps, ref: React.Ref<PopupInstance>) => {
@@ -37,7 +33,6 @@ export const Popup = memo(
3733
return document.createElement('div');
3834
}, []);
3935
const thisRef = useRef({props});
40-
thisRef.current.props = props;
4136

4237
const popup: PopupInstance = useMemo(() => {
4338
const options = {...props};
@@ -75,32 +70,26 @@ export const Popup = memo(
7570
useImperativeHandle(ref, () => popup, []);
7671

7772
if (popup.isOpen()) {
73+
const oldProps = thisRef.current.props;
7874
if (popup.getLngLat().lng !== props.longitude || popup.getLngLat().lat !== props.latitude) {
7975
popup.setLngLat([props.longitude, props.latitude]);
8076
}
81-
if (props.offset && !deepEqual(popup.options.offset, props.offset)) {
77+
if (props.offset && !deepEqual(oldProps.offset, props.offset)) {
8278
popup.setOffset(props.offset);
8379
}
84-
if (popup.options.anchor !== props.anchor || popup.options.maxWidth !== props.maxWidth) {
85-
popup.options.anchor = props.anchor;
80+
if (oldProps.anchor !== props.anchor || oldProps.maxWidth !== props.maxWidth) {
8681
popup.setMaxWidth(props.maxWidth);
8782
}
88-
if (popup.options.className !== props.className) {
89-
const prevClassList = getClassList(popup.options.className);
90-
const nextClassList = getClassList(props.className);
91-
92-
for (const c of prevClassList) {
93-
if (!nextClassList.has(c)) {
94-
popup.removeClassName(c);
95-
}
83+
const classNameDiff = compareClassNames(oldProps.className, props.className);
84+
if (classNameDiff) {
85+
for (const c of classNameDiff[0]) {
86+
popup.addClassName(c);
9687
}
97-
for (const c of nextClassList) {
98-
if (!prevClassList.has(c)) {
99-
popup.addClassName(c);
100-
}
88+
for (const c of classNameDiff[1]) {
89+
popup.removeClassName(c);
10190
}
102-
popup.options.className = props.className;
10391
}
92+
thisRef.current.props = props;
10493
}
10594

10695
return createPortal(props.children, container);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/** Compare two classNames string and return the difference */
2+
export function compareClassNames(
3+
prevClassName: string | undefined,
4+
nextClassName: string | undefined
5+
): [added: string[], removed: string[]] | null {
6+
if (prevClassName === nextClassName) {
7+
return null;
8+
}
9+
10+
const prevClassList = getClassList(prevClassName);
11+
const nextClassList = getClassList(nextClassName);
12+
const added: string[] = [];
13+
const removed: string[] = [];
14+
15+
for (const c of prevClassList) {
16+
if (!nextClassList.has(c)) {
17+
removed.push(c);
18+
}
19+
}
20+
21+
if (removed.length === 0 && prevClassList.size === nextClassList.size) {
22+
return null;
23+
}
24+
25+
for (const c of nextClassList) {
26+
if (!prevClassList.has(c)) {
27+
added.push(c);
28+
}
29+
}
30+
31+
return [added, removed];
32+
}
33+
34+
function getClassList(className: string | undefined) {
35+
return new Set(className ? className.trim().split(/\s+/) : []);
36+
}

0 commit comments

Comments
 (0)