-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: add Avatar support for ComboBoxItem and PickerItem #8931
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
a373806
b6e9288
da609c6
753b3db
894e49d
a24272d
804afae
865c517
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ import { | |
| Virtualizer | ||
| } from 'react-aria-components'; | ||
| import {AsyncLoadable, FocusableRef, FocusableRefValue, GlobalDOMAttributes, HelpTextProps, LoadingState, PressEvent, RefObject, SpectrumLabelableProps} from '@react-types/shared'; | ||
| import {AvatarContext} from './Avatar'; | ||
| import {baseColor, edgeToText, focusRing, style} from '../style' with {type: 'macro'}; | ||
| import {box, iconStyles as checkboxIconStyles} from './Checkbox'; | ||
| import {centerBaseline} from './CenterBaseline'; | ||
|
|
@@ -241,6 +242,11 @@ const iconStyles = style({ | |
| } | ||
| }); | ||
|
|
||
| const avatar = style({ | ||
| gridArea: 'icon', | ||
| marginEnd: 'text-to-visual' | ||
| }); | ||
|
|
||
| const loadingWrapperStyles = style({ | ||
| gridColumnStart: '1', | ||
| gridColumnEnd: '-1', | ||
|
|
@@ -468,6 +474,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> | ||
|
|
@@ -519,7 +532,7 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj | |
| })}> | ||
| {(renderProps) => ( | ||
| <> | ||
| <SelectValue className={valueStyles({isQuiet}) + ' ' + raw('&> * {display: none;}')}> | ||
| <SelectValue className={valueStyles({isQuiet}) + ' ' + raw('&> :not([slot=icon], [slot=avatar], [slot=label]) {display: none;}')}> | ||
| {({selectedItems, defaultChildren}) => { | ||
| return ( | ||
| <Provider | ||
|
|
@@ -532,6 +545,14 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj | |
| } | ||
| } | ||
| }], | ||
| [AvatarContext, { | ||
| slots: { | ||
| avatar: { | ||
| size: avatarSize[size ?? 'M'], | ||
| styles: avatar | ||
| } | ||
| } | ||
| }], | ||
| [TextContext, { | ||
| slots: { | ||
| description: {}, | ||
|
|
@@ -606,21 +627,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} | ||
| }}}> | ||
| <DefaultProvider | ||
|
Comment on lines
+630
to
+634
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the right approach to add another child
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like it says I haven't spent time thinking about it yet (hopefully can soon), is this what you want?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks that helps, I just wanted to make sure that sandwiching this between the DefaultProviders for IconContext and TextContent is okay, since this is diff than the usual Provider that takes an array of contexts
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this should be fine since those contexts won't collide since they are different contexts after all. Technically you could use a standard Provider I believe since the styles passed down are the same between the context provided at the SelectValue level and the one at the PickerItem level in this case but its probably safer to use DefaultProvider here so those can diverge if need be |
||
| 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> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) => ( | ||
| <ComboBox {...args}> | ||
| <ComboBoxItem textValue="User One"> | ||
| <Avatar slot="avatar" src={SRC_URL_1} /> | ||
| <Text slot="label">User One</Text> | ||
| <Text slot="description">[email protected]</Text> | ||
| </ComboBoxItem> | ||
| <ComboBoxItem textValue="User Two"> | ||
| <Avatar slot="avatar" src={SRC_URL_2} /> | ||
| <Text slot="label">User Two</Text> | ||
| <Text slot="description">[email protected]<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> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) => ( | ||
| <Picker {...args}> | ||
| <PickerItem textValue="User One"> | ||
| <Avatar slot="avatar" src={SRC_URL_1} /> | ||
| <Text slot="label">User One</Text> | ||
| <Text slot="description">[email protected]</Text> | ||
| </PickerItem> | ||
| <PickerItem textValue="User Two"> | ||
| <Avatar slot="avatar" src={SRC_URL_2} /> | ||
| <Text slot="label">User Two</Text> | ||
| <Text slot="description">[email protected]<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++) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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> | ||
| ``` | ||
|
||
|
|
||
| <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> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.