Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {AsyncComboBoxStory, AsyncComboBoxStoryType, ContextualHelpExample, CustomWidth, Dynamic, EmptyCombobox, Example, Sections, WithIcons} from '../stories/ComboBox.stories';
import {AsyncComboBoxStory, AsyncComboBoxStoryType, ContextualHelpExample, CustomWidth, Dynamic, EmptyCombobox, Example, Sections, WithAvatars, WithIcons} from '../stories/ComboBox.stories';
import {ComboBox} from '../src';
import {expect} from '@storybook/jest';
import type {Meta, StoryObj} from '@storybook/react';
Expand Down Expand Up @@ -58,6 +58,12 @@ export const Icons: Story = {
play: Static.play
};

export const Avatars: Story = {
...WithAvatars,
name: 'With Avatars',
play: Static.play
};

export const ContextualHelp: Story = {
...ContextualHelpExample,
play: async ({canvasElement}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ const meta: Meta<typeof ComboBox<any>> = {
};

export default meta;
export {Static, WithSections, WithDynamic, Icons, ContextualHelp, WithCustomWidth} from './Combobox.stories';
export {Static, WithSections, WithDynamic, Icons, Avatars, ContextualHelp, WithCustomWidth} from './Combobox.stories';
8 changes: 7 additions & 1 deletion packages/@react-spectrum/s2/chromatic/Picker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {AsyncPickerStory, AsyncPickerStoryType, ContextualHelpExample, CustomWidth, Dynamic, Example, Sections, WithIcons} from '../stories/Picker.stories';
import {AsyncPickerStory, AsyncPickerStoryType, ContextualHelpExample, CustomWidth, Dynamic, Example, Sections, WithAvatars, WithIcons} from '../stories/Picker.stories';
import {expect} from '@storybook/jest';
import type {Meta, StoryObj} from '@storybook/react';
import {Picker} from '../src';
Expand Down Expand Up @@ -56,6 +56,12 @@ export const Icons: Story = {
play: Default.play
};

export const Avatars: Story = {
...WithAvatars,
name: 'With Avatars',
play: Default.play
};

export const WithCustomWidth: Story = {
...CustomWidth,
play: Default.play
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ const meta: Meta<typeof Picker<any>> = {
};

export default meta;
export {Default, WithSections, DynamicExample, Icons, WithCustomWidth, ContextualHelp} from './Picker.stories';
export {Default, WithSections, DynamicExample, Icons, Avatars, WithCustomWidth, ContextualHelp} from './Picker.stories';
19 changes: 15 additions & 4 deletions packages/@react-spectrum/s2/src/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {ContextValue, SlotProps} from 'react-aria-components';
import {createContext, forwardRef} from 'react';
import {createContext, forwardRef, ReactNode} from 'react';
import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared';
import {filterDOMProps} from '@react-aria/utils';
import {getAllowedOverrides, StylesPropWithoutWidth, UnsafeStyles} from './style-utils' with {type: 'macro'};
Expand Down Expand Up @@ -53,13 +53,17 @@ const imageStyles = style({
}
}, getAllowedOverrides({width: false}));

export const AvatarContext = createContext<ContextValue<Partial<AvatarProps>, DOMRefValue<HTMLImageElement>>>(null);
export interface AvatarContextValue extends AvatarProps {
render?: (avatar: ReactNode) => ReactNode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think rather than adding this prop we could just make Avatar have CenterBaseline by default. I can't really think of a case where you'd want to align text with it at the bottom. We may be able to avoid the extra wrapper div as well since Image already has a wrapper div. We could potentially put the ::before directly on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this!

}

export const AvatarContext = createContext<ContextValue<Partial<AvatarContextValue>, DOMRefValue<HTMLImageElement>>>(null);

/**
* An avatar is a thumbnail representation of an entity, such as a user or an organization.
*/
export const Avatar = forwardRef(function Avatar(props: AvatarProps, ref: DOMRef<HTMLImageElement>) {
[props, ref] = useSpectrumContextProps(props, ref, AvatarContext);
[props, ref] = useSpectrumContextProps(props as AvatarContextValue, ref, AvatarContext);
let domRef = useDOMRef(ref);
let {
alt = '',
Expand All @@ -71,11 +75,12 @@ export const Avatar = forwardRef(function Avatar(props: AvatarProps, ref: DOMRef
slot = 'avatar',
...otherProps
} = props;
let render = (props as AvatarContextValue).render;
const domProps = filterDOMProps(otherProps);

let remSize = size / 16 + 'rem';
let isLarge = size >= 64;
return (
let image = (
<Image
{...domProps}
ref={domRef}
Expand All @@ -90,4 +95,10 @@ export const Avatar = forwardRef(function Avatar(props: AvatarProps, ref: DOMRef
styles={imageStyles({isOverBackground, isLarge}, props.styles)}
src={src} />
);

if (render) {
return render(image);
}

return image;
});
22 changes: 21 additions & 1 deletion packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import {
Virtualizer
} from 'react-aria-components';
import {AsyncLoadable, GlobalDOMAttributes, HelpTextProps, LoadingState, SpectrumLabelableProps} from '@react-types/shared';
import {AvatarContext} from './Avatar';
import {BaseCollection, CollectionNode, createLeafComponent} from '@react-aria/collections';
import {baseColor, edgeToText, focusRing, space, style} from '../style' with {type: 'macro'};
import {baseColor, edgeToText, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'};
import {centerBaseline} from './CenterBaseline';
import {centerPadding, control, controlBorderRadius, controlFont, controlSize, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {
Expand Down Expand Up @@ -306,6 +307,13 @@ const dividerStyle = style({
width: 'full'
});

const avatar = style({
gridArea: 'icon',
marginEnd: 'text-to-visual',
marginTop: fontRelative(6), // made up, need feedback
alignSelf: 'center'
});

// Not from any design, just following the sizing of the existing rows
export const LOADER_ROW_HEIGHTS = {
S: {
Expand Down Expand Up @@ -365,6 +373,13 @@ export interface ComboBoxItemProps extends Omit<ListBoxItemProps, 'children' | '
children: ReactNode
}

const avatarSize = {
S: 16,
M: 20,
L: 22,
XL: 26
} as const;

const checkmarkIconSize = {
S: 'XS',
M: 'M',
Expand Down Expand Up @@ -394,6 +409,11 @@ export function ComboBoxItem(props: ComboBoxItemProps): ReactNode {
icon: {render: centerBaseline({slot: 'icon', styles: iconCenterWrapper}), styles: icon}
}
}],
[AvatarContext, {
slots: {
avatar: {size: avatarSize[size], styles: avatar}
}
}],
[TextContext, {
slots: {
label: {styles: label({size})},
Expand Down
62 changes: 47 additions & 15 deletions packages/@react-spectrum/s2/src/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import {
Virtualizer
} from 'react-aria-components';
import {AsyncLoadable, FocusableRef, FocusableRefValue, GlobalDOMAttributes, HelpTextProps, LoadingState, PressEvent, RefObject, SpectrumLabelableProps} from '@react-types/shared';
import {baseColor, edgeToText, focusRing, style} from '../style' with {type: 'macro'};
import {AvatarContext} from './Avatar';
import {baseColor, edgeToText, focusRing, fontRelative, style} from '../style' with {type: 'macro'};
import {box, iconStyles as checkboxIconStyles} from './Checkbox';
import {centerBaseline} from './CenterBaseline';
import {
Expand Down Expand Up @@ -241,6 +242,16 @@ const iconStyles = style({
}
});

const buttonAvatar = style({
gridArea: 'icon',
marginEnd: 'text-to-visual',
alignSelf: 'center'
});

const itemAvatar = mergeStyles(buttonAvatar, style({
marginTop: fontRelative(6) // made up, need feedback
}));

const loadingWrapperStyles = style({
gridColumnStart: '1',
gridColumnEnd: '-1',
Expand Down Expand Up @@ -468,6 +479,13 @@ function PickerProgressCircle(props) {
);
}

const avatarSize = {
S: 16,
M: 20,
L: 22,
XL: 26
} as const;

interface PickerButtonInnerProps<T extends object> extends PickerStyleProps, Omit<AriaSelectRenderProps, 'isRequired' | 'isFocused'>, Pick<PickerProps<T>, 'loadingState'> {
loadingCircle: ReactNode,
buttonRef: RefObject<HTMLButtonElement | null>
Expand Down Expand Up @@ -532,6 +550,14 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj
}
}
}],
[AvatarContext, {
slots: {
avatar: {
render: centerBaseline({slot: 'avatar', styles: iconCenterWrapper}),
size: avatarSize[size ?? 'M'], styles: buttonAvatar
}
}
}],
[TextContext, {
slots: {
description: {},
Expand Down Expand Up @@ -606,21 +632,27 @@ export function PickerItem(props: PickerItemProps): ReactNode {
icon: {render: centerBaseline({slot: 'icon', styles: iconCenterWrapper}), styles: icon}
}}}>
<DefaultProvider
context={TextContext}
value={{
slots: {
[DEFAULT_SLOT]: {styles: label({size})},
label: {styles: label({size})},
description: {styles: description({...renderProps, size})}
}
}}>
{renderProps.selectionMode === 'single' && !isLink && <CheckmarkIcon size={checkmarkIconSize[size]} className={checkmark({...renderProps, size})} />}
{renderProps.selectionMode === 'multiple' && !isLink && (
<div className={mergeStyles(checkbox, box(checkboxRenderProps))}>
<CheckmarkIcon size={size} className={checkboxIconStyles} />
</div>
context={AvatarContext}
value={{slots: {
avatar: {size: avatarSize[size], styles: itemAvatar}
}}}>
<DefaultProvider
context={TextContext}
value={{
slots: {
[DEFAULT_SLOT]: {styles: label({size})},
label: {styles: label({size})},
description: {styles: description({...renderProps, size})}
}
}}>
{renderProps.selectionMode === 'single' && !isLink && <CheckmarkIcon size={checkmarkIconSize[size]} className={checkmark({...renderProps, size})} />}
{renderProps.selectionMode === 'multiple' && !isLink && (
<div className={mergeStyles(checkbox, box(checkboxRenderProps))}>
<CheckmarkIcon size={size} className={checkboxIconStyles} />
</div>
)}
{typeof children === 'string' ? <Text slot="label">{children}</Text> : children}
{typeof children === 'string' ? <Text slot="label">{children}</Text> : children}
</DefaultProvider>
</DefaultProvider>
</DefaultProvider>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/s2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export type {ActionButtonProps} from './ActionButton';
export type {ActionButtonGroupProps} from './ActionButtonGroup';
export type {ActionMenuProps} from './ActionMenu';
export type {AlertDialogProps} from './AlertDialog';
export type {AvatarProps} from './Avatar';
export type {AvatarProps, AvatarContextValue} from './Avatar';
export type {AvatarGroupProps} from './AvatarGroup';
export type {BreadcrumbsProps, BreadcrumbProps} from './Breadcrumbs';
export type {BadgeProps} from './Badge';
Expand Down
29 changes: 28 additions & 1 deletion packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {Button, ComboBox, ComboBoxItem, ComboBoxSection, Content, ContextualHelp, Footer, Form, Header, Heading, Link, Text} from '../src';
import {Avatar, Button, ComboBox, ComboBoxItem, ComboBoxSection, Content, ContextualHelp, Footer, Form, Header, Heading, Link, Text} from '../src';
import {categorizeArgTypes, getActionArgs} from './utils';
import {ComboBoxProps} from 'react-aria-components';
import DeviceDesktopIcon from '../s2wf-icons/S2_Icon_DeviceDesktop_20_N.svg';
Expand Down Expand Up @@ -154,6 +154,33 @@ export const WithIcons: Story = {
}
};

const SRC_URL_1 = 'https://i.imgur.com/xIe7Wlb.png';
const SRC_URL_2 = 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png';

export const WithAvatars: Story = {
render: (args) => (
<ComboBox {...args}>
<ComboBoxItem textValue="User One">
<Avatar slot="avatar" src={SRC_URL_1} />
<Text slot="label">User One</Text>
<Text slot="description">user.one@example.com</Text>
</ComboBoxItem>
<ComboBoxItem textValue="User Two">
<Avatar slot="avatar" src={SRC_URL_2} />
<Text slot="label">User Two</Text>
<Text slot="description">user.two@example.com<br />123-456-7890</Text>
</ComboBoxItem>
<ComboBoxItem textValue="User Three">
<Avatar slot="avatar" src={SRC_URL_2} />
<Text slot="label">User Three</Text>
</ComboBoxItem>
</ComboBox>
),
args: {
label: 'Share'
}
};

export const Validation: Story = {
render: (args) => (
<Form>
Expand Down
28 changes: 28 additions & 0 deletions packages/@react-spectrum/s2/stories/Picker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import {
Avatar,
Button,
Content,
ContextualHelp,
Expand Down Expand Up @@ -142,6 +143,33 @@ export const WithIcons: Story = {
}
};

const SRC_URL_1 = 'https://i.imgur.com/xIe7Wlb.png';
const SRC_URL_2 = 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png';

export const WithAvatars: Story = {
render: (args) => (
<Picker {...args}>
<PickerItem textValue="User One">
<Avatar slot="avatar" src={SRC_URL_1} />
<Text slot="label">User One</Text>
<Text slot="description">user.one@example.com</Text>
</PickerItem>
<PickerItem textValue="User Two">
<Avatar slot="avatar" src={SRC_URL_2} />
<Text slot="label">User Two</Text>
<Text slot="description">user.two@example.com<br />123-456-7890</Text>
</PickerItem>
<PickerItem textValue="User Three">
<Avatar slot="avatar" src={SRC_URL_2} />
<Text slot="label">User Three</Text>
</PickerItem>
</Picker>
),
args: {
label: 'Share'
}
};

function VirtualizedPicker(props) {
let items: IExampleItem[] = [];
for (let i = 0; i < 10000; i++) {
Expand Down
26 changes: 25 additions & 1 deletion packages/dev/s2-docs/pages/s2/ComboBox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function Example() {

### Slots

`ComboBoxItem` supports icons, and `label` and `description` text slots.
`ComboBoxItem` supports icons, avatars, and `label` and `description` text slots.

```tsx render
"use client";
Expand Down Expand Up @@ -83,6 +83,30 @@ import UserSettings from '@react-spectrum/s2/icons/UserSettings';
</ComboBox>
```

```tsx render
"use client";
import {Avatar, ComboBox, ComboBoxItem, Text} from '@react-spectrum/s2';

const users = Array.from({length: 10}, (_, i) => ({
id: `user${i + 1}`,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
avatar: 'https://i.imgur.com/kJOwAdv.png'
}));

<ComboBox label="Share" items={users}>
{(item) => (
<ComboBoxItem id={item.id} textValue={item.name}>
{/*- begin highlight -*/}
<Avatar slot="avatar" src={item.avatar} />
{/*- end highlight -*/}
<Text slot="label">{item.name}</Text>
<Text slot="description">{item.email}</Text>
</ComboBoxItem>
)}
</ComboBox>
```
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this doc example is good enough for the site, or if it's okay to have two tsx render blocks one after another like I did here. Same thoughts for the Picker docs I added

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be moved up into the previous example, which would then render two comboboxes next to each other, one with icons, one with avatars (since they probably shouldn't be mixed)
It'll be hidden by the "expand" button, but should be obvious with the highlights once expanded

I'm sure we'll have some other opinions so happy to do this ourselves later if we can't come to an immediate consensus


<InlineAlert variant="notice">
<Heading>Accessibility</Heading>
<Content>Interactive elements (e.g. buttons) within picker items are not allowed. This will break keyboard and screen reader navigation. Only add textual or decorative graphics (e.g. icons) as children.</Content>
Expand Down
Loading