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 @@ -21,3 +21,73 @@
expect(div.style.zIndex, 'unitless numeric property').toBe('1');
expect(div.style.flexGrow, 'unitless numeric property').toBe('0.5');
});

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');

Check failure on line 36 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^6.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#unset removed properties

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:36:5

Check failure on line 36 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^4.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#unset removed properties

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:36:5

Check failure on line 36 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^5.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#unset removed properties

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:36:5
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');

Check failure on line 66 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^6.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#handles shorthand transitions

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:66:5

Check failure on line 66 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^4.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#handles shorthand transitions

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:66:5

Check failure on line 66 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^5.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#handles shorthand transitions

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:66:5
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');

Check failure on line 91 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^6.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#preserves unmanaged inline properties

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:91:5

Check failure on line 91 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^4.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#preserves unmanaged inline properties

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:91:5

Check failure on line 91 in modules/react-mapbox/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^5.0.0)

[node] modules/react-mapbox/test/utils/apply-react-style.spec.js > applyReactStyle#preserves unmanaged inline properties

TypeError: t.is is not a function ❯ modules/react-mapbox/test/utils/apply-react-style.spec.js:91:5
t.end();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests use incompatible tape assertions

Medium Severity

The new regression tests call tape-style t.is and t.end, but this suite runs on Vitest and the existing test in the same file uses expect. Vitest's test context has no those methods, so these cases throw at runtime and never validate the unset behavior.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c9fe608. Configure here.

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 @@ -21,3 +21,73 @@
expect(div.style.zIndex, 'unitless numeric property').toBe('1');
expect(div.style.flexGrow, 'unitless numeric property').toBe('0.5');
});

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');

Check failure on line 36 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^6.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#unset removed properties

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:36:5

Check failure on line 36 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^4.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#unset removed properties

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:36:5

Check failure on line 36 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^5.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#unset removed properties

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:36:5
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');

Check failure on line 66 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^6.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#handles shorthand transitions

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:66:5

Check failure on line 66 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^4.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#handles shorthand transitions

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:66:5

Check failure on line 66 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^5.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#handles shorthand transitions

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:66:5
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');

Check failure on line 91 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^6.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#preserves unmanaged inline properties

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:91:5

Check failure on line 91 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^4.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#preserves unmanaged inline properties

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:91:5

Check failure on line 91 in modules/react-maplibre/test/utils/apply-react-style.spec.js

View workflow job for this annotation

GitHub Actions / test-node-matrix (^5.0.0)

[node] modules/react-maplibre/test/utils/apply-react-style.spec.js > applyReactStyle#preserves unmanaged inline properties

TypeError: t.is is not a function ❯ modules/react-maplibre/test/utils/apply-react-style.spec.js:91:5
t.end();
});
Loading