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