Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions packages/react/src/select/item/SelectItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ export const SelectItem = React.memo(
} = componentProps;

const textRef = React.useRef<HTMLElement | null>(null);
const metadata = React.useMemo(() => ({ value: itemValue }), [itemValue]);
const listItem = useCompositeListItem({
guess: true,
label,
metadata,
textRef,
});

Expand All @@ -55,7 +57,6 @@ export const SelectItem = React.memo(
setValue,
selectionRef,
typingRef,
valuesRef,
multiple,
selectedItemTextRef,
disabled: selectDisabled,
Expand All @@ -72,15 +73,6 @@ export const SelectItem = React.memo(

const itemRef = React.useRef<HTMLDivElement | null>(null);

useIsoLayoutEffect(() => {
const values = valuesRef.current;
values[index] = itemValue;

return () => {
delete values[index];
};
}, [index, itemValue, valuesRef]);

useIsoLayoutEffect(() => {
const selectedValue = store.state.value;

Expand Down
114 changes: 60 additions & 54 deletions packages/react/src/select/positioner/SelectPositioner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -151,67 +154,66 @@ export const SelectPositioner = React.forwardRef(function SelectPositioner(
inert: !open,
});

const prevMapSizeRef = React.useRef(0);

const onMapChange = useStableCallback(
(map: Map<Element, { index?: number | null | undefined } | null>) => {
if (valuesRef.current.length === 0) {
return;
}
const onMapChange = useStableCallback((map: Map<Element, CompositeMetadata<{ value: any }>>) => {
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(
() => ({
Expand All @@ -226,7 +228,11 @@ export const SelectPositioner = React.forwardRef(function SelectPositioner(
);

return (
<CompositeList elementsRef={listRef} labelsRef={labelsRef} onMapChange={onMapChange}>
<CompositeList<{ value: any }>
elementsRef={listRef}
labelsRef={labelsRef}
onMapChange={onMapChange}
>
<SelectPositionerContext.Provider value={contextValue}>
{mounted && modal && <InternalBackdrop inert={inertValue(!open)} cutout={triggerElement} />}
{element}
Expand Down
30 changes: 30 additions & 0 deletions packages/react/src/tabs/panel/TabsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,36 @@ describe('<Tabs.Panel />', () => {
});

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 (
<React.Fragment>
<button type="button" onClick={() => setFirstValue('b')}>
share value
</button>
<Tabs.Root value="c">
<Tabs.List>
<Tabs.Tab value="b">B</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value={firstValue} keepMounted data-testid="first" />
<Tabs.Panel value="b" keepMounted data-testid="second" />
</Tabs.Root>
</React.Fragment>
);
}

const { user } = await render(<App />);
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);
Expand Down
Loading