Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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';
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 @@ -33,8 +33,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 @@ -304,6 +305,13 @@ const dividerStyle = style({
width: 'full'
});

const avatar = style({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The style macro variables in Picker.tsx and ComboBox.tsx are named a bit inconsistently, do you prefer this to be called avatar or avatarStyle(s)?

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 @@ -363,6 +371,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 @@ -392,6 +407,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
51 changes: 36 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,13 @@ const iconStyles = style({
}
});

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

const loadingWrapperStyles = style({
gridColumnStart: '1',
gridColumnEnd: '-1',
Expand Down Expand Up @@ -571,6 +579,13 @@ export interface PickerItemProps extends Omit<ListBoxItemProps, 'children' | 'st
children: ReactNode
}

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

const checkmarkIconSize = {
S: 'XS',
M: 'M',
Expand Down Expand Up @@ -599,21 +614,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: avatar}
}}}>
Comment on lines +617 to +620
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this the right approach to add another child DefaultProvider here?

<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
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 @@ -150,6 +150,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
25 changes: 24 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,29 @@ 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) => ({
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
avatar: `https://i.imgur.com/kJOwAdv.png`
}));

<ComboBox label="Share" items={users}>
{(item) => (
<ComboBoxItem 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>
```

<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
25 changes: 24 additions & 1 deletion packages/dev/s2-docs/pages/s2/Picker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function Example() {

### Slots

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

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

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

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

<Picker label="Share" items={users}>
{(item) => (
<PickerItem 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>
</PickerItem>
)}
</Picker>
```
Comment on lines 86 to 108
Copy link
Contributor Author

@dkario dkario Sep 25, 2025

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 ComboBox docs I added


<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