Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/__tests__/native/attributes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Text testID={testID} className="test" {...{ dataSet: { x: "a" } }} />,
);
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(
<Text testID={testID} className="test" {...{ dataSet: { x: "a" } }} />,
);
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();
});
});
31 changes: 31 additions & 0 deletions src/compiler/selector-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -582,6 +599,20 @@ type CamelCase<S extends string> =
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
: Lowercase<S>;

/**
* 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<Selector[number], { type: "attribute" }>,
): boolean {
return component.namespace?.type === "specific";
}

const operatorMap: Record<AttrOperation["operator"], AttrSelectorOperator> = {
"equal": "=",
"includes": "~=",
Expand Down