Skip to content

Commit

Permalink
S2 AvatarGroup (adobe#6781)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
5 people authored Aug 15, 2024
1 parent a18bcb3 commit b27e449
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 99 deletions.
49 changes: 34 additions & 15 deletions packages/@react-spectrum/s2/src/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,45 @@ import {ContextValue} from 'react-aria-components';
import {createContext, forwardRef} from 'react';
import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared';
import {filterDOMProps} from '@react-aria/utils';
import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {getAllowedOverrides, StylesPropWithoutWidth, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {style} from '../style/spectrum-theme' with { type: 'macro' };
import {useDOMRef} from '@react-spectrum/utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface AvatarProps extends StyleProps, DOMProps {
/** Text description of the avatar. */
alt?: string,
/** The image URL for the avatar. */
src?: string
}

export interface AvatarContextProps extends UnsafeStyles, DOMProps {
export interface AvatarProps extends UnsafeStyles, DOMProps {
/** Text description of the avatar. */
alt?: string,
/** The image URL for the avatar. */
src?: string,
/** Spectrum-defined styles, returned by the `style()` macro. */
styles?: StylesPropWithHeight
styles?: StylesPropWithoutWidth,
/**
* The size of the avatar.
* @default 24
*/
size?: 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 56 | 64 | 80 | 96 | 112 | (number & {}),
/** Whether the avatar is over a color background. */
isOverBackground?: boolean
}

const imageStyles = style({
borderRadius: 'full',
size: 20,
disableTapHighlight: true
}, getAllowedOverrides({height: true}));
flexShrink: 0,
flexGrow: 0,
disableTapHighlight: true,
outlineStyle: {
default: 'none',
isOverBackground: 'solid'
},
outlineColor: '--s2-container-bg',
outlineWidth: {
default: 1,
isLarge: 2
}
}, getAllowedOverrides({width: false}));

export const AvatarContext = createContext<ContextValue<AvatarContextProps, DOMRefValue<HTMLImageElement>>>(null);
export const AvatarContext = createContext<ContextValue<AvatarProps, DOMRefValue<HTMLImageElement>>>(null);

function Avatar(props: AvatarProps, ref: DOMRef<HTMLImageElement>) {
[props, ref] = useSpectrumContextProps(props, ref, AvatarContext);
Expand All @@ -51,17 +62,25 @@ function Avatar(props: AvatarProps, ref: DOMRef<HTMLImageElement>) {
src,
UNSAFE_style,
UNSAFE_className = '',
size,
isOverBackground,
...otherProps
} = props;
const domProps = filterDOMProps(otherProps);

let remSize = size / 16 + 'rem';
let isLarge = size >= 64;
return (
<img
{...domProps}
ref={domRef}
alt={alt}
style={UNSAFE_style}
className={UNSAFE_className + imageStyles(null, props.styles)}
style={{
...UNSAFE_style,
width: remSize,
height: remSize
}}
className={(UNSAFE_className ?? '') + imageStyles({isOverBackground, isLarge}, props.styles)}
src={src} />
);
}
Expand Down
100 changes: 100 additions & 0 deletions packages/@react-spectrum/s2/src/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {AriaLabelingProps, DOMProps, DOMRef, DOMRefValue} from '@react-types/shared';
import {AvatarContext} from './Avatar';
import {ContextValue} from 'react-aria-components';
import {createContext, CSSProperties, forwardRef, ReactNode} from 'react';
import {filterDOMProps} from '@react-aria/utils';
import {getAllowedOverrides, StylesPropWithoutWidth, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {style} from '../style/spectrum-theme' with {type: 'macro'};
import {useDOMRef} from '@react-spectrum/utils';
import {useLabel} from 'react-aria';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface AvatarGroupProps extends UnsafeStyles, DOMProps, AriaLabelingProps {
/** Avatar children of the avatar group. */
children: ReactNode,
/** The label for the avatar group. */
label?: string,
/**
* The size of the avatar group.
* @default 24
*/
size?: 16 | 20 | 24 | 28 | 32 | 36 | 40,
/** Spectrum-defined styles, returned by the `style()` macro. */
styles?: StylesPropWithoutWidth
}

export const AvatarGroupContext = createContext<ContextValue<AvatarGroupProps, DOMRefValue<HTMLDivElement>>>(null);

const avatar = style({
marginStart: {
default: '[calc(var(--size) / -4)]',
':first-child': 0
}
});

const text = style({
marginStart: 8,
truncate: true,
font: {
size: {
16: 'ui-xs',
20: 'ui-sm',
24: 'ui',
28: 'ui-lg',
32: 'ui-xl',
36: 'ui-2xl',
40: 'ui-3xl'
}
}
});

const container = style({
display: 'flex',
alignItems: 'center'
}, getAllowedOverrides({width: false}));

function AvatarGroup(props: AvatarGroupProps, ref: DOMRef<HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, AvatarGroupContext);
let domRef = useDOMRef(ref);
let {children, label, size = 24, styles, UNSAFE_style, UNSAFE_className, ...otherProps} = props;
let {labelProps, fieldProps} = useLabel({
...props,
labelElementType: 'span'
});

return (
<AvatarContext.Provider value={{styles: avatar, size, isOverBackground: true}}>
<div
ref={domRef}
{...filterDOMProps(otherProps)}
{...fieldProps}
role="group"
className={(UNSAFE_className ?? '') + container(null, styles)}
style={{
...UNSAFE_style,
'--size': size / 16 + 'rem'
} as CSSProperties}>
{children}
{label && <span {...labelProps} className={text({size: String(size)})}>{label}</span>}
</div>
</AvatarContext.Provider>
);
}

/**
* An avatar group is a grouping of avatars that are related to each other.
*/
let _AvatarGroup = forwardRef(AvatarGroup);
export {_AvatarGroup as AvatarGroup};
6 changes: 5 additions & 1 deletion packages/@react-spectrum/s2/src/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ function Modal(props: ModalProps, ref: DOMRef<HTMLDivElement>) {
L: '[90vh]'
}
},
backgroundColor: 'layer-2',
'--s2-container-bg': {
type: 'backgroundColor',
value: 'layer-2'
},
backgroundColor: '--s2-container-bg',
animation: {
isEntering: fadeAndSlide,
isExiting: fade
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/s2/src/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ const slideLeftKeyframes = keyframes(`

let popover = style({
...colorScheme(),
'--popoverBackground': {
'--s2-container-bg': {
type: 'backgroundColor',
value: 'layer-2'
},
backgroundColor: '--popoverBackground',
backgroundColor: '--s2-container-bg',
borderRadius: 'lg',
filter: {
isArrowShown: 'elevated'
Expand Down Expand Up @@ -174,7 +174,7 @@ let popover = style({

let arrow = style({
display: 'block',
fill: '--popoverBackground',
fill: '--s2-container-bg',
rotate: {
default: 180,
placement: {
Expand Down
16 changes: 10 additions & 6 deletions packages/@react-spectrum/s2/src/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,17 @@ export function Provider(props: ProviderProps) {

let providerStyles = style({
...colorScheme(),
backgroundColor: {
background: {
base: 'base',
'layer-1': 'layer-1',
'layer-2': 'layer-2'
'--s2-container-bg': {
type: 'backgroundColor',
value: {
background: {
base: 'base',
'layer-1': 'layer-1',
'layer-2': 'layer-2'
}
}
}
},
backgroundColor: '--s2-container-bg'
});

function ProviderInner(props: ProviderProps) {
Expand Down
18 changes: 16 additions & 2 deletions packages/@react-spectrum/s2/src/TagGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ const tagStyles = style({
}
});

const avatarSize = {
S: 16,
M: 20,
L: 24
} as const;

export function Tag({children, ...props}: TagProps) {
let textValue = typeof children === 'string' ? children : undefined;
let {size = 'M', isEmphasized} = useSlottedContext(TagGroupContext)!;
Expand Down Expand Up @@ -302,10 +308,18 @@ export function Tag({children, ...props}: TagProps) {
styles: style({size: fontRelative(20), marginStart: '--iconMargin', flexShrink: 0})
}],
[AvatarContext, {
styles: style({size: fontRelative(20), flexShrink: 0, order: 0})
size: avatarSize[size],
styles: style({order: 0})
}],
[ImageContext, {
className: style({size: fontRelative(20), flexShrink: 0, order: 0, aspectRatio: 'square', objectFit: 'contain'})
className: style({
size: fontRelative(20),
flexShrink: 0,
order: 0,
aspectRatio: 'square',
objectFit: 'contain',
borderRadius: 'sm'
})
}]
]}>
{typeof children === 'string' ? <Text>{children}</Text> : children}
Expand Down
2 changes: 2 additions & 0 deletions packages/@react-spectrum/s2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {ActionButton, ActionButtonContext} from './ActionButton';
export {ActionMenu, ActionMenuContext} from './ActionMenu';
export {AlertDialog} from './AlertDialog';
export {Avatar, AvatarContext} from './Avatar';
export {AvatarGroup, AvatarGroupContext} from './AvatarGroup';
export {Badge, BadgeContext} from './Badge';
export {Breadcrumbs, Breadcrumb, BreadcrumbsContext} from './Breadcrumbs';
export {Button, LinkButton, ButtonContext, LinkButtonContext} from './Button';
Expand Down Expand Up @@ -64,6 +65,7 @@ export type {ActionButtonProps} from './ActionButton';
export type {ActionMenuProps} from './ActionMenu';
export type {AlertDialogProps} from './AlertDialog';
export type {AvatarProps} from './Avatar';
export type {AvatarGroupProps} from './AvatarGroup';
export type {BreadcrumbsProps, BreadcrumbProps} from './Breadcrumbs';
export type {BadgeProps} from './Badge';
export type {ButtonProps, LinkButtonProps} from './Button';
Expand Down
7 changes: 4 additions & 3 deletions packages/@react-spectrum/s2/src/page.macro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export function generatePageStyles(this: MacroContext | void) {
type: 'css',
content: `html {
color-scheme: light dark;
background: ${colorToken(tokens['background-base-color'])};
--s2-container-bg: ${colorToken(tokens['background-base-color'])};
background: var(--s2-container-bg);
&[data-color-scheme=light] {
color-scheme: light;
Expand All @@ -38,11 +39,11 @@ export function generatePageStyles(this: MacroContext | void) {
}
&[data-background=layer-1] {
background: ${colorToken(tokens['background-layer-1-color'])};
--s2-container-bg: ${colorToken(tokens['background-layer-1-color'])};
}
&[data-background=layer-2] {
background: ${weirdColorToken(tokens['background-layer-2-color'])};
--s2-container-bg: ${weirdColorToken(tokens['background-layer-2-color'])};
}
}`
});
Expand Down
16 changes: 10 additions & 6 deletions packages/@react-spectrum/s2/src/style-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,6 @@ const allowedOverrides = [
'marginBottom',
'marginX',
'marginY',
'width',
'minWidth',
'maxWidth',
'flex',
'flexGrow',
'flexShrink',
Expand All @@ -175,15 +172,22 @@ const allowedOverrides = [
'insetEnd'
] as const;

const widthProperties = [
'width',
'minWidth',
'maxWidth'
] as const;

const heightProperties = [
'size',
'height',
'minHeight',
'maxHeight'
] as const;

export type StylesProp = StyleString<(typeof allowedOverrides)[number]>;
export type StylesProp = StyleString<(typeof allowedOverrides)[number] | (typeof widthProperties)[number]>;
export type StylesPropWithHeight = StyleString<(typeof allowedOverrides)[number] | (typeof heightProperties)[number]>;
export type StylesPropWithoutWidth = StyleString<(typeof allowedOverrides)[number]>;
export interface UnsafeStyles {
/** 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. */
UNSAFE_className?: string,
Expand All @@ -196,6 +200,6 @@ export interface StyleProps extends UnsafeStyles {
styles?: StylesProp
}

export function getAllowedOverrides({height = false} = {}) {
return (allowedOverrides as unknown as string[]).concat(height ? heightProperties : []);
export function getAllowedOverrides({width = true, height = false} = {}) {
return (allowedOverrides as unknown as string[]).concat(width ? widthProperties : []).concat(height ? heightProperties : []);
}
Loading

0 comments on commit b27e449

Please sign in to comment.