Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,47 @@ describe('@stylexjs/babel-plugin', () => {
`);
});

test('attribute selector with pseudo-class (nested, same value)', () => {
const { code, metadata } = transform(`
import * as stylex from '@stylexjs/stylex';
export const styles = stylex.create({
root: {
color: {
':hover': {
'[data-state="open"]': 'red',
},
'[data-state="open"]': {
':hover': 'red',
},
},
},
});
`);
expect(code).toMatchInlineSnapshot(`
"import * as stylex from '@stylexjs/stylex';
export const styles = {
root: {
kMwMTN: "x113j3rq",
$$css: true
}
};"
`);
expect(metadata).toMatchInlineSnapshot(`
{
"stylex": [
[
"x113j3rq",
{
"ltr": ".x113j3rq:hover[data-state="open"]{color:red}",
"rtl": null,
},
6130,
],
],
}
`);
});

test('pseudo-class with array fallbacks', () => {
const { code, metadata } = transform(`
import * as stylex from '@stylexjs/stylex';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,14 @@ describe('@stylexjs/babel-plugin', () => {
},
});
`),
).toThrow('Pseudo selector must start with ":"');
).toThrow('Pseudo selector must start with ":" or "["');
});

test('rejects pseudo-elements', () => {
expect(() =>
transform(`
import { when, create } from '@stylexjs/stylex';

const styles = create({
container: {
backgroundColor: {
Expand All @@ -312,6 +312,140 @@ describe('@stylexjs/babel-plugin', () => {
`),
).toThrow('Pseudo selector cannot start with "::"');
});

test('validates attribute selector format', () => {
expect(() =>
transform(`
import { when, create } from '@stylexjs/stylex';

const styles = create({
container: {
backgroundColor: {
default: 'blue',
[when.ancestor('[data-state="open"')]: 'red',
},
},
});
`),
).toThrow('Attribute selector must end with "]"');
});

test('rejects invalid selector format', () => {
expect(() =>
transform(`
import { when, create } from '@stylexjs/stylex';

const styles = create({
container: {
backgroundColor: {
default: 'blue',
[when.ancestor('invalid')]: 'red',
},
},
});
`),
).toThrow('Pseudo selector must start with ":" or "["');
});
});

describe('[transform] when functions with attribute selectors', () => {
test('when.ancestor with attribute selector', () => {
const { code, metadata } = transform(`
import { when, create } from '@stylexjs/stylex';

const styles = create({
container: {
backgroundColor: {
default: 'blue',
[when.ancestor('[data-panel-state="open"]')]: 'red',
},
},
});

console.log(styles.container);
`);

expect(code).toMatchInlineSnapshot(`
"import { when, create } from '@stylexjs/stylex';
const styles = {
container: {
kWkggS: "x1t391ir x11omtej",
$$css: true
}
};
console.log(styles.container);"
`);

expect(metadata.stylex).toMatchInlineSnapshot(`
[
[
"x1t391ir",
{
"ltr": ".x1t391ir{background-color:blue}",
"rtl": null,
},
3000,
],
[
"x11omtej",
{
"ltr": ".x11omtej.x11omtej:where(.x-default-marker[data-panel-state="open"] *){background-color:red}",
"rtl": null,
},
3040,
],
]
`);
});

test('when.descendant with attribute selector', () => {
const { code, metadata } = transform(`
import { when, create } from '@stylexjs/stylex';

const styles = create({
container: {
backgroundColor: {
default: 'blue',
[when.descendant('[data-panel-state="open"]')]: 'green',
},
},
});

console.log(styles.container);
`);

expect(code).toMatchInlineSnapshot(`
"import { when, create } from '@stylexjs/stylex';
const styles = {
container: {
kWkggS: "x1t391ir x1doj7mj",
$$css: true
}
};
console.log(styles.container);"
`);

expect(metadata.stylex).toMatchInlineSnapshot(`
[
[
"x1t391ir",
{
"ltr": ".x1t391ir{background-color:blue}",
"rtl": null,
},
3000,
],
[
"x1doj7mj",
{
"ltr": ".x1doj7mj.x1doj7mj:where(:has(.x-default-marker[data-panel-state="open"])){background-color:green}",
"rtl": null,
},
3040,
],
]
`);
});
});
describe('[transform] using stylex.defaultMarker', () => {
test('named import', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export class PreRule implements IPreRule {
}

get pseudos(): $ReadOnlyArray<string> {
const unsortedPseudos = this.keyPath.filter((key) => key.startsWith(':'));
const unsortedPseudos = this.keyPath.filter(
(key) => key.startsWith(':') || key.startsWith('['),
);
return sortPseudos(unsortedPseudos);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,27 @@ describe('Flatten Style Object with legacy shorthand expansion', () => {
],
]);
});
test('Attribute selector conditions', () => {
expect(
flattenRawStyleObject(
{
color: {
default: 'blue',
'[data-panel-state="open"]': 'red',
},
},
options,
),
).toEqual([
[
'color',
PreRuleSet.create([
new PreRule('color', 'blue', ['color', 'default']),
new PreRule('color', 'red', ['color', '[data-panel-state="open"]']),
]),
],
]);
});
});
describe('Multiple levels of nesting', () => {
test('Fallback styles within nested objects', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function validateNamespace(
continue;
}
if (isPlainObject(val)) {
if (key.startsWith('@') || key.startsWith(':')) {
if (key.startsWith('@') || key.startsWith(':') || key.startsWith('[')) {
if (conditions.includes(key)) {
throw new Error(messages.DUPLICATE_CONDITIONAL);
}
Expand Down Expand Up @@ -62,6 +62,7 @@ function validateConditionalStyles(
!(
key.startsWith('@') ||
key.startsWith(':') ||
key.startsWith('[') ||
// This is a placeholder for `defineConsts` values that are later inlined
key.startsWith('var(--') ||
key === 'default'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ export function _flattenRawStyleObject(
if (
typeof value === 'object' &&
!key.startsWith(':') &&
!key.startsWith('@')
!key.startsWith('@') &&
!key.startsWith('[')
) {
const equivalentPairs: { [string]: { [string]: AnyPreRule } } = {};
for (const condition in value) {
Expand Down Expand Up @@ -169,7 +170,7 @@ export function _flattenRawStyleObject(
// Object Values for pseudos and at-rules. e.g. { ':hover': { color: 'red' } }
if (
typeof value === 'object' &&
(key.startsWith(':') || key.startsWith('@'))
(key.startsWith(':') || key.startsWith('@') || key.startsWith('['))
) {
const pairs = _flattenRawStyleObject(value, [...keyPath, _key], options);
for (const [property, preRule] of pairs) {
Expand Down
22 changes: 14 additions & 8 deletions packages/@stylexjs/babel-plugin/src/shared/when/when.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,24 @@ function getDefaultMarkerClassName(
return `${prefix}default-marker`;
}

type WhenSelector = StringPrefix<':'> | StringPrefix<'['>;

/**
* Validates that a pseudo selector starts with ':' but not '::'
* Validates that a pseudo selector starts with ':' but not '::',
* or is an attribute selector that starts with '[' and ends with ']'
*/
function validatePseudoSelector(pseudo: string): void {
if (!pseudo.startsWith(':')) {
throw new Error('Pseudo selector must start with ":"');
if (!(pseudo.startsWith(':') || pseudo.startsWith('['))) {
throw new Error('Pseudo selector must start with ":" or "["');
}
if (pseudo.startsWith('::')) {
throw new Error(
'Pseudo selector cannot start with "::" (pseudo-elements are not supported)',
);
}
if (pseudo.startsWith('[') && !pseudo.endsWith(']')) {
throw new Error('Attribute selector must end with "]"');
}
}
/**
* Creates selector that observes if the given pseudo-class is
Expand All @@ -68,7 +74,7 @@ function validatePseudoSelector(pseudo: string): void {
* @returns A :where() clause for the ancestor observer
*/
export function ancestor(
pseudo: StringPrefix<':'>,
pseudo: WhenSelector,
options: string | StyleXOptions = defaultOptions,
): string {
validatePseudoSelector(pseudo);
Expand All @@ -86,7 +92,7 @@ export function ancestor(
* @returns A :has() clause for the descendant observer
*/
export function descendant(
pseudo: StringPrefix<':'>,
pseudo: WhenSelector,
options: string | StyleXOptions = defaultOptions,
): string {
validatePseudoSelector(pseudo);
Expand All @@ -104,7 +110,7 @@ export function descendant(
* @returns A :where() clause for the previous sibling observer
*/
export function siblingBefore(
pseudo: StringPrefix<':'>,
pseudo: WhenSelector,
options: string | StyleXOptions = defaultOptions,
): string {
validatePseudoSelector(pseudo);
Expand All @@ -122,7 +128,7 @@ export function siblingBefore(
* @returns A :has() clause for the next sibling observer
*/
export function siblingAfter(
pseudo: StringPrefix<':'>,
pseudo: WhenSelector,
options: string | StyleXOptions = defaultOptions,
): string {
validatePseudoSelector(pseudo);
Expand All @@ -140,7 +146,7 @@ export function siblingAfter(
* @returns A :where() clause for the any sibling observer
*/
export function anySibling(
pseudo: StringPrefix<':'>,
pseudo: WhenSelector,
options: string | StyleXOptions = defaultOptions,
): string {
validatePseudoSelector(pseudo);
Expand Down
13 changes: 8 additions & 5 deletions packages/@stylexjs/stylex/src/types/StyleXTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,37 +313,40 @@ export type StyleX$DefineMarker = () => MapNamespace<{
}>;

export type StyleX$When = {
ancestor: <const Pseudo extends string, MarkerSymbol extends symbol = symbol>(
ancestor: <
const Pseudo extends `:${string}` | `[${string}]`,
MarkerSymbol extends symbol = symbol,
>(
_pseudo?: Pseudo,
_customMarker?: MapNamespace<{ readonly marker: MarkerSymbol }>,
// @ts-expect-error - Trying to use a symbol in a string is not normally allowed
) => `:where-ancestor(${Pseudo}, ${MarkerSymbol})`;
descendant: <
const Pseudo extends string,
const Pseudo extends `:${string}` | `[${string}]`,
MarkerSymbol extends symbol = symbol,
>(
_pseudo?: Pseudo,
_customMarker?: MapNamespace<{ readonly marker: MarkerSymbol }>,
// @ts-expect-error - Trying to use a symbol in a string is not normally allowed
) => `:where-descendant(${Pseudo}, ${MarkerSymbol})`;
siblingBefore: <
const Pseudo extends string,
const Pseudo extends `:${string}` | `[${string}]`,
MarkerSymbol extends symbol = symbol,
>(
_pseudo?: Pseudo,
_customMarker?: MapNamespace<{ readonly marker: MarkerSymbol }>,
// @ts-expect-error - Trying to use a symbol in a string is not normally allowed
) => `:where-sibling-before(${Pseudo}, ${MarkerSymbol})`;
siblingAfter: <
const Pseudo extends string,
const Pseudo extends `:${string}` | `[${string}]`,
MarkerSymbol extends symbol = symbol,
>(
_pseudo?: Pseudo,
_customMarker?: MapNamespace<{ readonly marker: MarkerSymbol }>,
// @ts-expect-error - Trying to use a symbol in a string is not normally allowed
) => `:where-sibling-after(${Pseudo}, ${MarkerSymbol})`;
anySibling: <
const Pseudo extends string,
const Pseudo extends `:${string}` | `[${string}]`,
MarkerSymbol extends symbol = symbol,
>(
_pseudo?: Pseudo,
Expand Down
Loading