Skip to content

Commit b27ab4b

Browse files
authored
feat(tasty): selector affix for sub-elements (#858)
1 parent f6db220 commit b27ab4b

File tree

4 files changed

+120
-3
lines changed

4 files changed

+120
-3
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
Enhanced selector affix syntax (`$`) for sub-element styling in tasty. Capitalized words in the affix are now automatically transformed to sub-element selectors, allowing complex selector chains like `$: '> Body > Row >'` which generates `.table > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]`.
6+

src/stories/Tasty.docs.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,33 @@ const Card = tasty({
567567
</Card>
568568
```
569569

570+
#### Selector Affix (`$`)
571+
572+
Control sub-element selector combinator using the `$` property:
573+
574+
```jsx
575+
const Table = tasty({
576+
styles: {
577+
// Direct child selector
578+
Row: {
579+
$: '>', // .table > [data-element="Row"]
580+
padding: '1x',
581+
},
582+
583+
// Chained selectors (auto-transforms capitalized words)
584+
Cell: {
585+
$: '> Body > Row >', // .table > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]
586+
border: '1bw solid #border',
587+
},
588+
589+
// Default: descendant selector (space)
590+
Text: {
591+
color: '#text', // .table [data-element="Text"]
592+
},
593+
},
594+
});
595+
```
596+
570597
### Variants & Theming
571598

572599
Create themed component variations:

src/tasty/tasty.test.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,66 @@ describe('tastyGlobal() API', () => {
11701170
expect(styleContent).not.toMatch(/>\s*\[data-element="Content"\]/);
11711171
});
11721172

1173+
it('should support empty affix for sub-elements with $: ""', () => {
1174+
const Card = tasty({
1175+
styles: {
1176+
padding: '2x',
1177+
Content: {
1178+
$: '',
1179+
color: '#primary',
1180+
},
1181+
},
1182+
});
1183+
1184+
render(
1185+
<Card>
1186+
<div data-element="Content">Content text</div>
1187+
</Card>,
1188+
);
1189+
1190+
const styleElements = document.head.querySelectorAll('[data-tasty]');
1191+
const styleContent = Array.from(styleElements)
1192+
.map((el) => el.textContent)
1193+
.join('');
1194+
1195+
// Empty affix should result in descendant selector (space only)
1196+
expect(styleContent).toMatch(/\s+\[data-element="Content"\]/);
1197+
expect(styleContent).not.toMatch(/>\s*\[data-element="Content"\]/);
1198+
});
1199+
1200+
it('should support complex selector chain with $: "> Body > Row >"', () => {
1201+
const Table = tasty({
1202+
styles: {
1203+
display: 'table',
1204+
Cell: {
1205+
$: '> Body > Row >',
1206+
padding: '1x',
1207+
border: '1px solid #border',
1208+
},
1209+
},
1210+
});
1211+
1212+
render(
1213+
<Table>
1214+
<div data-element="Body">
1215+
<div data-element="Row">
1216+
<div data-element="Cell">Cell content</div>
1217+
</div>
1218+
</div>
1219+
</Table>,
1220+
);
1221+
1222+
const styleElements = document.head.querySelectorAll('[data-tasty]');
1223+
const styleContent = Array.from(styleElements)
1224+
.map((el) => el.textContent)
1225+
.join('');
1226+
1227+
// Should transform to: > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]
1228+
expect(styleContent).toMatch(
1229+
/>\s*\[data-element="Body"\]\s*>\s*\[data-element="Row"\]\s*>\s*\[data-element="Cell"\]/,
1230+
);
1231+
});
1232+
11731233
it('should support multiple global style components with different selectors', () => {
11741234
const GlobalHeading = tasty('h1.special', {
11751235
preset: 'h1',

src/tasty/utils/renderStyles.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ export function isSelector(key: string): boolean {
7272
);
7373
}
7474

75+
/**
76+
* Transform selector affix by converting capitalized words to sub-element selectors.
77+
* Returns a selector prefix with leading and trailing spaces.
78+
*
79+
* Examples:
80+
* '> Body > Row' -> ' > [data-element="Body"] > [data-element="Row"] '
81+
* '> Body > Row >' -> ' > [data-element="Body"] > [data-element="Row"] > '
82+
* '>' -> ' > '
83+
*/
84+
function transformSelectorAffix(affix: string): string {
85+
const trimmed = affix.trim();
86+
if (!trimmed) return ' ';
87+
88+
const tokens = trimmed.split(/\s+/);
89+
const transformed = tokens.map((token) =>
90+
/^[A-Z]/.test(token) ? `[data-element="${token}"]` : token,
91+
);
92+
93+
return ` ${transformed.join(' ')} `;
94+
}
95+
7596
/**
7697
* Get the selector suffix for a key
7798
*/
@@ -85,9 +106,12 @@ function getSelector(key: string, styles?: Styles): string | null {
85106
}
86107

87108
if (key.match(/^[A-Z]/)) {
88-
// Check if styles object has $: '>' for direct child selector
89-
const combinator = styles && (styles as any).$ === '>' ? ' > ' : ' ';
90-
return `${combinator}[data-element="${key}"]`;
109+
const affix = styles?.$;
110+
if (affix !== undefined) {
111+
const prefix = transformSelectorAffix(String(affix));
112+
return `${prefix}[data-element="${key}"]`;
113+
}
114+
return ` [data-element="${key}"]`;
91115
}
92116

93117
return null;

0 commit comments

Comments
 (0)