-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: allow custom react element for S2 Picker value #9541
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 all commits
7087906
6425a6f
eabc625
d5c4f3c
195d5d9
8324d81
061e14a
8b90e2b
7aeefae
d53b833
a0ee19c
be7431c
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 |
|---|---|---|
|
|
@@ -123,7 +123,13 @@ export interface PickerProps<T extends object, M extends SelectionMode = 'single | |
| /** Width of the menu. By default, matches width of the trigger. Note that the minimum width of the dropdown is always equal to the trigger's width. */ | ||
| menuWidth?: number, | ||
| /** The current loading state of the Picker. */ | ||
| loadingState?: LoadingState | ||
| loadingState?: LoadingState, | ||
| /** | ||
| * Custom renderer for the picker value. Allows one to provide a custom element to render selected items. | ||
| * | ||
| * @note The returned ReactNode should not have interactable elements as it will break accessibility. | ||
| */ | ||
| renderValue?: (selectedItems: T[]) => ReactNode | ||
| } | ||
|
|
||
| interface PickerButtonProps extends PickerStyleProps, ButtonRenderProps {} | ||
|
|
@@ -227,7 +233,8 @@ const valueStyles = style({ | |
| }, | ||
| truncate: true, | ||
| display: 'flex', | ||
| alignItems: 'center' | ||
| alignItems: 'center', | ||
| height: '100%' | ||
| }); | ||
|
|
||
| const iconStyles = style({ | ||
|
|
@@ -298,6 +305,7 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick | |
| placeholder = stringFormatter.format('picker.placeholder'), | ||
| isQuiet, | ||
| loadingState, | ||
| renderValue, | ||
| onLoadMore, | ||
| ...pickerProps | ||
| } = props; | ||
|
|
@@ -376,6 +384,7 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick | |
| </FieldLabel> | ||
| <PickerButton | ||
| loadingState={loadingState} | ||
| renderValue={renderValue} | ||
| isOpen={isOpen} | ||
| isQuiet={isQuiet} | ||
| isFocusVisible={isFocusVisible} | ||
|
|
@@ -482,7 +491,7 @@ const avatarSize = { | |
| XL: 26 | ||
| } as const; | ||
|
|
||
| interface PickerButtonInnerProps<T extends object> extends PickerStyleProps, Omit<AriaSelectRenderProps, 'isRequired' | 'isFocused'>, Pick<PickerProps<T>, 'loadingState'> { | ||
| interface PickerButtonInnerProps<T extends object> extends PickerStyleProps, Omit<AriaSelectRenderProps, 'isRequired' | 'isFocused'>, Pick<PickerProps<T>, 'loadingState' | 'renderValue'> { | ||
| loadingCircle: ReactNode, | ||
| buttonRef: RefObject<HTMLButtonElement | null> | ||
| } | ||
|
|
@@ -498,7 +507,8 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj | |
| isDisabled, | ||
| loadingState, | ||
| loadingCircle, | ||
| buttonRef | ||
| buttonRef, | ||
| renderValue | ||
| } = props; | ||
| let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); | ||
|
|
||
|
|
@@ -533,8 +543,20 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj | |
| })}> | ||
| {(renderProps) => ( | ||
| <> | ||
| <SelectValue className={valueStyles({isQuiet}) + ' ' + raw('&> :not([slot=icon], [slot=avatar], [slot=label], [data-slot=label]) {display: none;}')}> | ||
| <SelectValue | ||
| className={ | ||
| valueStyles({isQuiet}) + | ||
| (renderValue ? '' : ' ' + raw('&> :not([slot=icon], [slot=avatar], [slot=label], [data-slot=label]) {display: none;}')) | ||
|
devongovett marked this conversation as resolved.
|
||
| }> | ||
| {({selectedItems, defaultChildren}) => { | ||
| const selectedValues = selectedItems.filter((item): item is T => item != null); | ||
| const defaultRenderedValue = selectedItems.length <= 1 | ||
| ? defaultChildren | ||
| : <Text slot="label">{stringFormatter.format('picker.selectedCount', {count: selectedItems.length})}</Text>; | ||
| const renderedValue = selectedItems.length > 0 && renderValue | ||
| ? renderValue(selectedValues) | ||
| : defaultRenderedValue; | ||
|
Comment on lines
+556
to
+558
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. This should probably be |
||
|
|
||
| return ( | ||
| <Provider | ||
| values={[ | ||
|
|
@@ -579,10 +601,7 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj | |
| }], | ||
| [InsideSelectValueContext, true] | ||
| ]}> | ||
| {selectedItems.length <= 1 | ||
| ? defaultChildren | ||
| : <Text slot="label">{stringFormatter.format('picker.selectedCount', {count: selectedItems.length})}</Text> | ||
| } | ||
| {renderedValue} | ||
| </Provider> | ||
| ); | ||
| }} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -331,3 +331,39 @@ return ( | |
| } | ||
| } | ||
| }; | ||
|
|
||
|
|
||
| type ExampleIconItem = IExampleItem & { icon: string }; | ||
| const exampleIconItems: ExampleIconItem[] = Array.from({length: 5}, (_, i) => ({ | ||
| id: `user${i + 1}`, | ||
| label: `User ${i + 1}`, | ||
| icon: 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png' | ||
| })); | ||
|
|
||
| const CustomRenderValuePicker = (args: PickerProps<ExampleIconItem, 'multiple'>): ReactElement => ( | ||
| <Picker {...args}> | ||
| {(item: ExampleIconItem) => ( | ||
| <PickerItem id={item.id} textValue={item.label}> | ||
| <Avatar slot="avatar" src={item.icon} /> | ||
| <Text slot="label">{item.label}</Text> | ||
| </PickerItem> | ||
| )} | ||
| </Picker> | ||
| ); | ||
|
|
||
| export type CustomRenderValuePickerStoryType = typeof CustomRenderValuePicker; | ||
| export const CustomRenderValue: StoryObj<CustomRenderValuePickerStoryType> = { | ||
| render: CustomRenderValuePicker, | ||
| args: { | ||
| label: 'Pick users', | ||
| selectionMode: 'multiple', | ||
| items: exampleIconItems, | ||
| renderValue: (selectedItems) => ( | ||
| <div style={{display: 'flex', gap: 4, height: '80%'}}> | ||
| {selectedItems.map(item => ( | ||
| <img key={item.id} src={item.icon} alt={item.label} /> | ||
| ))} | ||
| </div> | ||
|
Comment on lines
+362
to
+366
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. Good example, we could probably use AvatarGroup here. |
||
| ) | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -243,6 +243,52 @@ function Example(props) { | |
| } | ||
| ``` | ||
|
|
||
| ### Custom Render Value | ||
|
|
||
| Use the `renderValue` prop to provide a custom element to display selected items. The callback is given an array of the selected user-defined objects. | ||
|
|
||
| ```tsx render | ||
| "use client"; | ||
| import {Avatar, Picker, PickerItem, Text} from '@react-spectrum/s2'; | ||
| import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; | ||
|
BrknRules marked this conversation as resolved.
|
||
|
|
||
| let users = [ | ||
| {id: 'abraham-baker', avatar: 'https://www.untitledui.com/images/avatars/abraham-baker', name: 'Abraham Baker', email: 'abraham@example.com'}, | ||
| {id: 'adriana-sullivan', avatar: 'https://www.untitledui.com/images/avatars/adriana-sullivan', name: 'Adriana Sullivan', email: 'adriana@example.com'}, | ||
| {id: 'jonathan-kelly', avatar: 'https://www.untitledui.com/images/avatars/jonathan-kelly', name: 'Jonathan Kelly', email: 'jonathan@example.com'}, | ||
| {id: 'zara-bush', avatar: 'https://www.untitledui.com/images/avatars/zara-bush', name: 'Zara Bush', email: 'zara@example.com'} | ||
| ]; | ||
|
|
||
| function Example() { | ||
| return ( | ||
| <div> | ||
| <Picker | ||
| label="Pick users" | ||
| items={users} | ||
| selectionMode={"multiple"} | ||
| ///- begin highlight -/// | ||
| renderValue={(selectedItems) => ( | ||
| <div className={style({ display: 'flex', gap: 4, height: '80%' })}> | ||
| {selectedItems.map(item => ( | ||
| <Avatar slot={null} key={item.id} src={item.avatar} alt={item.name} /> | ||
| ))} | ||
| </div> | ||
|
Comment on lines
+271
to
+275
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. Same, maybe AvatarGroup would better since it's an established pattern. |
||
| )} | ||
| ///- end highlight -/// | ||
|
devongovett marked this conversation as resolved.
|
||
| > | ||
| {(item) => | ||
| <PickerItem textValue={item.name}> | ||
| <Avatar slot="avatar" src={item.avatar} /> | ||
| <Text slot="label">{item.name}</Text> | ||
| <Text slot="description">{item.email}</Text> | ||
| </PickerItem> | ||
| } | ||
| </Picker> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Forms | ||
|
|
||
| Use the `name` prop to submit the `id` of the selected item to the server. Set the `isRequired` prop to validate that the user selects an option, or implement custom client or server-side validation. See the [Forms](forms) guide to learn more. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Locked height to picker button height. Enough for our use case as we don't have anything larger than the button that should cause it to resize.