Skip to content

[Select] Move item anchoring prop to Positioner #1713

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 29, 2025
7 changes: 6 additions & 1 deletion docs/reference/generated/select-positioner.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
{
"name": "SelectPositioner",
"description": "Positions the select menu popup against the trigger.\nRenders a `<div>` element.",
"description": "Positions the select menu popup.\nRenders a `<div>` element.",
"props": {
"alignItemWithTrigger": {
"type": "boolean",
"default": "true",
"description": "Whether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space."
},
"align": {
"type": "'center' | 'end' | 'start'",
"default": "'center'",
Expand Down
5 changes: 0 additions & 5 deletions docs/reference/generated/select-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@
"type": "RefObject<Actions>",
"description": "A ref to imperative actions.\n- `unmount`: When specified, the select will not be unmounted when closed.\nInstead, the `unmount` function must be called to unmount the select manually.\nUseful when the select's animation is controlled by an external library."
},
"alignItemToTrigger": {
"type": "boolean",
"default": "true",
"description": "Determines if the selected item inside the popup should align to the trigger element."
},
"modal": {
"type": "boolean",
"default": "true",
Expand Down
8 changes: 6 additions & 2 deletions docs/src/app/(private)/experiments/modality.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function Modality() {

function SelectDemo({ modal, withBackdrop }: Props) {
return (
<Select.Root defaultValue="system" modal={modal} alignItemToTrigger={false}>
<Select.Root defaultValue="system" modal={modal}>
<Select.Trigger aria-label="Select font" render={<Trigger />}>
<Select.Value placeholder="System font" />
<SelectDropdownArrow />
Expand All @@ -46,7 +46,11 @@ function SelectDemo({ modal, withBackdrop }: Props) {
{withBackdrop && <Select.Backdrop render={<Backdrop />} />}

<Select.Portal>
<Select.Positioner sideOffset={5} render={<Positioner />}>
<Select.Positioner
sideOffset={5}
render={<Positioner />}
alignItemWithTrigger={false}
>
<SelectPopup>
<SelectItem value="system">
<SelectItemIndicator render={<CheckIcon />} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function PopupsInPopups() {

function SelectDemo({ modal }: Props) {
return (
<Select.Root modal={modal} defaultValue="system" alignItemToTrigger={false}>
<Select.Root modal={modal} defaultValue="system">
<Tooltip.Root>
<Select.Trigger
aria-label="Select font"
Expand All @@ -71,7 +71,11 @@ function SelectDemo({ modal }: Props) {
</Tooltip.Root>

<Select.Portal>
<Select.Positioner sideOffset={5} render={<Positioner />}>
<Select.Positioner
sideOffset={5}
render={<Positioner />}
alignItemWithTrigger={false}
>
<SelectPopup>
<SelectItem value="system">
<SelectItemIndicator render={<CheckIcon />} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import styles from './inside-select.module.css';

export default function ExampleSelect() {
return (
<Select.Root defaultValue="item-1" alignItemToTrigger={false}>
<Select.Root defaultValue="item-1">
<Select.Trigger className={styles.Select}>
<Select.Value placeholder="Item 1" />
<Select.Icon className={styles.SelectIcon}>
<ChevronUpDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8}>
<Select.Positioner
className={styles.Positioner}
sideOffset={8}
alignItemWithTrigger={false}
>
<Select.ScrollUpArrow className={styles.ScrollArrow} />
<Select.Popup className={styles.Popup}>
<ScrollArea.Root className={styles.ScrollArea}>
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/select/arrow/SelectArrow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ describe('<Select.Arrow />', () => {
refInstanceof: window.HTMLDivElement,
render(node) {
return render(
<Select.Root open alignItemToTrigger={false}>
<Select.Positioner>{node}</Select.Positioner>
<Select.Root open>
<Select.Positioner alignItemWithTrigger={false}>{node}</Select.Positioner>
</Select.Root>,
);
},
Expand Down
7 changes: 4 additions & 3 deletions packages/react/src/select/arrow/SelectArrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ const SelectArrow = React.forwardRef(function SelectArrow(
) {
const { className, render, ...otherProps } = props;

const { open, alignItemToTrigger } = useSelectRootContext();
const { arrowRef, side, align, arrowUncentered, arrowStyles } = useSelectPositionerContext();
const { open } = useSelectRootContext();
const { arrowRef, side, align, arrowUncentered, arrowStyles, alignItemWithTriggerActive } =
useSelectPositionerContext();

const getArrowProps = React.useCallback(
(externalProps = {}) =>
Expand Down Expand Up @@ -58,7 +59,7 @@ const SelectArrow = React.forwardRef(function SelectArrow(
customStyleHookMapping: popupStateMapping,
});

if (alignItemToTrigger) {
if (alignItemWithTriggerActive) {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/select/item/useSelectItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R
cursorMovementTimerRef.current = -1;
}

// With `alignItemToTrigger`, avoid re-rendering the root due to `onMouseLeave`
// With `alignItemWithTrigger=true`, avoid re-rendering the root due to `onMouseLeave`
// firing and causing a performance issue when expanding the popup.
if (popup.offsetHeight === prevPopupHeightRef.current) {
// Prevent `onFocus` from causing the highlight to be stuck when quickly moving
Expand Down
13 changes: 3 additions & 10 deletions packages/react/src/select/popup/SelectPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,8 @@ const SelectPopup = React.forwardRef(function SelectPopup(
) {
const { render, className, ...otherProps } = props;

const {
id,
open,
popupRef,
transitionStatus,
alignItemToTrigger,
mounted,
onOpenChangeComplete,
} = useSelectRootContext();
const { id, open, popupRef, transitionStatus, mounted, onOpenChangeComplete } =
useSelectRootContext();
const positioner = useSelectPositionerContext();

useOpenChangeComplete({
Expand Down Expand Up @@ -96,7 +89,7 @@ const SelectPopup = React.forwardRef(function SelectPopup(

return (
<React.Fragment>
{id && alignItemToTrigger && (
{id && positioner.alignItemWithTriggerActive && (
<style
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={html}
Expand Down
34 changes: 17 additions & 17 deletions packages/react/src/select/popup/useSelectPopup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { clearPositionerStyles } from './utils';
import { isWebKit } from '../../utils/detectBrowser';
import { useSelectIndexContext } from '../root/SelectIndexContext';
import { isMouseWithinBounds } from '../../utils/isMouseWithinBounds';
import { useSelectPositionerContext } from '../positioner/SelectPositionerContext';

export function useSelectPopup(): useSelectPopup.ReturnValue {
const {
mounted,
id,
setOpen,
getRootPositionerProps,
alignItemToTrigger,
triggerElement,
positionerElement,
valueRef,
Expand All @@ -26,11 +26,11 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
scrollDownArrowVisible,
setScrollUpArrowVisible,
setScrollDownArrowVisible,
setControlledAlignItemToTrigger,
keyboardActiveRef,
floatingRootContext,
} = useSelectRootContext();
const { setActiveIndex } = useSelectIndexContext();
const { alignItemWithTriggerActive, setControlledItemAnchor } = useSelectPositionerContext();

const initialHeightRef = React.useRef(0);
const reachedMaxHeightRef = React.useRef(false);
Expand All @@ -39,7 +39,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
const originalPositionerStylesRef = React.useRef<React.CSSProperties>({});

const handleScrollArrowVisibility = useEventCallback(() => {
if (!alignItemToTrigger || !popupRef.current) {
if (!alignItemWithTriggerActive || !popupRef.current) {
return;
}

Expand All @@ -58,7 +58,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {

useModernLayoutEffect(() => {
if (
alignItemToTrigger ||
alignItemWithTriggerActive ||
!positionerElement ||
Object.keys(originalPositionerStylesRef.current).length
) {
Expand All @@ -76,10 +76,10 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
marginTop: positionerElement.style.marginTop,
marginBottom: positionerElement.style.marginBottom,
};
}, [alignItemToTrigger, positionerElement]);
}, [alignItemWithTriggerActive, positionerElement]);

useModernLayoutEffect(() => {
if (mounted || alignItemToTrigger) {
if (mounted || alignItemWithTriggerActive) {
return;
}

Expand All @@ -91,12 +91,12 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
if (positionerElement) {
clearPositionerStyles(positionerElement, originalPositionerStylesRef.current);
}
}, [mounted, alignItemToTrigger, positionerElement]);
}, [mounted, alignItemWithTriggerActive, positionerElement]);

useModernLayoutEffect(() => {
if (
!mounted ||
!alignItemToTrigger ||
!alignItemWithTriggerActive ||
!triggerElement ||
!positionerElement ||
!popupRef.current
Expand Down Expand Up @@ -180,7 +180,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
if (fallbackToAlignPopupToTrigger || isPinchZoomed) {
initialPlacedRef.current = true;
clearPositionerStyles(positionerElement, originalPositionerStylesRef.current);
setControlledAlignItemToTrigger(false);
setControlledItemAnchor(false);
return;
}

Expand Down Expand Up @@ -208,7 +208,6 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
});
}, [
mounted,
alignItemToTrigger,
positionerElement,
triggerElement,
valueRef,
Expand All @@ -217,11 +216,12 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
setScrollUpArrowVisible,
setScrollDownArrowVisible,
handleScrollArrowVisibility,
setControlledAlignItemToTrigger,
alignItemWithTriggerActive,
setControlledItemAnchor,
]);

React.useEffect(() => {
if (!alignItemToTrigger || !positionerElement || !mounted) {
if (!alignItemWithTriggerActive || !positionerElement || !mounted) {
return undefined;
}

Expand All @@ -236,7 +236,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
return () => {
win.removeEventListener('resize', handleResize);
};
}, [setOpen, alignItemToTrigger, positionerElement, mounted]);
}, [setOpen, alignItemWithTriggerActive, positionerElement, mounted]);

const getPopupProps: useSelectPopup.ReturnValue['getPopupProps'] = React.useCallback(
(externalProps = {}) => {
Expand All @@ -259,15 +259,15 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
},
onScroll(event) {
if (
!alignItemToTrigger ||
!alignItemWithTriggerActive ||
!positionerElement ||
!popupRef.current ||
!initialPlacedRef.current
) {
return;
}

if (reachedMaxHeightRef.current || !alignItemToTrigger) {
if (reachedMaxHeightRef.current || !alignItemWithTriggerActive) {
handleScrollArrowVisibility();
return;
}
Expand Down Expand Up @@ -318,7 +318,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {

handleScrollArrowVisibility();
},
...(alignItemToTrigger && {
...(alignItemWithTriggerActive && {
style: {
position: 'relative',
maxHeight: '100%',
Expand All @@ -332,7 +332,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue {
},
[
id,
alignItemToTrigger,
alignItemWithTriggerActive,
getRootPositionerProps,
keyboardActiveRef,
setActiveIndex,
Expand Down
Loading