diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 766d4be7882..1e7ff28afe2 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -123,7 +123,13 @@ export interface PickerProps 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; @@ -377,6 +385,7 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick extends PickerStyleProps, Omit, Pick, 'loadingState'> { +interface PickerButtonInnerProps extends PickerStyleProps, Omit, Pick, 'loadingState' | 'renderValue'> { loadingCircle: ReactNode, buttonRef: RefObject } @@ -499,7 +508,8 @@ const PickerButton = createHideableComponent(function PickerButton {(renderProps) => ( <> - :not([slot=icon], [slot=avatar], [slot=label], [data-slot=label]) {display: none;}')}> + :not([slot=icon], [slot=avatar], [slot=label], [data-slot=label]) {display: none;}')) + }> {({selectedItems, defaultChildren}) => { + const selectedValues = selectedItems.filter((item): item is T => item != null); + const defaultRenderedValue = selectedItems.length <= 1 + ? defaultChildren + : {stringFormatter.format('picker.selectedCount', {count: selectedItems.length})}; + const renderedValue = selectedItems.length > 0 && renderValue + ? renderValue(selectedValues) + : defaultRenderedValue; + return ( - {selectedItems.length <= 1 - ? defaultChildren - : {stringFormatter.format('picker.selectedCount', {count: selectedItems.length})} - } + {renderedValue} ); }} diff --git a/packages/@react-spectrum/s2/stories/Picker.stories.tsx b/packages/@react-spectrum/s2/stories/Picker.stories.tsx index e5685f6b536..6e385b40c20 100644 --- a/packages/@react-spectrum/s2/stories/Picker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Picker.stories.tsx @@ -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): ReactElement => ( + + {(item: ExampleIconItem) => ( + + + {item.label} + + )} + +); + +export type CustomRenderValuePickerStoryType = typeof CustomRenderValuePicker; +export const CustomRenderValue: StoryObj = { + render: CustomRenderValuePicker, + args: { + label: 'Pick users', + selectionMode: 'multiple', + items: exampleIconItems, + renderValue: (selectedItems) => ( +
+ {selectedItems.map(item => ( + {item.label} + ))} +
+ ) + } +}; diff --git a/packages/@react-spectrum/s2/test/Picker.test.tsx b/packages/@react-spectrum/s2/test/Picker.test.tsx index cc6769f7475..61cd74fa021 100644 --- a/packages/@react-spectrum/s2/test/Picker.test.tsx +++ b/packages/@react-spectrum/s2/test/Picker.test.tsx @@ -129,6 +129,42 @@ describe('Picker', () => { } }); + it('should support custom renderValue output', async () => { + let items = [ + {id: 'chocolate', name: 'Chocolate'}, + {id: 'strawberry', name: 'Strawberry'}, + {id: 'vanilla', name: 'Vanilla'} + ]; + let renderValue = jest.fn((selectedItems) => ( + + {selectedItems.map((item) => item.name).join(', ')} + + )); + let tree = render( + + {(item: any) => {item.name}} + + ); + + // expect the placeholder to be rendered when no items are selected + expect(tree.queryByTestId('custom-value')).toBeNull(); + + let selectTester = testUtilUser.createTester('Select', {root: tree.container, interactionType: 'mouse'}); + await selectTester.open(); + await selectTester.selectOption({option: 0}); + await selectTester.selectOption({option: 2}); + await selectTester.close(); + + // check that the clicked items are rendered in the custom renderValue output + let lastSelectedItems = renderValue.mock.calls[renderValue.mock.calls.length - 1][0]; + expect(lastSelectedItems.map((item) => item.name)).toEqual(['Chocolate', 'Vanilla']); + expect(tree.getByTestId('custom-value')).toHaveTextContent('Chocolate, Vanilla'); + }); + it('should support contextual help', async () => { // Issue with how we don't render the contextual help button in the fake DOM since PressResponder isn't using createHideableComponent let warn = jest.spyOn(global.console, 'warn').mockImplementation(); diff --git a/packages/dev/s2-docs/pages/s2/Picker.mdx b/packages/dev/s2-docs/pages/s2/Picker.mdx index 9c753d2f71b..25e65a4bd5b 100644 --- a/packages/dev/s2-docs/pages/s2/Picker.mdx +++ b/packages/dev/s2-docs/pages/s2/Picker.mdx @@ -242,6 +242,54 @@ 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'}; +import {useState} from 'react'; + +function Example(props) { + const users = Array.from({length: 5}, (_, i) => ({ + id: `user${i + 1}`, + name: `User ${i + 1}`, + email: `user${i + 1}@example.com`, + avatar: 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png', + })); + + return ( +
+ ( +
+ {selectedItems.map(item => ( + + ))} +
+ )} + ///- end highlight -/// + > + {(item) => + + + {item.name} + {item.email} + + } +
+
+ ); +} +``` + ## 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.