From 86b3b994b5d14e37bfd42b47a2225a9c89fb917a Mon Sep 17 00:00:00 2001 From: hasib9797 Date: Mon, 2 Nov 2026 23:51:53 +0400 Subject: [PATCH 1/2] [native] Fix textShadow parsing of negative, decimal and color-first values LENGTH_REG only accepted unsigned integers, and the color was assumed to be the last token. As a result: - "2px -2px" used "-2px" as the color - "1px 1px 1.5px" used "1.5px" as the color - "red 1px 2px 3px" used "red" as offsetX and shifted the other values Accept signed and decimal lengths, take the first non-length token as the color wherever it appears, and ignore empty tokens from repeated spaces. --- .../css/__tests__/parseTextShadow-test.js | 60 +++++++++++++++++++ .../src/native/css/parseTextShadow.js | 8 +-- 2 files changed, 64 insertions(+), 4 deletions(-) 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..17fb7be5 --- /dev/null +++ b/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js @@ -0,0 +1,60 @@ +/** + * 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 lengths followed by a color', () => { + expect(parseTextShadow('1px 2px 3px red')).toEqual({ + textShadowColor: 'red', + textShadowOffset: { width: 1, height: 2 }, + textShadowRadius: 3 + }); + }); + + test('parses a color that precedes the lengths', () => { + expect(parseTextShadow('red 1px 2px 3px')).toEqual({ + textShadowColor: 'red', + textShadowOffset: { width: 1, height: 2 }, + textShadowRadius: 3 + }); + }); + + test('parses functional colors', () => { + expect(parseTextShadow('rgba(0, 0, 0, 0.5) 1px 2px')).toEqual({ + textShadowColor: 'rgba(0, 0, 0, 0.5)', + textShadowOffset: { width: 1, height: 2 }, + textShadowRadius: undefined + }); + }); + + test('parses negative and decimal lengths without a color', () => { + expect(parseTextShadow('2px -2px')).toEqual({ + textShadowColor: null, + textShadowOffset: { width: 2, height: -2 }, + textShadowRadius: undefined + }); + expect(parseTextShadow('-1px 0.5px 1.5px')).toEqual({ + textShadowColor: null, + textShadowOffset: { width: -1, height: 0.5 }, + textShadowRadius: 1.5 + }); + expect(parseTextShadow('1px 1px .5px blue')).toEqual({ + textShadowColor: 'blue', + textShadowOffset: { width: 1, height: 1 }, + textShadowRadius: 0.5 + }); + }); + + test('ignores repeated whitespace', () => { + expect(parseTextShadow('1px 2px red')).toEqual({ + textShadowColor: 'red', + textShadowOffset: { width: 1, height: 2 }, + textShadowRadius: undefined + }); + }); +}); diff --git a/packages/react-strict-dom/src/native/css/parseTextShadow.js b/packages/react-strict-dom/src/native/css/parseTextShadow.js index 6b641600..8bca6f0d 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); @@ -24,10 +24,10 @@ function toMaybeNum(v: string): number | string { } function parseValue(str: string) { - const parts = str.split(PARTS_REG); + const parts = str.split(PARTS_REG).filter((p) => p !== ''); const inset = parts.includes('inset'); - const last = parts.slice(-1)[0]; - const color = !isLength(last) ? last : null; + // The color may appear before or after the lengths + const color = parts.find((p) => p !== 'inset' && !isLength(p)) ?? null; const nums = parts .filter((n) => n !== 'inset') From 3926efe902f024554aa67e38385d8c8d4012a426 Mon Sep 17 00:00:00 2001 From: hasib9797 Date: Mon, 2 Nov 2026 23:51:53 +0400 Subject: [PATCH 2/2] [native] Fall back to default value for variables without a dark value In dark mode, resolveVariableReferenceValue always read the '@media (prefers-color-scheme: dark)' key. A variable defined without it resolved to undefined, so the whole style property was dropped. Only use the dark value when it is defined, matching how regular style values are resolved in css/index.js. --- .../css/__tests__/customProperties-test.js | 36 +++++++++++++++++++ .../src/native/css/customProperties.js | 5 ++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 packages/react-strict-dom/src/native/css/__tests__/customProperties-test.js diff --git a/packages/react-strict-dom/src/native/css/__tests__/customProperties-test.js b/packages/react-strict-dom/src/native/css/__tests__/customProperties-test.js new file mode 100644 index 00000000..c12c7368 --- /dev/null +++ b/packages/react-strict-dom/src/native/css/__tests__/customProperties-test.js @@ -0,0 +1,36 @@ +/** + * 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 { CSSUnparsedValue } from '../typed-om/CSSUnparsedValue'; +import { resolveVariableReferences } from '../customProperties'; + +function resolve(registry, colorScheme) { + return resolveVariableReferences( + 'color', + CSSUnparsedValue.parse('color', 'var(--fg)'), + registry, + colorScheme + ); +} + +describe('resolveVariableReferences', () => { + test('uses the dark value in dark mode', () => { + const registry = { + fg: { default: 'black', '@media (prefers-color-scheme: dark)': 'white' } + }; + expect(resolve(registry, 'light')).toBe('black'); + expect(resolve(registry, 'dark')).toBe('white'); + }); + + test('falls back to the default value in dark mode when no dark value is defined', () => { + const registry = { + fg: { default: 'black', '@media (min-width: 400px)': 'gray' } + }; + expect(resolve(registry, 'light')).toBe('black'); + expect(resolve(registry, 'dark')).toBe('black'); + }); +}); diff --git a/packages/react-strict-dom/src/native/css/customProperties.js b/packages/react-strict-dom/src/native/css/customProperties.js index 7389215f..d9f56abe 100644 --- a/packages/react-strict-dom/src/native/css/customProperties.js +++ b/packages/react-strict-dom/src/native/css/customProperties.js @@ -68,7 +68,10 @@ function resolveVariableReferenceValue( if (variableValue != null) { if (typeof variableValue === 'object' && variableValue.default != null) { let defaultValue = variableValue.default; - if (colorScheme === 'dark') { + if ( + colorScheme === 'dark' && + Object.hasOwn(variableValue, '@media (prefers-color-scheme: dark)') + ) { defaultValue = variableValue['@media (prefers-color-scheme: dark)']; } return defaultValue;