Skip to content

Commit 1bff41b

Browse files
committed
Design updates
1 parent c3efc31 commit 1bff41b

File tree

2 files changed

+88
-51
lines changed

2 files changed

+88
-51
lines changed

packages/@react-spectrum/s2/src/Card.tsx

Lines changed: 86 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {ImageContext} from './Image';
2727
import {ImageCoordinator} from './ImageCoordinator';
2828
import {lightDark, size, style} from '../style/spectrum-theme' with {type: 'macro'};
2929
import {mergeStyles} from '../style/runtime';
30-
import {PressResponder} from '@react-aria/interactions';
3130
import {pressScale} from './pressScale';
3231
import {SkeletonContext, SkeletonWrapper, useIsSkeleton} from './Skeleton';
3332
import {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

217221
let 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

267272
let description = style({
@@ -276,23 +281,38 @@ let description = style({
276281
}
277282
},
278283
lineClamp: 3,
279-
gridColumnEnd: 'span 2'
284+
gridArea: 'description'
280285
});
281286

282287
let 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

328350
const 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

336360
const 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

444449
function 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

449485
export interface CardPreviewProps extends UnsafeStyles, DOMProps {
450486
children: ReactNode
451487
}
452488

453489
export 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

609646
const 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

617654
export interface ProductCardProps extends Omit<CardProps, 'density' | 'variant'> {

packages/@react-spectrum/s2/stories/CardView.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function PhotoCard({item, layout}: {item: Item, layout: string}) {
8282
<ActionMenu>
8383
<MenuItem>Test</MenuItem>
8484
</ActionMenu>
85-
<div className={style({display: 'flex', alignItems: 'center', gap: 8, gridColumnEnd: 'span 2'})}>
85+
<div className={style({display: 'flex', alignItems: 'center', gap: 8, gridArea: 'description'})}>
8686
<Avatar src={item.user.profile_image.small} />
8787
<Text slot="description">{item.user.name}</Text>
8888
</div>
@@ -183,7 +183,7 @@ function TopicCard({topic}: {topic: Topic}) {
183183
</CollectionCardPreview>
184184
<Content>
185185
<Text slot="title">{topic.title}</Text>
186-
<div className={style({gridColumnEnd: 'span 2', display: 'flex', alignItems: 'center', gap: 8})}>
186+
<div className={style({display: 'flex', alignItems: 'center', gap: 8})}>
187187
<Folder />
188188
<Text slot="description">{topic.total_photos.toLocaleString()} photos</Text>
189189
</div>

0 commit comments

Comments
 (0)