diff --git a/docs/reference/generated/menu-root.json b/docs/reference/generated/menu-root.json index aee7fbf47e..df50a3806f 100644 --- a/docs/reference/generated/menu-root.json +++ b/docs/reference/generated/menu-root.json @@ -23,7 +23,7 @@ "modal": { "type": "boolean", "default": "true", - "description": "Whether the menu should prevent outside clicks and lock page scroll when open." + "description": "Whether the menu should prevent interactivity of other elements\non the page when open and its positioning anchor is visible." }, "disabled": { "type": "boolean", diff --git a/docs/reference/generated/popover-root.json b/docs/reference/generated/popover-root.json index 6b3efcddcc..30515a9d5e 100644 --- a/docs/reference/generated/popover-root.json +++ b/docs/reference/generated/popover-root.json @@ -15,6 +15,11 @@ "type": "(open, event, reason) => void", "description": "Event handler called when the popover is opened or closed." }, + "modal": { + "type": "boolean", + "default": "true", + "description": "Whether the popover should prevent interactivity of other elements\non the page when open and its positioning anchor is visible." + }, "openOnHover": { "type": "boolean", "default": "false", diff --git a/docs/reference/generated/select-root.json b/docs/reference/generated/select-root.json index 50fb8031c1..68d109c879 100644 --- a/docs/reference/generated/select-root.json +++ b/docs/reference/generated/select-root.json @@ -40,7 +40,7 @@ "modal": { "type": "boolean", "default": "true", - "description": "Whether the select should prevent outside clicks and lock page scroll when open." + "description": "Whether the select should prevent interactivity of other elements\non the page when open and its positioning anchor is visible." }, "disabled": { "type": "boolean", diff --git a/packages/react/src/menu/backdrop/MenuBackdrop.tsx b/packages/react/src/menu/backdrop/MenuBackdrop.tsx index 7955ac1df8..d554b11e43 100644 --- a/packages/react/src/menu/backdrop/MenuBackdrop.tsx +++ b/packages/react/src/menu/backdrop/MenuBackdrop.tsx @@ -8,6 +8,7 @@ import { type CustomStyleHookMapping } from '../../utils/getStyleHookProps'; import { popupStateMapping as baseMapping } from '../../utils/popupStateMapping'; import type { TransitionStatus } from '../../utils/useTransitionStatus'; import { transitionStatusMapping } from '../../utils/styleHookMapping'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; const customStyleHookMapping: CustomStyleHookMapping = { ...baseMapping, @@ -25,7 +26,15 @@ const MenuBackdrop = React.forwardRef(function MenuBackdrop( forwardedRef: React.ForwardedRef, ) { const { className, render, keepMounted = false, ...other } = props; - const { open, mounted, transitionStatus } = useMenuRootContext(); + + const { open, mounted, transitionStatus, setBackdropRendered } = useMenuRootContext(); + + useEnhancedEffect(() => { + setBackdropRendered(true); + return () => { + setBackdropRendered(false); + }; + }, [setBackdropRendered]); const state: MenuBackdrop.State = React.useMemo( () => ({ diff --git a/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx b/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx index d39bd9d53d..b94ce200d2 100644 --- a/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx +++ b/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx @@ -29,6 +29,8 @@ const testRootContext: MenuRootContext = { modal: false, positionerRef: { current: null }, allowMouseUpTriggerRef: { current: false }, + backdropRendered: false, + setBackdropRendered: () => {}, }; describe('', () => { diff --git a/packages/react/src/menu/item/MenuItem.test.tsx b/packages/react/src/menu/item/MenuItem.test.tsx index d9fecfcee1..bd9c0e1fd8 100644 --- a/packages/react/src/menu/item/MenuItem.test.tsx +++ b/packages/react/src/menu/item/MenuItem.test.tsx @@ -29,6 +29,8 @@ const testRootContext: MenuRootContext = { modal: false, positionerRef: { current: null }, allowMouseUpTriggerRef: { current: false }, + backdropRendered: false, + setBackdropRendered: () => {}, }; describe('', () => { diff --git a/packages/react/src/menu/positioner/MenuPositioner.test.tsx b/packages/react/src/menu/positioner/MenuPositioner.test.tsx index 934fb73d2f..2cd5525229 100644 --- a/packages/react/src/menu/positioner/MenuPositioner.test.tsx +++ b/packages/react/src/menu/positioner/MenuPositioner.test.tsx @@ -29,6 +29,8 @@ const testRootContext: MenuRootContext = { modal: false, positionerRef: { current: null }, allowMouseUpTriggerRef: { current: false }, + backdropRendered: false, + setBackdropRendered: () => {}, }; describe('', () => { diff --git a/packages/react/src/menu/positioner/MenuPositioner.tsx b/packages/react/src/menu/positioner/MenuPositioner.tsx index 1552afa781..17f5a60e58 100644 --- a/packages/react/src/menu/positioner/MenuPositioner.tsx +++ b/packages/react/src/menu/positioner/MenuPositioner.tsx @@ -141,7 +141,9 @@ const MenuPositioner = React.forwardRef(function MenuPositioner( return ( - {mounted && modal && parentNodeId === null && } + {mounted && modal && parentNodeId === null && ( + + )} {renderElement()} diff --git a/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx b/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx index 012555604d..ddc3a6e058 100644 --- a/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx +++ b/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx @@ -30,6 +30,8 @@ const testRootContext: MenuRootContext = { modal: false, positionerRef: { current: null }, allowMouseUpTriggerRef: { current: false }, + backdropRendered: false, + setBackdropRendered: () => {}, }; const testRadioGroupContext = { diff --git a/packages/react/src/menu/root/MenuRoot.tsx b/packages/react/src/menu/root/MenuRoot.tsx index c56b0129e1..2e6a5bf39b 100644 --- a/packages/react/src/menu/root/MenuRoot.tsx +++ b/packages/react/src/menu/root/MenuRoot.tsx @@ -105,7 +105,8 @@ namespace MenuRoot { */ loop?: boolean; /** - * Whether the menu should prevent outside clicks and lock page scroll when open. + * Whether the menu should prevent interactivity of other elements + * on the page when open and its positioning anchor is visible. * @default true */ modal?: boolean; @@ -191,7 +192,8 @@ MenuRoot.propTypes /* remove-proptypes */ = { */ loop: PropTypes.bool, /** - * Whether the menu should prevent outside clicks and lock page scroll when open. + * Whether the menu should prevent interactivity of other elements + * on the page when open and its positioning anchor is visible. * @default true */ modal: PropTypes.bool, diff --git a/packages/react/src/menu/root/useMenuRoot.ts b/packages/react/src/menu/root/useMenuRoot.ts index 72d0e7008d..77113ccf5d 100644 --- a/packages/react/src/menu/root/useMenuRoot.ts +++ b/packages/react/src/menu/root/useMenuRoot.ts @@ -49,6 +49,7 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret const positionerRef = React.useRef(null); const [hoverEnabled, setHoverEnabled] = React.useState(true); const [activeIndex, setActiveIndex] = React.useState(null); + const [backdropRendered, setBackdropRendered] = React.useState(false); const [open, setOpenUnwrapped] = useControlled({ controlled: openParam, @@ -66,7 +67,7 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret const { mounted, setMounted, transitionStatus } = useTransitionStatus(open); - useScrollLock(open && modal, triggerElement); + useScrollLock(open && modal && backdropRendered, triggerElement); const setOpen = useEventCallback((nextOpen: boolean, event?: Event) => { onOpenChange?.(nextOpen, event); @@ -106,7 +107,7 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret const dismiss = useDismiss(floatingRootContext, { bubbles: closeParentOnEsc && nested, - outsidePressEvent: 'mousedown', + outsidePressEvent: modal || backdropRendered ? 'mousedown' : undefined, }); const role = useRole(floatingRootContext, { @@ -191,6 +192,8 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret setPositionerElement, setTriggerElement, transitionStatus, + backdropRendered, + setBackdropRendered, }), [ activeIndex, @@ -198,14 +201,12 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret getItemProps, getPopupProps, getTriggerProps, - itemDomElements, - itemLabels, mounted, open, - positionerRef, setOpen, - transitionStatus, setPositionerElement, + transitionStatus, + backdropRendered, ], ); } @@ -289,5 +290,7 @@ export namespace useMenuRoot { setTriggerElement: (element: HTMLElement | null) => void; transitionStatus: TransitionStatus; allowMouseUpTriggerRef: React.RefObject; + backdropRendered: boolean; + setBackdropRendered: (value: boolean) => void; } } diff --git a/packages/react/src/menu/trigger/MenuTrigger.test.tsx b/packages/react/src/menu/trigger/MenuTrigger.test.tsx index b994e047fd..c0f88dcfa4 100644 --- a/packages/react/src/menu/trigger/MenuTrigger.test.tsx +++ b/packages/react/src/menu/trigger/MenuTrigger.test.tsx @@ -29,6 +29,8 @@ const testRootContext: MenuRootContext = { modal: false, positionerRef: { current: null }, allowMouseUpTriggerRef: { current: false }, + backdropRendered: false, + setBackdropRendered: () => {}, }; describe('', () => { diff --git a/packages/react/src/popover/backdrop/PopoverBackdrop.tsx b/packages/react/src/popover/backdrop/PopoverBackdrop.tsx index 0ebee86807..fc2ef4b193 100644 --- a/packages/react/src/popover/backdrop/PopoverBackdrop.tsx +++ b/packages/react/src/popover/backdrop/PopoverBackdrop.tsx @@ -8,6 +8,7 @@ import { type CustomStyleHookMapping } from '../../utils/getStyleHookProps'; import { popupStateMapping as baseMapping } from '../../utils/popupStateMapping'; import type { TransitionStatus } from '../../utils/useTransitionStatus'; import { transitionStatusMapping } from '../../utils/styleHookMapping'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; const customStyleHookMapping: CustomStyleHookMapping = { ...baseMapping, @@ -26,7 +27,14 @@ const PopoverBackdrop = React.forwardRef(function PopoverBackdrop( ) { const { className, render, keepMounted = false, ...other } = props; - const { open, mounted, transitionStatus } = usePopoverRootContext(); + const { open, mounted, transitionStatus, setBackdropRendered } = usePopoverRootContext(); + + useEnhancedEffect(() => { + setBackdropRendered(true); + return () => { + setBackdropRendered(false); + }; + }, [setBackdropRendered]); const state: PopoverBackdrop.State = React.useMemo( () => ({ diff --git a/packages/react/src/popover/positioner/PopoverPositioner.tsx b/packages/react/src/popover/positioner/PopoverPositioner.tsx index 0b8ded616d..d755e7d8ae 100644 --- a/packages/react/src/popover/positioner/PopoverPositioner.tsx +++ b/packages/react/src/popover/positioner/PopoverPositioner.tsx @@ -10,6 +10,7 @@ import { HTMLElementType } from '../../utils/proptypes'; import type { BaseUIComponentProps } from '../../utils/types'; import type { Side, Align } from '../../utils/useAnchorPositioning'; import { popupStateMapping } from '../../utils/popupStateMapping'; +import { InternalBackdrop } from '../../utils/InternalBackdrop'; /** * Positions the popover against the trigger. @@ -38,7 +39,7 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner( ...otherProps } = props; - const { floatingRootContext, open, mounted, setPositionerElement, popupRef, openMethod } = + const { floatingRootContext, open, mounted, setPositionerElement, popupRef, openMethod, modal } = usePopoverRootContext(); const positioner = usePopoverPositioner({ @@ -46,7 +47,6 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner( floatingRootContext, positionMethod, mounted, - open, keepMounted, side, sideOffset, @@ -89,6 +89,7 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner( return ( + {mounted && modal && } {renderElement()} ); diff --git a/packages/react/src/popover/positioner/usePopoverPositioner.tsx b/packages/react/src/popover/positioner/usePopoverPositioner.tsx index 6d9456b5ec..ffac3f9774 100644 --- a/packages/react/src/popover/positioner/usePopoverPositioner.tsx +++ b/packages/react/src/popover/positioner/usePopoverPositioner.tsx @@ -9,11 +9,14 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; import { type Boundary, type Side, useAnchorPositioning } from '../../utils/useAnchorPositioning'; import type { GenericHTMLProps } from '../../utils/types'; import { InteractionType } from '../../utils/useEnhancedClickHandler'; +import { usePopoverRootContext } from '../root/PopoverRootContext'; export function usePopoverPositioner( params: usePopoverPositioner.Parameters, ): usePopoverPositioner.ReturnValue { - const { open = false, keepMounted = false, mounted } = params; + const { keepMounted = false, mounted } = params; + + const { open } = usePopoverRootContext(); const { positionerStyles, @@ -149,10 +152,6 @@ export namespace usePopoverPositioner { * Whether the popover is mounted. */ mounted: boolean; - /** - * Whether the popover is currently open. - */ - open?: boolean; /** * The floating root context. */ diff --git a/packages/react/src/popover/root/PopoverRoot.tsx b/packages/react/src/popover/root/PopoverRoot.tsx index 1feeb4dd2c..d59b22cc0a 100644 --- a/packages/react/src/popover/root/PopoverRoot.tsx +++ b/packages/react/src/popover/root/PopoverRoot.tsx @@ -13,32 +13,13 @@ import { PortalContext } from '../../portal/PortalContext'; * Documentation: [Base UI Popover](https://base-ui.com/react/components/popover) */ const PopoverRoot: React.FC = function PopoverRoot(props) { - const { openOnHover = false, delay, closeDelay = 0 } = props; + const { openOnHover = false, modal = true, delay, closeDelay = 0 } = props; const delayWithDefault = delay ?? OPEN_DELAY; - const { - open, - setOpen, - mounted, - setMounted, - setTriggerElement, - positionerElement, - setPositionerElement, - popupRef, - instantType, - transitionStatus, - floatingRootContext, - getRootTriggerProps, - getRootPopupProps, - titleId, - setTitleId, - descriptionId, - setDescriptionId, - openMethod, - openReason, - } = usePopoverRoot({ + const popoverRoot = usePopoverRoot({ openOnHover, + modal, delay: delayWithDefault, closeDelay, open: props.open, @@ -48,58 +29,18 @@ const PopoverRoot: React.FC = function PopoverRoot(props) { const contextValue: PopoverRootContext = React.useMemo( () => ({ + ...popoverRoot, openOnHover, delay: delayWithDefault, closeDelay, - open, - setOpen, - setTriggerElement, - positionerElement, - setPositionerElement, - popupRef, - mounted, - setMounted, - instantType, - transitionStatus, - titleId, - setTitleId, - descriptionId, - setDescriptionId, - floatingRootContext, - getRootPopupProps, - getRootTriggerProps, - openMethod, - openReason, + modal, }), - [ - openOnHover, - delayWithDefault, - closeDelay, - open, - setOpen, - setTriggerElement, - positionerElement, - setPositionerElement, - popupRef, - mounted, - setMounted, - instantType, - transitionStatus, - titleId, - setTitleId, - descriptionId, - setDescriptionId, - floatingRootContext, - getRootPopupProps, - getRootTriggerProps, - openMethod, - openReason, - ], + [popoverRoot, openOnHover, delayWithDefault, closeDelay, modal], ); return ( - {props.children} + {props.children} ); }; @@ -143,6 +84,12 @@ PopoverRoot.propTypes /* remove-proptypes */ = { * @default 300 */ delay: PropTypes.number, + /** + * Whether the popover should prevent interactivity of other elements + * on the page when open and its positioning anchor is visible. + * @default true + */ + modal: PropTypes.bool, /** * Event handler called when the popover is opened or closed. */ diff --git a/packages/react/src/popover/root/PopoverRootContext.ts b/packages/react/src/popover/root/PopoverRootContext.ts index 63a47e91ac..953e630476 100644 --- a/packages/react/src/popover/root/PopoverRootContext.ts +++ b/packages/react/src/popover/root/PopoverRootContext.ts @@ -29,6 +29,9 @@ export interface PopoverRootContext { getRootPopupProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; openMethod: InteractionType | null; openReason: OpenChangeReason | null; + modal: boolean; + backdropRendered: boolean; + setBackdropRendered: React.Dispatch>; } export const PopoverRootContext = React.createContext(undefined); diff --git a/packages/react/src/popover/root/usePopoverRoot.ts b/packages/react/src/popover/root/usePopoverRoot.ts index 02ecfe22f5..f7cd2a1990 100644 --- a/packages/react/src/popover/root/usePopoverRoot.ts +++ b/packages/react/src/popover/root/usePopoverRoot.ts @@ -24,6 +24,7 @@ import { type OpenChangeReason, } from '../../utils/translateOpenChangeReason'; import { useAfterExitAnimation } from '../../utils/useAfterExitAnimation'; +import { useScrollLock } from '../../utils/useScrollLock'; export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoot.ReturnValue { const { @@ -33,6 +34,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo delay, closeDelay, openOnHover = false, + modal, } = params; const delayWithDefault = delay ?? OPEN_DELAY; @@ -45,6 +47,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo const [positionerElement, setPositionerElement] = React.useState(null); const [openReason, setOpenReason] = React.useState(null); const [clickEnabled, setClickEnabled] = React.useState(true); + const [backdropRendered, setBackdropRendered] = React.useState(false); const popupRef = React.useRef(null); const clickEnabledTimeoutRef = React.useRef(-1); @@ -64,6 +67,10 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo const { mounted, setMounted, transitionStatus } = useTransitionStatus(open); + const { openMethod, triggerProps } = useOpenInteractionType(open); + + useScrollLock(open && modal && backdropRendered, triggerElement); + const setOpen = useEventCallback( (nextOpen: boolean, event?: Event, reason?: OpenChangeReason) => { onOpenChange(nextOpen, event, reason); @@ -129,7 +136,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo enabled: openOnHover, mouseOnly: true, move: false, - handleClose: safePolygon(), + handleClose: safePolygon({ blockPointerEvents: true }), restMs: computedRestMs, delay: { close: closeDelayWithDefault, @@ -141,14 +148,20 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo stickIfOpen: false, }); - const dismiss = useDismiss(context); + let outsidePressEvent: 'mousedown' | undefined = + modal || backdropRendered ? 'mousedown' : undefined; + // For infotips (`openOnHover`), ensure another infotip can immediately open on tap + if (!backdropRendered && openOnHover && openMethod === 'touch') { + outsidePressEvent = undefined; + } + const dismiss = useDismiss(context, { + outsidePressEvent, + }); const role = useRole(context); const { getReferenceProps, getFloatingProps } = useInteractions([hover, click, dismiss, role]); - const { openMethod, triggerProps } = useOpenInteractionType(open); - return React.useMemo( () => ({ open, @@ -171,6 +184,8 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo instantType, openMethod, openReason, + backdropRendered, + setBackdropRendered, }), [ mounted, @@ -188,6 +203,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo openMethod, triggerProps, openReason, + backdropRendered, ], ); } @@ -229,6 +245,12 @@ export namespace usePopoverRoot { * @default 0 */ closeDelay?: number; + /** + * Whether the popover should prevent interactivity of other elements + * on the page when open and its positioning anchor is visible. + * @default true + */ + modal?: boolean; } export interface ReturnValue { @@ -251,5 +273,7 @@ export namespace usePopoverRoot { popupRef: React.RefObject; openMethod: InteractionType | null; openReason: OpenChangeReason | null; + backdropRendered: boolean; + setBackdropRendered: React.Dispatch>; } } diff --git a/packages/react/src/select/backdrop/SelectBackdrop.tsx b/packages/react/src/select/backdrop/SelectBackdrop.tsx index f45cf3f592..957feaaaa2 100644 --- a/packages/react/src/select/backdrop/SelectBackdrop.tsx +++ b/packages/react/src/select/backdrop/SelectBackdrop.tsx @@ -8,6 +8,7 @@ import { popupStateMapping } from '../../utils/popupStateMapping'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; import type { TransitionStatus } from '../../utils/useTransitionStatus'; import { transitionStatusMapping } from '../../utils/styleHookMapping'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; const customStyleHookMapping: CustomStyleHookMapping = { ...popupStateMapping, @@ -26,7 +27,14 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop( ) { const { className, render, keepMounted = false, ...other } = props; - const { open, mounted, transitionStatus } = useSelectRootContext(); + const { open, mounted, transitionStatus, setBackdropRendered } = useSelectRootContext(); + + useEnhancedEffect(() => { + setBackdropRendered(true); + return () => { + setBackdropRendered(false); + }; + }, [setBackdropRendered]); const state: SelectBackdrop.State = React.useMemo( () => ({ open, transitionStatus }), diff --git a/packages/react/src/select/positioner/SelectPositioner.tsx b/packages/react/src/select/positioner/SelectPositioner.tsx index cb3a629092..78f1dc252e 100644 --- a/packages/react/src/select/positioner/SelectPositioner.tsx +++ b/packages/react/src/select/positioner/SelectPositioner.tsx @@ -84,7 +84,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( return ( - {mounted && modal && } + {mounted && modal && } {renderElement()} diff --git a/packages/react/src/select/positioner/useSelectPositioner.ts b/packages/react/src/select/positioner/useSelectPositioner.ts index d672929ac6..fb29495ce1 100644 --- a/packages/react/src/select/positioner/useSelectPositioner.ts +++ b/packages/react/src/select/positioner/useSelectPositioner.ts @@ -10,14 +10,11 @@ import type { GenericHTMLProps } from '../../utils/types'; import { type Boundary, type Side, useAnchorPositioning } from '../../utils/useAnchorPositioning'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useSelectRootContext } from '../root/SelectRootContext'; -import { useScrollLock } from '../../utils/useScrollLock'; export function useSelectPositioner( params: useSelectPositioner.Parameters, ): useSelectPositioner.ReturnValue { - const { open, alignItemToTrigger, mounted, triggerElement, modal } = useSelectRootContext(); - - useScrollLock((alignItemToTrigger || modal) && open, triggerElement); + const { open, alignItemToTrigger, mounted } = useSelectRootContext(); const { positionerStyles: enabledPositionerStyles, diff --git a/packages/react/src/select/root/SelectRoot.tsx b/packages/react/src/select/root/SelectRoot.tsx index c7848d5f23..d1e5337bd2 100644 --- a/packages/react/src/select/root/SelectRoot.tsx +++ b/packages/react/src/select/root/SelectRoot.tsx @@ -145,7 +145,8 @@ SelectRoot.propTypes /* remove-proptypes */ = { */ disabled: PropTypes.bool, /** - * Whether the select should prevent outside clicks and lock page scroll when open. + * Whether the select should prevent interactivity of other elements + * on the page when open and its positioning anchor is visible. * @default true */ modal: PropTypes.bool, diff --git a/packages/react/src/select/root/SelectRootContext.ts b/packages/react/src/select/root/SelectRootContext.ts index 2c569abc4a..fd7b572910 100644 --- a/packages/react/src/select/root/SelectRootContext.ts +++ b/packages/react/src/select/root/SelectRootContext.ts @@ -51,6 +51,8 @@ export interface SelectRootContext { id: string | undefined; fieldControlValidation: ReturnType; modal: boolean; + backdropRendered: boolean; + setBackdropRendered: React.Dispatch>; } export const SelectRootContext = React.createContext(null); diff --git a/packages/react/src/select/root/useSelectRoot.ts b/packages/react/src/select/root/useSelectRoot.ts index ea66f29e15..8d82b78689 100644 --- a/packages/react/src/select/root/useSelectRoot.ts +++ b/packages/react/src/select/root/useSelectRoot.ts @@ -12,7 +12,7 @@ import { useFieldControlValidation } from '../../field/control/useFieldControlVa import { useFieldRootContext } from '../../field/root/FieldRootContext'; import { useBaseUiId } from '../../utils/useBaseUiId'; import { useControlled } from '../../utils/useControlled'; -import { type TransitionStatus, useTransitionStatus } from '../../utils'; +import { type TransitionStatus, useScrollLock, useTransitionStatus } from '../../utils'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useEventCallback } from '../../utils/useEventCallback'; import { warn } from '../../utils/warn'; @@ -80,11 +80,14 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelect const [touchModality, setTouchModality] = React.useState(false); const [scrollUpArrowVisible, setScrollUpArrowVisible] = React.useState(false); const [scrollDownArrowVisible, setScrollDownArrowVisible] = React.useState(false); + const [backdropRendered, setBackdropRendered] = React.useState(false); const { mounted, setMounted, transitionStatus } = useTransitionStatus(open); const alignItemToTrigger = Boolean(mounted && controlledAlignItemToTrigger && !touchModality); + useScrollLock(open && (alignItemToTrigger || (modal && backdropRendered)), triggerElement); + if (!mounted && controlledAlignItemToTrigger !== alignItemToTriggerParam) { setcontrolledAlignItemToTrigger(alignItemToTriggerParam); } @@ -171,7 +174,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelect const dismiss = useDismiss(floatingRootContext, { bubbles: false, - outsidePressEvent: 'mousedown', + outsidePressEvent: modal || backdropRendered ? 'mousedown' : undefined, }); const role = useRole(floatingRootContext, { @@ -263,6 +266,8 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelect transitionStatus, fieldControlValidation, modal, + backdropRendered, + setBackdropRendered, }), [ id, @@ -290,6 +295,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelect transitionStatus, fieldControlValidation, modal, + backdropRendered, ], ); @@ -377,7 +383,8 @@ export namespace useSelectRoot { */ transitionStatus?: TransitionStatus; /** - * Whether the select should prevent outside clicks and lock page scroll when open. + * Whether the select should prevent interactivity of other elements + * on the page when open and its positioning anchor is visible. * @default true */ modal?: boolean;