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..0142ba57 --- /dev/null +++ b/packages/react-strict-dom/src/native/css/__tests__/parseTextShadow-test.js @@ -0,0 +1,55 @@ +/** + * 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 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. + 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..cbd7e49e 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); @@ -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;