Repository: preactjs/preact (JavaScript)

Issue:
Different behavior for keys with undefined properties in style objects using preact/compat
It seems as though there's an issue with how Preact handles style props when values are explicitly `undefined`. It seems to be a little more nuanced than it just leaving `undefined` in though – I think it happens only when the object property was _previously_ defined, and later becomes undefined:


**To Reproduce**

* [Preact demo](https://codesandbox.io/s/preact-undefined-style-properties-5n4giu?file=/src/App.js)
* [React demo](https://codesandbox.io/s/react-undefined-style-properties-sutlv2?file=/src/App.js)

**Expected behavior**

I would expect the `--x` css property to actually be deleted when it becomes `undefined`, as this is what happens on initial render.



Discussion:

Patch:
diff --git a/src/diff/props.js b/src/diff/props.js
index 1936e0d049..89e773ab89 100644
--- a/src/diff/props.js
+++ b/src/diff/props.js
@@ -35,7 +35,7 @@ export function diffProps(dom, newProps, oldProps, isSvg, hydrate) {
 
 function setStyle(style, key, value) {
 	if (key[0] === '-') {
-		style.setProperty(key, value);
+		style.setProperty(key, value == null ? '' : value);
 	} else if (value == null) {
 		style[key] = '';
 	} else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) {


Test patch:
diff --git a/test/_util/helpers.js b/test/_util/helpers.js
index 28340afdb5..780dab47c1 100644
--- a/test/_util/helpers.js
+++ b/test/_util/helpers.js
@@ -204,7 +204,7 @@ export function clearOptions() {
 
 /**
  * Teardown test environment and reset preact's internal state
- * @param {HTMLDivElement} scratch
+ * @param {HTMLElement} scratch
  */
 export function teardown(scratch) {
 	if (scratch) {
diff --git a/test/browser/style.test.js b/test/browser/style.test.js
index a2b6afce56..117eaf7fef 100644
--- a/test/browser/style.test.js
+++ b/test/browser/style.test.js
@@ -4,6 +4,7 @@ import { setupScratch, teardown, sortCss } from '../_util/helpers';
 /** @jsx createElement */
 
 describe('style attribute', () => {
+	/** @type {HTMLElement} */
 	let scratch;
 
 	beforeEach(() => {
@@ -221,5 +222,56 @@ describe('style attribute', () => {
 				'10px'
 			);
 		});
+
+		it('should clear css properties when passed an empty string, null, or undefined', () => {
+			const red = 'rgb(255, 0, 0)';
+			const blue = 'rgb(0, 0, 255)';
+			const green = 'rgb(0, 128, 0)';
+			const yellow = 'rgb(255, 255, 0)';
+			const violet = 'rgb(238, 130, 238)';
+
+			function App({ color }) {
+				return (
+					<div style={{ '--color': color }}>
+						<span style={{ color: 'var(--color, red)' }}>Hello World!</span>
+					</div>
+				);
+			}
+
+			const getDiv = () => /** @type {HTMLDivElement} */ (scratch.firstChild);
+			const getSpan = () =>
+				/** @type {HTMLSpanElement} */ (scratch.firstChild.firstChild);
+			const getCSSVariableValue = () =>
+				getDiv().style.getPropertyValue('--color');
+			const getTextColor = () => window.getComputedStyle(getSpan()).color;
+
+			render(<App color="blue" />, scratch);
+			expect(getCSSVariableValue()).to.equal('blue');
+			expect(getTextColor()).to.equal(blue);
+
+			render(<App color="" />, scratch);
+			expect(getCSSVariableValue(), 'empty string').to.equal('');
+			expect(getTextColor(), 'empty string').to.equal(red);
+
+			render(<App color="green" />, scratch);
+			expect(getCSSVariableValue()).to.equal('green');
+			expect(getTextColor()).to.equal(green);
+
+			render(<App color={undefined} />, scratch);
+			expect(getCSSVariableValue(), 'undefined').to.equal('');
+			expect(getTextColor(), 'undefined').to.equal(red);
+
+			render(<App color="yellow" />, scratch);
+			expect(getCSSVariableValue()).to.equal('yellow');
+			expect(getTextColor()).to.equal(yellow);
+
+			render(<App color={null} />, scratch);
+			expect(getCSSVariableValue(), 'null').to.equal('');
+			expect(getTextColor(), 'null').to.equal(red);
+
+			render(<App color="violet" />, scratch);
+			expect(getCSSVariableValue()).to.equal('violet');
+			expect(getTextColor()).to.equal(violet);
+		});
 	}
 });