Skip to content

Commit 7d5d483

Browse files
committed
feat(tasty): sub-element selector
1 parent 35960b4 commit 7d5d483

File tree

7 files changed

+61
-5
lines changed

7 files changed

+61
-5
lines changed

.changeset/witty-dolls-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
A new property for sub-element styles `$`. Set `$: '>'` for sub-elements styles so they will only apply to the direct child of the root element.

src/components/actions/ItemAction/ItemAction.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,13 @@ const ItemActionElement = tasty({
8181
'$local-icon-size': 'var(--icon-size)',
8282

8383
Icon: {
84+
$: '>',
8485
...(ITEM_ACTION_BASE_STYLES.Icon as Styles),
8586
'$icon-size': 'min($local-icon-size, ($action-size - .25x))',
8687
},
8788

8889
RightIcon: {
90+
$: '>',
8991
...(ITEM_ACTION_BASE_STYLES.Icon as Styles),
9092
'$icon-size': 'min($local-icon-size, ($action-size - .25x))',
9193
},

src/components/actions/ItemButton/ItemButton.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ const ActionsWrapper = tasty({
5959
'[data-size="xlarge"]': '$size-xl',
6060
},
6161

62-
'& > [data-element="Actions"]': {
62+
Actions: {
63+
$: '>',
6364
position: 'absolute',
6465
inset: '1bw 1bw auto auto',
6566
display: 'flex',

src/components/content/Item/Item.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export interface CubeItemProps extends BaseProps, ContainerStyleProps {
154154
}
155155

156156
const DEFAULT_ICON_STYLES: Styles = {
157+
$: '>',
157158
display: 'grid',
158159
placeItems: 'center',
159160
placeContent: 'stretch',
@@ -169,6 +170,7 @@ const DEFAULT_ICON_STYLES: Styles = {
169170
};
170171

171172
const ADDITION_STYLES: Styles = {
173+
$: '>',
172174
display: 'grid',
173175
flow: 'column',
174176
placeItems: 'center',
@@ -289,6 +291,7 @@ const ItemElement = tasty({
289291
RightIcon: DEFAULT_ICON_STYLES,
290292

291293
Label: {
294+
$: '>',
292295
display: 'block',
293296
placeSelf: 'center start',
294297
boxSizing: 'border-box',
@@ -322,6 +325,7 @@ const ItemElement = tasty({
322325
},
323326

324327
Description: {
328+
$: '>',
325329
preset: 't4',
326330
color: 'inherit',
327331
opacity: 0.75,
@@ -375,6 +379,7 @@ const ItemElement = tasty({
375379
},
376380

377381
Actions: {
382+
$: '>',
378383
display: 'flex',
379384
gap: '1bw',
380385
placeItems: 'center',

src/data/item-themes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const ITEM_ACTION_BASE_STYLES: Styles = {
4444

4545
// Icon styles
4646
Icon: {
47+
$: '>',
4748
display: 'grid',
4849
placeItems: 'center',
4950
height: '($action-size - 2bw) ($action-size - 2bw)',

src/tasty/tasty.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,44 @@ describe('tastyGlobal() API', () => {
11321132
expect(styleContent).toContain('background-color: var(--surface-color)');
11331133
});
11341134

1135+
it('should support direct child selector for sub-elements with $: ">"', () => {
1136+
const Card = tasty({
1137+
styles: {
1138+
padding: '2x',
1139+
Actions: {
1140+
$: '>',
1141+
display: 'flex',
1142+
gap: '1x',
1143+
},
1144+
Content: {
1145+
color: '#text',
1146+
},
1147+
},
1148+
});
1149+
1150+
const { container } = render(
1151+
<Card>
1152+
<div data-element="Actions">
1153+
<button>Action 1</button>
1154+
</div>
1155+
<div data-element="Content">Content text</div>
1156+
</Card>,
1157+
);
1158+
1159+
// Verify direct child selector is used for Actions
1160+
const styleElements = document.head.querySelectorAll('[data-tasty]');
1161+
const styleContent = Array.from(styleElements)
1162+
.map((el) => el.textContent)
1163+
.join('');
1164+
1165+
// Should have direct child selector for Actions
1166+
expect(styleContent).toMatch(/>\s*\[data-element="Actions"\]/);
1167+
// Should have descendant selector for Content (backward compatibility)
1168+
expect(styleContent).toMatch(/\s+\[data-element="Content"\]/);
1169+
// Should not match "> [data-element="Content"]"
1170+
expect(styleContent).not.toMatch(/>\s*\[data-element="Content"\]/);
1171+
});
1172+
11351173
it('should support multiple global style components with different selectors', () => {
11361174
const GlobalHeading = tasty('h1.special', {
11371175
preset: 'h1',

src/tasty/utils/renderStyles.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function isSelector(key: string): boolean {
7575
/**
7676
* Get the selector suffix for a key
7777
*/
78-
function getSelector(key: string): string | null {
78+
function getSelector(key: string, styles?: Styles): string | null {
7979
if (key.startsWith('&')) {
8080
return key.slice(1);
8181
}
@@ -85,7 +85,9 @@ function getSelector(key: string): string | null {
8585
}
8686

8787
if (key.match(/^[A-Z]/)) {
88-
return ` [data-element="${key}"]`;
88+
// Check if styles object has $: '>' for direct child selector
89+
const combinator = styles && (styles as any).$ === '>' ? ' > ' : ' ';
90+
return `${combinator}[data-element="${key}"]`;
8991
}
9092

9193
return null;
@@ -613,9 +615,11 @@ function generateLogicalRules(
613615

614616
// Recurse into nested selectors first to compute proper suffix chaining
615617
for (const key of selectorKeys) {
616-
const suffix = getSelector(key);
618+
const suffix = getSelector(key, currentStyles[key] as Styles);
617619
if (suffix && currentStyles[key]) {
618-
processStyles(currentStyles[key] as Styles, `${parentSuffix}${suffix}`);
620+
// Remove $ key to prevent it from being processed as a style property
621+
const { $: _omit, ...cleanedStyles } = currentStyles[key] as any;
622+
processStyles(cleanedStyles as Styles, `${parentSuffix}${suffix}`);
619623
}
620624
}
621625

0 commit comments

Comments
 (0)