diff --git a/modules/react-mapbox/src/utils/apply-react-style.ts b/modules/react-mapbox/src/utils/apply-react-style.ts index 2ff1b9b64..3b20ec130 100644 --- a/modules/react-mapbox/src/utils/apply-react-style.ts +++ b/modules/react-mapbox/src/utils/apply-react-style.ts @@ -3,18 +3,43 @@ import * as React from 'react'; // https://github.com/facebook/react/blob/4131af3e4bf52f3a003537ec95a1655147c81270/src/renderers/dom/shared/CSSPropertyOperations.js#L62 const unitlessNumber = /box|flex|grid|column|lineHeight|fontWeight|opacity|order|tabSize|zIndex/; +// Remembers the style keys applied to each element on the previous call, so that +// properties which are later removed (set to undefined/null or omitted from the +// style object) can be unset instead of lingering on the element. +const appliedStyleKeys = new WeakMap(); + export function applyReactStyle(element: HTMLElement, styles: React.CSSProperties) { - if (!element || !styles) { + if (!element) { return; } const style = element.style; + const previousKeys = appliedStyleKeys.get(element); + const nextKeys: string[] = []; for (const key in styles) { const value = styles[key]; + if (value === undefined || value === null) { + continue; + } if (Number.isFinite(value) && !unitlessNumber.test(key)) { style[key] = `${value}px`; } else { style[key] = value; } + nextKeys.push(key); + } + + if (previousKeys) { + for (const key of previousKeys) { + if (!nextKeys.includes(key)) { + style[key] = ''; + } + } + } + + if (nextKeys.length) { + appliedStyleKeys.set(element, nextKeys); + } else { + appliedStyleKeys.delete(element); } } diff --git a/modules/react-mapbox/test/utils/apply-react-style.spec.js b/modules/react-mapbox/test/utils/apply-react-style.spec.js index d246dcb71..f34d99566 100644 --- a/modules/react-mapbox/test/utils/apply-react-style.spec.js +++ b/modules/react-mapbox/test/utils/apply-react-style.spec.js @@ -24,3 +24,28 @@ test('applyReactStyle', t => { t.end(); }); + +test('applyReactStyle#unset removed properties', t => { + /* global document */ + if (typeof document === 'undefined') { + t.end(); + return; + } + + const div = document.createElement('div'); + + applyReactStyle(div, {background: 'red', color: 'blue'}); + t.is(div.style.background, 'red', 'sets background'); + t.is(div.style.color, 'blue', 'sets color'); + + // A property whose value becomes undefined should be unset, others preserved + applyReactStyle(div, {background: undefined, color: 'blue'}); + t.is(div.style.background, '', 'unset property that became undefined'); + t.is(div.style.color, 'blue', 'kept property that is still set'); + + // A property omitted from the style object should also be unset + applyReactStyle(div, {}); + t.is(div.style.color, '', 'unset property that was removed from styles'); + + t.end(); +}); diff --git a/modules/react-maplibre/src/utils/apply-react-style.ts b/modules/react-maplibre/src/utils/apply-react-style.ts index 2ff1b9b64..3b20ec130 100644 --- a/modules/react-maplibre/src/utils/apply-react-style.ts +++ b/modules/react-maplibre/src/utils/apply-react-style.ts @@ -3,18 +3,43 @@ import * as React from 'react'; // https://github.com/facebook/react/blob/4131af3e4bf52f3a003537ec95a1655147c81270/src/renderers/dom/shared/CSSPropertyOperations.js#L62 const unitlessNumber = /box|flex|grid|column|lineHeight|fontWeight|opacity|order|tabSize|zIndex/; +// Remembers the style keys applied to each element on the previous call, so that +// properties which are later removed (set to undefined/null or omitted from the +// style object) can be unset instead of lingering on the element. +const appliedStyleKeys = new WeakMap(); + export function applyReactStyle(element: HTMLElement, styles: React.CSSProperties) { - if (!element || !styles) { + if (!element) { return; } const style = element.style; + const previousKeys = appliedStyleKeys.get(element); + const nextKeys: string[] = []; for (const key in styles) { const value = styles[key]; + if (value === undefined || value === null) { + continue; + } if (Number.isFinite(value) && !unitlessNumber.test(key)) { style[key] = `${value}px`; } else { style[key] = value; } + nextKeys.push(key); + } + + if (previousKeys) { + for (const key of previousKeys) { + if (!nextKeys.includes(key)) { + style[key] = ''; + } + } + } + + if (nextKeys.length) { + appliedStyleKeys.set(element, nextKeys); + } else { + appliedStyleKeys.delete(element); } } diff --git a/modules/react-maplibre/test/utils/apply-react-style.spec.js b/modules/react-maplibre/test/utils/apply-react-style.spec.js index 6250b2d01..7a7dd18b1 100644 --- a/modules/react-maplibre/test/utils/apply-react-style.spec.js +++ b/modules/react-maplibre/test/utils/apply-react-style.spec.js @@ -24,3 +24,28 @@ test('applyReactStyle', t => { t.end(); }); + +test('applyReactStyle#unset removed properties', t => { + /* global document */ + if (typeof document === 'undefined') { + t.end(); + return; + } + + const div = document.createElement('div'); + + applyReactStyle(div, {background: 'red', color: 'blue'}); + t.is(div.style.background, 'red', 'sets background'); + t.is(div.style.color, 'blue', 'sets color'); + + // A property whose value becomes undefined should be unset, others preserved + applyReactStyle(div, {background: undefined, color: 'blue'}); + t.is(div.style.background, '', 'unset property that became undefined'); + t.is(div.style.color, 'blue', 'kept property that is still set'); + + // A property omitted from the style object should also be unset + applyReactStyle(div, {}); + t.is(div.style.color, '', 'unset property that was removed from styles'); + + t.end(); +});