Skip to content

Commit c44619e

Browse files
committed
Added section support to Picker (#plugins-292)
1 parent c6ef5b3 commit c44619e

2 files changed

Lines changed: 107 additions & 23 deletions

File tree

packages/components/src/spectrum/picker/Picker.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { useMemo } from 'react';
2-
import { Item, Picker as SpectrumPicker } from '@adobe/react-spectrum';
1+
import { useCallback, useMemo } from 'react';
2+
import { Item, Picker as SpectrumPicker, Section } from '@adobe/react-spectrum';
33
import { Tooltip } from '../../popper';
44
import {
55
NormalizedSpectrumPickerProps,
66
normalizePickerItemList,
77
normalizeTooltipOptions,
8-
PickerItem,
8+
PickerItemOrSection,
99
PickerItemKey,
1010
TooltipOptions,
11+
NormalizedPickerItem,
1112
} from './PickerUtils';
1213
import { PickerItemContent } from './PickerItemContent';
1314

1415
export type PickerProps = {
15-
children: PickerItem | PickerItem[];
16+
children: PickerItemOrSection | PickerItemOrSection[];
1617
/** Can be set to true or a TooltipOptions to enable item tooltips */
1718
tooltip?: boolean | TooltipOptions;
1819
/** The currently selected key in the collection (controlled). */
@@ -71,6 +72,18 @@ export function Picker({
7172
[tooltip]
7273
);
7374

75+
const renderItem = useCallback(
76+
({ content, textValue }: NormalizedPickerItem) => (
77+
<Item textValue={textValue === '' ? 'Empty' : textValue}>
78+
<PickerItemContent>{content}</PickerItemContent>
79+
{tooltipOptions == null || content === '' ? null : (
80+
<Tooltip options={tooltipOptions}>{content}</Tooltip>
81+
)}
82+
</Item>
83+
),
84+
[tooltipOptions]
85+
);
86+
7487
return (
7588
<SpectrumPicker
7689
// eslint-disable-next-line react/jsx-props-no-spreading
@@ -89,14 +102,21 @@ export function Picker({
89102
onSelectionChange) as NormalizedSpectrumPickerProps['onSelectionChange']
90103
}
91104
>
92-
{({ content, textValue }) => (
93-
<Item textValue={textValue === '' ? 'Empty' : textValue}>
94-
<PickerItemContent>{content}</PickerItemContent>
95-
{tooltipOptions == null || content === '' ? null : (
96-
<Tooltip options={tooltipOptions}>{content}</Tooltip>
97-
)}
98-
</Item>
99-
)}
105+
{itemOrSection => {
106+
if ('items' in itemOrSection) {
107+
return (
108+
<Section
109+
key={itemOrSection.key}
110+
title={itemOrSection.title}
111+
items={itemOrSection.items}
112+
>
113+
{renderItem}
114+
</Section>
115+
);
116+
}
117+
118+
return renderItem(itemOrSection);
119+
}}
100120
</SpectrumPicker>
101121
);
102122
}

packages/components/src/spectrum/picker/PickerUtils.ts

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1-
import { Key, ReactElement, ReactNode } from 'react';
2-
import type { SpectrumPickerProps } from '@adobe/react-spectrum';
3-
import type { ItemProps } from '@react-types/shared';
1+
import { isValidElement, Key, ReactElement, ReactNode } from 'react';
2+
import { Item, Section, SpectrumPickerProps } from '@adobe/react-spectrum';
3+
import type {
4+
ItemProps,
5+
ItemRenderer,
6+
SectionProps,
7+
} from '@react-types/shared';
48
import { PopperOptions } from '../../popper';
59

10+
export type SectionPropsNoItemRenderer<T> = Omit<
11+
SectionProps<T>,
12+
'children'
13+
> & {
14+
children: Exclude<SectionProps<T>['children'], ItemRenderer<T>>;
15+
};
16+
617
export type ItemElement = ReactElement<ItemProps<unknown>>;
18+
export type SectionElement = ReactElement<SectionPropsNoItemRenderer<unknown>>;
719
export type PickerItem = number | string | boolean | ItemElement;
20+
export type PickerSection = SectionElement;
21+
export type PickerItemOrSection = PickerItem | PickerSection;
822

923
/**
1024
* Augment the Spectrum selection key type to include boolean values.
@@ -32,32 +46,64 @@ export interface NormalizedPickerItem {
3246
textValue: string;
3347
}
3448

49+
export interface NormalizedPickerSection {
50+
key: Key;
51+
title: ReactNode;
52+
items: NormalizedPickerItem[];
53+
}
54+
3555
export type NormalizedSpectrumPickerProps =
3656
SpectrumPickerProps<NormalizedPickerItem>;
3757

3858
export type TooltipOptions = { placement: PopperOptions['placement'] };
3959

60+
export function isSectionElement<T>(
61+
node: ReactNode
62+
): node is ReactElement<SectionProps<T>> {
63+
return isValidElement<SectionProps<T>>(node) && node.type === Section;
64+
}
65+
66+
export function isItemElement<T>(
67+
node: ReactNode
68+
): node is ReactElement<ItemProps<T>> {
69+
return isValidElement<ItemProps<T>>(node) && node.type === Item;
70+
}
71+
4072
/**
4173
* Determine the `key` of a picker item.
42-
* @param item The picker item
74+
* @param item The picker item or section
4375
* @returns A `PickerItemKey` for the picker item
4476
*/
45-
function normalizeItemKey(item: PickerItem): PickerItemKey {
77+
function normalizeItemKey(item: PickerItem): PickerItemKey;
78+
function normalizeItemKey(item: PickerSection): Key;
79+
function normalizeItemKey(
80+
item: PickerItem | PickerSection
81+
): Key | PickerItemKey {
4682
// string, number, or boolean
4783
if (typeof item !== 'object') {
48-
return item;
84+
return item as PickerItemKey;
4985
}
5086

51-
// `ItemElement` with `key` prop set
87+
// If `key` prop is explicitly set
5288
if (item.key != null) {
5389
return item.key;
5490
}
5591

92+
if (isSectionElement(item)) {
93+
if (typeof item.props.title === 'string') {
94+
return item.props.title;
95+
}
96+
} else if (isItemElement(item)) {
97+
if (item.props.textValue != null) {
98+
return item.props.textValue;
99+
}
100+
}
101+
56102
if (typeof item.props.children === 'string') {
57103
return item.props.children;
58104
}
59105

60-
return item.props.textValue ?? '';
106+
return '';
61107
}
62108

63109
/**
@@ -86,7 +132,25 @@ function normalizeTextValue(item: PickerItem): string {
86132
* @param item item to normalize
87133
* @returns NormalizedPickerItem object
88134
*/
89-
function normalizePickerItem(item: PickerItem): NormalizedPickerItem {
135+
function normalizePickerItem(
136+
item: PickerItemOrSection
137+
): NormalizedPickerItem | NormalizedPickerSection {
138+
if (isSectionElement(item)) {
139+
const key = normalizeItemKey(item);
140+
const { title } = item.props;
141+
142+
const items = normalizePickerItemList(item.props.children).filter(
143+
// We don't support nested section elements
144+
childItem => !isSectionElement(childItem)
145+
) as NormalizedPickerItem[];
146+
147+
return {
148+
key,
149+
title,
150+
items,
151+
};
152+
}
153+
90154
const key = normalizeItemKey(item);
91155
const content = typeof item === 'object' ? item.props.children : String(item);
92156
const textValue = normalizeTextValue(item);
@@ -104,8 +168,8 @@ function normalizePickerItem(item: PickerItem): NormalizedPickerItem {
104168
* @returns An array of normalized picker items
105169
*/
106170
export function normalizePickerItemList(
107-
items: PickerItem | PickerItem[]
108-
): NormalizedPickerItem[] {
171+
items: PickerItemOrSection | PickerItemOrSection[]
172+
): (NormalizedPickerItem | NormalizedPickerSection)[] {
109173
const itemsArray = Array.isArray(items) ? items : [items];
110174
return itemsArray.map(normalizePickerItem);
111175
}

0 commit comments

Comments
 (0)