From fe6d0069203bf521a794723567649dfc40ce42b6 Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 23 Dec 2025 16:43:34 +1100 Subject: [PATCH 1/5] Add updateZoomConstraint helper --- .../react-maplibre/src/maplibre/maplibre.ts | 22 ++++-- modules/react-maplibre/src/utils/transform.ts | 33 ++++++++ .../test/utils/transform.spec.js | 75 ++++++++++++++++++- 3 files changed, 123 insertions(+), 7 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 4ef1122ad..e81339a25 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -1,4 +1,4 @@ -import {transformToViewState, applyViewStateToTransform} from '../utils/transform'; +import {transformToViewState, applyViewStateToTransform, updateZoomConstraint} from '../utils/transform'; import {normalizeStyle} from '../utils/style-utils'; import {deepEqual} from '../utils/deep-equal'; @@ -146,7 +146,7 @@ const settingNames = [ 'maxBounds', 'projection', 'renderWorldCopies' -]; +] as const; const handlerNames = [ 'scrollZoom', 'boxZoom', @@ -427,9 +427,21 @@ export default class Maplibre { if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) { changed = true; - const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; - const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; - setter?.call(map, nextValue); + if (propName === 'minZoom' || propName === 'maxZoom') { + const next = { + min: 'minZoom' in nextProps ? nextProps.minZoom as number : DEFAULT_SETTINGS.minZoom, + max: 'maxZoom' in nextProps ? nextProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom + } + const curr = { + min: 'minZoom' in currProps ? currProps.minZoom as number : DEFAULT_SETTINGS.minZoom, + max: 'maxZoom' in currProps ? currProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom + } + updateZoomConstraint(map, next, curr) + } else { + const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; + const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; + setter?.call(map, nextValue); + } } } return changed; diff --git a/modules/react-maplibre/src/utils/transform.ts b/modules/react-maplibre/src/utils/transform.ts index bd7f74880..e8f8ee095 100644 --- a/modules/react-maplibre/src/utils/transform.ts +++ b/modules/react-maplibre/src/utils/transform.ts @@ -1,6 +1,7 @@ import type {MaplibreProps} from '../maplibre/maplibre'; import type {ViewState} from '../types/common'; import type {TransformLike} from '../types/internal'; +import { MapInstance } from '../types/lib'; import {deepEqual} from './deep-equal'; /** @@ -56,3 +57,35 @@ export function applyViewStateToTransform( } return changes; } + +/** + * Update zoom constraints to match props by calling + * `setMinZoom` and `setMaxZoom` in the right order + * @param {object} nextRange + * @param {object} currRange + **/ +export function updateZoomConstraint(map: MapInstance, nextRange: { min: number; max: number}, currentRange: { min: number; max: number }): void { + if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { + return + } + + // if moving up ie. 1 - 3 -> 5 - 10 + if (nextRange.min >= currentRange.min) { + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max) + } + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min) + } + } + + // if moving down ie. 5 - 10 -> 1 - 3 + if (nextRange.min < currentRange.min) { + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min) + } + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max) + } + } + } \ No newline at end of file diff --git a/modules/react-maplibre/test/utils/transform.spec.js b/modules/react-maplibre/test/utils/transform.spec.js index 25e575540..a686cd052 100644 --- a/modules/react-maplibre/test/utils/transform.spec.js +++ b/modules/react-maplibre/test/utils/transform.spec.js @@ -1,7 +1,8 @@ -import test from 'tape-promise/tape'; +import test from 'tape'; import { transformToViewState, - applyViewStateToTransform + applyViewStateToTransform, + updateZoomConstraint } from '@vis.gl/react-maplibre/utils/transform'; import maplibregl from 'maplibre-gl'; @@ -64,3 +65,73 @@ test('applyViewStateToTransform', t => { t.end(); }); + +test('updateZoomConstraint', t => { + let first = null + let currentMinZoom = 0 + let currentMaxZoom = 0 + const map = { + setMinZoom: (nextMinZoom) => { + if (nextMinZoom > currentMaxZoom) { + throw new Error('Setting minZoom > maxZoom') + } + currentMinZoom = nextMinZoom + if (!first) { + first = 'min' + } + }, + setMaxZoom: (nextMaxZoom) => { + if (nextMaxZoom < currentMinZoom) { + throw new Error('Setting maxZoom < minZoom') + } + currentMaxZoom = nextMaxZoom + if (!first) { + first = 'max' + } + } + } + + // moving down. 5 - 10 -> 1 - 3 + currentMinZoom = 5 + currentMaxZoom = 10 + updateZoomConstraint(map, { min: 1, max: 3 }, { min: 5, max: 10 }); + t.equal(first, 'min', 'min first') + first = null + + // moving up. 1 - 3 -> 5 - 10 + currentMinZoom = 1 + currentMaxZoom = 3 + updateZoomConstraint(map, { min: 5, max: 10 }, { min: 1, max: 3 }); + t.equal(first, 'max', 'max first') + first = null + + // expanding. 5 - 18 -> 3 - 22 + currentMinZoom = 5 + currentMaxZoom = 18 + updateZoomConstraint(map, { min: 3, max: 22 }, { min: 5, max: 18 }); + t.equal(first, 'min', 'min first') + first = null + + // expanding down. 5 - 18 -> 3 - 18 + currentMinZoom = 5 + currentMaxZoom = 18 + updateZoomConstraint(map, { min: 3, max: 18 }, { min: 5, max: 18 }); + t.equal(first, 'min', 'min first') + first = null + + // contracting. 3 - 22 -> 5 - 18 + currentMinZoom = 5 + currentMaxZoom = 18 + updateZoomConstraint(map, { min: 5, max: 18 }, { min: 3, max: 22 }); + t.equal(first, 'max', 'max first') + first = null + + // contracting down. 12 - 22 -> 5 - 10 + currentMinZoom = 12 + currentMaxZoom = 22 + updateZoomConstraint(map, { min: 5, max: 10 }, { min: 12, max: 22 }); + t.equal(first, 'min', 'min first') + first = null + + t.end(); +}); From 310aa49721d8840c30de64c619a002b11fb13c7e Mon Sep 17 00:00:00 2001 From: Auspicus Date: Wed, 24 Dec 2025 15:31:08 +1100 Subject: [PATCH 2/5] Fix types, prevent duplicate call to setMinZoom or setMaxZoom --- .../react-maplibre/src/maplibre/maplibre.ts | 77 +++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index e81339a25..54d76abec 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -76,7 +76,32 @@ export type MaplibreProps = Partial & interactiveLayerIds?: string[]; /** CSS cursor */ cursor?: string; - }; + + /** Minimum zoom available to the map. + * @default 0 + */ + minZoom?: number + /** Maximum zoom available to the map. + * @default 22 + */ + maxZoom?: number + /** Minimum pitch available to the map. + * @default 0 + */ + minPitch?: number + /** Maximum pitch available to the map. + * @default 85 + */ + maxPitch?: number + /** Bounds of the map. + * @default [-180, -85.051129, 180, 85.051129] + */ + maxBounds?: [number, number, number, number] + /** Whether to render copies of the world or not. + * @default true + */ + renderWorldCopies?: boolean + } const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as StyleSpecification; @@ -414,6 +439,22 @@ export default class Maplibre { return false; } + private _updateZoomConstraint(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { + if (!('minZoom' in nextProps) && !('maxZoom' in nextProps)) { + return false + } + + updateZoomConstraint(this._map, { + min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: nextProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom + }, { + min: currProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom, + }) + + return true + } + /* Update camera constraints and projection settings to match props @param {object} nextProps @param {object} currProps @@ -421,30 +462,22 @@ export default class Maplibre { */ private _updateSettings(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { const map = this._map; - let changed = false; + let settingsChanged = false; for (const propName of settingNames) { - const propPresent = propName in nextProps || propName in currProps; - - if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) { - changed = true; - if (propName === 'minZoom' || propName === 'maxZoom') { - const next = { - min: 'minZoom' in nextProps ? nextProps.minZoom as number : DEFAULT_SETTINGS.minZoom, - max: 'maxZoom' in nextProps ? nextProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom - } - const curr = { - min: 'minZoom' in currProps ? currProps.minZoom as number : DEFAULT_SETTINGS.minZoom, - max: 'maxZoom' in currProps ? currProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom - } - updateZoomConstraint(map, next, curr) - } else { - const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; - const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; - setter?.call(map, nextValue); - } + if (propName === 'minZoom' || propName === 'maxZoom') { + // eslint-disable-next-line no-continue + continue + } + + if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) { + settingsChanged = true; + const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName] + const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; + setter?.call(map, nextValue); } } - return changed; + const zoomChanged = this._updateZoomConstraint(nextProps, currProps) + return settingsChanged || zoomChanged; } /* Update map style to match props */ From e4931200a2ca23d6fc8b82094cf828250848e8cd Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 20 Jan 2026 23:36:19 +1100 Subject: [PATCH 3/5] Add the same logic for pitch, format code --- .../react-maplibre/src/maplibre/maplibre.ts | 94 +++++++++------- modules/react-maplibre/src/utils/transform.ts | 84 ++++++++++---- .../test/utils/transform.spec.js | 103 ++++++++++++++---- 3 files changed, 197 insertions(+), 84 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 54d76abec..6bfe1e1f1 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -1,4 +1,9 @@ -import {transformToViewState, applyViewStateToTransform, updateZoomConstraint} from '../utils/transform'; +import { + transformToViewState, + applyViewStateToTransform, + updateZoomConstraint, + updatePitchConstraint +} from '../utils/transform'; import {normalizeStyle} from '../utils/style-utils'; import {deepEqual} from '../utils/deep-equal'; @@ -80,28 +85,28 @@ export type MaplibreProps = Partial & /** Minimum zoom available to the map. * @default 0 */ - minZoom?: number + minZoom?: number; /** Maximum zoom available to the map. * @default 22 - */ - maxZoom?: number + */ + maxZoom?: number; /** Minimum pitch available to the map. * @default 0 - */ - minPitch?: number + */ + minPitch?: number; /** Maximum pitch available to the map. * @default 85 - */ - maxPitch?: number + */ + maxPitch?: number; /** Bounds of the map. * @default [-180, -85.051129, 180, 85.051129] - */ - maxBounds?: [number, number, number, number] + */ + maxBounds?: [number, number, number, number]; /** Whether to render copies of the world or not. * @default true - */ - renderWorldCopies?: boolean - } + */ + renderWorldCopies?: boolean; + }; const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as StyleSpecification; @@ -163,15 +168,8 @@ const otherEvents = { sourcedata: 'onSourceData', error: 'onError' }; -const settingNames = [ - 'minZoom', - 'maxZoom', - 'minPitch', - 'maxPitch', - 'maxBounds', - 'projection', - 'renderWorldCopies' -] as const; +const constraintNames = ['minZoom', 'maxZoom', 'minPitch', 'maxPitch'] as const; +const settingNames = [...constraintNames, 'maxBounds', 'projection', 'renderWorldCopies'] as const; const handlerNames = [ 'scrollZoom', 'boxZoom', @@ -439,20 +437,36 @@ export default class Maplibre { return false; } - private _updateZoomConstraint(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { - if (!('minZoom' in nextProps) && !('maxZoom' in nextProps)) { - return false - } - - updateZoomConstraint(this._map, { - min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, - max: nextProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom - }, { - min: currProps.minZoom ?? DEFAULT_SETTINGS.minZoom, - max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom, - }) + /* Update camera constraints to match props + @param {object} nextProps + @param {object} currProps + @returns {bool} true if anything is changed + */ + private _updateConstraints(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { + updateZoomConstraint( + this._map, + { + min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: nextProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom + }, + { + min: currProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom + } + ); + updatePitchConstraint( + this._map, + { + min: nextProps.minPitch ?? DEFAULT_SETTINGS.minPitch, + max: nextProps.maxPitch ?? DEFAULT_SETTINGS.maxPitch + }, + { + min: currProps.minPitch ?? DEFAULT_SETTINGS.minPitch, + max: currProps.maxPitch ?? DEFAULT_SETTINGS.maxPitch + } + ); - return true + return true; } /* Update camera constraints and projection settings to match props @@ -464,20 +478,20 @@ export default class Maplibre { const map = this._map; let settingsChanged = false; for (const propName of settingNames) { - if (propName === 'minZoom' || propName === 'maxZoom') { + if (constraintNames.includes(propName as (typeof constraintNames)[number])) { // eslint-disable-next-line no-continue - continue + continue; } if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) { settingsChanged = true; - const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName] + const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; setter?.call(map, nextValue); } } - const zoomChanged = this._updateZoomConstraint(nextProps, currProps) - return settingsChanged || zoomChanged; + const constraintsChanged = this._updateConstraints(nextProps, currProps); + return settingsChanged || constraintsChanged; } /* Update map style to match props */ diff --git a/modules/react-maplibre/src/utils/transform.ts b/modules/react-maplibre/src/utils/transform.ts index e8f8ee095..b44823d3e 100644 --- a/modules/react-maplibre/src/utils/transform.ts +++ b/modules/react-maplibre/src/utils/transform.ts @@ -1,7 +1,7 @@ import type {MaplibreProps} from '../maplibre/maplibre'; import type {ViewState} from '../types/common'; import type {TransformLike} from '../types/internal'; -import { MapInstance } from '../types/lib'; +import type {MapInstance} from '../types/lib'; import {deepEqual} from './deep-equal'; /** @@ -64,28 +64,68 @@ export function applyViewStateToTransform( * @param {object} nextRange * @param {object} currRange **/ -export function updateZoomConstraint(map: MapInstance, nextRange: { min: number; max: number}, currentRange: { min: number; max: number }): void { - if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { - return +export function updateZoomConstraint( + map: MapInstance, + nextRange: {min: number; max: number}, + currentRange: {min: number; max: number} +): void { + if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { + return; + } + + // if moving up ie. 1 - 3 -> 5 - 10 + if (nextRange.min >= currentRange.min) { + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max); } - - // if moving up ie. 1 - 3 -> 5 - 10 - if (nextRange.min >= currentRange.min) { - if (nextRange.max !== currentRange.max) { - map.setMaxZoom(nextRange.max) - } - if (nextRange.min !== currentRange.min) { - map.setMinZoom(nextRange.min) - } + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min); } + } - // if moving down ie. 5 - 10 -> 1 - 3 - if (nextRange.min < currentRange.min) { - if (nextRange.min !== currentRange.min) { - map.setMinZoom(nextRange.min) - } - if (nextRange.max !== currentRange.max) { - map.setMaxZoom(nextRange.max) - } + // if moving down ie. 5 - 10 -> 1 - 3 + if (nextRange.min < currentRange.min) { + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min); } - } \ No newline at end of file + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max); + } + } +} + +/** + * Update pitch constraints to match props by calling + * `setMinPitch` and `setMaxPitch` in the right order + * @param {object} nextRange + * @param {object} currRange + **/ +export function updatePitchConstraint( + map: MapInstance, + nextRange: {min: number; max: number}, + currentRange: {min: number; max: number} +): void { + if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { + return; + } + + // if moving up ie. 1 - 3 -> 5 - 10 + if (nextRange.min >= currentRange.min) { + if (nextRange.max !== currentRange.max) { + map.setMaxPitch(nextRange.max); + } + if (nextRange.min !== currentRange.min) { + map.setMinPitch(nextRange.min); + } + } + + // if moving down ie. 5 - 10 -> 1 - 3 + if (nextRange.min < currentRange.min) { + if (nextRange.min !== currentRange.min) { + map.setMinPitch(nextRange.min); + } + if (nextRange.max !== currentRange.max) { + map.setMaxPitch(nextRange.max); + } + } +} diff --git a/modules/react-maplibre/test/utils/transform.spec.js b/modules/react-maplibre/test/utils/transform.spec.js index a686cd052..e9173627a 100644 --- a/modules/react-maplibre/test/utils/transform.spec.js +++ b/modules/react-maplibre/test/utils/transform.spec.js @@ -1,8 +1,9 @@ -import test from 'tape'; +import test from 'tape-promise/tape'; import { transformToViewState, applyViewStateToTransform, - updateZoomConstraint + updateZoomConstraint, + updatePitchConstraint, } from '@vis.gl/react-maplibre/utils/transform'; import maplibregl from 'maplibre-gl'; @@ -91,46 +92,104 @@ test('updateZoomConstraint', t => { } } - // moving down. 5 - 10 -> 1 - 3 currentMinZoom = 5 currentMaxZoom = 10 - updateZoomConstraint(map, { min: 1, max: 3 }, { min: 5, max: 10 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 1, max: 3 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first') first = null - // moving up. 1 - 3 -> 5 - 10 currentMinZoom = 1 currentMaxZoom = 3 - updateZoomConstraint(map, { min: 5, max: 10 }, { min: 1, max: 3 }); - t.equal(first, 'max', 'max first') + updateZoomConstraint(map, { min: 5, max: 10 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first') first = null - // expanding. 5 - 18 -> 3 - 22 currentMinZoom = 5 currentMaxZoom = 18 - updateZoomConstraint(map, { min: 3, max: 22 }, { min: 5, max: 18 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 3, max: 22 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first') first = null - // expanding down. 5 - 18 -> 3 - 18 currentMinZoom = 5 currentMaxZoom = 18 - updateZoomConstraint(map, { min: 3, max: 18 }, { min: 5, max: 18 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 3, max: 18 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first') first = null - // contracting. 3 - 22 -> 5 - 18 - currentMinZoom = 5 - currentMaxZoom = 18 - updateZoomConstraint(map, { min: 5, max: 18 }, { min: 3, max: 22 }); - t.equal(first, 'max', 'max first') + currentMinZoom = 3 + currentMaxZoom = 22 + updateZoomConstraint(map, { min: 5, max: 18 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first') first = null - // contracting down. 12 - 22 -> 5 - 10 currentMinZoom = 12 currentMaxZoom = 22 - updateZoomConstraint(map, { min: 5, max: 10 }, { min: 12, max: 22 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 5, max: 10 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first') + first = null + + t.end(); +}); + +test('updatePitchConstraint', t => { + let first = null + let currentMinPitch = 0 + let currentMaxPitch = 0 + const map = { + setMinPitch: (nextMinPitch) => { + if (nextMinPitch > currentMaxPitch) { + throw new Error('Setting minPitch > maxPitch') + } + currentMinPitch = nextMinPitch + if (!first) { + first = 'min' + } + }, + setMaxPitch: (nextMaxPitch) => { + if (nextMaxPitch < currentMinPitch) { + throw new Error('Setting maxPitch < minPitch') + } + currentMaxPitch = nextMaxPitch + if (!first) { + first = 'max' + } + } + } + + currentMinPitch = 5 + currentMaxPitch = 10 + updatePitchConstraint(map, { min: 1, max: 3 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first') + first = null + + currentMinPitch = 1 + currentMaxPitch = 3 + updatePitchConstraint(map, { min: 5, max: 10 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first') + first = null + + currentMinPitch = 5 + currentMaxPitch = 18 + updatePitchConstraint(map, { min: 3, max: 22 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first') + first = null + + currentMinPitch = 5 + currentMaxPitch = 18 + updatePitchConstraint(map, { min: 3, max: 18 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first') + first = null + + currentMinPitch = 3 + currentMaxPitch = 22 + updatePitchConstraint(map, { min: 5, max: 18 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first') + first = null + + currentMinPitch = 12 + currentMaxPitch = 22 + updatePitchConstraint(map, { min: 5, max: 10 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first') first = null t.end(); From e86b75d371b7ee7ca6bdb8e7669d637abbcd2202 Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 20 Jan 2026 23:41:03 +1100 Subject: [PATCH 4/5] Return whether or not changes were applied from the updatePitchConstraint and updateZoomConstraint func --- modules/react-maplibre/src/maplibre/maplibre.ts | 6 +++--- modules/react-maplibre/src/utils/transform.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 6bfe1e1f1..346143b87 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -443,7 +443,7 @@ export default class Maplibre { @returns {bool} true if anything is changed */ private _updateConstraints(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { - updateZoomConstraint( + const didUpdateZoom = updateZoomConstraint( this._map, { min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, @@ -454,7 +454,7 @@ export default class Maplibre { max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom } ); - updatePitchConstraint( + const didUpdatePitch = updatePitchConstraint( this._map, { min: nextProps.minPitch ?? DEFAULT_SETTINGS.minPitch, @@ -466,7 +466,7 @@ export default class Maplibre { } ); - return true; + return didUpdateZoom || didUpdatePitch; } /* Update camera constraints and projection settings to match props diff --git a/modules/react-maplibre/src/utils/transform.ts b/modules/react-maplibre/src/utils/transform.ts index b44823d3e..a317402f6 100644 --- a/modules/react-maplibre/src/utils/transform.ts +++ b/modules/react-maplibre/src/utils/transform.ts @@ -68,9 +68,9 @@ export function updateZoomConstraint( map: MapInstance, nextRange: {min: number; max: number}, currentRange: {min: number; max: number} -): void { +): boolean { if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { - return; + return false; } // if moving up ie. 1 - 3 -> 5 - 10 @@ -92,6 +92,8 @@ export function updateZoomConstraint( map.setMaxZoom(nextRange.max); } } + + return true; } /** @@ -104,9 +106,9 @@ export function updatePitchConstraint( map: MapInstance, nextRange: {min: number; max: number}, currentRange: {min: number; max: number} -): void { +): boolean { if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { - return; + return false; } // if moving up ie. 1 - 3 -> 5 - 10 @@ -128,4 +130,6 @@ export function updatePitchConstraint( map.setMaxPitch(nextRange.max); } } + + return true; } From 3a1ddfd1d51fd599e6df1829cf01f278d56198eb Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 20 Jan 2026 23:47:35 +1100 Subject: [PATCH 5/5] Add back propPresent --- modules/react-maplibre/src/maplibre/maplibre.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 346143b87..751a32d40 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -483,7 +483,8 @@ export default class Maplibre { continue; } - if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) { + const propPresent = propName in nextProps || propName in currProps; + if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) { settingsChanged = true; const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`];