primitives icon indicating copy to clipboard operation
primitives copied to clipboard

Add support for popper-content-wrapper styling

Open eshelsil opened this issue 1 year ago • 11 comments

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:

Issue Example on Codesandbox Screenshot 2024-05-18 at 14 27 34

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;
}

Hack Solution Example in Codesandbox Screenshot 2024-05-18 at 14 24 12

eshelsil avatar May 18 '24 11:05 eshelsil

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.

mrdavidjcole avatar Sep 18 '24 18:09 mrdavidjcole

+1 on this, give us some control on data-radix-popper-content-wrapper

JUDE-EQ avatar Sep 30 '24 20:09 JUDE-EQ

+1 on this, I want to be able to style the options like a popover, so it behaves kinda like a native mobile dropdown

christian-reichart avatar Oct 01 '24 06:10 christian-reichart

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,

christian-reichart avatar Oct 09 '24 07:10 christian-reichart

Facing the same issue

RomanSkrypnik avatar Oct 09 '24 10:10 RomanSkrypnik

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]);

ivkrpv avatar Oct 14 '24 14:10 ivkrpv

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>

samuel99-c avatar Oct 31 '24 09:10 samuel99-c

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?

subvertallchris avatar Nov 05 '24 21:11 subvertallchris

I need to add some classes to this wrapper, what seems not possible too, thats weird

kachurun avatar Nov 13 '24 00:11 kachurun

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> 

ashwinchandran13 avatar Mar 15 '25 09:03 ashwinchandran13