Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions modules/react-mapbox/src/utils/apply-react-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,59 @@ 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/;

export function applyReactStyle(element: HTMLElement, styles: React.CSSProperties) {
if (!element || !styles) {
// 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<HTMLElement, Set<string>>();

function getDefinedStyleKeys(styles: React.CSSProperties): Set<string> {
const keys = new Set<string>();
for (const key in styles) {
if (styles[key] !== undefined && styles[key] !== null) {
keys.add(key);
}
}
return keys;
}

function setStyleValue(style: CSSStyleDeclaration, key: string, value) {
if (Number.isFinite(value) && !unitlessNumber.test(key)) {
style[key] = `${value}px`;
} else {
style[key] = value;
}
}

export function applyReactStyle(
element: HTMLElement | null | undefined,
styles: React.CSSProperties | null | undefined
) {
if (!element) {
return;
}
const style = element.style;
const nextStyles = styles ?? {};
const previousKeys = appliedStyleKeys.get(element);
const nextKeys = getDefinedStyleKeys(nextStyles);

for (const key in styles) {
const value = styles[key];
if (Number.isFinite(value) && !unitlessNumber.test(key)) {
style[key] = `${value}px`;
} else {
style[key] = value;
if (previousKeys) {
for (const key of previousKeys) {
if (!nextKeys.has(key)) {
style[key] = '';
}
}
}

for (const key in nextStyles) {
const value = nextStyles[key];
if (value !== undefined && value !== null) {
setStyleValue(style, key, value);
}
}

if (nextKeys.size) {
appliedStyleKeys.set(element, nextKeys);
} else {
appliedStyleKeys.delete(element);
}
}
70 changes: 70 additions & 0 deletions modules/react-mapbox/test/utils/apply-react-style.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,73 @@ 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', borderColor: 'green'});
applyReactStyle(div, {background: undefined, color: 'blue', borderColor: null});
t.is(div.style.background, '', 'unsets a property that became undefined');
t.is(div.style.borderColor, '', 'unsets a property that became null');
t.is(div.style.color, 'blue', 'keeps a property that is still set');

applyReactStyle(div, {});
t.is(div.style.color, '', 'unsets a property omitted from the style object');

applyReactStyle(div, {background: 'green'});
t.is(div.style.background, 'green', 'reapplies a property after it was cleared');

applyReactStyle(div, undefined);
t.is(div.style.background, '', 'clears applied properties when styles become undefined');

applyReactStyle(div, {color: 'purple'});
applyReactStyle(div, null);
t.is(div.style.color, '', 'clears applied properties when styles become null');

t.end();
});

test('applyReactStyle#handles shorthand transitions', t => {
/* global document */
if (typeof document === 'undefined') {
t.end();
return;
}

const shorthandToLonghand = document.createElement('div');
applyReactStyle(shorthandToLonghand, {padding: '10px'});
applyReactStyle(shorthandToLonghand, {paddingTop: '20px'});
t.is(shorthandToLonghand.style.paddingTop, '20px', 'preserves a new longhand property');
t.is(shorthandToLonghand.style.paddingRight, '', 'clears the removed shorthand property');

const longhandToShorthand = document.createElement('div');
applyReactStyle(longhandToShorthand, {paddingTop: '20px'});
applyReactStyle(longhandToShorthand, {padding: '10px'});
t.is(longhandToShorthand.style.paddingTop, '10px', 'preserves a new shorthand property');
t.is(longhandToShorthand.style.paddingRight, '10px', 'applies all parts of the shorthand');

t.end();
});

test('applyReactStyle#preserves unmanaged inline properties', t => {
/* global document */
if (typeof document === 'undefined') {
t.end();
return;
}

const div = document.createElement('div');
div.style.opacity = '0.5';

applyReactStyle(div, {color: 'red'});
applyReactStyle(div, {});

t.is(div.style.opacity, '0.5', 'does not clear properties it did not apply');
t.end();
});
57 changes: 49 additions & 8 deletions modules/react-maplibre/src/utils/apply-react-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,59 @@ 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/;

export function applyReactStyle(element: HTMLElement, styles: React.CSSProperties) {
if (!element || !styles) {
// 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<HTMLElement, Set<string>>();

function getDefinedStyleKeys(styles: React.CSSProperties): Set<string> {
const keys = new Set<string>();
for (const key in styles) {
if (styles[key] !== undefined && styles[key] !== null) {
keys.add(key);
}
}
return keys;
}

function setStyleValue(style: CSSStyleDeclaration, key: string, value) {
if (Number.isFinite(value) && !unitlessNumber.test(key)) {
style[key] = `${value}px`;
} else {
style[key] = value;
}
}

export function applyReactStyle(
element: HTMLElement | null | undefined,
styles: React.CSSProperties | null | undefined
) {
if (!element) {
return;
}
const style = element.style;
const nextStyles = styles ?? {};
const previousKeys = appliedStyleKeys.get(element);
const nextKeys = getDefinedStyleKeys(nextStyles);

for (const key in styles) {
const value = styles[key];
if (Number.isFinite(value) && !unitlessNumber.test(key)) {
style[key] = `${value}px`;
} else {
style[key] = value;
if (previousKeys) {
for (const key of previousKeys) {
if (!nextKeys.has(key)) {
style[key] = '';
}
}
}

for (const key in nextStyles) {
const value = nextStyles[key];
if (value !== undefined && value !== null) {
setStyleValue(style, key, value);
}
}

if (nextKeys.size) {
appliedStyleKeys.set(element, nextKeys);
} else {
appliedStyleKeys.delete(element);
}
}
70 changes: 70 additions & 0 deletions modules/react-maplibre/test/utils/apply-react-style.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,73 @@ 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', borderColor: 'green'});
applyReactStyle(div, {background: undefined, color: 'blue', borderColor: null});
t.is(div.style.background, '', 'unsets a property that became undefined');
t.is(div.style.borderColor, '', 'unsets a property that became null');
t.is(div.style.color, 'blue', 'keeps a property that is still set');

applyReactStyle(div, {});
t.is(div.style.color, '', 'unsets a property omitted from the style object');

applyReactStyle(div, {background: 'green'});
t.is(div.style.background, 'green', 'reapplies a property after it was cleared');

applyReactStyle(div, undefined);
t.is(div.style.background, '', 'clears applied properties when styles become undefined');

applyReactStyle(div, {color: 'purple'});
applyReactStyle(div, null);
t.is(div.style.color, '', 'clears applied properties when styles become null');

t.end();
});

test('applyReactStyle#handles shorthand transitions', t => {
/* global document */
if (typeof document === 'undefined') {
t.end();
return;
}

const shorthandToLonghand = document.createElement('div');
applyReactStyle(shorthandToLonghand, {padding: '10px'});
applyReactStyle(shorthandToLonghand, {paddingTop: '20px'});
t.is(shorthandToLonghand.style.paddingTop, '20px', 'preserves a new longhand property');
t.is(shorthandToLonghand.style.paddingRight, '', 'clears the removed shorthand property');

const longhandToShorthand = document.createElement('div');
applyReactStyle(longhandToShorthand, {paddingTop: '20px'});
applyReactStyle(longhandToShorthand, {padding: '10px'});
t.is(longhandToShorthand.style.paddingTop, '10px', 'preserves a new shorthand property');
t.is(longhandToShorthand.style.paddingRight, '10px', 'applies all parts of the shorthand');

t.end();
});

test('applyReactStyle#preserves unmanaged inline properties', t => {
/* global document */
if (typeof document === 'undefined') {
t.end();
return;
}

const div = document.createElement('div');
div.style.opacity = '0.5';

applyReactStyle(div, {color: 'red'});
applyReactStyle(div, {});

t.is(div.style.opacity, '0.5', 'does not clear properties it did not apply');
t.end();
});
Loading