Skip to content

Commit b27e449

Browse files
devongovettjluyausnowystingeryihuiliaoktabors
authored
S2 AvatarGroup (#6781)
* Add Spectrum 2 docs to storybook * add avatar group * lint * updates * feedback updates * more feedback changes * properly forward ref and add aria label interface * Move stroke to Avatar and switch to pixel-based sizing * Fix TS * Add support for aria labeling --------- Co-authored-by: Jeff Luyau <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Yihui Liao <[email protected]> Co-authored-by: Kyle Taborski <[email protected]>
1 parent a18bcb3 commit b27e449

File tree

16 files changed

+305
-99
lines changed

16 files changed

+305
-99
lines changed

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

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,45 @@ import {ContextValue} from 'react-aria-components';
1414
import {createContext, forwardRef} from 'react';
1515
import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared';
1616
import {filterDOMProps} from '@react-aria/utils';
17-
import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
17+
import {getAllowedOverrides, StylesPropWithoutWidth, UnsafeStyles} from './style-utils' with {type: 'macro'};
1818
import {style} from '../style/spectrum-theme' with { type: 'macro' };
1919
import {useDOMRef} from '@react-spectrum/utils';
2020
import {useSpectrumContextProps} from './useSpectrumContextProps';
2121

22-
export interface AvatarProps extends StyleProps, DOMProps {
23-
/** Text description of the avatar. */
24-
alt?: string,
25-
/** The image URL for the avatar. */
26-
src?: string
27-
}
28-
29-
export interface AvatarContextProps extends UnsafeStyles, DOMProps {
22+
export interface AvatarProps extends UnsafeStyles, DOMProps {
3023
/** Text description of the avatar. */
3124
alt?: string,
3225
/** The image URL for the avatar. */
3326
src?: string,
3427
/** Spectrum-defined styles, returned by the `style()` macro. */
35-
styles?: StylesPropWithHeight
28+
styles?: StylesPropWithoutWidth,
29+
/**
30+
* The size of the avatar.
31+
* @default 24
32+
*/
33+
size?: 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 56 | 64 | 80 | 96 | 112 | (number & {}),
34+
/** Whether the avatar is over a color background. */
35+
isOverBackground?: boolean
3636
}
3737

3838
const imageStyles = style({
3939
borderRadius: 'full',
4040
size: 20,
41-
disableTapHighlight: true
42-
}, getAllowedOverrides({height: true}));
41+
flexShrink: 0,
42+
flexGrow: 0,
43+
disableTapHighlight: true,
44+
outlineStyle: {
45+
default: 'none',
46+
isOverBackground: 'solid'
47+
},
48+
outlineColor: '--s2-container-bg',
49+
outlineWidth: {
50+
default: 1,
51+
isLarge: 2
52+
}
53+
}, getAllowedOverrides({width: false}));
4354

44-
export const AvatarContext = createContext<ContextValue<AvatarContextProps, DOMRefValue<HTMLImageElement>>>(null);
55+
export const AvatarContext = createContext<ContextValue<AvatarProps, DOMRefValue<HTMLImageElement>>>(null);
4556

4657
function Avatar(props: AvatarProps, ref: DOMRef<HTMLImageElement>) {
4758
[props, ref] = useSpectrumContextProps(props, ref, AvatarContext);
@@ -51,17 +62,25 @@ function Avatar(props: AvatarProps, ref: DOMRef<HTMLImageElement>) {
5162
src,
5263
UNSAFE_style,
5364
UNSAFE_className = '',
65+
size,
66+
isOverBackground,
5467
...otherProps
5568
} = props;
5669
const domProps = filterDOMProps(otherProps);
5770

71+
let remSize = size / 16 + 'rem';
72+
let isLarge = size >= 64;
5873
return (
5974
<img
6075
{...domProps}
6176
ref={domRef}
6277
alt={alt}
63-
style={UNSAFE_style}
64-
className={UNSAFE_className + imageStyles(null, props.styles)}
78+
style={{
79+
...UNSAFE_style,
80+
width: remSize,
81+
height: remSize
82+
}}
83+
className={(UNSAFE_className ?? '') + imageStyles({isOverBackground, isLarge}, props.styles)}
6584
src={src} />
6685
);
6786
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {AriaLabelingProps, DOMProps, DOMRef, DOMRefValue} from '@react-types/shared';
14+
import {AvatarContext} from './Avatar';
15+
import {ContextValue} from 'react-aria-components';
16+
import {createContext, CSSProperties, forwardRef, ReactNode} from 'react';
17+
import {filterDOMProps} from '@react-aria/utils';
18+
import {getAllowedOverrides, StylesPropWithoutWidth, UnsafeStyles} from './style-utils' with {type: 'macro'};
19+
import {style} from '../style/spectrum-theme' with {type: 'macro'};
20+
import {useDOMRef} from '@react-spectrum/utils';
21+
import {useLabel} from 'react-aria';
22+
import {useSpectrumContextProps} from './useSpectrumContextProps';
23+
24+
export interface AvatarGroupProps extends UnsafeStyles, DOMProps, AriaLabelingProps {
25+
/** Avatar children of the avatar group. */
26+
children: ReactNode,
27+
/** The label for the avatar group. */
28+
label?: string,
29+
/**
30+
* The size of the avatar group.
31+
* @default 24
32+
*/
33+
size?: 16 | 20 | 24 | 28 | 32 | 36 | 40,
34+
/** Spectrum-defined styles, returned by the `style()` macro. */
35+
styles?: StylesPropWithoutWidth
36+
}
37+
38+
export const AvatarGroupContext = createContext<ContextValue<AvatarGroupProps, DOMRefValue<HTMLDivElement>>>(null);
39+
40+
const avatar = style({
41+
marginStart: {
42+
default: '[calc(var(--size) / -4)]',
43+
':first-child': 0
44+
}
45+
});
46+
47+
const text = style({
48+
marginStart: 8,
49+
truncate: true,
50+
font: {
51+
size: {
52+
16: 'ui-xs',
53+
20: 'ui-sm',
54+
24: 'ui',
55+
28: 'ui-lg',
56+
32: 'ui-xl',
57+
36: 'ui-2xl',
58+
40: 'ui-3xl'
59+
}
60+
}
61+
});
62+
63+
const container = style({
64+
display: 'flex',
65+
alignItems: 'center'
66+
}, getAllowedOverrides({width: false}));
67+
68+
function AvatarGroup(props: AvatarGroupProps, ref: DOMRef<HTMLDivElement>) {
69+
[props, ref] = useSpectrumContextProps(props, ref, AvatarGroupContext);
70+
let domRef = useDOMRef(ref);
71+
let {children, label, size = 24, styles, UNSAFE_style, UNSAFE_className, ...otherProps} = props;
72+
let {labelProps, fieldProps} = useLabel({
73+
...props,
74+
labelElementType: 'span'
75+
});
76+
77+
return (
78+
<AvatarContext.Provider value={{styles: avatar, size, isOverBackground: true}}>
79+
<div
80+
ref={domRef}
81+
{...filterDOMProps(otherProps)}
82+
{...fieldProps}
83+
role="group"
84+
className={(UNSAFE_className ?? '') + container(null, styles)}
85+
style={{
86+
...UNSAFE_style,
87+
'--size': size / 16 + 'rem'
88+
} as CSSProperties}>
89+
{children}
90+
{label && <span {...labelProps} className={text({size: String(size)})}>{label}</span>}
91+
</div>
92+
</AvatarContext.Provider>
93+
);
94+
}
95+
96+
/**
97+
* An avatar group is a grouping of avatars that are related to each other.
98+
*/
99+
let _AvatarGroup = forwardRef(AvatarGroup);
100+
export {_AvatarGroup as AvatarGroup};

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ function Modal(props: ModalProps, ref: DOMRef<HTMLDivElement>) {
133133
L: '[90vh]'
134134
}
135135
},
136-
backgroundColor: 'layer-2',
136+
'--s2-container-bg': {
137+
type: 'backgroundColor',
138+
value: 'layer-2'
139+
},
140+
backgroundColor: '--s2-container-bg',
137141
animation: {
138142
isEntering: fadeAndSlide,
139143
isExiting: fade

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ const slideLeftKeyframes = keyframes(`
8686

8787
let popover = style({
8888
...colorScheme(),
89-
'--popoverBackground': {
89+
'--s2-container-bg': {
9090
type: 'backgroundColor',
9191
value: 'layer-2'
9292
},
93-
backgroundColor: '--popoverBackground',
93+
backgroundColor: '--s2-container-bg',
9494
borderRadius: 'lg',
9595
filter: {
9696
isArrowShown: 'elevated'
@@ -174,7 +174,7 @@ let popover = style({
174174

175175
let arrow = style({
176176
display: 'block',
177-
fill: '--popoverBackground',
177+
fill: '--s2-container-bg',
178178
rotate: {
179179
default: 180,
180180
placement: {

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,17 @@ export function Provider(props: ProviderProps) {
7070

7171
let providerStyles = style({
7272
...colorScheme(),
73-
backgroundColor: {
74-
background: {
75-
base: 'base',
76-
'layer-1': 'layer-1',
77-
'layer-2': 'layer-2'
73+
'--s2-container-bg': {
74+
type: 'backgroundColor',
75+
value: {
76+
background: {
77+
base: 'base',
78+
'layer-1': 'layer-1',
79+
'layer-2': 'layer-2'
80+
}
7881
}
79-
}
82+
},
83+
backgroundColor: '--s2-container-bg'
8084
});
8185

8286
function ProviderInner(props: ProviderProps) {

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@ const tagStyles = style({
270270
}
271271
});
272272

273+
const avatarSize = {
274+
S: 16,
275+
M: 20,
276+
L: 24
277+
} as const;
278+
273279
export function Tag({children, ...props}: TagProps) {
274280
let textValue = typeof children === 'string' ? children : undefined;
275281
let {size = 'M', isEmphasized} = useSlottedContext(TagGroupContext)!;
@@ -302,10 +308,18 @@ export function Tag({children, ...props}: TagProps) {
302308
styles: style({size: fontRelative(20), marginStart: '--iconMargin', flexShrink: 0})
303309
}],
304310
[AvatarContext, {
305-
styles: style({size: fontRelative(20), flexShrink: 0, order: 0})
311+
size: avatarSize[size],
312+
styles: style({order: 0})
306313
}],
307314
[ImageContext, {
308-
className: style({size: fontRelative(20), flexShrink: 0, order: 0, aspectRatio: 'square', objectFit: 'contain'})
315+
className: style({
316+
size: fontRelative(20),
317+
flexShrink: 0,
318+
order: 0,
319+
aspectRatio: 'square',
320+
objectFit: 'contain',
321+
borderRadius: 'sm'
322+
})
309323
}]
310324
]}>
311325
{typeof children === 'string' ? <Text>{children}</Text> : children}

packages/@react-spectrum/s2/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {ActionButton, ActionButtonContext} from './ActionButton';
1414
export {ActionMenu, ActionMenuContext} from './ActionMenu';
1515
export {AlertDialog} from './AlertDialog';
1616
export {Avatar, AvatarContext} from './Avatar';
17+
export {AvatarGroup, AvatarGroupContext} from './AvatarGroup';
1718
export {Badge, BadgeContext} from './Badge';
1819
export {Breadcrumbs, Breadcrumb, BreadcrumbsContext} from './Breadcrumbs';
1920
export {Button, LinkButton, ButtonContext, LinkButtonContext} from './Button';
@@ -64,6 +65,7 @@ export type {ActionButtonProps} from './ActionButton';
6465
export type {ActionMenuProps} from './ActionMenu';
6566
export type {AlertDialogProps} from './AlertDialog';
6667
export type {AvatarProps} from './Avatar';
68+
export type {AvatarGroupProps} from './AvatarGroup';
6769
export type {BreadcrumbsProps, BreadcrumbProps} from './Breadcrumbs';
6870
export type {BadgeProps} from './Badge';
6971
export type {ButtonProps, LinkButtonProps} from './Button';

packages/@react-spectrum/s2/src/page.macro.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export function generatePageStyles(this: MacroContext | void) {
2727
type: 'css',
2828
content: `html {
2929
color-scheme: light dark;
30-
background: ${colorToken(tokens['background-base-color'])};
30+
--s2-container-bg: ${colorToken(tokens['background-base-color'])};
31+
background: var(--s2-container-bg);
3132
3233
&[data-color-scheme=light] {
3334
color-scheme: light;
@@ -38,11 +39,11 @@ export function generatePageStyles(this: MacroContext | void) {
3839
}
3940
4041
&[data-background=layer-1] {
41-
background: ${colorToken(tokens['background-layer-1-color'])};
42+
--s2-container-bg: ${colorToken(tokens['background-layer-1-color'])};
4243
}
4344
4445
&[data-background=layer-2] {
45-
background: ${weirdColorToken(tokens['background-layer-2-color'])};
46+
--s2-container-bg: ${weirdColorToken(tokens['background-layer-2-color'])};
4647
}
4748
}`
4849
});

packages/@react-spectrum/s2/src/style-utils.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,6 @@ const allowedOverrides = [
147147
'marginBottom',
148148
'marginX',
149149
'marginY',
150-
'width',
151-
'minWidth',
152-
'maxWidth',
153150
'flex',
154151
'flexGrow',
155152
'flexShrink',
@@ -175,15 +172,22 @@ const allowedOverrides = [
175172
'insetEnd'
176173
] as const;
177174

175+
const widthProperties = [
176+
'width',
177+
'minWidth',
178+
'maxWidth'
179+
] as const;
180+
178181
const heightProperties = [
179182
'size',
180183
'height',
181184
'minHeight',
182185
'maxHeight'
183186
] as const;
184187

185-
export type StylesProp = StyleString<(typeof allowedOverrides)[number]>;
188+
export type StylesProp = StyleString<(typeof allowedOverrides)[number] | (typeof widthProperties)[number]>;
186189
export type StylesPropWithHeight = StyleString<(typeof allowedOverrides)[number] | (typeof heightProperties)[number]>;
190+
export type StylesPropWithoutWidth = StyleString<(typeof allowedOverrides)[number]>;
187191
export interface UnsafeStyles {
188192
/** Sets the CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. Only use as a **last resort**. Use the `style` macro via the `styles` prop instead. */
189193
UNSAFE_className?: string,
@@ -196,6 +200,6 @@ export interface StyleProps extends UnsafeStyles {
196200
styles?: StylesProp
197201
}
198202

199-
export function getAllowedOverrides({height = false} = {}) {
200-
return (allowedOverrides as unknown as string[]).concat(height ? heightProperties : []);
203+
export function getAllowedOverrides({width = true, height = false} = {}) {
204+
return (allowedOverrides as unknown as string[]).concat(width ? widthProperties : []).concat(height ? heightProperties : []);
201205
}

0 commit comments

Comments
 (0)