diff --git a/src/__tests__/native/attributes.test.tsx b/src/__tests__/native/attributes.test.tsx index 78e1814c..d806bf7a 100644 --- a/src/__tests__/native/attributes.test.tsx +++ b/src/__tests__/native/attributes.test.tsx @@ -133,3 +133,43 @@ describe("dataSet attribute selector", () => { }); }); }); + +describe("a namespace-qualified attribute selector represents nothing", () => { + const matchedWidth = (selector: string): number | undefined => { + registerCSS(`.test${selector} { width: 10px; }`); + render( + , + ); + const style = screen.getByTestId(testID).props.style as + | { width?: number } + | undefined; + return style?.width; + }; + + // Measured against lightningcss: only `[ns|att]` reports a `specific` namespace. + // `[att]` and `[|att]` report none and `[*|att]` reports `any`, and with no + // namespaced props in the tree those three denote the same set. + test.each([ + ["no namespace", `[data-x='a']`], + ["explicitly no namespace", `[|data-x='a']`], + ["any namespace", `[*|data-x='a']`], + ])("%s matches the prop", (_label, selector) => { + expect(matchedWidth(selector)).toBe(10); + }); + + test("a declared prefix matches nothing — no prop is in a namespace", () => { + registerCSS( + `@namespace ns url(http://example.com/ns); .test[ns|data-x='a'] { width: 10px; }`, + ); + render( + , + ); + expect(screen.getByTestId(testID).props.style).toBeUndefined(); + }); + + test("an undeclared prefix matches nothing — the selector is invalid", () => { + // Selectors L3 §6.3.3. lightningcss passes the prefix through rather than + // rejecting it, so dropping the selector is this compiler's job. + expect(matchedWidth(`[undeclared|data-x='a']`)).toBeUndefined(); + }); +}); diff --git a/src/compiler/selector-builder.ts b/src/compiler/selector-builder.ts index 88561b78..f86ac199 100644 --- a/src/compiler/selector-builder.ts +++ b/src/compiler/selector-builder.ts @@ -251,6 +251,17 @@ function parseComponents( getMediaQuery(ref).push([operator, "dir", component.operation.value]); return parseComponents(rest, options, root, ref, specificity); + } else if (isNamespacedAttribute(component)) { + // Selectors §6 — `[ns|att]` represents only attributes in `ns`. A React + // Native prop is in no namespace, so nothing can match and the selector is + // dropped. An UNDECLARED prefix reaches here too: lightningcss passes it + // through rather than rejecting it, and Selectors L3 §6.3.3 makes such a + // selector invalid, which is the same outcome. + // + // `[att]`, `[|att]` and `[*|att]` are all unaffected — the first two name + // no namespace and the third names any, and with no namespaced props in + // the tree those three denote the same set. + return []; } else { // specificity[Specificity.ClassName] = // (specificity[Specificity.ClassName] ?? 0) + 1; @@ -456,6 +467,12 @@ function parseIsWhereComponents( return null; } + if (isNamespacedAttribute(component)) { + // See the compound path: no prop carries a namespace, so this argument + // represents nothing and the selector it belongs to cannot match. + return null; + } + if (type !== "where") { // specificity[Specificity.ClassName] = // (specificity[Specificity.ClassName] ?? 0) + 1; @@ -582,6 +599,20 @@ type CamelCase = ? `${Lowercase}${Uppercase}${CamelCase}` : Lowercase; +/** + * Whether an attribute selector names a specific namespace. + * + * Measured against lightningcss: `[att]` and `[|att]` both report `null`, `[*|att]` + * reports `{ type: "any" }`, and only `[ns|att]` reports `{ type: "specific" }` — + * for a DECLARED prefix and an undeclared one alike, the second being a selector + * Selectors L3 §6.3.3 makes invalid. + */ +function isNamespacedAttribute( + component: Extract, +): boolean { + return component.namespace?.type === "specific"; +} + const operatorMap: Record = { "equal": "=", "includes": "~=",