Skip to content

Commit be6d4d0

Browse files
committed
chore: add warnings about selectors
1 parent 4b144a8 commit be6d4d0

File tree

7 files changed

+152
-4
lines changed

7 files changed

+152
-4
lines changed

.changeset/has-selector-element-transformation.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,3 @@
33
---
44

55
Add support for `:has(Item)` syntax in style mappings. Capitalized element names inside `:has()` pseudo-class selectors are now automatically transformed to `data-element` attribute selectors (`:has(Item)``:has([data-element="Item"])`).
6-

.size-limit.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ module.exports = [
2727
path: './dist/es/index.js',
2828
webpack: true,
2929
import: '{ Button }',
30-
limit: '33 kB',
30+
limit: '34 kB',
3131
},
3232
{
3333
name: 'Tree shaking (just an Icon)',
3434
path: './dist/es/index.js',
3535
webpack: true,
3636
import: '{ AiIcon }',
37-
limit: '20 kB',
37+
limit: '21 kB',
3838
},
3939
];

src/stories/Tasty.docs.mdx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ fill: { '': '#white', hovered: '#gray.05', 'theme=danger': '#red' }
101101

102102
Element styled using a capitalized key. Identified by `data-element` attribute. Capitalized words in selectors (`:has(Item)`, `> Body > Row`) transform to `[data-element="..."]`.
103103

104+
**Important:** Combinators (`>`, `+`, `~`) must have spaces around them: `:has(Body > Row)` ✅, not `:has(Body>Row)` ❌. This is a design choice for parser performance.
105+
104106
```jsx
105107
styles: { Title: { preset: 'h3' } }
106108
// Targets: <div data-element="Title">
@@ -715,8 +717,11 @@ const Table = tasty({
715717
},
716718

717719
// Chained selectors (auto-transforms capitalized words)
720+
// ⚠️ Important: Spaces required around combinators for parsing
718721
Cell: {
719-
$: '> Body > Row >', // .table > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]
722+
$: '> Body > Row >', // ✅ Correct: spaces around >
723+
// ❌ Wrong: '>Body>Row>' (parser can't split)
724+
// → .table > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]
720725
border: '1bw solid #border',
721726
},
722727

@@ -728,6 +733,33 @@ const Table = tasty({
728733
});
729734
```
730735

736+
> **⚠️ Space Requirements:** Combinators (`>`, `+`, `~`) in selector affixes **must have spaces** around them when used with capitalized element names. Use `$: '> Body > Row'` ✅, not `$: '>Body>Row'` ❌. This is a performance consideration—the parser uses simple whitespace splitting to identify element names.
737+
738+
#### `:has()` Selector with Sub-elements
739+
740+
Style components based on their children using the `:has()` pseudo-class:
741+
742+
```jsx
743+
const List = tasty({
744+
styles: {
745+
border: {
746+
'': 'none',
747+
// ⚠️ Spaces required around combinators (>, +, ~)
748+
':has(Body > Row)': '1bw solid #border', // ✅ Correct
749+
// ':has(Body>Row)': ... // ❌ Wrong: parser can't split
750+
},
751+
752+
padding: {
753+
'': '2x',
754+
':has(Item)': '4x', // Single element, no combinator
755+
'hovered & :has(Item)': '6x', // Combining with other modifiers
756+
},
757+
},
758+
});
759+
```
760+
761+
> **⚠️ Space Requirements:** Combinators (`>`, `+`, `~`) **must have spaces** around them when used with capitalized element names. Use `:has(Body > Row)` ✅, not `:has(Body>Row)` ❌. This is a performance consideration—the parser uses whitespace splitting to identify element names. A console error will warn if spaces are missing.
762+
731763
### Variants & Theming
732764

733765
Create themed component variations:

src/tasty/tasty.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,53 @@ describe('tastyGlobal() API', () => {
12301230
);
12311231
});
12321232

1233+
it('should warn when combinator lacks spaces in selector affix ($)', () => {
1234+
const consoleErrorSpy = jest
1235+
.spyOn(console, 'error')
1236+
.mockImplementation(() => {});
1237+
1238+
const Component = tasty({
1239+
styles: {
1240+
Item: {
1241+
$: '>Body>Row',
1242+
color: '#primary',
1243+
},
1244+
},
1245+
});
1246+
1247+
render(<Component />);
1248+
1249+
expect(consoleErrorSpy).toHaveBeenCalledWith(
1250+
expect.stringContaining('[Tasty] Invalid selector affix ($) syntax'),
1251+
);
1252+
expect(consoleErrorSpy).toHaveBeenCalledWith(
1253+
expect.stringContaining('>Body>Row'),
1254+
);
1255+
1256+
consoleErrorSpy.mockRestore();
1257+
});
1258+
1259+
it('should not warn when combinator has proper spaces in selector affix ($)', () => {
1260+
const consoleErrorSpy = jest
1261+
.spyOn(console, 'error')
1262+
.mockImplementation(() => {});
1263+
1264+
const Component = tasty({
1265+
styles: {
1266+
Item: {
1267+
$: '> Body > Row',
1268+
color: '#primary',
1269+
},
1270+
},
1271+
});
1272+
1273+
render(<Component />);
1274+
1275+
expect(consoleErrorSpy).not.toHaveBeenCalled();
1276+
1277+
consoleErrorSpy.mockRestore();
1278+
});
1279+
12331280
it('should support multiple global style components with different selectors', () => {
12341281
const GlobalHeading = tasty('h1.special', {
12351282
preset: 'h1',

src/tasty/utils/renderStyles.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ function transformSelectorAffix(affix: string): string {
8585
const trimmed = affix.trim();
8686
if (!trimmed) return ' ';
8787

88+
// Validate that combinators have spaces around them
89+
// Check for capitalized words adjacent to combinators without spaces
90+
const invalidPattern = /[A-Z][a-z]*[>+~]|[>+~][A-Z][a-z]*/;
91+
if (invalidPattern.test(trimmed)) {
92+
console.error(
93+
`[Tasty] Invalid selector affix ($) syntax: "${affix}"\n` +
94+
`Combinators (>, +, ~) must have spaces around them when used with element names.\n` +
95+
`Example: Use "$: '> Body > Row'" instead of "$: '>Body>Row'"\n` +
96+
`This is a design choice: the parser uses simple whitespace splitting for performance.`,
97+
);
98+
}
99+
88100
const tokens = trimmed.split(/\s+/);
89101
const transformed = tokens.map((token) =>
90102
/^[A-Z]/.test(token) ? `[data-element="${token}"]` : token,

src/tasty/utils/styles.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ export function getModSelector(modName: string): string {
143143
// Check if it contains :has() with capitalized element names
144144
if (modName.includes(':has(')) {
145145
selector = modName.replace(/:has\(([^)]+)\)/g, (match, content) => {
146+
// Validate that combinators have spaces around them
147+
// Check for capitalized words adjacent to combinators without spaces
148+
const invalidPattern = /[A-Z][a-z]*[>+~]|[>+~][A-Z][a-z]*/;
149+
if (invalidPattern.test(content)) {
150+
console.error(
151+
`[Tasty] Invalid :has() selector syntax: "${modName}"\n` +
152+
`Combinators (>, +, ~) must have spaces around them when used with element names.\n` +
153+
`Example: Use ":has(Body > Row)" instead of ":has(Body>Row)"\n` +
154+
`This is a design choice: the parser uses simple whitespace splitting for performance.`,
155+
);
156+
}
157+
146158
// Transform capitalized words to [data-element="..."] selectors
147159
const tokens = content.split(/\s+/);
148160
const transformed = tokens.map((token: string) =>

src/tasty/value-mods.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,52 @@ describe('Value Mods', () => {
446446
':has([data-element="Body"] [data-element="Item"] [data-element="Row"])',
447447
);
448448
});
449+
450+
it('should warn when combinator lacks spaces in :has()', () => {
451+
const consoleErrorSpy = jest
452+
.spyOn(console, 'error')
453+
.mockImplementation(() => {});
454+
455+
// Test various invalid patterns
456+
getModSelector(':has(Body>Row)');
457+
expect(consoleErrorSpy).toHaveBeenCalledWith(
458+
expect.stringContaining('[Tasty] Invalid :has() selector syntax'),
459+
);
460+
expect(consoleErrorSpy).toHaveBeenCalledWith(
461+
expect.stringContaining(':has(Body>Row)'),
462+
);
463+
464+
consoleErrorSpy.mockClear();
465+
466+
getModSelector(':has(Header+Content)');
467+
expect(consoleErrorSpy).toHaveBeenCalledWith(
468+
expect.stringContaining('[Tasty] Invalid :has() selector syntax'),
469+
);
470+
471+
consoleErrorSpy.mockClear();
472+
473+
getModSelector(':has(List~Item)');
474+
expect(consoleErrorSpy).toHaveBeenCalledWith(
475+
expect.stringContaining('[Tasty] Invalid :has() selector syntax'),
476+
);
477+
478+
consoleErrorSpy.mockRestore();
479+
});
480+
481+
it('should not warn when combinator has proper spaces in :has()', () => {
482+
const consoleErrorSpy = jest
483+
.spyOn(console, 'error')
484+
.mockImplementation(() => {});
485+
486+
getModSelector(':has(Body > Row)');
487+
getModSelector(':has(Header + Content)');
488+
getModSelector(':has(List ~ Item)');
489+
getModSelector(':has(> Item)');
490+
491+
expect(consoleErrorSpy).not.toHaveBeenCalled();
492+
493+
consoleErrorSpy.mockRestore();
494+
});
449495
});
450496

451497
it('should transform :has(Item) to :has([data-element="Item"]) in component', () => {

0 commit comments

Comments
 (0)