Add support for popper-content-wrapper styling
Feature request
Mainly I would like to request the support for using Popper while using an "absolute" floating strategy rather than fixed. Eventually, the popper-content-wrapper will get styled with "position: fixed;" and there is no straight-forward way to change it.
Overview
In the codebase you can see that the strategy is hardcoded strategy: 'fixed',
which is the cause of the "position: fixed;" style.
So all components that use the popper must use this "fixed" strategy and it would be quite hard to choose the "absolute" strategy.
The main reason I had this need is due to the following issue: Popover inside a scrollable container will appear outside of the container when exceeding its boundaries, even if its anchor is in the hidden overflown section of the container:
Who does this impact? Who is this for?
I think it might impact everyone
Additional context
In order to solve that issue I had to add {absolute: !important} to css because the data-radix-popper-content-wrapper's fixed position is set as a style:
.absolute-popover-container > [data-radix-popper-content-wrapper] {
position: absolute !important;
pointer-events: all;
}
Just ran into this issue today as well and found myself here after some googling. Would love to be able to either style it directly, or pass a prop to make it position: absolute instead of position: fixed.
+1 on this, give us some control on data-radix-popper-content-wrapper
+1 on this, I want to be able to style the options like a popover, so it behaves kinda like a native mobile dropdown
if anybody cares I wrote this patch (with patch-package) to allow me to set a className to the wrapper (because I use Tailwind, same could be done with style). I also added a position mode called "modal" that takes the whole screen and centers the options.
This could be done cleaner for sure, I just need a fix quickly for now. :)
diff --git a/node_modules/@radix-ui/react-select/dist/index.d.ts b/node_modules/@radix-ui/react-select/dist/index.d.ts
index 9775d79..11c7cdf 100644
--- a/node_modules/@radix-ui/react-select/dist/index.d.ts
+++ b/node_modules/@radix-ui/react-select/dist/index.d.ts
@@ -78,7 +78,8 @@ interface SelectContentImplProps extends Omit<SelectPopperPositionProps, keyof S
* Can be prevented.
*/
onPointerDownOutside?: DismissableLayerProps['onPointerDownOutside'];
- position?: 'item-aligned' | 'popper';
+ wrapperClassName?: string;
+ position?: 'item-aligned' | 'popper' | 'modal';
}
interface SelectItemAlignedPositionProps extends PrimitiveDivProps, SelectPopperPrivateProps {
}
diff --git a/node_modules/@radix-ui/react-select/dist/index.js b/node_modules/@radix-ui/react-select/dist/index.js
index 25d9296..603225c 100644
--- a/node_modules/@radix-ui/react-select/dist/index.js
+++ b/node_modules/@radix-ui/react-select/dist/index.js
@@ -461,7 +461,7 @@ var SelectContentImpl = React.forwardRef(
},
[context.value]
);
- const SelectPosition = position === "popper" ? SelectPopperPosition : SelectItemAlignedPosition;
+ const SelectPosition = position === "popper" ? SelectPopperPosition : position === "modal" ? SelectModalPosition : SelectItemAlignedPosition;
const popperContentProps = SelectPosition === SelectPopperPosition ? {
side,
sideOffset,
@@ -563,7 +563,7 @@ var SelectContentImpl = React.forwardRef(
SelectContentImpl.displayName = CONTENT_IMPL_NAME;
var ITEM_ALIGNED_POSITION_NAME = "SelectItemAlignedPosition";
var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
- const { __scopeSelect, onPlaced, ...popperProps } = props;
+ const { __scopeSelect, onPlaced, wrapperClassName, ...popperProps } = props;
const context = useSelectContext(CONTENT_NAME, __scopeSelect);
const contentContext = useSelectContentContext(CONTENT_NAME, __scopeSelect);
const [contentWrapper, setContentWrapper] = React.useState(null);
@@ -693,6 +693,7 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
position: "fixed",
zIndex: contentZIndex
},
+ className: wrapperClassName,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_primitive.Primitive.div,
{
@@ -714,12 +715,58 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
);
});
SelectItemAlignedPosition.displayName = ITEM_ALIGNED_POSITION_NAME;
+var MODAL_POSITION_NAME = "SelectModalPosition";
+var SelectModalPosition = React.forwardRef((props, forwardedRef) => {
+ const { __scopeSelect, onPlaced, wrapperClassName, ...popperProps } = props;
+ const [contentWrapper, setContentWrapper] = React.useState(null);
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
+ SelectViewportProvider,
+ {
+ scope: __scopeSelect,
+ contentWrapper,
+ shouldExpandOnScrollRef,
+ onScrollButtonChange: handleScrollButtonChange,
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
+ "div",
+ {
+ ref: setContentWrapper,
+ style: {
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ position: "fixed",
+ left: 0,
+ top: 0,
+ width: "100vw",
+ height: "100vh",
+ zIndex: 100,
+ },
+ className: wrapperClassName,
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
+ import_react_primitive.Primitive.div,
+ {
+ ...popperProps,
+ ref: composedRefs,
+ style: popperProps.style,
+ }
+ )
+ }
+ )
+ }
+ );
+});
+SelectModalPosition.displayName = MODAL_POSITION_NAME;
+
+
var POPPER_POSITION_NAME = "SelectPopperPosition";
var SelectPopperPosition = React.forwardRef((props, forwardedRef) => {
const {
__scopeSelect,
align = "start",
collisionPadding = CONTENT_MARGIN,
+ wrapperClassName,
+ className,
...popperProps
} = props;
const popperScope = usePopperScope(__scopeSelect);
@@ -730,6 +777,7 @@ var SelectPopperPosition = React.forwardRef((props, forwardedRef) => {
...popperProps,
ref: forwardedRef,
align,
+ className: [className, wrapperClassName].filter(Boolean).join(" "),
collisionPadding,
style: {
// Ensure border-box for floating-ui calculations
diff --git a/node_modules/@radix-ui/react-select/dist/index.mjs b/node_modules/@radix-ui/react-select/dist/index.mjs
index 5d874b7..d8cc59c 100644
--- a/node_modules/@radix-ui/react-select/dist/index.mjs
+++ b/node_modules/@radix-ui/react-select/dist/index.mjs
@@ -394,7 +394,7 @@ var SelectContentImpl = React.forwardRef(
},
[context.value]
);
- const SelectPosition = position === "popper" ? SelectPopperPosition : SelectItemAlignedPosition;
+ const SelectPosition = position === "popper" ? SelectPopperPosition : position === "modal" ? SelectModalPosition : SelectItemAlignedPosition;
const popperContentProps = SelectPosition === SelectPopperPosition ? {
side,
sideOffset,
@@ -496,7 +496,7 @@ var SelectContentImpl = React.forwardRef(
SelectContentImpl.displayName = CONTENT_IMPL_NAME;
var ITEM_ALIGNED_POSITION_NAME = "SelectItemAlignedPosition";
var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
- const { __scopeSelect, onPlaced, ...popperProps } = props;
+ const { __scopeSelect, onPlaced, wrapperClassName, ...popperProps } = props;
const context = useSelectContext(CONTENT_NAME, __scopeSelect);
const contentContext = useSelectContentContext(CONTENT_NAME, __scopeSelect);
const [contentWrapper, setContentWrapper] = React.useState(null);
@@ -624,7 +624,8 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
display: "flex",
flexDirection: "column",
position: "fixed",
- zIndex: contentZIndex
+ zIndex: contentZIndex,
+ className: wrapperClassName,
},
children: /* @__PURE__ */ jsx(
Primitive.div,
@@ -647,12 +648,58 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
);
});
SelectItemAlignedPosition.displayName = ITEM_ALIGNED_POSITION_NAME;
+var MODAL_POSITION_NAME = "SelectModalPosition";
+var SelectModalPosition = React.forwardRef((props, forwardedRef) => {
+ const { __scopeSelect, onPlaced, wrapperClassName, ...popperProps } = props;
+ const [content, setContent] = React.useState(null);
+ const [contentWrapper, setContentWrapper] = React.useState(null);
+ const composedRefs = useComposedRefs(forwardedRef, (node) => setContent(node));
+ return /* @__PURE__ */ jsx(
+ SelectViewportProvider,
+ {
+ scope: __scopeSelect,
+ contentWrapper,
+ shouldExpandOnScrollRef: false,
+ onScrollButtonChange: () => {},
+ children: /* @__PURE__ */ jsx(
+ "div",
+ {
+ ref: setContentWrapper,
+ style: {
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ position: "fixed",
+ left: 0,
+ top: 0,
+ width: "100vw",
+ height: "100vh",
+ zIndex: 100,
+ },
+ className: wrapperClassName,
+ children: /* @__PURE__ */ jsx(
+ Primitive.div,
+ {
+ ...popperProps,
+ ref: composedRefs,
+ style: popperProps.style,
+ }
+ )
+ }
+ )
+ }
+ );
+});
+SelectModalPosition.displayName = MODAL_POSITION_NAME;
var POPPER_POSITION_NAME = "SelectPopperPosition";
var SelectPopperPosition = React.forwardRef((props, forwardedRef) => {
const {
__scopeSelect,
align = "start",
collisionPadding = CONTENT_MARGIN,
+ wrapperClassName,
+ className,
...popperProps
} = props;
const popperScope = usePopperScope(__scopeSelect);
@@ -661,6 +708,7 @@ var SelectPopperPosition = React.forwardRef((props, forwardedRef) => {
{
...popperScope,
...popperProps,
+ className: [className, wrapperClassName].filter(Boolean).join(" "),
ref: forwardedRef,
align,
collisionPadding,
Facing the same issue
a possible workaround (here it changes the wrapper's styles on small screens):
@media not all and (min-width: 640px) {
[data-radix-popper-content-wrapper] {
position: absolute !important;
bottom: 1rem !important;
left: 1rem !important;
top: 1rem !important;
transform: none !important;
}
}
PS: but this will change all your popovers PS2: ended up adding this rule dynamically on popover opening
function fixMobilePopover(open: boolean) {
const ID = 'popover-fix';
if (open) {
const style = document.createElement('style');
style.id = ID;
style.innerHTML = `
@media (max-width: 640px) {
[data-radix-popper-content-wrapper] {
position: absolute !important;
bottom: 1rem !important;
left: unset !important;
right: 1rem !important;
top: 1rem !important;
transform: none !important;
}
}
`;
document.head.appendChild(style);
} else {
const style = document.getElementById(ID);
if (style) {
// wait for the popover to close before removing the style
setTimeout(() => {
document.head.removeChild(style);
}, 150);
}
}
}
in your react component:
useEffect(() => {
fixMobilePopover(isOpen);
}, [isOpen]);
A workaround using tailwind:
tailwind.config.js:
plugins: [
function ({ addVariant }) {
addVariant(
'radix-popper-wrapper',
'& [data-radix-popper-content-wrapper]'
);
},
],
The component:
<Popover.Root>
<div
ref={containerRef}
className="radix-popper-wrapper:!absolute radix-popper-wrapper:!transform-none radix-popper-wrapper:!right-0 radix-popper-wrapper:!top-12 relative"
>
<Popover.Trigger asChild>
<button>
TRIGGER
</button>
</Popover.Trigger>
<Popover.Portal container={containerRef.current}>
<Popover.Content
>
CONTENT
</Popover.Content>
</Popover.Portal>
</div>
</Popover.Root>
Is there a reason it's not exposed? It seems like a very obvious change... Is there some implementation detail that's hidden to keep folks out of trouble?
I need to add some classes to this wrapper, what seems not possible too, thats weird
Alright I got a solution for this (workaround). I'm still testing the coverage of effect of this solution but to minimize the effect of it I had to introduce a class:
// css
.DropdownTbody
[data-radix-popper-content-wrapper] {
position: absolute !important;
}
// jsx
<tbody className='relative DropdownTbody'> {/* placing class here /*}
<DropdownMenu open={filterMenuOpen} onOpenChange={setFilterMenuOpen}>
<DropdownMenuContent
className="DropdownMenuContent outline-none drop-shadow-2xl shadow-2xl bg-[#1A1A1A] z-50"
alignOffset={9}
sideOffset={10}
align="end"
side='bottom'
>
<DropdownMenuItem className="outline-none">
<div className="flex gap-2 text-xs items-center p-4 bg-[#1A1A1A] text-white hover:cursor-pointer hover:bg-[#333] absolute z-50 min-w-fit w-[250px]" >
Filter menu
</div> {/* making this div absolute to the tbody of a table which is now relative /*}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</tbody>