diff --git a/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx b/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx index de29ee3b0c8..7377d63e179 100644 --- a/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx @@ -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'; @@ -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}) => { diff --git a/packages/@react-spectrum/s2/chromatic/ComboboxRTL.stories.tsx b/packages/@react-spectrum/s2/chromatic/ComboboxRTL.stories.tsx index d800658904d..8d7198fb9fe 100644 --- a/packages/@react-spectrum/s2/chromatic/ComboboxRTL.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ComboboxRTL.stories.tsx @@ -23,4 +23,4 @@ const meta: Meta> = { }; export default meta; -export {Static, WithSections, WithDynamic, Icons, ContextualHelp, WithCustomWidth} from './Combobox.stories'; +export {Static, WithSections, WithDynamic, Icons, Avatars, ContextualHelp, WithCustomWidth} from './Combobox.stories'; diff --git a/packages/@react-spectrum/s2/chromatic/Picker.stories.tsx b/packages/@react-spectrum/s2/chromatic/Picker.stories.tsx index 756cce5bdb6..5f231dc7c20 100644 --- a/packages/@react-spectrum/s2/chromatic/Picker.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Picker.stories.tsx @@ -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'; @@ -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 diff --git a/packages/@react-spectrum/s2/chromatic/PickerRTL.stories.tsx b/packages/@react-spectrum/s2/chromatic/PickerRTL.stories.tsx index 27c8a353b8c..daa7151d66c 100644 --- a/packages/@react-spectrum/s2/chromatic/PickerRTL.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/PickerRTL.stories.tsx @@ -22,4 +22,4 @@ const meta: Meta> = { }; export default meta; -export {Default, WithSections, DynamicExample, Icons, WithCustomWidth, ContextualHelp} from './Picker.stories'; +export {Default, WithSections, DynamicExample, Icons, Avatars, WithCustomWidth, ContextualHelp} from './Picker.stories'; diff --git a/packages/@react-spectrum/s2/src/Avatar.tsx b/packages/@react-spectrum/s2/src/Avatar.tsx index 6ea34ff2bdd..e4d088defe5 100644 --- a/packages/@react-spectrum/s2/src/Avatar.tsx +++ b/packages/@react-spectrum/s2/src/Avatar.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {centerBaselineBefore} from './CenterBaseline'; import {ContextValue, SlotProps} from 'react-aria-components'; import {createContext, forwardRef} from 'react'; import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared'; @@ -37,6 +38,8 @@ export interface AvatarProps extends UnsafeStyles, DOMProps, SlotProps { } const imageStyles = style({ + display: 'flex', + alignItems: 'center', borderRadius: 'full', size: 20, flexShrink: 0, @@ -86,7 +89,7 @@ export const Avatar = forwardRef(function Avatar(props: AvatarProps, ref: DOMRef width: remSize, height: remSize }} - UNSAFE_className={UNSAFE_className} + UNSAFE_className={UNSAFE_className + ' ' + centerBaselineBefore} styles={imageStyles({isOverBackground, isLarge}, props.styles)} src={src} /> ); diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 7f603dd1c8f..9f5ea1cfb9d 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -34,6 +34,7 @@ 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 {centerBaseline} from './CenterBaseline'; @@ -306,6 +307,11 @@ const dividerStyle = style({ width: 'full' }); +const avatar = style({ + gridArea: 'icon', + marginEnd: 'text-to-visual' +}); + // Not from any design, just following the sizing of the existing rows export const LOADER_ROW_HEIGHTS = { S: { @@ -365,6 +371,13 @@ export interface ComboBoxItemProps extends Omit extends PickerStyleProps, Omit, Pick, 'loadingState'> { loadingCircle: ReactNode, buttonRef: RefObject @@ -519,7 +532,7 @@ const PickerButton = createHideableComponent(function PickerButton {(renderProps) => ( <> - * {display: none;}')}> + :not([slot=icon], [slot=avatar], [slot=label]) {display: none;}')}> {({selectedItems, defaultChildren}) => { return ( - {renderProps.selectionMode === 'single' && !isLink && } - {renderProps.selectionMode === 'multiple' && !isLink && ( -
- -
+ context={AvatarContext} + value={{slots: { + avatar: {size: avatarSize[size], styles: avatar} + }}}> + + {renderProps.selectionMode === 'single' && !isLink && } + {renderProps.selectionMode === 'multiple' && !isLink && ( +
+ +
)} - {typeof children === 'string' ? {children} : children} + {typeof children === 'string' ? {children} : children} +
); diff --git a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx index 73dea75fd1c..3514cdee326 100644 --- a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx @@ -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'; @@ -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) => ( + + + + User One + user.one@example.com + + + + User Two + user.two@example.com
123-456-7890
+
+ + + User Three + +
+ ), + args: { + label: 'Share' + } +}; + export const Validation: Story = { render: (args) => (
diff --git a/packages/@react-spectrum/s2/stories/Picker.stories.tsx b/packages/@react-spectrum/s2/stories/Picker.stories.tsx index 89d2c4513e3..b945860c127 100644 --- a/packages/@react-spectrum/s2/stories/Picker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Picker.stories.tsx @@ -11,6 +11,7 @@ */ import { + Avatar, Button, Content, ContextualHelp, @@ -144,6 +145,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) => ( + + + + User One + user.one@example.com + + + + User Two + user.two@example.com
123-456-7890
+
+ + + User Three + +
+ ), + args: { + label: 'Share' + } +}; + function VirtualizedPicker(props) { let items: IExampleItem[] = []; for (let i = 0; i < 10000; i++) { diff --git a/packages/dev/s2-docs/pages/s2/ComboBox.mdx b/packages/dev/s2-docs/pages/s2/ComboBox.mdx index 07e559e9bc1..51213a1fc1e 100644 --- a/packages/dev/s2-docs/pages/s2/ComboBox.mdx +++ b/packages/dev/s2-docs/pages/s2/ComboBox.mdx @@ -53,34 +53,56 @@ 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"; -import {ComboBox, ComboBoxItem, Text} from '@react-spectrum/s2'; +import {Avatar, ComboBox, ComboBoxItem, Text} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import Comment from '@react-spectrum/s2/icons/Comment'; import Edit from '@react-spectrum/s2/icons/Edit'; import UserSettings from '@react-spectrum/s2/icons/UserSettings'; - - - {/*- begin highlight -*/} - - Read - Comment only - {/*- end highlight -*/} - - - - Write - Read and write only - - - - Admin - Full access - - +const users = Array.from({length: 5}, (_, i) => ({ + id: `user${i + 1}`, + name: `User ${i + 1}`, + email: `user${i + 1}@example.com`, + avatar: 'https://i.imgur.com/kJOwAdv.png' +})); + +
+ + + {/*- begin highlight -*/} + + Read + Comment only + {/*- end highlight -*/} + + + + Write + Read and write only + + + + Admin + Full access + + + + {(item) => ( + + {/*- begin highlight -*/} + + {/*- end highlight -*/} + {item.name} + {item.email} + + )} + +
+ ``` diff --git a/packages/dev/s2-docs/pages/s2/Picker.mdx b/packages/dev/s2-docs/pages/s2/Picker.mdx index 9199d6e1fbd..8c03add9b8e 100644 --- a/packages/dev/s2-docs/pages/s2/Picker.mdx +++ b/packages/dev/s2-docs/pages/s2/Picker.mdx @@ -53,34 +53,55 @@ 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"; -import {Picker, PickerItem, Text} from '@react-spectrum/s2'; +import {Avatar, Picker, PickerItem, Text} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import Comment from '@react-spectrum/s2/icons/Comment'; import Edit from '@react-spectrum/s2/icons/Edit'; import UserSettings from '@react-spectrum/s2/icons/UserSettings'; - - - {/*- begin highlight -*/} - - Read - Comment only - {/*- end highlight -*/} - - - - Write - Read and write only - - - - Admin - Full access - - +const users = Array.from({length: 5}, (_, i) => ({ + id: `user${i + 1}`, + name: `User ${i + 1}`, + email: `user${i + 1}@example.com`, + avatar: 'https://i.imgur.com/kJOwAdv.png' +})); + +
+ + + {/*- begin highlight -*/} + + Read + Comment only + {/*- end highlight -*/} + + + + Write + Read and write only + + + + Admin + Full access + + + + {(item) => ( + + {/*- begin highlight -*/} + + {/*- end highlight -*/} + {item.name} + {item.email} + + )} + +
```