diff --git a/packages/@react-spectrum/s2/src/ActionMenu.tsx b/packages/@react-spectrum/s2/src/ActionMenu.tsx index 72f5f6ff85b..94a72825835 100644 --- a/packages/@react-spectrum/s2/src/ActionMenu.tsx +++ b/packages/@react-spectrum/s2/src/ActionMenu.tsx @@ -51,9 +51,10 @@ export const ActionMenuContext = /** * ActionMenu combines an ActionButton with a Menu for simple "more actions" use cases. */ -export const ActionMenu = /*#__PURE__*/ (forwardRef as forwardRefType)(function ActionMenu< - T extends object ->(props: ActionMenuProps, ref: FocusableRef) { +export const ActionMenu = /*#__PURE__*/ (forwardRef as forwardRefType)(function ActionMenu( + props: ActionMenuProps, + ref: FocusableRef +) { let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); [props, ref] = useSpectrumContextProps(props, ref, ActionMenuContext); let buttonProps = filterDOMProps(props, {labelable: true}); diff --git a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx index a4984f9d2b2..5316eb4fe78 100644 --- a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx +++ b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx @@ -140,9 +140,10 @@ const wrapper = style( const InternalBreadcrumbsContext = createContext>>({}); /** Breadcrumbs show hierarchy and navigational context for a user's location within an application. */ -export const Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(function Breadcrumbs< - T extends object ->(props: BreadcrumbsProps, ref: DOMRef) { +export const Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(function Breadcrumbs( + props: BreadcrumbsProps, + ref: DOMRef +) { [props, ref] = useSpectrumContextProps(props, ref, BreadcrumbsContext); let domRef = useDOMRef(ref); let { diff --git a/packages/@react-spectrum/s2/src/CardView.tsx b/packages/@react-spectrum/s2/src/CardView.tsx index 161a4c55d86..0eb6675dd7a 100644 --- a/packages/@react-spectrum/s2/src/CardView.tsx +++ b/packages/@react-spectrum/s2/src/CardView.tsx @@ -234,9 +234,10 @@ export const CardViewContext = /** * A CardView displays a group of related objects, with support for selection and bulk actions. */ -export const CardView = /*#__PURE__*/ (forwardRef as forwardRefType)(function CardView< - T extends object ->(props: CardViewProps, ref: DOMRef) { +export const CardView = /*#__PURE__*/ (forwardRef as forwardRefType)(function CardView( + props: CardViewProps, + ref: DOMRef +) { [props, ref] = useSpectrumContextProps(props, ref, CardViewContext); let { children, diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 92d94d51453..0412a90ebe5 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -100,7 +100,7 @@ export interface ComboboxStyleProps { */ size?: 'S' | 'M' | 'L' | 'XL'; } -export interface ComboBoxProps +export interface ComboBoxProps extends Omit< AriaComboBoxProps, @@ -379,9 +379,10 @@ let InternalComboboxContext = createContext<{size: 'S' | 'M' | 'L' | 'XL'}>({siz /** * ComboBox allow users to choose a single option from a collapsible list of options when space is limited. */ -export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function ComboBox< - T extends object ->(props: ComboBoxProps, ref: Ref) { +export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function ComboBox( + props: ComboBoxProps, + ref: Ref +) { [props, ref] = useSpectrumContextProps(props, ref, ComboBoxContext); let formContext = useContext(FormContext); @@ -521,11 +522,11 @@ export function ComboBoxItem(props: ComboBoxItemProps): ReactNode { ); } -export interface ComboBoxSectionProps extends Omit< +export interface ComboBoxSectionProps extends Omit< ListBoxSectionProps, 'style' | 'className' | 'render' | keyof GlobalDOMAttributes > {} -export function ComboBoxSection(props: ComboBoxSectionProps): ReactNode { +export function ComboBoxSection(props: ComboBoxSectionProps): ReactNode { let {size} = useContext(InternalComboboxContext); return ( <> diff --git a/packages/@react-spectrum/s2/src/ListBox.tsx b/packages/@react-spectrum/s2/src/ListBox.tsx index 9be6184376e..5a352b7058a 100644 --- a/packages/@react-spectrum/s2/src/ListBox.tsx +++ b/packages/@react-spectrum/s2/src/ListBox.tsx @@ -18,7 +18,7 @@ import { } from 'react-aria-components/ListBox'; import {ReactNode} from 'react'; -export function ListBox({children, ...props}: ListBoxProps): ReactNode { +export function ListBox({children, ...props}: ListBoxProps): ReactNode { return {children}; } diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 90337eddb24..05284dd2a93 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -208,9 +208,10 @@ const listView = style({ /** * A ListView displays a list of interactive items, and allows a user to navigate, select, or perform an action. */ -export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListView< - T extends object ->(props: ListViewProps, ref: DOMRef) { +export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListView( + props: ListViewProps, + ref: DOMRef +) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); let { children, diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 0d865dc0fd5..c2e46a7803b 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -422,7 +422,7 @@ let wrappingDiv = style({ /** * Menus display a list of actions or options that a user can choose. */ -export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu( +export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu( props: MenuProps, ref: DOMRef ) { @@ -509,12 +509,12 @@ export function Divider(props: SeparatorProps): ReactNode { ); } -export interface MenuSectionProps extends Omit< +export interface MenuSectionProps extends Omit< AriaMenuSectionProps, 'style' | 'className' | 'render' | keyof GlobalDOMAttributes > {} -export function MenuSection(props: MenuSectionProps): ReactNode { +export function MenuSection(props: MenuSectionProps): ReactNode { // remember, context doesn't work if it's around Section nor inside let {size} = useContext(InternalMenuContext); return ( @@ -823,8 +823,8 @@ function UnavailableMenuItemTrigger(props: UnavailableMenuItemTriggerProps): JSX export {MenuTrigger, SubmenuTrigger, UnavailableMenuItemTrigger}; // This is purely so that storybook generates the types for both Menu and MenuTrigger -interface ICombined extends MenuProps, Omit {} +interface ICombined extends MenuProps, Omit {} // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function CombinedMenu(props: ICombined): ReactNode { +export function CombinedMenu(props: ICombined): ReactNode { return
; } diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 4e7dc1350ee..c81eb26a6fb 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -119,7 +119,7 @@ export interface PickerStyleProps { } type SelectionMode = 'single' | 'multiple'; -export interface PickerProps +export interface PickerProps extends Omit< AriaSelectProps, @@ -315,7 +315,7 @@ let InsideSelectValueContext = createContext(false); * Pickers allow users to choose a single option from a collapsible list of options when space is limited. */ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Picker< - T extends object, + T, M extends SelectionMode = 'single' >(props: PickerProps, ref: FocusableRef) { let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); @@ -565,7 +565,7 @@ let INTERACTIVE_ARIA_ROLES = new Set([ 'treeitem' ]); -interface PickerButtonInnerProps +interface PickerButtonInnerProps extends PickerStyleProps, Omit, @@ -575,7 +575,7 @@ interface PickerButtonInnerProps } // Needs to be hidable component or otherwise the PressResponder throws a warning when rendered in the fake DOM and tries to register -const PickerButton = createHideableComponent(function PickerButton( +const PickerButton = createHideableComponent(function PickerButton( props: PickerButtonInnerProps ) { let { @@ -875,11 +875,11 @@ function DefaultProvider({ return {children}; } -export interface PickerSectionProps extends Omit< +export interface PickerSectionProps extends Omit< ListBoxSectionProps, 'style' | 'className' | 'render' | keyof GlobalDOMAttributes > {} -export function PickerSection(props: PickerSectionProps): ReactNode { +export function PickerSection(props: PickerSectionProps): ReactNode { let {size} = useContext(InternalPickerContext); return ( <> diff --git a/packages/@react-spectrum/s2/src/SelectBoxGroup.tsx b/packages/@react-spectrum/s2/src/SelectBoxGroup.tsx index 6cc18eea904..b7c14812bc2 100644 --- a/packages/@react-spectrum/s2/src/SelectBoxGroup.tsx +++ b/packages/@react-spectrum/s2/src/SelectBoxGroup.tsx @@ -413,7 +413,7 @@ export function SelectBox(props: SelectBoxProps): ReactNode { * SelectBoxGroup allows users to select one or more options from a list. */ export const SelectBoxGroup = /*#__PURE__*/ (forwardRef as forwardRefType)(function SelectBoxGroup< - T extends object + T >(props: SelectBoxGroupProps, ref: DOMRef) { [props, ref] = useSpectrumContextProps(props, ref, SelectBoxGroupContext); diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 6ddd5b9e841..8a1ec353d2d 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -500,9 +500,10 @@ export interface TableBodyProps extends Omit< /** * The body of a ``, containing the table rows. */ -export const TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableBody< - T extends object ->(props: TableBodyProps, ref: DOMRef) { +export const TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableBody( + props: TableBodyProps, + ref: DOMRef +) { let {items, renderEmptyState, children, dependencies = []} = props; let domRef = useDOMRef(ref); let {loadingState, onLoadMore} = useContext(InternalTableContext); @@ -1066,9 +1067,10 @@ export interface TableHeaderProps extends Omit< /** * A header within a `
`, containing the table columns. */ -export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableHeader< - T extends object ->({columns, dependencies, children}: TableHeaderProps, ref: DOMRef) { +export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableHeader( + {columns, dependencies, children}: TableHeaderProps, + ref: DOMRef +) { let scale = useScale(); let {selectionBehavior, selectionMode} = useTableOptions(); let {isQuiet} = useContext(InternalTableContext); @@ -1843,7 +1845,7 @@ export interface RowProps /** * A row within a `
`. */ -export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row( +export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row( {id, columns, children, dependencies = [], ...otherProps}: RowProps, ref: DOMRef ) { @@ -1905,9 +1907,10 @@ const FooterContext = createContext(false); /** * A footer within a `
`, containing summary rows. */ -export const TableFooter = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableFooter< - T extends object ->(props: TableFooterProps, ref: DOMRef) { +export const TableFooter = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableFooter( + props: TableFooterProps, + ref: DOMRef +) { let domRef = useDOMRef(ref); return ( diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 897475abc3d..71ef7138940 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -280,7 +280,7 @@ const tablistWrapper = style( getAllowedOverrides() ); -export function TabList(props: TabListProps): ReactNode | null { +export function TabList(props: TabListProps): ReactNode | null { let {showTabs, menuId, valueId, tabs, listRef, onSelectionChange, ariaLabel, ariaDescribedBy} = useContext(CollapseContext) ?? {}; let {density, orientation, labelBehavior} = useContext(InternalTabsContext); @@ -308,7 +308,7 @@ export function TabList(props: TabListProps): ReactNode | n ); } -function TabListInner(props: TabListProps) { +function TabListInner(props: TabListProps) { let { tablistRef, orientation, diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index f9033785906..349e86bdb6a 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -41,7 +41,7 @@ import {useFormProps} from './Form'; import {useLocale} from 'react-aria/I18nProvider'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface PickerStyleProps {} -export interface PickerProps +export interface PickerProps extends Omit, 'children' | 'style' | 'className' | 'render' | 'placeholder'>, PickerStyleProps, @@ -159,7 +159,7 @@ const iconCenterWrapper = style({ } }); let InsideSelectValueContext = createContext(false); -function Picker(props: PickerProps, ref: FocusableRef) { +function Picker(props: PickerProps, ref: FocusableRef) { [props, ref] = useSpectrumContextProps(props, ref, PickerContext); let domRef = useFocusableRef(ref); props = useFormProps(props); diff --git a/packages/@react-spectrum/s2/src/TagGroup.tsx b/packages/@react-spectrum/s2/src/TagGroup.tsx index c1c9a3ad998..663fed686e6 100644 --- a/packages/@react-spectrum/s2/src/TagGroup.tsx +++ b/packages/@react-spectrum/s2/src/TagGroup.tsx @@ -123,9 +123,10 @@ export const TagGroupContext = const InternalTagGroupContext = createContext>({}); /** Tags allow users to categorize content. They can represent keywords or people, and are grouped to describe an item or a search request. */ -export const TagGroup = /*#__PURE__*/ (forwardRef as forwardRefType)(function TagGroup< - T extends object ->(props: TagGroupProps, ref: DOMRef) { +export const TagGroup = /*#__PURE__*/ (forwardRef as forwardRefType)(function TagGroup( + props: TagGroupProps, + ref: DOMRef +) { [props, ref] = useSpectrumContextProps(props, ref, TagGroupContext); props = useFormProps(props); let {onRemove} = props; diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 18e11d145c9..e753f6cfeef 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -158,9 +158,10 @@ let InternalTreeViewContext = createContext<{selectionStyle?: 'highlight' | 'che /** * A tree view provides users with a way to navigate nested hierarchical information. */ -export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function TreeView< - T extends object ->(props: TreeViewProps, ref: DOMRef) { +export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function TreeView( + props: TreeViewProps, + ref: DOMRef +) { let {children, selectionStyle = 'checkbox', UNSAFE_className, UNSAFE_style} = props; let scale = useScale(); diff --git a/packages/react-aria-components/src/Autocomplete.tsx b/packages/react-aria-components/src/Autocomplete.tsx index d7276f8abb2..e8fadc0f6eb 100644 --- a/packages/react-aria-components/src/Autocomplete.tsx +++ b/packages/react-aria-components/src/Autocomplete.tsx @@ -73,7 +73,7 @@ export const FieldInputContext = /** * An autocomplete allows users to search or filter a list of suggestions. */ -export function Autocomplete(props: AutocompleteProps): JSX.Element { +export function Autocomplete(props: AutocompleteProps): JSX.Element { let ctx = useSlottedContext(AutocompleteContext, props.slot); props = mergeProps(ctx, props); let {filter, disableAutoFocusFirst} = props; diff --git a/packages/react-aria-components/src/Breadcrumbs.tsx b/packages/react-aria-components/src/Breadcrumbs.tsx index 7df168c6b15..2a48951259a 100644 --- a/packages/react-aria-components/src/Breadcrumbs.tsx +++ b/packages/react-aria-components/src/Breadcrumbs.tsx @@ -60,9 +60,10 @@ export const BreadcrumbsContext = /** * Breadcrumbs display a hierarchy of links to the current page or resource in an application. */ -export const Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(function Breadcrumbs< - T extends object ->(props: BreadcrumbsProps, ref: ForwardedRef) { +export const Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(function Breadcrumbs( + props: BreadcrumbsProps, + ref: ForwardedRef +) { [props, ref] = useContextProps(props, ref, BreadcrumbsContext); let {CollectionRoot} = useContext(CollectionRendererContext); let {navProps} = useBreadcrumbs(props); diff --git a/packages/react-aria-components/src/Collection.tsx b/packages/react-aria-components/src/Collection.tsx index 777de219bc4..db29c05e2be 100644 --- a/packages/react-aria-components/src/Collection.tsx +++ b/packages/react-aria-components/src/Collection.tsx @@ -139,7 +139,7 @@ export const SectionContext = createContext(null); /** @deprecated */ export const Section = /*#__PURE__*/ createBranchComponent( 'section', - ( + ( props: SectionProps, ref: ForwardedRef, section: Node diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx index cb55bbfc669..1b2d1d8bf2b 100644 --- a/packages/react-aria-components/src/ComboBox.tsx +++ b/packages/react-aria-components/src/ComboBox.tsx @@ -87,7 +87,7 @@ export interface ComboBoxRenderProps { isReadOnly: boolean; } -export interface ComboBoxProps +export interface ComboBoxProps extends Omit< AriaComboBoxProps, @@ -128,7 +128,7 @@ export const ComboBoxStateContext = createContext(props: ComboBoxProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, ComboBoxContext); @@ -167,17 +167,13 @@ export const ComboBox = /*#__PURE__*/ createHideableComponent(function ComboBox< // Contexts to clear inside the popover. const CLEAR_CONTEXTS = [LabelContext, ButtonContext, InputContext, GroupContext, TextContext]; -interface ComboBoxInnerProps { +interface ComboBoxInnerProps { props: ComboBoxProps; collection: Collection>; comboBoxRef: RefObject; } -function ComboBoxInner({ - props, - collection, - comboBoxRef: ref -}: ComboBoxInnerProps) { +function ComboBoxInner({props, collection, comboBoxRef: ref}: ComboBoxInnerProps) { let {name, formValue = 'key', allowsCustomValue} = props; if (allowsCustomValue) { formValue = 'text'; @@ -357,7 +353,7 @@ export interface ComboBoxValueRenderProps { state: ComboBoxState; } -export interface ComboBoxValueProps +export interface ComboBoxValueProps extends Omit, keyof RenderProps>, RenderProps, 'div'> { @@ -377,9 +373,10 @@ export const ComboBoxValueContext = * ComboBoxValue renders the selected values of a ComboBox, or a placeholder if no value is selected. * By default, the items are rendered as a comma separated list. Use the render function to customize this. */ -export const ComboBoxValue = /*#__PURE__*/ createHideableComponent(function ComboBoxValue< - T extends object ->(props: ComboBoxValueProps, ref: ForwardedRef) { +export const ComboBoxValue = /*#__PURE__*/ createHideableComponent(function ComboBoxValue( + props: ComboBoxValueProps, + ref: ForwardedRef +) { [props, ref] = useContextProps(props, ref, ComboBoxValueContext); let state = useContext(ComboBoxStateContext)!; let formatter = useListFormatter(); diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 878f2b4afc5..82ad717302d 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -197,9 +197,10 @@ export const GridListContext = * A grid list displays a list of interactive items, with support for keyboard navigation, * single or multiple selection, and row actions. */ -export const GridList = /*#__PURE__*/ (forwardRef as forwardRefType)(function GridList< - T extends object ->(props: GridListProps, ref: ForwardedRef) { +export const GridList = /*#__PURE__*/ (forwardRef as forwardRefType)(function GridList( + props: GridListProps, + ref: ForwardedRef +) { // Render the portal first so that we have the collection by the time we render the DOM in SSR. [props, ref] = useContextProps(props, ref, GridListContext); @@ -210,17 +211,13 @@ export const GridList = /*#__PURE__*/ (forwardRef as forwardRefType)(function Gr ); }); -interface GridListInnerProps { +interface GridListInnerProps { props: GridListProps & SelectableCollectionContextValue; - collection: ICollection>; + collection: ICollection>; gridListRef: RefObject; } -function GridListInner({ - props, - collection, - gridListRef: ref -}: GridListInnerProps) { +function GridListInner({props, collection, gridListRef: ref}: GridListInnerProps) { [props, ref] = useContextProps(props, ref, SelectableCollectionContext); // eslint-disable-next-line @typescript-eslint/no-unused-vars let {shouldUseVirtualFocus, filter, disallowTypeAhead, ...DOMCollectionProps} = props; @@ -472,7 +469,7 @@ export interface GridListItemProps * A GridListItem represents an individual item in a GridList. */ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function GridListItem< - T extends object + T >(props: GridListItemProps, forwardedRef: ForwardedRef, item: Node) { let state = useContext(ListStateContext)!; let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext); @@ -839,7 +836,7 @@ export interface GridListSectionProps extends SectionProps, DOMRenderProps */ export const GridListSection = /*#__PURE__*/ createBranchComponent( SectionNode, - ( + ( props: GridListSectionProps, ref: ForwardedRef, item: Node diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index 6fc5dead6a2..6eb600ca455 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -185,9 +185,10 @@ export const ListStateContext = createContext | null>(null); /** * A listbox displays a list of options and allows a user to select one or more of them. */ -export const ListBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListBox< - T extends object ->(props: ListBoxProps, ref: ForwardedRef) { +export const ListBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListBox( + props: ListBoxProps, + ref: ForwardedRef +) { [props, ref] = useContextProps(props, ref, ListBoxContext); let state = useContext(ListStateContext); @@ -221,11 +222,7 @@ interface ListBoxInnerProps { listBoxRef: RefObject; } -function ListBoxInner({ - state: inputState, - props, - listBoxRef -}: ListBoxInnerProps) { +function ListBoxInner({state: inputState, props, listBoxRef}: ListBoxInnerProps) { [props, listBoxRef] = useContextProps(props, listBoxRef, SelectableCollectionContext); let {dragAndDropHooks, layout = 'stack', orientation = 'vertical', filter} = props; let state = UNSTABLE_useFilteredListState(inputState, filter); @@ -430,7 +427,7 @@ export interface ListBoxSectionProps className?: string; } -function ListBoxSectionInner( +function ListBoxSectionInner( props: ListBoxSectionProps, ref: ForwardedRef, section: Node, @@ -511,7 +508,7 @@ export interface ListBoxItemProps * A ListBoxItem represents an individual option in a ListBox. */ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function ListBoxItem< - T extends object + T >(props: ListBoxItemProps, forwardedRef: ForwardedRef, item: Node) { let ref = useObjectRef(forwardedRef); let state = useContext(ListStateContext)!; diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 0b9a4edddb1..854dc891908 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -79,10 +79,10 @@ import { } from '@react-types/shared'; import {FocusScope} from 'react-aria/FocusScope'; import {HeaderContext} from './Header'; -import {Collection as ICollection, Node} from '@react-types/shared'; import {KeyboardContext} from './Keyboard'; import {mergeProps} from 'react-aria/mergeProps'; import {MultipleSelectionState} from 'react-stately/useMultipleSelectionState'; +import {Node} from '@react-types/shared'; import {OverlayTriggerStateContext} from './Dialog'; import {PopoverContext} from './Popover'; import {PressResponder} from 'react-aria/private/interactions/PressResponder'; @@ -284,7 +284,7 @@ export interface MenuProps /** * A menu displays a list of actions or options that a user can choose. */ -export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu( +export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu( props: MenuProps, ref: ForwardedRef ) { @@ -305,11 +305,11 @@ interface MenuInnerProps { filter?: SelectableCollectionContextValue['filter']; shouldUseVirtualFocus?: boolean; }; - collection: BaseCollection; + collection: BaseCollection; menuRef: RefObject; } -function MenuInner({props, collection, menuRef: ref}: MenuInnerProps) { +function MenuInner({props, collection, menuRef: ref}: MenuInnerProps) { [props, ref] = useContextProps(props, ref, SelectableCollectionContext); let {filter, ...autocompleteMenuProps} = props; let filteredCollection = useMemo( @@ -318,7 +318,7 @@ function MenuInner({props, collection, menuRef: ref}: MenuInne ); let state = useTreeState({ ...props, - collection: filteredCollection as ICollection>, + collection: filteredCollection, children: undefined }); let triggerState = useContext(RootMenuTriggerStateContext); @@ -436,7 +436,7 @@ class GroupSelectionManager extends SelectionManager { } } -function MenuSectionInner( +function MenuSectionInner( props: MenuSectionProps, ref: ForwardedRef, section: Node, @@ -539,7 +539,7 @@ const MenuItemContext = createContext(props: MenuItemProps, forwardedRef: ForwardedRef, item: Node) { [props, forwardedRef] = useContextProps(props, forwardedRef, MenuItemContext); let id = useSlottedContext(MenuItemContext)?.id as string; diff --git a/packages/react-aria-components/src/Select.tsx b/packages/react-aria-components/src/Select.tsx index 8eaec022cb7..ddb47c06479 100644 --- a/packages/react-aria-components/src/Select.tsx +++ b/packages/react-aria-components/src/Select.tsx @@ -93,7 +93,7 @@ export interface SelectRenderProps { isRequired: boolean; } -export interface SelectProps +export interface SelectProps extends Omit< AriaSelectProps, @@ -129,7 +129,7 @@ export const SelectStateContext = createContext(props: SelectProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, SelectContext); @@ -160,13 +160,13 @@ export const Select = /*#__PURE__*/ createHideableComponent(function Select< // Contexts to clear inside the popover. const CLEAR_CONTEXTS = [LabelContext, ButtonContext, TextContext]; -interface SelectInnerProps { +interface SelectInnerProps { props: SelectProps; selectRef: ForwardedRef; collection: Collection>; } -function SelectInner({props, selectRef: ref, collection}: SelectInnerProps) { +function SelectInner({props, selectRef: ref, collection}: SelectInnerProps) { let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {}; let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native'; let state = useSelectState({ @@ -303,7 +303,7 @@ export interface SelectValueRenderProps { state: SelectState; } -export interface SelectValueProps +export interface SelectValueProps extends Omit, keyof RenderProps>, RenderProps, 'span'> { @@ -321,9 +321,10 @@ export const SelectValueContext = * SelectValue renders the current value of a Select, or a placeholder if no value is selected. * It is usually placed within the button element. */ -export const SelectValue = /*#__PURE__*/ createHideableComponent(function SelectValue< - T extends object ->(props: SelectValueProps, ref: ForwardedRef) { +export const SelectValue = /*#__PURE__*/ createHideableComponent(function SelectValue( + props: SelectValueProps, + ref: ForwardedRef +) { [props, ref] = useContextProps(props, ref, SelectValueContext); let state = useContext(SelectStateContext)! as SelectState; let {placeholder} = useSlottedContext(SelectContext)!; diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index e020e0369ba..c5b9aff9fa0 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -974,7 +974,7 @@ let THeadElementType = forwardRef(function THeadElementType( */ export const TableHeader = /*#__PURE__*/ createBranchComponent( TableHeaderNode, - ( + ( props: TableHeaderProps, ref: ForwardedRef ) => { @@ -1449,7 +1449,7 @@ let TableBodyElementType = forwardRef(function TableBodyElementType( */ export const TableBody = /*#__PURE__*/ createBranchComponent( TableBodyNode, - ( + ( props: TableBodyProps, ref: ForwardedRef, node: Node @@ -1554,7 +1554,7 @@ let TableFooterElementType = forwardRef(function TableFooterElementType( */ export const TableFooter = /*#__PURE__*/ createBranchComponent( TableFooterNode, - ( + ( props: TableFooterProps, ref: ForwardedRef, node: Node @@ -1683,7 +1683,7 @@ let TableRowElementType = forwardRef(function TableRowElementType( */ export const Row = /*#__PURE__*/ createBranchComponent( TableRowNode, - ( + ( props: RowProps, forwardedRef: ForwardedRef, item: GridNode diff --git a/packages/react-aria-components/src/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index ba5a44454a5..626a67b4fe9 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -226,7 +226,7 @@ export interface TabPanelRenderProps { } export const TabsContext = createContext>(null); -export const TabListStateContext = createContext | null>(null); +export const TabListStateContext = createContext | null>(null); /** * Tabs organize content into multiple sections and allow users to navigate between them. @@ -304,9 +304,10 @@ function TabsInner({props, tabsRef: ref, collection}: TabsInnerProps) { * A TabList is used within Tabs to group tabs that a user can switch between. * The ids of the items within the must match up with a corresponding item inside the . */ -export const TabList = /*#__PURE__*/ (forwardRef as forwardRefType)(function TabList< - T extends object ->(props: TabListProps, ref: ForwardedRef): JSX.Element { +export const TabList = /*#__PURE__*/ (forwardRef as forwardRefType)(function TabList( + props: TabListProps, + ref: ForwardedRef +): JSX.Element { let state = useContext(TabListStateContext); return state ? : ; }); @@ -316,7 +317,7 @@ interface TabListInnerProps { forwardedRef: ForwardedRef; } -function TabListInner({props, forwardedRef: ref}: TabListInnerProps) { +function TabListInner({props, forwardedRef: ref}: TabListInnerProps) { let state = useContext(TabListStateContext)!; let {CollectionRoot} = useContext(CollectionRendererContext); let {orientation = 'horizontal', keyboardActivation = 'automatic'} = @@ -440,7 +441,7 @@ export interface TabPanelsProps /** * Groups multiple `` elements, and provides CSS variables for animated transitions. */ -export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanels( +export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanels( props: TabPanelsProps, forwardedRef: ForwardedRef ) { diff --git a/packages/react-aria-components/src/TagGroup.tsx b/packages/react-aria-components/src/TagGroup.tsx index 675090527a3..31b384fd6a0 100644 --- a/packages/react-aria-components/src/TagGroup.tsx +++ b/packages/react-aria-components/src/TagGroup.tsx @@ -150,11 +150,7 @@ interface TagGroupInnerProps { collection: any; } -function TagGroupInner({ - props, - forwardedRef: ref, - collection -}: TagGroupInnerProps) { +function TagGroupInner({props, forwardedRef: ref, collection}: TagGroupInnerProps) { let tagListRef = useRef(null); // Extract the user provided id so it doesn't clash with the collection id provided by Autocomplete let {id, ...otherProps} = props; @@ -218,9 +214,10 @@ function TagGroupInner({ /** * A tag list is a container for tags within a TagGroup. */ -export const TagList = /*#__PURE__*/ (forwardRef as forwardRefType)(function TagList< - T extends object ->(props: TagListProps, ref: ForwardedRef): JSX.Element { +export const TagList = /*#__PURE__*/ (forwardRef as forwardRefType)(function TagList( + props: TagListProps, + ref: ForwardedRef +): JSX.Element { let state = useContext(ListStateContext); return state ? : ; }); @@ -230,7 +227,7 @@ interface TagListInnerProps { forwardedRef: ForwardedRef; } -function TagListInner({props, forwardedRef}: TagListInnerProps) { +function TagListInner({props, forwardedRef}: TagListInnerProps) { let state = useContext(ListStateContext)!; let {CollectionRoot} = useContext(CollectionRendererContext); let [gridProps, ref] = useContextProps({}, forwardedRef, TagListContext); diff --git a/packages/react-aria-components/src/Tree.tsx b/packages/react-aria-components/src/Tree.tsx index f9eefd6a968..c96ecea2fcc 100644 --- a/packages/react-aria-components/src/Tree.tsx +++ b/packages/react-aria-components/src/Tree.tsx @@ -349,7 +349,7 @@ export const TreeStateContext = createContext | null>(null); * A tree provides users with a way to navigate nested hierarchical information, with support for keyboard navigation * and selection. */ -export const Tree = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tree( +export const Tree = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tree( props: TreeProps, ref: ForwardedRef ) { @@ -376,13 +376,13 @@ const EXPANSION_KEYS = { } }; -interface TreeInnerProps { +interface TreeInnerProps { props: TreeProps; collection: TreeCollection; treeRef: RefObject; } -function TreeInner({props, collection, treeRef: ref}: TreeInnerProps) { +function TreeInner({props, collection, treeRef: ref}: TreeInnerProps) { const {dragAndDropHooks} = props; let {direction} = useLocale(); let collator = useCollator({usage: 'search', sensitivity: 'base'}); @@ -738,7 +738,7 @@ class TreeItemNode extends CollectionNode { */ export const TreeItem = /*#__PURE__*/ createBranchComponent( TreeItemNode, - (props: TreeItemProps, ref: ForwardedRef, item: Node) => { + (props: TreeItemProps, ref: ForwardedRef, item: Node) => { let state = useContext(TreeStateContext)!; ref = useObjectRef(ref); let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext)!; @@ -1039,7 +1039,7 @@ export interface TreeLoadMoreItemProps } export const TreeLoadMoreItem = createLeafComponent(LoaderNode, function TreeLoadingSentinel< - T extends object + T >(props: TreeLoadMoreItemProps, ref: ForwardedRef, item: Node) { let {isVirtualized} = useContext(CollectionRendererContext); let state = useContext(TreeStateContext)!; @@ -1218,7 +1218,7 @@ export interface GridListSectionProps */ export const TreeSection = /*#__PURE__*/ createBranchComponent( SectionNode, - ( + ( props: GridListSectionProps, ref: ForwardedRef, item: Node diff --git a/packages/react-aria-components/stories/Autocomplete.stories.tsx b/packages/react-aria-components/stories/Autocomplete.stories.tsx index 7ae8ed4da95..74141650ca0 100644 --- a/packages/react-aria-components/stories/Autocomplete.stories.tsx +++ b/packages/react-aria-components/stories/Autocomplete.stories.tsx @@ -949,7 +949,10 @@ export const AutocompleteMenuInPopoverDialogTrigger: MenuStory = { Please select an option below. - + + {...args} + className={styles.menu} + items={dynamicAutocompleteSubdialog}> {item => dynamicRenderFuncSections(item)} @@ -1298,7 +1301,7 @@ function AutocompleteNodeFiltering(args) { Please select an option below. - + {...args} className={styles.menu} items={dynamicAutocompleteSubdialog}> {item => dynamicRenderFuncSections(item)} diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx index 7363b4613dd..35c1bed91cf 100644 --- a/packages/react-aria-components/stories/ListBox.stories.tsx +++ b/packages/react-aria-components/stories/ListBox.stories.tsx @@ -123,6 +123,16 @@ export const ListBoxComplex: ListBoxStory = () => ( ); +export const ListBoxDynamicStrings: ListBoxStory = args => ( + + {item => {item}} + +); + interface Album { id: number; image: string; diff --git a/packages/react-aria-components/stories/Tree.stories.tsx b/packages/react-aria-components/stories/Tree.stories.tsx index b5412dac868..ac045363e5b 100644 --- a/packages/react-aria-components/stories/Tree.stories.tsx +++ b/packages/react-aria-components/stories/Tree.stories.tsx @@ -184,7 +184,7 @@ const StaticTreeItemNoActions = (props: StaticTreeItemProps) => { ); }; -export function TreeExampleStaticRender(props: TreeProps) { +export function TreeExampleStaticRender(props: TreeProps) { return ( (props: TreeProps) { ); } -const TreeExampleStaticNoActionsRender = (args: TreeProps): JSX.Element => ( +const TreeExampleStaticNoActionsRender = (args: TreeProps): JSX.Element => ( (args: TreeProps): JSX.Element => { +const TreeExampleDynamicRender = (args: TreeProps): JSX.Element => { let treeData = useTreeData({ initialItems: (args.items as any) ?? rows, getKey: item => item.id, @@ -712,7 +712,7 @@ const TreeExampleDynamicRender = (args: TreeProps): JSX.Ele ); }; -const TreeSectionExampleDynamicRender = (args: TreeProps): JSX.Element => { +const TreeSectionExampleDynamicRender = (args: TreeProps): JSX.Element => { let treeData = useTreeData({ initialItems: (args.items as any) ?? rowsWithSections, getKey: item => item.id, @@ -770,7 +770,7 @@ export const WithActions: StoryObj = { name: 'Tree with actions' }; -const WithLinksRender = (args: TreeProps): JSX.Element => { +const WithLinksRender = (args: TreeProps): JSX.Element => { let treeData = useTreeData({ initialItems: rows, getKey: item => item.id, @@ -812,9 +812,7 @@ function renderEmptyLoader({isLoading}) { return isLoading ? 'Root level loading spinner' : 'Nothing in tree'; } -const EmptyTreeStatic = ( - args: TreeProps & {isLoading: boolean} -): JSX.Element => ( +const EmptyTreeStatic = (args: TreeProps & {isLoading: boolean}): JSX.Element => ( = { name: 'Empty/Loading Tree rendered with TreeLoader collection element' }; -function LoadingStoryDepOnCollection( +function LoadingStoryDepOnCollection( props: TreeProps & {isLoading: boolean} ): JSX.Element { let {isLoading, ...args} = props; @@ -892,9 +890,7 @@ export const LoadingStoryDepOnCollectionStory: StoryObj( - args: TreeProps & {isLoading: boolean} -): JSX.Element { +function LoadingStoryDepOnTop(args: TreeProps & {isLoading: boolean}): JSX.Element { let treeData = useTreeData({ initialItems: rows, getKey: item => item.id, @@ -1019,9 +1015,7 @@ const DynamicTreeItemWithButtonLoader = (props: DynamicTreeItemProps) => { ); }; -function ButtonLoadingIndicator( - args: TreeProps & {isLoading: boolean} -): JSX.Element { +function ButtonLoadingIndicator(args: TreeProps & {isLoading: boolean}): JSX.Element { let treeData = useTreeData({ initialItems: rows, getKey: item => item.id, @@ -1065,7 +1059,7 @@ export const ButtonLoadingIndicatorStory: StoryObj(args: TreeProps): JSX.Element { +function VirtualizedTreeRender(args: TreeProps): JSX.Element { return ( @@ -1377,7 +1371,7 @@ dependencies={[isRootLoading]}> }} */ } -function TreeDragAndDropExample( +function TreeDragAndDropExample( args: TreeProps & { shouldAcceptItemDrop: 'folders' | 'all'; dropFunction: 'onMove' | 'onInsert' | 'onRootDrop'; @@ -1777,7 +1771,7 @@ function generateTreeData(): Array { const treeData = generateTreeData(); -function HugeVirtualizedTreeRender(args: TreeProps): JSX.Element { +function HugeVirtualizedTreeRender(args: TreeProps): JSX.Element { let [expandedKeys, setExpandedKeys] = useState(new Set()); let expandAll = () => { setExpandedKeys(itemKeys); diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index 060ec96a1c4..80ee2845f7e 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -454,6 +454,62 @@ describe('ListBox', () => { expect(items).toHaveLength(2); }); + it('should support dynamic collection with non-object entries', () => { + let {getAllByRole, rerender} = render( + + {item => {item}} + + ); + + let items = getAllByRole('option'); + expect(items).toHaveLength(3); + expect(items[0].textContent).toBe('Foo'); + expect(items[1].textContent).toBe('Bar'); + expect(items[2].textContent).toBe('Baz'); + + rerender( + + {item => {item}} + + ); + + items = getAllByRole('option'); + expect(items).toHaveLength(2); + expect(items[0].textContent).toBe('Foo'); + expect(items[1].textContent).toBe('Baz'); + }); + + it('should update collection if render function changes', () => { + let objects = [{name: 'Foo'}, {name: 'Bar'}, {name: 'Baz'}]; + let {getAllByRole, rerender} = render( + + {item => {item.name}} + + ); + + let items = getAllByRole('option'); + expect(items).toHaveLength(3); + expect(items[0].textContent).toBe('Foo'); + expect(items[1].textContent).toBe('Bar'); + expect(items[2].textContent).toBe('Baz'); + + rerender( + + {item => ( + + Updated: {item.name} + + )} + + ); + + items = getAllByRole('option'); + expect(items).toHaveLength(3); + expect(items[0].textContent).toBe('Updated: Foo'); + expect(items[1].textContent).toBe('Updated: Bar'); + expect(items[2].textContent).toBe('Updated: Baz'); + }); + it('should support autoFocus', () => { let {getByRole} = renderListbox({autoFocus: true}); let listbox = getByRole('listbox'); diff --git a/packages/react-aria/src/collections/CollectionBuilder.tsx b/packages/react-aria/src/collections/CollectionBuilder.tsx index d702aaa600d..97b62595233 100644 --- a/packages/react-aria/src/collections/CollectionBuilder.tsx +++ b/packages/react-aria/src/collections/CollectionBuilder.tsx @@ -36,7 +36,7 @@ import {useSyncExternalStore as useSyncExternalStoreShim} from 'use-sync-externa const ShallowRenderContext = createContext(false); const CollectionDocumentContext = createContext> | null>(null); -export interface CollectionBuilderProps> { +export interface CollectionBuilderProps> { content: ReactNode; children: (collection: C) => ReactNode; createCollection?: () => C; @@ -45,7 +45,7 @@ export interface CollectionBuilderProps> { /** * Builds a `Collection` from the children provided to the `content` prop, and passes it to the child render prop function. */ -export function CollectionBuilder>( +export function CollectionBuilder>( props: CollectionBuilderProps ): ReactElement { // If a document was provided above us, we're already in a hidden tree. Just render the content. @@ -197,11 +197,11 @@ function useSSRCollectionNode( } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function createLeafComponent( +export function createLeafComponent( CollectionNodeClass: CollectionNodeClass | string, render: (props: P, ref: ForwardedRef) => ReactElement | null ): (props: P & React.RefAttributes) => ReactElement | null; -export function createLeafComponent( +export function createLeafComponent( CollectionNodeClass: CollectionNodeClass | string, render: (props: P, ref: ForwardedRef, node: Node) => ReactElement | null ): (props: P & React.RefAttributes) => ReactElement | null; @@ -239,11 +239,7 @@ export function createLeafComponent

( return Result; } -export function createBranchComponent< - T extends object, - P extends {children?: any}, - E extends Element ->( +export function createBranchComponent( CollectionNodeClass: CollectionNodeClass | string, render: (props: P, ref: ForwardedRef, node: Node) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren @@ -262,7 +258,7 @@ export function createBranchComponent< return Result; } -function useCollectionChildren(options: CachedChildrenOptions) { +function useCollectionChildren(options: CachedChildrenOptions) { return useCachedChildren({...options, addIdAndValue: true}); } @@ -271,7 +267,7 @@ export interface CollectionProps extends CachedChildrenOptions {} const CollectionContext = createContext | null>(null); /** A Collection renders a list of items, automatically managing caching and keys. */ -export function Collection(props: CollectionProps): JSX.Element { +export function Collection(props: CollectionProps): JSX.Element { let ctx = useContext(CollectionContext)!; let dependencies = (ctx?.dependencies || []).concat(props.dependencies); let idScope = props.idScope ?? ctx?.idScope; diff --git a/packages/react-aria/src/collections/useCachedChildren.ts b/packages/react-aria/src/collections/useCachedChildren.ts index 521d2479ad2..789b30b0df2 100644 --- a/packages/react-aria/src/collections/useCachedChildren.ts +++ b/packages/react-aria/src/collections/useCachedChildren.ts @@ -30,32 +30,45 @@ export interface CachedChildrenOptions { * Maps over a list of items and renders React elements for them. Each rendered item is * cached based on object identity, and React keys are generated from the `key` or `id` property. */ -export function useCachedChildren(props: CachedChildrenOptions): ReactNode { +export function useCachedChildren(props: CachedChildrenOptions): ReactNode { let {children, items, idScope, addIdAndValue, dependencies = []} = props; - // Invalidate the cache whenever the parent value changes. + // In development, invalidate when the children function updates (e.g. HMR). + let childrenString = useMemo( + () => + process.env.NODE_ENV !== 'production' && typeof children === 'function' + ? children.toString() + : undefined, + [children] + ); + + // Invalidate the cache whenever dependencies change. // eslint-disable-next-line react-hooks/exhaustive-deps - let cache = useMemo(() => new WeakMap(), dependencies); + let cache = useMemo(() => new WeakMap(), [...dependencies, childrenString]); + return useMemo(() => { if (items && typeof children === 'function') { let res: ReactElement[] = []; for (let item of items) { - let rendered = cache.get(item); + let cacheKey = isWeakKey(item) ? item : null; + let rendered = cacheKey ? cache.get(cacheKey) : null; if (!rendered) { rendered = children(item); // @ts-ignore - let key = rendered.props.id ?? item.key ?? item.id; - - if (key == null) { - throw new Error('Could not determine key for item'); - } - + let id = rendered.props.id ?? item.key ?? item.id; if (idScope != null && rendered.props.id == null) { - key = idScope + ':' + key; + id = idScope + ':' + id; } + + // If no id is inferred from data, use the index as the React key. + // An id will be generated by the collection document. + let key = id ?? res.length; + // Note: only works if wrapped Item passes through id... - rendered = cloneElement(rendered, addIdAndValue ? {key, id: key, value: item} : {key}); - cache.set(item, rendered); + rendered = cloneElement(rendered, addIdAndValue ? {key, id, value: item} : {key}); + if (cacheKey) { + cache.set(cacheKey, rendered); + } } res.push(rendered); } @@ -65,3 +78,15 @@ export function useCachedChildren(props: CachedChildrenOptions } }, [children, items, cache, idScope, addIdAndValue]); } + +function isWeakKey(value: any): value is WeakKey { + switch (typeof value) { + case 'object': + return value != null; + case 'function': + case 'symbol': + return true; + default: + return false; + } +} diff --git a/packages/react-aria/stories/dnd/DroppableGrid.tsx b/packages/react-aria/stories/dnd/DroppableGrid.tsx index 5359aa2ccc6..f160e397b34 100644 --- a/packages/react-aria/stories/dnd/DroppableGrid.tsx +++ b/packages/react-aria/stories/dnd/DroppableGrid.tsx @@ -14,7 +14,7 @@ import {classNames} from '@adobe/react-spectrum/private/utils/classNames'; import dndStyles from './dnd.css'; import dropIndicatorStyles from '@adobe/spectrum-css-temp/components/dropindicator/vars.css'; -import {DroppableCollectionDropEvent} from '@react-types/shared'; +import {DroppableCollectionDropEvent, Node} from '@react-types/shared'; import {FocusRing} from '../../src/focus/FocusRing'; import Folder from '@spectrum-icons/workflow/Folder'; import {GridCollection} from 'react-stately/private/grid/GridCollection'; @@ -132,22 +132,25 @@ const DroppableGrid = React.forwardRef(function (props: any, ref) { () => new GridCollection({ columnCount: 1, - items: [...state.collection].map(item => ({ - ...item, - childNodes: [ - { - key: `cell-${item.key}`, - type: 'cell', - index: 0, - value: null, - level: 0, - rendered: null, - textValue: item.textValue, - hasChildNodes: false, - childNodes: [] - } - ] - })) + items: [...state.collection].map( + item => + ({ + ...item, + childNodes: [ + { + key: `cell-${item.key}`, + type: 'cell', + index: 0, + value: null, + level: 0, + rendered: null, + textValue: item.textValue, + hasChildNodes: false, + childNodes: [] + } + ] + }) as Node + ) }), [state.collection] ) diff --git a/packages/react-aria/stories/dnd/Reorderable.tsx b/packages/react-aria/stories/dnd/Reorderable.tsx index ec72d291516..b42c91efe68 100644 --- a/packages/react-aria/stories/dnd/Reorderable.tsx +++ b/packages/react-aria/stories/dnd/Reorderable.tsx @@ -20,7 +20,7 @@ import {FocusRing} from '../../src/focus/FocusRing'; import Folder from '@spectrum-icons/workflow/Folder'; import {GridCollection} from 'react-stately/private/grid/GridCollection'; import {Item} from 'react-stately/Item'; -import {ItemDropTarget, Key} from '@react-types/shared'; +import {ItemDropTarget, Key, Node} from '@react-types/shared'; import {ListDropTargetDelegate} from '../../src/dnd/ListDropTargetDelegate'; import {ListKeyboardDelegate} from '../../src/selection/ListKeyboardDelegate'; import {mergeProps} from '../../src/utils/mergeProps'; @@ -85,22 +85,25 @@ function ReorderableGrid(props) { () => new GridCollection({ columnCount: 1, - items: [...state.collection].map(item => ({ - ...item, - childNodes: [ - { - key: `cell-${item.key}`, - type: 'cell', - index: 0, - value: null, - level: 0, - rendered: null, - textValue: item.textValue, - hasChildNodes: false, - childNodes: [] - } - ] - })) + items: [...state.collection].map( + item => + ({ + ...item, + childNodes: [ + { + key: `cell-${item.key}`, + type: 'cell', + index: 0, + value: null, + level: 0, + rendered: null, + textValue: item.textValue, + hasChildNodes: false, + childNodes: [] + } + ] + }) as Node + ) }), [state.collection] ) diff --git a/packages/react-aria/stories/dnd/VirtualizedListBox.tsx b/packages/react-aria/stories/dnd/VirtualizedListBox.tsx index dae6a86429b..82b47b01a73 100644 --- a/packages/react-aria/stories/dnd/VirtualizedListBox.tsx +++ b/packages/react-aria/stories/dnd/VirtualizedListBox.tsx @@ -104,7 +104,7 @@ export function VirtualizedListBoxExample(props: any): JSX.Element { } const Context = React.createContext<{ - state: ListState; + state: ListState; dropState: DroppableCollectionState; } | null>(null); const acceptedTypes = ['item', 'folder']; diff --git a/packages/react-stately/src/collections/useCollection.ts b/packages/react-stately/src/collections/useCollection.ts index 532f75028dc..78bd0ac7843 100644 --- a/packages/react-stately/src/collections/useCollection.ts +++ b/packages/react-stately/src/collections/useCollection.ts @@ -27,11 +27,12 @@ interface CollectionOptions>> extends Omit< type CollectionFactory>> = (node: Iterable>) => C; -export function useCollection< - T extends object, - C extends Collection> = Collection> ->(props: CollectionOptions, factory: CollectionFactory, context?: unknown): C { - let builder = useMemo(() => new CollectionBuilder(), []); +export function useCollection> = Collection>>( + props: CollectionOptions, + factory: CollectionFactory, + context?: unknown +): C { + let builder = useMemo(() => new CollectionBuilder(), []); let {children, items, collection} = props; let result = useMemo(() => { if (collection) { diff --git a/packages/react-stately/src/combobox/useComboBoxState.ts b/packages/react-stately/src/combobox/useComboBoxState.ts index fe81a917b66..1958b0dace0 100644 --- a/packages/react-stately/src/combobox/useComboBoxState.ts +++ b/packages/react-stately/src/combobox/useComboBoxState.ts @@ -178,7 +178,7 @@ export interface ComboBoxStateOptions * of items from props and manages the option selection state of the combo box. In addition, it tracks the input value, * focus state, and other properties of the combo box. */ -export function useComboBoxState( +export function useComboBoxState( props: ComboBoxStateOptions ): ComboBoxState { let { @@ -627,7 +627,7 @@ export function useComboBoxState * of items from props, handles the open state for the popup menu, and manages * multiple selection state. */ -export function useSelectState( +export function useSelectState( props: SelectStateOptions ): SelectState { let {selectionMode = 'single' as M, shouldCloseOnSelect = selectionMode === 'single'} = props; diff --git a/packages/react-stately/src/tree/useTreeState.ts b/packages/react-stately/src/tree/useTreeState.ts index 144232af62c..f8bd1642917 100644 --- a/packages/react-stately/src/tree/useTreeState.ts +++ b/packages/react-stately/src/tree/useTreeState.ts @@ -54,7 +54,7 @@ export interface TreeState { * Provides state management for tree-like components. Handles building a collection * of items from props, item expanded state, and manages multiple selection state. */ -export function useTreeState(props: TreeProps): TreeState { +export function useTreeState(props: TreeProps): TreeState { let {onExpandedChange} = props; let [expandedKeys, setExpandedKeys] = useControlledState( diff --git a/packages/react-stately/stories/tree/useTreeState.stories.tsx b/packages/react-stately/stories/tree/useTreeState.stories.tsx index a401384ddd2..29078d288fb 100644 --- a/packages/react-stately/stories/tree/useTreeState.stories.tsx +++ b/packages/react-stately/stories/tree/useTreeState.stories.tsx @@ -72,7 +72,7 @@ function Tree(props) { ); } -function TreeNodes({nodes, state}: {nodes: Collection>; state: any}) { +function TreeNodes({nodes, state}: {nodes: Collection>; state: any}) { return Array.from(nodes).map(node => ); } diff --git a/starters/docs/src/Breadcrumbs.tsx b/starters/docs/src/Breadcrumbs.tsx index e1b8e4b740d..c987bd195e3 100644 --- a/starters/docs/src/Breadcrumbs.tsx +++ b/starters/docs/src/Breadcrumbs.tsx @@ -10,7 +10,7 @@ import { import {ChevronRight} from 'lucide-react'; import './Breadcrumbs.css'; -export function Breadcrumbs(props: BreadcrumbsProps) { +export function Breadcrumbs(props: BreadcrumbsProps) { return ; } diff --git a/starters/docs/src/ComboBox.tsx b/starters/docs/src/ComboBox.tsx index d7d19ec2777..e0ec8c027a6 100644 --- a/starters/docs/src/ComboBox.tsx +++ b/starters/docs/src/ComboBox.tsx @@ -14,7 +14,7 @@ import {Popover} from './Popover'; import {ChevronDown} from 'lucide-react'; import './ComboBox.css'; -export interface ComboBoxProps extends Omit< +export interface ComboBoxProps extends Omit< AriaComboBoxProps, 'children' > { @@ -25,7 +25,7 @@ export interface ComboBoxProps({ +export function ComboBox({ label, description, errorMessage, @@ -52,7 +52,7 @@ export function ComboBox +export interface CommandPaletteProps extends Omit, AriaMenuProps { isOpen: boolean; onOpenChange: (isOpen?: boolean) => void; } -export function CommandPalette(props: CommandPaletteProps) { +export function CommandPalette(props: CommandPaletteProps) { let {isOpen, onOpenChange} = props; let {contains} = useFilter({sensitivity: 'base'}); diff --git a/starters/docs/src/GridList.tsx b/starters/docs/src/GridList.tsx index 41e1510503b..4c33d1425af 100644 --- a/starters/docs/src/GridList.tsx +++ b/starters/docs/src/GridList.tsx @@ -16,11 +16,7 @@ import {GripVertical} from 'lucide-react'; import {ProgressCircle} from './ProgressCircle'; import './GridList.css'; -export function GridList({ - children, - layout = 'grid', - ...props -}: GridListProps) { +export function GridList({children, layout = 'grid', ...props}: GridListProps) { return ( {children} diff --git a/starters/docs/src/ListBox.tsx b/starters/docs/src/ListBox.tsx index f31c9ba0b04..8e0045867cb 100644 --- a/starters/docs/src/ListBox.tsx +++ b/starters/docs/src/ListBox.tsx @@ -16,7 +16,7 @@ import {Text} from './Content'; import {ProgressCircle} from './ProgressCircle'; import './ListBox.css'; -export function ListBox({children, ...props}: ListBoxProps) { +export function ListBox({children, ...props}: ListBoxProps) { return {children}; } @@ -32,7 +32,7 @@ export function ListBoxItem(props: ListBoxItemProps) { ); } -export function ListBoxSection(props: ListBoxSectionProps) { +export function ListBoxSection(props: ListBoxSectionProps) { return ; } @@ -44,7 +44,7 @@ export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps) { ); } -export function DropdownListBox(props: ListBoxProps) { +export function DropdownListBox(props: ListBoxProps) { return ; } diff --git a/starters/docs/src/Menu.tsx b/starters/docs/src/Menu.tsx index bf6793d4768..9a2684a2b27 100644 --- a/starters/docs/src/Menu.tsx +++ b/starters/docs/src/Menu.tsx @@ -33,7 +33,7 @@ export function MenuTrigger(props: MenuTriggerProps) { ); } -export function Menu(props: MenuProps) { +export function Menu(props: MenuProps) { return {props.children}; } @@ -58,7 +58,7 @@ export function MenuItem(props: Omit & {children?: Re ); } -export function MenuSection(props: MenuSectionProps) { +export function MenuSection(props: MenuSectionProps) { return ; } diff --git a/starters/docs/src/Select.tsx b/starters/docs/src/Select.tsx index 7e864737a61..4966636d0ca 100644 --- a/starters/docs/src/Select.tsx +++ b/starters/docs/src/Select.tsx @@ -14,7 +14,7 @@ import {Popover} from './Popover'; import {Label, FieldError, Description} from './Form'; import './Select.css'; -export interface SelectProps extends Omit< +export interface SelectProps extends Omit< AriaSelectProps, 'children' > { @@ -25,7 +25,7 @@ export interface SelectProps children: React.ReactNode | ((item: T) => React.ReactNode); } -export function Select({ +export function Select({ label, description, errorMessage, @@ -49,7 +49,7 @@ export function Select & {children?: React. ); } -export function TableHeader({ - columns, - children, - ...otherProps -}: TableHeaderProps) { +export function TableHeader({columns, children, ...otherProps}: TableHeaderProps) { let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions(); return ( @@ -88,7 +84,7 @@ export function TableHeader({ ); } -export function Row({id, columns, children, ...otherProps}: RowProps) { +export function Row({id, columns, children, ...otherProps}: RowProps) { let {selectionBehavior, allowsDragging} = useTableOptions(); return ( @@ -110,11 +106,11 @@ export function Row({id, columns, children, ...otherProps}: Ro ); } -export function TableBody(props: TableBodyProps) { +export function TableBody(props: TableBodyProps) { return ; } -export function TableFooter(props: TableFooterProps) { +export function TableFooter(props: TableFooterProps) { return ; } diff --git a/starters/docs/src/Tabs.tsx b/starters/docs/src/Tabs.tsx index 1cb6d2c9446..5361a124b04 100644 --- a/starters/docs/src/Tabs.tsx +++ b/starters/docs/src/Tabs.tsx @@ -19,7 +19,7 @@ export function Tabs(props: TabsProps) { return ; } -export function TabList(props: TabListProps) { +export function TabList(props: TabListProps) { return ; } @@ -36,7 +36,7 @@ export function Tab(props: TabProps) { ); } -export function TabPanels(props: TabPanelsProps) { +export function TabPanels(props: TabPanelsProps) { return ; } diff --git a/starters/docs/src/TagGroup.tsx b/starters/docs/src/TagGroup.tsx index 5e87f175f04..8c137fecf9f 100644 --- a/starters/docs/src/TagGroup.tsx +++ b/starters/docs/src/TagGroup.tsx @@ -22,7 +22,7 @@ export interface TagGroupProps errorMessage?: string; } -export function TagGroup({ +export function TagGroup({ label, description, errorMessage, diff --git a/starters/docs/src/Tree.tsx b/starters/docs/src/Tree.tsx index c1579b07f0d..3928dfd5a4e 100644 --- a/starters/docs/src/Tree.tsx +++ b/starters/docs/src/Tree.tsx @@ -18,7 +18,7 @@ import {Checkbox} from './Checkbox'; import {ProgressCircle} from './ProgressCircle'; import './Tree.css'; -export function Tree(props: TreeProps) { +export function Tree(props: TreeProps) { return ; } @@ -69,7 +69,7 @@ export function TreeLoadMoreItem(props: TreeLoadMoreItemProps) { ); } -export function TreeSection(props: React.ComponentProps) { +export function TreeSection(props: React.ComponentProps) { return ; } diff --git a/starters/tailwind/src/Breadcrumbs.tsx b/starters/tailwind/src/Breadcrumbs.tsx index a6985c9e4d2..5272d9485d0 100644 --- a/starters/tailwind/src/Breadcrumbs.tsx +++ b/starters/tailwind/src/Breadcrumbs.tsx @@ -12,7 +12,7 @@ import {twMerge} from 'tailwind-merge'; import {Link} from './Link'; import {composeTailwindRenderProps} from './utils'; -export function Breadcrumbs(props: BreadcrumbsProps) { +export function Breadcrumbs(props: BreadcrumbsProps) { return ; } diff --git a/starters/tailwind/src/ComboBox.tsx b/starters/tailwind/src/ComboBox.tsx index d66600c392b..d482db9098c 100644 --- a/starters/tailwind/src/ComboBox.tsx +++ b/starters/tailwind/src/ComboBox.tsx @@ -15,7 +15,7 @@ import {Popover} from './Popover'; import {composeTailwindRenderProps} from './utils'; import {FieldButton} from './FieldButton'; -export interface ComboBoxProps extends Omit< +export interface ComboBoxProps extends Omit< AriaComboBoxProps, 'children' > { @@ -26,7 +26,7 @@ export interface ComboBoxProps React.ReactNode); } -export function ComboBox({ +export function ComboBox({ label, description, errorMessage, @@ -71,6 +71,6 @@ export function ComboBoxItem(props: ListBoxItemProps) { return ; } -export function ComboBoxSection(props: DropdownSectionProps) { +export function ComboBoxSection(props: DropdownSectionProps) { return ; } diff --git a/starters/tailwind/src/CommandPalette.tsx b/starters/tailwind/src/CommandPalette.tsx index 2244d5fa3fa..daa7cff8e79 100644 --- a/starters/tailwind/src/CommandPalette.tsx +++ b/starters/tailwind/src/CommandPalette.tsx @@ -11,13 +11,13 @@ import {SearchField} from './SearchField'; import {Modal} from './Modal'; import React, {useEffect} from 'react'; -export interface CommandPaletteProps +export interface CommandPaletteProps extends Omit, AriaMenuProps { isOpen: boolean; onOpenChange: (isOpen?: boolean) => void; } -export function CommandPalette(props: CommandPaletteProps) { +export function CommandPalette(props: CommandPaletteProps) { let {isOpen, onOpenChange} = props; let {contains} = useFilter({sensitivity: 'base'}); diff --git a/starters/tailwind/src/GridList.tsx b/starters/tailwind/src/GridList.tsx index ef2326acc19..43bd0c5f05a 100644 --- a/starters/tailwind/src/GridList.tsx +++ b/starters/tailwind/src/GridList.tsx @@ -15,7 +15,7 @@ import {composeTailwindRenderProps, focusRing} from './utils'; import {type HTMLAttributes} from 'react'; import {twMerge} from 'tailwind-merge'; -export function GridList({children, ...props}: GridListProps) { +export function GridList({children, ...props}: GridListProps) { let isHorizontal = (props as {orientation?: 'horizontal' | 'vertical'}).orientation === 'horizontal'; return ( diff --git a/starters/tailwind/src/ListBox.tsx b/starters/tailwind/src/ListBox.tsx index de84d44517b..35591324b12 100644 --- a/starters/tailwind/src/ListBox.tsx +++ b/starters/tailwind/src/ListBox.tsx @@ -17,7 +17,7 @@ import {composeTailwindRenderProps, focusRing} from './utils'; interface ListBoxProps extends Omit, 'layout' | 'orientation'> {} -export function ListBox({children, ...props}: ListBoxProps) { +export function ListBox({children, ...props}: ListBoxProps) { return ( extends ListBoxSectionProps { items?: any; } -export function DropdownSection(props: DropdownSectionProps) { +export function DropdownSection(props: DropdownSectionProps) { return (
diff --git a/starters/tailwind/src/Menu.tsx b/starters/tailwind/src/Menu.tsx index 5e2ae16d996..b4eab5e57fa 100644 --- a/starters/tailwind/src/Menu.tsx +++ b/starters/tailwind/src/Menu.tsx @@ -21,7 +21,7 @@ import {composeRenderProps} from 'react-aria-components/composeRenderProps'; import {dropdownItemStyles} from './ListBox'; import {Popover, type PopoverProps} from './Popover'; -export function Menu(props: MenuProps) { +export function Menu(props: MenuProps) { return ( extends AriaMenuSectionProps { items?: any; } -export function MenuSection(props: MenuSectionProps) { +export function MenuSection(props: MenuSectionProps) { return ( extends Omit< +export interface SelectProps extends Omit< AriaSelectProps, 'children' > { @@ -39,7 +39,7 @@ export interface SelectProps children: React.ReactNode | ((item: T) => React.ReactNode); } -export function Select({ +export function Select({ label, description, errorMessage, @@ -81,6 +81,6 @@ export function SelectItem(props: ListBoxItemProps) { return ; } -export function SelectSection(props: DropdownSectionProps) { +export function SelectSection(props: DropdownSectionProps) { return ; } diff --git a/starters/tailwind/src/Table.tsx b/starters/tailwind/src/Table.tsx index a38b4d4094a..fe3e140db3d 100644 --- a/starters/tailwind/src/Table.tsx +++ b/starters/tailwind/src/Table.tsx @@ -92,7 +92,7 @@ export function Column(props: ColumnProps) { ); } -export function TableHeader(props: TableHeaderProps) { +export function TableHeader(props: TableHeaderProps) { let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions(); return ( @@ -117,11 +117,11 @@ export function TableHeader(props: TableHeaderProps) { ); } -export function TableBody(props: TableBodyProps) { +export function TableBody(props: TableBodyProps) { return ; } -export function TableFooter(props: TableFooterProps) { +export function TableFooter(props: TableFooterProps) { return ; } @@ -130,7 +130,7 @@ const rowStyles = tv({ base: 'group/row relative cursor-default select-none -outline-offset-2 text-neutral-900 disabled:text-neutral-300 dark:text-neutral-200 dark:disabled:text-neutral-600 text-sm hover:bg-neutral-100 pressed:bg-neutral-100 dark:hover:bg-neutral-800 dark:pressed:bg-neutral-800 selected:bg-blue-100 selected:hover:bg-blue-200 selected:pressed:bg-blue-200 dark:selected:bg-blue-700/30 dark:selected:hover:bg-blue-700/40 dark:selected:pressed:bg-blue-700/40 last:rounded-b-lg' }); -export function Row({id, columns, children, ...otherProps}: RowProps) { +export function Row({id, columns, children, ...otherProps}: RowProps) { let {selectionBehavior, allowsDragging} = useTableOptions(); return ( diff --git a/starters/tailwind/src/Tabs.tsx b/starters/tailwind/src/Tabs.tsx index 4514cdd3129..966d1738f62 100644 --- a/starters/tailwind/src/Tabs.tsx +++ b/starters/tailwind/src/Tabs.tsx @@ -49,7 +49,7 @@ const tabListStyles = tv({ } }); -export function TabList(props: TabListProps) { +export function TabList(props: TabListProps) { return ( (props: TabPanelsProps) { +export function TabPanels(props: TabPanelsProps) { return ( ({ +export function TagGroup({ label, description, errorMessage, diff --git a/starters/tailwind/src/Tree.tsx b/starters/tailwind/src/Tree.tsx index 36685781f74..9a824087788 100644 --- a/starters/tailwind/src/Tree.tsx +++ b/starters/tailwind/src/Tree.tsx @@ -28,7 +28,7 @@ const itemStyles = tv({ } }); -export function Tree({children, ...props}: TreeProps) { +export function Tree({children, ...props}: TreeProps) { return (