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": "~=",