diff --git a/packages/react/src/select/item/SelectItem.tsx b/packages/react/src/select/item/SelectItem.tsx index 29f8fe8afcf..355bca0e8fd 100644 --- a/packages/react/src/select/item/SelectItem.tsx +++ b/packages/react/src/select/item/SelectItem.tsx @@ -42,9 +42,11 @@ export const SelectItem = React.memo( } = componentProps; const textRef = React.useRef(null); + const metadata = React.useMemo(() => ({ value: itemValue }), [itemValue]); const listItem = useCompositeListItem({ guess: true, label, + metadata, textRef, }); @@ -55,7 +57,6 @@ export const SelectItem = React.memo( setValue, selectionRef, typingRef, - valuesRef, multiple, selectedItemTextRef, disabled: selectDisabled, @@ -72,15 +73,6 @@ export const SelectItem = React.memo( const itemRef = React.useRef(null); - useIsoLayoutEffect(() => { - const values = valuesRef.current; - values[index] = itemValue; - - return () => { - delete values[index]; - }; - }, [index, itemValue, valuesRef]); - useIsoLayoutEffect(() => { const selectedValue = store.state.value; diff --git a/packages/react/src/select/positioner/SelectPositioner.tsx b/packages/react/src/select/positioner/SelectPositioner.tsx index f6f46e725a8..2f1324ed69e 100644 --- a/packages/react/src/select/positioner/SelectPositioner.tsx +++ b/packages/react/src/select/positioner/SelectPositioner.tsx @@ -5,7 +5,10 @@ import { useIsoLayoutEffect } from '@base-ui/utils/useIsoLayoutEffect'; import { useStableCallback } from '@base-ui/utils/useStableCallback'; import { useStore } from '@base-ui/utils/store'; import { useSelectRootContext } from '../root/SelectRootContext'; -import { CompositeList } from '../../internals/composite/list/CompositeList'; +import { + CompositeList, + type CompositeMetadata, +} from '../../internals/composite/list/CompositeList'; import type { BaseUIComponentProps } from '../../internals/types'; import { useAnchorPositioning, @@ -151,67 +154,66 @@ export const SelectPositioner = React.forwardRef(function SelectPositioner( inert: !open, }); - const prevMapSizeRef = React.useRef(0); - - const onMapChange = useStableCallback( - (map: Map) => { - if (valuesRef.current.length === 0) { - return; - } + const onMapChange = useStableCallback((map: Map>) => { + const previousValues = valuesRef.current; + const nextValues = Array.from(map.values(), (item) => item.value); + valuesRef.current = nextValues; - const prevSize = prevMapSizeRef.current; - prevMapSizeRef.current = map.size; - - if (map.size === prevSize) { - return; - } - - const eventDetails = createChangeEventDetails(REASONS.none); - - if (prevSize !== 0 && !store.state.multiple && value !== null) { - const selectedValueIndex = findItemIndex(valuesRef.current, value, isItemEqualToValue); - if (selectedValueIndex === -1) { - const initialSelectedValue = initialValueRef.current; - const hasInitial = - initialSelectedValue != null && - findItemIndex(valuesRef.current, initialSelectedValue, isItemEqualToValue) !== -1; - const nextValue = hasInitial ? initialSelectedValue : null; - setValue(nextValue, eventDetails); + if (nextValues.length === previousValues.length) { + return; + } - if (nextValue === null) { - store.set('selectedIndex', null); - selectedItemTextRef.current = null; - } + const eventDetails = createChangeEventDetails(REASONS.none); + + if (previousValues.length !== 0 && !store.state.multiple && value !== null) { + const selectedValueIndex = findItemIndex(nextValues, value, isItemEqualToValue); + if (selectedValueIndex === -1) { + const initialSelectedValue = initialValueRef.current; + const hasInitial = + initialSelectedValue != null && + findItemIndex(nextValues, initialSelectedValue, isItemEqualToValue) !== -1; + const nextValue = hasInitial ? initialSelectedValue : null; + setValue(nextValue, eventDetails); + + if (nextValue === null) { + store.set('selectedIndex', null); + selectedItemTextRef.current = null; } } + } - if (prevSize !== 0 && store.state.multiple && Array.isArray(value)) { - const nextValue = value.filter( - (selectedItemValue) => - findItemIndex(valuesRef.current, selectedItemValue, isItemEqualToValue) !== -1, - ); - if (nextValue.length !== value.length) { - setValue(nextValue, eventDetails); - - if (nextValue.length === 0) { - store.set('selectedIndex', null); - selectedItemTextRef.current = null; - } + if (previousValues.length !== 0 && store.state.multiple && Array.isArray(value)) { + const nextValue = value.filter( + (selectedItemValue) => + findItemIndex(nextValues, selectedItemValue, isItemEqualToValue) !== -1, + ); + if (nextValue.length !== value.length) { + setValue(nextValue, eventDetails); + + if (nextValue.length === 0) { + store.set('selectedIndex', null); + selectedItemTextRef.current = null; } } + } - if (open && alignItemWithTriggerActive) { - store.update({ - scrollUpArrowVisible: false, - scrollDownArrowVisible: false, - }); + if (open && alignItemWithTriggerActive) { + store.update({ + scrollUpArrowVisible: false, + scrollDownArrowVisible: false, + }); - const stylesToClear: React.CSSProperties = { height: '' }; - clearStyles(positionerElement, stylesToClear); - clearStyles(popupRef.current, stylesToClear); - } - }, - ); + const stylesToClear: React.CSSProperties = { height: '' }; + clearStyles(positionerElement, stylesToClear); + clearStyles(popupRef.current, stylesToClear); + } + }); + + useIsoLayoutEffect(() => { + return () => { + valuesRef.current = []; + }; + }, [valuesRef]); const contextValue: SelectPositionerContext = React.useMemo( () => ({ @@ -226,7 +228,11 @@ export const SelectPositioner = React.forwardRef(function SelectPositioner( ); return ( - + + elementsRef={listRef} + labelsRef={labelsRef} + onMapChange={onMapChange} + > {mounted && modal && } {element} diff --git a/packages/react/src/tabs/panel/TabsPanel.test.tsx b/packages/react/src/tabs/panel/TabsPanel.test.tsx index 87213e7aa50..3f5671c3623 100644 --- a/packages/react/src/tabs/panel/TabsPanel.test.tsx +++ b/packages/react/src/tabs/panel/TabsPanel.test.tsx @@ -25,6 +25,36 @@ describe('', () => { }); describe('panels sharing a value', () => { + it('gives ownership to a panel that starts sharing the value later', async () => { + function App() { + const [firstValue, setFirstValue] = React.useState('a'); + + return ( + + + + + B + + + + + + ); + } + + const { user } = await render(); + const tab = screen.getByRole('tab'); + + expect(tab).toHaveAttribute('aria-controls', screen.getByTestId('second').id); + + await user.click(screen.getByRole('button', { name: 'share value' })); + + expect(tab).toHaveAttribute('aria-controls', screen.getByTestId('first').id); + }); + it('keeps the surviving registration when a shadowed panel unmounts', async () => { function App() { const [shadowedMounted, setShadowedMounted] = React.useState(true);