Skip to content

Commit dc90b77

Browse files
committed
fix(tasty): selector logic
1 parent cd2b549 commit dc90b77

File tree

14 files changed

+385
-210
lines changed

14 files changed

+385
-210
lines changed

src/components/actions/Button/Button.docs.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ The `mods` prop accepts the following modifiers you can override:
5454
| disabled | `boolean` | Forces disabled appearance. |
5555
| loading | `boolean` | Displays loading spinner. |
5656
| selected | `boolean` | Displays selected state. |
57-
| with-icons | `boolean` | Indicates that the button contains at least one icon. |
57+
| has-icons | `boolean` | Indicates that the button contains at least one icon. |
5858
| single-icon | `boolean` | Icon-only button without text. |
5959

6060
## Variants

src/components/actions/Button/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export const Button = forwardRef(function Button(
272272
() => ({
273273
loading: isLoading,
274274
selected: isSelected,
275-
'with-icons': hasIcons,
275+
'has-icons': hasIcons,
276276
'left-icon': !!icon,
277277
'right-icon': !!rightIcon,
278278
'single-icon': singleIcon,

src/components/actions/ItemAction/ItemAction.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ const ItemActionElement = tasty({
8080
cursor: { '': 'pointer', disabled: 'default' },
8181
padding: {
8282
'': '0 $inline-padding',
83-
'with-icon': 0,
84-
'with-icon & with-label': '0 $inline-padding 0 0',
83+
'has-icon': 0,
84+
'has-icon & has-label': '0 $inline-padding 0 0',
8585
},
8686

8787
'$inline-padding': {
@@ -162,9 +162,9 @@ export const ItemAction = forwardRef(function ItemAction(
162162
checkbox: hasCheckbox,
163163
selected: isSelected,
164164
loading: isLoading,
165-
'with-label': !!children,
165+
'has-label': !!children,
166166
context: !!contextType,
167-
'with-icon': !!icon,
167+
'has-icon': !!icon,
168168
...mods,
169169
}),
170170
[hasCheckbox, isSelected, isLoading, children, contextType, mods],

src/components/content/CopyPasteBlock/CopyPasteBlock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const CopyButton = tasty(Button, {
104104
},
105105
radius: {
106106
'': '0 1r 1r 0',
107-
'multiline | with-scroll': '0 1r 0 0',
107+
'multiline | has-scroll': '0 1r 0 0',
108108
},
109109
height: 'auto',
110110
outline: false,

src/components/content/Item/Item.docs.mdx

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ A foundational component that provides a standardized layout and styling for ite
2525

2626
<Controls of={ItemStories.Default} />
2727

28-
### Content Properties
29-
30-
#### actions
31-
- **Type**: `ReactNode`
32-
- **Description**: Inline action buttons displayed on the right side of the item. Use `Item.Action` for consistent styling. Actions automatically inherit the parent's `type` prop and the component reserves space to prevent content overlap.
33-
3428
### Base Properties
3529

3630
Supports [Base properties](/BaseProperties)
@@ -48,26 +42,39 @@ Customizes the root element of the component.
4842
- `Description` - The secondary text below or inline with the main content
4943
- `Prefix` - Additional content displayed before the main content
5044
- `Suffix` - Additional content displayed after the main content
45+
- `Actions` - Container for inline action buttons displayed on the right side
5146

5247
### Style Properties
5348

54-
Direct style application without using the `styles` prop: `width`, `height`, `padding`, `margin`, `color`, `fill`, `opacity`, `display`, `position`, `zIndex`, `gap`, `flow`, `placeItems`, `placeContent`, `alignItems`, `justifyContent`, `border`, `radius`, `shadow`, `overflow`.
49+
Direct style application without using the `styles` prop: `width`, `height`, `padding`, `paddingInline`, `paddingBlock`, `margin`, `inset`, `color`, `fill`, `fade`, `opacity`, `display`, `font`, `preset`, `hide`, `whiteSpace`, `position`, `gridArea`, `order`, `gridColumn`, `gridRow`, `placeSelf`, `alignSelf`, `justifySelf`, `zIndex`, `gap`, `columnGap`, `rowGap`, `flow`, `placeItems`, `placeContent`, `alignItems`, `alignContent`, `justifyItems`, `justifyContent`, `align`, `justify`, `gridColumns`, `gridRows`, `gridTemplate`, `gridAreas`, `border`, `radius`, `shadow`, `overflow`, `scrollbar`, `outline`, `textAlign`, `reset`, `flexBasis`, `flexGrow`, `flexShrink`, `flex`, `fontWeight`, `fontStyle`, `textTransform`.
5550

5651
### Modifiers
5752

5853
The `mods` property accepts the following modifiers:
5954

6055
| Modifier | Type | Description |
6156
|----------|------|-------------|
57+
| has-icon | `boolean` | Applied when icon prop is provided |
58+
| has-right-icon | `boolean` | Applied when rightIcon prop is provided |
59+
| has-prefix | `boolean` | Applied when prefix prop is provided |
60+
| has-suffix | `boolean` | Applied when suffix prop is provided |
61+
| has-label | `boolean` | Applied when children or labelProps are provided |
62+
| has-start-content | `boolean` | Applied when icon or prefix is present |
63+
| has-end-content | `boolean` | Applied when rightIcon, suffix, or actions are present |
64+
| has-description | `boolean` | Applied when description is provided |
6265
| has-description-block | `boolean` | Applied when description placement is "block" |
66+
| has-actions | `boolean` | Applied when actions prop is provided |
6367
| has-actions-content | `boolean` | Applied when actions have actual content (not just placeholder) |
6468
| checkbox | `boolean` | Applied when using checkbox icon (icon="checkbox") |
6569
| selected | `boolean` | Applied when isSelected is true |
66-
| shape-card | `boolean` | Applied when shape is "card" |
67-
| shape-button | `boolean` | Applied when shape is "button" |
68-
| shape-sharp | `boolean` | Applied when shape is "sharp" |
69-
70-
**Note:** Element presence (Icon, RightIcon, Prefix, Suffix, Description, Actions) is now automatically detected via CSS `:has()` selectors and doesn't require manual modifiers.
70+
| disabled | `boolean` | Applied when isDisabled is true or when loading |
71+
| loading | `boolean` | Applied when isLoading is true |
72+
| card | `boolean` | Applied when isCard is true |
73+
| button | `boolean` | Applied when isButton is true |
74+
| size | `string` | Applied based on size prop value (xsmall, small, medium, large, xlarge, inline) |
75+
| type | `string` | Applied based on type prop value (item, primary, secondary, outline, neutral, clear, link) |
76+
| theme | `string` | Applied based on theme prop value (default, danger, success, special) |
77+
| shape | `string` | Applied based on shape prop value (card, button, sharp) |
7178

7279
## Variants
7380

@@ -155,6 +162,31 @@ The `mods` property accepts the following modifiers:
155162

156163
<Story of={ItemStories.DifferentShapes} />
157164

165+
### With Loading State
166+
167+
The `isLoading` prop displays a loading indicator and disables the item. The `loadingSlot` prop controls which slot the loading icon replaces:
168+
169+
```jsx
170+
<Item
171+
icon={<IconSave />}
172+
isLoading={true}
173+
loadingSlot="icon"
174+
>
175+
Saving...
176+
</Item>
177+
178+
<Item
179+
icon={<IconFile />}
180+
rightIcon={<IconDownload />}
181+
isLoading={true}
182+
loadingSlot="rightIcon"
183+
>
184+
Downloading...
185+
</Item>
186+
```
187+
188+
When `loadingSlot="auto"` (default), the loading icon intelligently selects the best slot: prefers `icon` if present, then `rightIcon`, or falls back to `icon`.
189+
158190
### With Actions
159191

160192
Item supports inline actions that appear on the right side. Use the `Item.Action` compound component for consistent styling:

src/components/content/Item/Item.tsx

Lines changed: 103 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -204,31 +204,15 @@ const ACTIONS_EVENT_HANDLERS = {
204204
};
205205

206206
const ItemElement = tasty({
207+
as: 'div',
207208
styles: {
208209
display: 'inline-grid',
209210
flow: 'column dense',
210211
gap: 0,
211212
outline: 0,
212213
placeItems: 'stretch',
213214
placeContent: 'stretch',
214-
gridColumns: {
215-
'': '1sf max-content max-content',
216-
':has(> Actions)': '1sf max-content max-content max-content',
217-
':has(> Icon) ^ :has(> Prefix)':
218-
'max-content 1sf max-content max-content',
219-
'(:has(> Icon) ^ :has(> Prefix)) & :has(> Actions)':
220-
'max-content 1sf max-content max-content max-content',
221-
':has(> Icon) & :has(> Prefix)':
222-
'max-content max-content 1sf max-content max-content',
223-
':has(> Icon) & :has(> Prefix) & :has(> Actions)':
224-
'max-content max-content 1sf max-content max-content max-content',
225-
'(:has(> Icon) ^ :has(> RightIcon)) & !:has(> Description) & !:has(> Prefix) & !:has(> Suffix) & !:has(> Label)':
226-
'max-content',
227-
},
228-
gridRows: {
229-
'': 'auto auto',
230-
'has-description-block': 'auto auto auto',
231-
},
215+
gridColumns: '($left-columns, ) ($label-column, ) ($right-columns, )',
232216
// Prevent items from shrinking inside vertical flex layouts (Menu, ListBox, etc)
233217
flexShrink: {
234218
'': 'initial',
@@ -239,9 +223,9 @@ const ItemElement = tasty({
239223
margin: 0,
240224
radius: {
241225
'': true,
242-
'shape-card': '1cr',
243-
'shape-button': true,
244-
'shape-sharp': '0',
226+
'shape=card': '1cr',
227+
'shape=button': true,
228+
'shape=sharp': '0',
245229
},
246230
height: {
247231
'': 'min $size',
@@ -294,6 +278,23 @@ const ItemElement = tasty({
294278
'size=xlarge': '$size-xl',
295279
'size=inline': '1lh',
296280
},
281+
'$label-column': {
282+
'': '1sf',
283+
'!has-label': '',
284+
},
285+
'$left-columns': {
286+
'': '',
287+
'has-icon ^ has-prefix': 'max-content',
288+
'has-icon & has-prefix': 'max-content max-content',
289+
},
290+
'$right-columns': {
291+
'': '',
292+
'has-right-icon ^ has-suffix ^ has-actions': 'max-content',
293+
'(has-right-icon & has-suffix) | (has-right-icon & has-actions) | (has-suffix & has-actions)':
294+
'max-content max-content',
295+
'has-right-icon & has-suffix & has-actions':
296+
'max-content max-content max-content max-content',
297+
},
297298
'$inline-padding': {
298299
'': 'max($min-inline-padding, (($size - 1lh - 2bw) / 2 + $inline-compensation))',
299300
'size=inline': '.25x',
@@ -306,6 +307,37 @@ const ItemElement = tasty({
306307
'$inline-compensation': '.5x',
307308
'$min-inline-padding': '(1x - 1bw)',
308309

310+
'$label-padding-left': {
311+
'': '$inline-padding',
312+
'has-start-content': '0',
313+
},
314+
'$label-padding-right': {
315+
'': '$inline-padding',
316+
'has-end-content': '0',
317+
},
318+
'$label-padding-bottom': {
319+
'': '$block-padding',
320+
'has-description & !has-description-block': '0',
321+
},
322+
'$description-padding-left': {
323+
'': '$inline-padding',
324+
'has-start-content': 0,
325+
'has-description-block': '($inline-padding - $inline-compensation + 1bw)',
326+
'has-description-block & !has-start-content': '$inline-padding',
327+
},
328+
'$description-padding-right': {
329+
'': '$inline-padding',
330+
'has-end-content': 0,
331+
'has-description-block': '($inline-padding - $inline-compensation + 1bw)',
332+
'has-description-block & !has-end-content': '$inline-padding',
333+
},
334+
'$description-padding-bottom': {
335+
'': '$block-padding',
336+
'has-description-block': '$bottom-padding',
337+
},
338+
'$bottom-padding':
339+
'max($block-padding, (($size - 4x) / 2) + $block-padding)',
340+
309341
Icon: DEFAULT_ICON_STYLES,
310342

311343
RightIcon: DEFAULT_ICON_STYLES,
@@ -320,28 +352,13 @@ const ItemElement = tasty({
320352
overflow: 'hidden',
321353
textOverflow: 'ellipsis',
322354
maxWidth: '100%',
323-
padding: {
324-
'': '$block-padding $inline-padding',
325-
':has(> Icon) | :has(> Prefix)':
326-
'$block-padding $inline-padding $block-padding 0',
327-
':has(> RightIcon) | :has(> Suffix) | :has(> Actions)':
328-
'$block-padding 0 $block-padding $inline-padding',
329-
'(:has(> Icon) | :has(> Prefix)) & (:has(> RightIcon) | :has(> Suffix) | :has(> Actions))':
330-
'$block-padding 0',
331-
':has(> Description) & !has-description-block':
332-
'$block-padding $inline-padding 0 $inline-padding',
333-
':has(> Description) & !has-description-block & (:has(> Icon) | :has(> Prefix))':
334-
'$block-padding $inline-padding 0 0',
335-
':has(> Description) & !has-description-block & (:has(> RightIcon) | :has(> Suffix) | :has(> Actions))':
336-
'$block-padding 0 0 $inline-padding',
337-
':has(> Description) & !has-description-block & (:has(> Icon) | :has(> Prefix)) & (:has(> RightIcon) | :has(> Suffix) | :has(> Actions))':
338-
'$block-padding 0 0 0',
339-
},
340355
gridRow: {
341356
'': 'span 2',
342-
':has(> Description)': 'span 1',
357+
'has-description': 'span 1',
343358
'has-description-block': 'span 2',
344359
},
360+
padding:
361+
'$block-padding $label-padding-right $label-padding-bottom $label-padding-left',
345362
},
346363

347364
Description: {
@@ -362,40 +379,23 @@ const ItemElement = tasty({
362379
'': 'span 1',
363380
'has-description-block': '1 / -1',
364381
},
365-
padding: {
366-
'': '0 $inline-padding $block-padding $inline-padding',
367-
':has(> Icon) | :has(> Prefix)': '0 $inline-padding $block-padding 0',
368-
':has(> RightIcon) | :has(> Suffix)':
369-
'0 0 $block-padding $inline-padding',
370-
'(:has(> Icon) | :has(> Prefix)) & (:has(> RightIcon) | :has(> Suffix))':
371-
'0 0 $block-padding 0',
372-
'has-description-block':
373-
'0 ($inline-padding - $inline-compensation + 1bw) $bottom-padding ($inline-padding - $inline-compensation + 1bw)',
374-
'has-description-block & !:has(> Icon)':
375-
'0 ($inline-padding - $inline-compensation + 1bw) $bottom-padding $inline-padding',
376-
'has-description-block & !:has(> RightIcon)':
377-
'0 $inline-padding $bottom-padding ($inline-padding - $inline-compensation + 1bw)',
378-
'has-description-block & !:has(> RightIcon) & !:has(> Icon)':
379-
'0 $inline-padding $bottom-padding $inline-padding',
380-
},
381-
382-
'$bottom-padding':
383-
'max($block-padding, (($size - 4x) / 2) + $block-padding)',
382+
padding:
383+
'0 $description-padding-right $description-padding-bottom $description-padding-left',
384384
},
385385

386386
Prefix: {
387387
...ADDITION_STYLES,
388388
padding: {
389389
'': '$inline-padding left',
390-
':has(> Icon)': 0,
390+
'has-icon': 0,
391391
},
392392
},
393393

394394
Suffix: {
395395
...ADDITION_STYLES,
396396
padding: {
397397
'': '$inline-padding right',
398-
':has(> RightIcon)': 0,
398+
'has-right-icon': 0,
399399
},
400400
},
401401

@@ -780,23 +780,51 @@ const Item = <T extends HTMLElement = HTMLDivElement>(
780780
[hotkeys, finalIsDisabled],
781781
);
782782

783-
mods = {
784-
'has-description-block':
785-
showDescriptions && descriptionPlacement === 'block',
786-
'has-actions-content': !!(actions && actions !== true),
787-
checkbox: hasCheckbox,
788-
disabled: finalIsDisabled,
789-
selected: isSelected === true,
790-
loading: isLoading,
791-
card: isCard === true,
792-
button: isButton === true,
793-
[`shape-${shape}`]: true,
794-
// Use value mods for data-* attributes
795-
size: typeof size === 'number' ? undefined : size,
783+
mods = useMemo(() => {
784+
return {
785+
'has-icon': !!finalIcon,
786+
'has-start-content': !!(finalIcon || finalPrefix),
787+
'has-end-content': !!(finalRightIcon || finalSuffix || actions),
788+
'has-right-icon': !!finalRightIcon,
789+
'has-label': !!(children || labelProps),
790+
'has-prefix': !!finalPrefix,
791+
'has-suffix': !!finalSuffix,
792+
'has-description': showDescriptions,
793+
'has-description-block':
794+
showDescriptions && descriptionPlacement === 'block',
795+
'has-actions': !!actions,
796+
'has-actions-content': !!(actions && actions !== true),
797+
checkbox: hasCheckbox,
798+
disabled: finalIsDisabled,
799+
selected: isSelected === true,
800+
loading: isLoading,
801+
card: isCard === true,
802+
button: isButton === true,
803+
...(typeof size === 'number' ? {} : { size }),
804+
type,
805+
theme,
806+
shape,
807+
...mods,
808+
};
809+
}, [
810+
finalIcon,
811+
finalRightIcon,
812+
finalPrefix,
813+
finalSuffix,
814+
showDescriptions,
815+
descriptionPlacement,
816+
hasCheckbox,
817+
isSelected,
818+
isLoading,
819+
isCard,
820+
isButton,
821+
shape,
822+
actions,
823+
size,
796824
type,
797825
theme,
798-
...mods,
799-
};
826+
mods,
827+
]);
800828

801829
const {
802830
labelProps: finalLabelProps,

0 commit comments

Comments
 (0)