diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json
index c631e2beaf8..74c0a29fcda 100644
--- a/packages/@react-spectrum/s2/package.json
+++ b/packages/@react-spectrum/s2/package.json
@@ -136,6 +136,7 @@
"@react-aria/interactions": "^3.25.0",
"@react-aria/live-announcer": "^3.4.2",
"@react-aria/overlays": "^3.27.0",
+ "@react-aria/separator": "^3.4.8",
"@react-aria/utils": "^3.28.2",
"@react-spectrum/utils": "^3.12.4",
"@react-stately/layout": "^4.2.2",
diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx
index 99439d91e38..bbb69b968ba 100644
--- a/packages/@react-spectrum/s2/src/ComboBox.tsx
+++ b/packages/@react-spectrum/s2/src/ComboBox.tsx
@@ -23,43 +23,44 @@ import {
ListBoxItem,
ListBoxItemProps,
ListBoxProps,
+ ListLayout,
Provider,
- SectionProps
+ SectionProps,
+ SeparatorContext,
+ SeparatorProps,
+ useContextProps,
+ Virtualizer
} from 'react-aria-components';
-import {baseColor, style} from '../style' with {type: 'macro'};
+import {baseColor, edgeToText, focusRing, space, style} from '../style' with {type: 'macro'};
import {centerBaseline} from './CenterBaseline';
+import {centerPadding, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {
checkmark,
description,
- Divider,
icon,
iconCenterWrapper,
- label,
- menuitem,
- section,
- sectionHeader,
- sectionHeading
+ label
} from './Menu';
import CheckmarkIcon from '../ui-icons/Checkmark';
import ChevronIcon from '../ui-icons/Chevron';
-import {createContext, CSSProperties, forwardRef, ReactNode, Ref, useCallback, useContext, useImperativeHandle, useRef, useState} from 'react';
+import {createContext, CSSProperties, ElementType, ForwardedRef, forwardRef, ReactNode, Ref, useCallback, useContext, useImperativeHandle, useRef, useState} from 'react';
import {createFocusableRef} from '@react-spectrum/utils';
-import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
+import {createLeafComponent} from '@react-aria/collections';
+import {divider} from './Divider';
import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText, Input} from './Field';
+import {filterDOMProps, mergeRefs, useResizeObserver} from '@react-aria/utils';
import {FormContext, useFormProps} from './Form';
import {forwardRefType} from './types';
import {HeaderContext, HeadingContext, Text, TextContext} from './Content';
import {HelpTextProps, SpectrumLabelableProps} from '@react-types/shared';
import {IconContext} from './Icon';
-import {menu} from './Picker';
-import {mergeRefs, useResizeObserver} from '@react-aria/utils';
-import {Placement} from 'react-aria';
+import {mergeStyles} from '../style/runtime';
+import {Placement, useSeparator} from 'react-aria';
import {PopoverBase} from './Popover';
import {pressScale} from './pressScale';
import {TextFieldRef} from '@react-types/textfield';
import {useSpectrumContextProps} from './useSpectrumContextProps';
-
export interface ComboboxStyleProps {
/**
* The size of the Combobox.
@@ -148,6 +149,112 @@ const iconStyles = style({
}
});
+export let listbox = style<{size: 'S' | 'M' | 'L' | 'XL'}>({
+ width: 'full',
+ boxSizing: 'border-box',
+ maxHeight: '[inherit]',
+ overflow: 'auto',
+ fontFamily: 'sans',
+ fontSize: 'control'
+});
+
+export let listboxItem = style({
+ ...focusRing(),
+ boxSizing: 'border-box',
+ borderRadius: 'control',
+ font: 'control',
+ '--labelPadding': {
+ type: 'paddingTop',
+ value: centerPadding()
+ },
+ paddingBottom: '--labelPadding',
+ backgroundColor: { // TODO: revisit color when I have access to dev mode again
+ default: 'transparent',
+ isFocused: baseColor('gray-100').isFocusVisible
+ },
+ color: {
+ default: 'neutral',
+ isDisabled: {
+ default: 'disabled',
+ forcedColors: 'GrayText'
+ }
+ },
+ position: 'relative',
+ // each menu item should take up the entire width, the subgrid will handle within the item
+ gridColumnStart: 1,
+ gridColumnEnd: -1,
+ display: 'grid',
+ gridTemplateAreas: [
+ '. checkmark icon label .',
+ '. . . description .'
+ ],
+ gridTemplateColumns: {
+ size: {
+ S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', edgeToText(24)],
+ M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', edgeToText(32)],
+ L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', edgeToText(40)],
+ XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', edgeToText(48)]
+ }
+ },
+ gridTemplateRows: {
+ // min-content prevents second row from 'auto'ing to a size larger then 0 when empty
+ default: 'auto minmax(0, min-content)',
+ ':has([slot=description])': 'auto auto'
+ },
+ rowGap: {
+ ':has([slot=description])': space(1)
+ },
+ alignItems: 'baseline',
+ minHeight: 'control',
+ height: 'min',
+ textDecoration: 'none',
+ cursor: {
+ default: 'default',
+ isLink: 'pointer'
+ },
+ transition: 'default'
+}, getAllowedOverrides());
+
+export let listboxHeader = style<{size?: 'S' | 'M' | 'L' | 'XL'}>({
+ color: 'neutral',
+ boxSizing: 'border-box',
+ minHeight: 'control',
+ paddingY: centerPadding(),
+ marginX: {
+ size: {
+ S: `[${edgeToText(24)}]`,
+ M: `[${edgeToText(32)}]`,
+ L: `[${edgeToText(40)}]`,
+ XL: `[${edgeToText(48)}]`
+ }
+ }
+});
+
+export let listboxHeading = style({
+ fontSize: 'ui',
+ fontWeight: 'bold',
+ lineHeight: 'ui',
+ margin: 0
+});
+
+// not sure why edgeToText won't work...
+const separatorWrapper = style({
+ display: {
+ ':is(:last-child > *)': 'none',
+ default: 'flex'
+ },
+ // A workaround since edgeToText() returns undefined for some reason
+ marginX: {
+ size: {
+ S: `[${edgeToText(24)}]`,
+ M: `[${edgeToText(32)}]`,
+ L: `[${edgeToText(40)}]`,
+ XL: `[${edgeToText(48)}]`
+ }
+ },
+ height: 12
+});
+
let InternalComboboxContext = createContext<{size: 'S' | 'M' | 'L' | 'XL'}>({size: 'M'});
/**
@@ -305,19 +412,27 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
})}>
-
- {children}
-
+
+
+ {children}
+
+
@@ -327,7 +442,6 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
);
});
-
export interface ComboBoxItemProps extends Omit, StyleProps {
children: ReactNode
}
@@ -349,7 +463,7 @@ export function ComboBoxItem(props: ComboBoxItemProps): ReactNode {
ref={ref}
textValue={props.textValue || (typeof props.children === 'string' ? props.children as string : undefined)}
style={pressScale(ref, props.UNSAFE_style)}
- className={renderProps => (props.UNSAFE_className || '') + menuitem({...renderProps, size, isLink}, props.styles)}>
+ className={renderProps => (props.UNSAFE_className || '') + listboxItem({...renderProps, size, isLink}, props.styles)}>
{(renderProps) => {
let {children} = props;
return (
@@ -384,11 +498,52 @@ export function ComboBoxSection(props: ComboBoxSectionProps
return (
<>
+ {...props}>
{props.children}
-
+
>
);
}
+
+export function Divider(props: SeparatorProps & {size?: 'S' | 'M' | 'L' | 'XL' | undefined}): ReactNode {
+ return (
+
+ );
+}
+
+const Separator = /*#__PURE__*/ createLeafComponent('separator', function Separator(props: SeparatorProps & {size?: 'S' | 'M' | 'L' | 'XL'}, ref: ForwardedRef) {
+ [props, ref] = useContextProps(props, ref, SeparatorContext);
+
+ let {elementType, orientation, size, style, className, slot, ...otherProps} = props;
+ let Element = (elementType as ElementType) || 'hr';
+ if (Element === 'hr' && orientation === 'vertical') {
+ Element = 'div';
+ }
+
+ let {separatorProps} = useSeparator({
+ ...otherProps,
+ elementType,
+ orientation
+ });
+
+ return (
+
+
+
+ );
+});
+
diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx
index ea3a15d66d5..f3bbcf9e1a1 100644
--- a/packages/@react-spectrum/s2/src/Picker.tsx
+++ b/packages/@react-spectrum/s2/src/Picker.tsx
@@ -23,26 +23,30 @@ import {
ListBoxItem,
ListBoxItemProps,
ListBoxProps,
+ ListLayout,
Provider,
SectionProps,
- SelectValue
+ SelectValue,
+ Virtualizer
} from 'react-aria-components';
import {baseColor, edgeToText, focusRing, style} from '../style' with {type: 'macro'};
import {centerBaseline} from './CenterBaseline';
import {
checkmark,
description,
- Divider,
icon,
iconCenterWrapper,
- label,
- menuitem,
- section,
- sectionHeader,
- sectionHeading
+ label
} from './Menu';
import CheckmarkIcon from '../ui-icons/Checkmark';
import ChevronIcon from '../ui-icons/Chevron';
+import {
+ Divider,
+ listbox,
+ listboxHeader,
+ listboxHeading,
+ listboxItem
+} from './ComboBox';
import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {
FieldErrorIcon,
@@ -403,19 +407,27 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
})(props)}>
-
- {children}
-
+
+
+ {children}
+
+
@@ -446,7 +458,7 @@ export function PickerItem(props: PickerItemProps): ReactNode {
ref={ref}
textValue={props.textValue || (typeof props.children === 'string' ? props.children as string : undefined)}
style={pressScale(ref, props.UNSAFE_style)}
- className={renderProps => (props.UNSAFE_className || '') + menuitem({...renderProps, size, isLink}, props.styles)}>
+ className={renderProps => (props.UNSAFE_className || '') + listboxItem({...renderProps, size, isLink}, props.styles)}>
{(renderProps) => {
let {children} = props;
return (
@@ -489,11 +501,10 @@ export function PickerSection(props: PickerSectionProps): R
return (
<>
+ {...props}>
{props.children}
-
+
>
);
}
diff --git a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
index 802b7971840..e1f931163fe 100644
--- a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
+++ b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
@@ -99,6 +99,28 @@ export const Dynamic: Story = {
};
+function VirtualizedCombobox(props) {
+ let items: IExampleItem[] = [];
+ for (let i = 0; i < 10000; i++) {
+ items.push({id: i.toString(), label: `Item ${i}`});
+ }
+
+ return (
+
+ {(item) => {(item as IExampleItem).label}}
+
+ );
+}
+
+export const ManyItems: Story = {
+ render: (args) => (
+
+ ),
+ args: {
+ label: 'Many items'
+ }
+};
+
export const WithIcons: Story = {
render: (args) => (
diff --git a/packages/@react-spectrum/s2/stories/Picker.stories.tsx b/packages/@react-spectrum/s2/stories/Picker.stories.tsx
index 0520c236b9e..8b514fc9b6e 100644
--- a/packages/@react-spectrum/s2/stories/Picker.stories.tsx
+++ b/packages/@react-spectrum/s2/stories/Picker.stories.tsx
@@ -131,6 +131,28 @@ export const WithIcons: Story = {
}
};
+function VirtualizedPicker(props) {
+ let items: IExampleItem[] = [];
+ for (let i = 0; i < 10000; i++) {
+ items.push({id: i.toString(), label: `Item ${i}`});
+ }
+
+ return (
+
+ {(item) => {(item as IExampleItem).label}}
+
+ );
+}
+
+export const ManyItems: Story = {
+ render: (args) => (
+
+ ),
+ args: {
+ label: 'Many items'
+ }
+};
+
const ValidationRender = (props) => (