@@ -27,7 +27,6 @@ import {ImageContext} from './Image';
2727import { ImageCoordinator } from './ImageCoordinator' ;
2828import { lightDark , size , style } from '../style/spectrum-theme' with { type : 'macro' } ;
2929import { mergeStyles } from '../style/runtime' ;
30- import { PressResponder } from '@react-aria/interactions' ;
3130import { pressScale } from './pressScale' ;
3231import { SkeletonContext , SkeletonWrapper , useIsSkeleton } from './Skeleton' ;
3332import { useDOMRef } from '@react-spectrum/utils' ;
@@ -135,7 +134,7 @@ let card = style({
135134 compact : {
136135 size : {
137136 XS : size ( 6 ) ,
138- S : size ( 10 ) ,
137+ S : 8 ,
139138 M : 12 ,
140139 L : 16 ,
141140 XL : 20
@@ -207,11 +206,16 @@ let selectionIndicator = style({
207206 opacity : {
208207 default : 0 ,
209208 isSelected : 1
210- }
211- // outlineColor: 'white',
212- // outlineOffset: -4,
213- // outlineStyle: 'solid',
214- // outlineWidth: 2
209+ } ,
210+ // Quiet cards with no checkbox have an extra inner stroke
211+ // to distinguish the selection indicator from the preview.
212+ outlineColor : 'gray-25' ,
213+ outlineOffset : - 4 ,
214+ outlineStyle : {
215+ default : 'none' ,
216+ isStrokeInner : 'solid'
217+ } ,
218+ outlineWidth : 2
215219} ) ;
216220
217221let preview = style ( {
@@ -261,7 +265,8 @@ let title = style({
261265 XL : 'title-lg'
262266 }
263267 } ,
264- lineClamp : 3
268+ lineClamp : 3 ,
269+ gridArea : 'title'
265270} ) ;
266271
267272let description = style ( {
@@ -276,23 +281,38 @@ let description = style({
276281 }
277282 } ,
278283 lineClamp : 3 ,
279- gridColumnEnd : 'span 2 '
284+ gridArea : 'description '
280285} ) ;
281286
282287let content = style ( {
283288 display : 'grid' ,
284289 // By default, all elements are displayed in a stack.
285290 // If an action menu is present, place it next to the title.
286291 gridTemplateColumns : {
292+ default : [ '1fr' ] ,
287293 ':has([data-slot=menu])' : [ 'minmax(0, 1fr)' , 'auto' ]
288294 } ,
295+ gridTemplateAreas : {
296+ default : [
297+ 'title' ,
298+ 'description'
299+ ] ,
300+ ':has([data-slot=menu])' : [
301+ 'title menu' ,
302+ 'description description'
303+ ]
304+ } ,
289305 columnGap : 4 ,
306+ flexGrow : 1 ,
290307 alignItems : 'baseline' ,
291308 alignContent : 'space-between' ,
292309 rowGap : {
293- default : 8 ,
294310 size : {
295- XS : 4
311+ XS : 4 ,
312+ S : 4 ,
313+ M : size ( 6 ) ,
314+ L : size ( 6 ) ,
315+ XL : 8
296316 }
297317 } ,
298318 paddingTop : {
@@ -322,15 +342,19 @@ interface InternalCardContextValue {
322342 size : 'XS' | 'S' | 'M' | 'L' | 'XL' ,
323343 isSelected : boolean ,
324344 isHovered : boolean ,
325- isFocusVisible : boolean
345+ isFocusVisible : boolean ,
346+ isPressed : boolean ,
347+ isCheckboxSelection : boolean
326348}
327349
328350const InternalCardContext = createContext < InternalCardContextValue > ( {
329351 isQuiet : false ,
330352 size : 'M' ,
331353 isSelected : false ,
332354 isHovered : false ,
333- isFocusVisible : false
355+ isFocusVisible : false ,
356+ isPressed : false ,
357+ isCheckboxSelection : true
334358} ) ;
335359
336360const actionButtonSize = {
@@ -366,7 +390,8 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef<HTMLD
366390 size : actionButtonSize [ size ] ,
367391 isDisabled : isSkeleton ,
368392 // @ts -ignore
369- 'data-slot' : 'menu'
393+ 'data-slot' : 'menu' ,
394+ styles : style ( { gridArea : 'menu' } )
370395 } ] ,
371396 [ SkeletonContext , isSkeleton ]
372397 ] } >
@@ -387,7 +412,7 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef<HTMLD
387412 ref = { domRef }
388413 className = { UNSAFE_className + card ( { size, density, variant, isCardView : ElementType !== 'div' } , styles ) }
389414 style = { UNSAFE_style } >
390- < InternalCardContext . Provider value = { { size, isQuiet, isHovered : false , isFocusVisible : false , isSelected : false } } >
415+ < InternalCardContext . Provider value = { { size, isQuiet, isCheckboxSelection : false , isHovered : false , isFocusVisible : false , isSelected : false , isPressed : false } } >
391416 { children }
392417 </ InternalCardContext . Provider >
393418 </ div >
@@ -401,36 +426,16 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef<HTMLD
401426 ref = { domRef }
402427 className = { renderProps => UNSAFE_className + card ( { ...renderProps , isCardView : true , isLink : ! ! props . href , size, density, variant} , styles ) }
403428 style = { renderProps =>
404- // Only apply press scaling to card when it has an action, not selection.
405- // When clicking the card selects it, the checkbox will scale down instead.
406- // TODO: confirm with design
407- // @ts -ignore - do we want to expose hasAction publically in RAC?
408- renderProps . hasAction && ( renderProps . selectionMode === 'none' || renderProps . selectionBehavior === 'toggle' )
409- ? press ( renderProps )
410- : UNSAFE_style
429+ // Only the preview in quiet cards scales down on press
430+ variant === 'quiet' ? UNSAFE_style : press ( renderProps )
411431 } >
412432 { ( { selectionMode, selectionBehavior, isHovered, isFocusVisible, isSelected, isPressed} ) => (
413- < InternalCardContext . Provider value = { { size, isQuiet, isHovered, isFocusVisible, isSelected} } >
433+ < InternalCardContext . Provider value = { { size, isQuiet, isCheckboxSelection : selectionMode !== 'none' && selectionBehavior === 'toggle' , isHovered, isFocusVisible, isSelected, isPressed} } >
434+ { /* Selection indicator and checkbox move inside the preview for quiet cards */ }
414435 { ! isQuiet && < SelectionIndicator /> }
415- { selectionMode !== 'none' && selectionBehavior === 'toggle' && (
416- < PressResponder isPressed = { isPressed } >
417- < div
418- className = { style ( {
419- position : 'absolute' ,
420- top : 8 ,
421- zIndex : 2 ,
422- insetStart : 8 ,
423- padding : '[6px]' ,
424- backgroundColor : lightDark ( 'transparent-white-600' , 'transparent-black-600' ) ,
425- borderRadius : 'default' ,
426- boxShadow : 'emphasized'
427- } ) } >
428- < Checkbox
429- slot = "selection"
430- excludeFromTabOrder />
431- </ div >
432- </ PressResponder >
433- ) }
436+ { ! isQuiet && selectionMode !== 'none' && selectionBehavior === 'toggle' &&
437+ < CardCheckbox />
438+ }
434439 { /* this makes the :first-child selector work even with the checkbox */ }
435440 < div className = { style ( { display : 'contents' } ) } >
436441 { children }
@@ -442,16 +447,47 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef<HTMLD
442447} ) ;
443448
444449function SelectionIndicator ( ) {
445- let { isSelected} = useContext ( InternalCardContext ) ;
446- return < div className = { selectionIndicator ( { isSelected} ) } /> ;
450+ let { size, isSelected, isQuiet, isCheckboxSelection} = useContext ( InternalCardContext ) ;
451+ return (
452+ < div
453+ className = { selectionIndicator ( {
454+ size,
455+ isSelected,
456+ // Add an inner stroke only for quiet cards with no checkbox to
457+ // help distinguish the selected state from the preview.
458+ isStrokeInner : isQuiet && ! isCheckboxSelection
459+ } ) } />
460+ ) ;
461+ }
462+
463+ function CardCheckbox ( ) {
464+ let { size} = useContext ( InternalCardContext ) ;
465+ return (
466+ < div
467+ className = { style ( {
468+ position : 'absolute' ,
469+ top : '--card-spacing' ,
470+ insetStart : '--card-spacing' ,
471+ zIndex : 2 ,
472+ padding : 4 ,
473+ backgroundColor : lightDark ( 'transparent-white-600' , 'transparent-black-600' ) ,
474+ borderRadius : 'default' ,
475+ boxShadow : 'emphasized'
476+ } ) } >
477+ < Checkbox
478+ slot = "selection"
479+ excludeFromTabOrder
480+ size = { size === 'XS' ? 'S' : size } />
481+ </ div >
482+ ) ;
447483}
448484
449485export interface CardPreviewProps extends UnsafeStyles , DOMProps {
450486 children : ReactNode
451487}
452488
453489export const CardPreview = forwardRef ( function CardPreview ( props : CardPreviewProps , ref : DOMRef < HTMLDivElement > ) {
454- let { size, isQuiet, isHovered, isFocusVisible, isSelected} = useContext ( InternalCardContext ) ;
490+ let { size, isQuiet, isHovered, isFocusVisible, isSelected, isPressed , isCheckboxSelection } = useContext ( InternalCardContext ) ;
455491 let { UNSAFE_className, UNSAFE_style} = props ;
456492 let domRef = useDOMRef ( ref ) ;
457493 return (
@@ -460,8 +496,9 @@ export const CardPreview = forwardRef(function CardPreview(props: CardPreviewPro
460496 slot = "preview"
461497 ref = { domRef }
462498 className = { UNSAFE_className + preview ( { size, isQuiet, isHovered, isFocusVisible, isSelected} ) }
463- style = { UNSAFE_style } >
499+ style = { isQuiet ? pressScale ( domRef ) ( { isPressed } ) : UNSAFE_style } >
464500 { isQuiet && < SelectionIndicator /> }
501+ { isQuiet && isCheckboxSelection && < CardCheckbox /> }
465502 < div className = { style ( { borderRadius : '[inherit]' , overflow : 'clip' } ) } >
466503 { props . children }
467504 </ div >
@@ -608,10 +645,10 @@ export const UserCard = forwardRef(function UserCard(props: CardProps, ref: DOMR
608645
609646const buttonSize = {
610647 XS : 'S' ,
611- S : 'M ' ,
648+ S : 'S ' ,
612649 M : 'M' ,
613- L : 'M ' ,
614- XL : 'L '
650+ L : 'L ' ,
651+ XL : 'XL '
615652} as const ;
616653
617654export interface ProductCardProps extends Omit < CardProps , 'density' | 'variant' > {
0 commit comments