From 8498cdbc639a06cb3a8f0cfcf2783ab4ad28ba09 Mon Sep 17 00:00:00 2001 From: Durvesh Pilankar Date: Mon, 29 Jun 2026 12:00:37 -0700 Subject: [PATCH 1/2] Fix textShadow parsing of negative and decimal length values The length regex used to detect whether the final token of a text-shadow is a length or a color only matched non-negative integer lengths (e.g. "3px"). As a result, a trailing negative offset ("1px -2px") or a trailing decimal blur radius ("1px 1px 2.5px") was misclassified as the color, dropping the offset/blur and producing a bogus color value. Allow an optional leading minus sign and decimal values in the length regex, matching the pattern already used by CSSLengthUnitValue. Add unit tests covering negative offsets and decimal blur radii with and without an explicit color. --- .../css/__tests__/parseTextShadow-test.js | 46 +++++++++++++++++++ .../src/native/css/parseTextShadow.js | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js diff --git a/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js b/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js new file mode 100644 index 00000000..52dfee04 --- /dev/null +++ b/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { parseTextShadow } from '../parseTextShadow'; + +describe('parseTextShadow', () => { + test('parses offsets, blur radius and color', () => { + expect(parseTextShadow('1px 2px 3px red')).toEqual({ + textShadowColor: 'red', + textShadowOffset: { width: 1, height: 2 }, + textShadowRadius: 3 + }); + }); + + test('parses a trailing color with a negative offset', () => { + expect(parseTextShadow('1px -2px 3px red')).toEqual({ + textShadowColor: 'red', + textShadowOffset: { width: 1, height: -2 }, + textShadowRadius: 3 + }); + }); + + test('parses a negative offset when no color is provided', () => { + // The vertical offset is negative and is the last token. It must be + // treated as a length, not as a color. + expect(parseTextShadow('1px -2px')).toEqual({ + textShadowColor: null, + textShadowOffset: { width: 1, height: -2 }, + textShadowRadius: undefined + }); + }); + + test('parses a decimal blur radius when no color is provided', () => { + // The blur radius is a decimal length and is the last token. It must be + // treated as a length, not as a color. + expect(parseTextShadow('1px 1px 2.5px')).toEqual({ + textShadowColor: null, + textShadowOffset: { width: 1, height: 1 }, + textShadowRadius: 2.5 + }); + }); +}); diff --git a/packages/react-strict-dom/src/native/css/parseTextShadow.js b/packages/react-strict-dom/src/native/css/parseTextShadow.js index 6b641600..c198e13b 100644 --- a/packages/react-strict-dom/src/native/css/parseTextShadow.js +++ b/packages/react-strict-dom/src/native/css/parseTextShadow.js @@ -11,7 +11,7 @@ import { warnMsg } from '../../shared/logUtils'; const VALUES_REG = /,(?![^(]*\))/; const PARTS_REG = /\s(?![^(]*\))/; -const LENGTH_REG = /^[0-9]+[a-zA-Z%]+?$/; +const LENGTH_REG = /^-?(?:[0-9]*\.)?[0-9]+[a-zA-Z%]+$/; function isLength(v: string): boolean { return v === '0' || LENGTH_REG.test(v); From dd4269af61d12fd53ee1ec6bb2ff9fc77b43485e Mon Sep 17 00:00:00 2001 From: Durvesh Pilankar Date: Mon, 29 Jun 2026 13:13:45 -0700 Subject: [PATCH 2/2] Also handle text-shadow color specified before the offsets CSS allows the text-shadow color to appear either before or after the offset values. parseValue only inspected the last token, so a leading color (e.g. "red 1px 1px") was misparsed: the color was dropped and the first offset became the color string. Detect the color as the single non-length token regardless of position. --- .../src/native/css/__tests__/parseTextShadow-test.js | 9 +++++++++ .../src/native/css/parseTextShadow.js | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js b/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js index 52dfee04..0142ba57 100644 --- a/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js +++ b/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js @@ -34,6 +34,15 @@ describe('parseTextShadow', () => { }); }); + test('parses a color that precedes the offset values', () => { + // CSS allows the color to be specified before or after the offsets. + expect(parseTextShadow('red 1px 2px 3px')).toEqual({ + textShadowColor: 'red', + textShadowOffset: { width: 1, height: 2 }, + textShadowRadius: 3 + }); + }); + test('parses a decimal blur radius when no color is provided', () => { // The blur radius is a decimal length and is the last token. It must be // treated as a length, not as a color. diff --git a/packages/react-strict-dom/src/native/css/parseTextShadow.js b/packages/react-strict-dom/src/native/css/parseTextShadow.js index c198e13b..cbd7e49e 100644 --- a/packages/react-strict-dom/src/native/css/parseTextShadow.js +++ b/packages/react-strict-dom/src/native/css/parseTextShadow.js @@ -26,13 +26,13 @@ function toMaybeNum(v: string): number | string { function parseValue(str: string) { const parts = str.split(PARTS_REG); const inset = parts.includes('inset'); - const last = parts.slice(-1)[0]; - const color = !isLength(last) ? last : null; + const tokens = parts.filter((n) => n !== 'inset'); + // The color is the single token that is not a length value. CSS allows it to + // be specified either before or after the offset values, so search all tokens + // rather than assuming it is last. + const color = tokens.find((n) => !isLength(n)) ?? null; - const nums = parts - .filter((n) => n !== 'inset') - .filter((n) => n !== color) - .map(toMaybeNum); + const nums = tokens.filter((n) => n !== color).map(toMaybeNum); const [offsetX, offsetY, blurRadius, spreadRadius] = nums;