Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ const Components = (props: ComponentsProps) => {
componentName={'UserProfileModal'}
modalContainerSx={{ alignItems: 'center' }}
modalContentSx={t => ({ height: `min(${t.sizes.$176}, calc(100% - ${t.sizes.$12}))`, margin: 0 })}
portal={(userProfileModal as UserProfileProps & { portal?: boolean | (() => HTMLElement | null) })?.portal}
>
<UserProfileModal {...userProfileModal} />
</LazyModalRenderer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@ import { withCardStateProvider, withFloatingTree } from '@/ui/elements/contexts'
import { Popover } from '@/ui/elements/Popover';

import { AcceptedInvitationsProvider, useOrganizationSwitcherContext, withCoreUserGuard } from '../../contexts';
import { usePortalContext } from '../../contexts/PortalContext';
import { Flow } from '../../customizables';
import { usePopover } from '../../hooks';
import { OrganizationSwitcherPopover } from './OrganizationSwitcherPopover';
import { OrganizationSwitcherTrigger } from './OrganizationSwitcherTrigger';

const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
const { defaultOpen } = useOrganizationSwitcherContext();
const { defaultOpen, portal: portalProp = true } = useOrganizationSwitcherContext();

// Get portal from context, fallback to prop, then default to true
const portalFromContext = usePortalContext();
const portal = portalProp !== undefined ? portalProp : (portalFromContext ?? true);

// Detect if custom portal is used (function or from context indicates intentional custom portal)
const useFixedPosition = typeof portal === 'function' || (portal !== false && portalFromContext !== undefined);

Comment on lines +15 to 23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PortalContext fallback is effectively disabled for OrganizationSwitcher.

  • Line 15 destructures portal with a default (portal: portalProp = true), so portalProp is never undefined.
  • Lines 18–19 then compute portal as portalProp !== undefined ? portalProp : (portalFromContext ?? true), which means the portalFromContext and ?? true fallbacks are dead code.
  • As a result, PortalProvider / usePortalContext cannot influence this Popover at all; only the OrganizationSwitcher context prop can, and even that defaults to true. The comment on line 17 also no longer matches the behavior.

This breaks the intent of wiring global portal configuration through PortalContext into OrganizationSwitcher.

You can fix both the behavior and the comment with something like:

-  const { defaultOpen, portal: portalProp = true } = useOrganizationSwitcherContext();
-
-  // Get portal from context, fallback to prop, then default to true
-  const portalFromContext = usePortalContext();
-  const portal = portalProp !== undefined ? portalProp : (portalFromContext ?? true);
+  const { defaultOpen, portal: portalProp } = useOrganizationSwitcherContext();
+
+  // Get portal from prop, fallback to context, then default to true
+  const portalFromContext = usePortalContext();
+  const portal = portalProp ?? portalFromContext ?? true;

This lets:

  • Per-component portal override the global context when provided.
  • Global PortalProvider portal config apply when the component doesn’t specify portal.
  • The default still be true when neither is set.

Also applies to: 28-29, 46-46

🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/OrganizationSwitcher/index.tsx around
lines 15 to 23, the current destructure sets portalProp default to true which
prevents PortalContext fallback from ever being used; remove the default from
the destructure so portalProp can be undefined, compute portal as portalProp ??
portalFromContext ?? true so per-component prop overrides context and context
overrides the final default, update the comment to reflect that ordering, and
adjust the useFixedPosition detection to check typeof portal === 'function' ||
(portal !== false && portalFromContext !== undefined); make the identical fixes
at the other mentioned locations (lines 28-29 and 46) to restore PortalProvider
influence.

const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
placement: 'bottom-start',
offset: 8,
strategy: useFixedPosition ? 'fixed' : 'absolute',
});

const switcherButtonMenuId = useId();
Expand All @@ -34,6 +43,7 @@ const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactE
nodeId={nodeId}
context={context}
isOpen={isOpen}
portal={portal}
>
{cloneElement(children, {
id: switcherButtonMenuId,
Expand Down
12 changes: 11 additions & 1 deletion packages/clerk-js/src/ui/components/UserButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ import { withCardStateProvider, withFloatingTree } from '@/ui/elements/contexts'
import { Popover } from '@/ui/elements/Popover';

import { useUserButtonContext, withCoreUserGuard } from '../../contexts';
import { usePortalContext } from '../../contexts/PortalContext';
import { Flow } from '../../customizables';
import { usePopover } from '../../hooks';
import { UserButtonPopover } from './UserButtonPopover';
import { UserButtonTrigger } from './UserButtonTrigger';

const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
const { defaultOpen } = useUserButtonContext();
const { defaultOpen, portal: portalProp = true } = useUserButtonContext();

// Get portal from context, fallback to prop, then default to true
const portalFromContext = usePortalContext();
const portal = portalProp !== undefined ? portalProp : (portalFromContext ?? true);

// Detect if custom portal is used (function or from context indicates intentional custom portal)
const useFixedPosition = typeof portal === 'function' || (portal !== false && portalFromContext !== undefined);

const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
placement: 'bottom-end',
offset: 8,
strategy: useFixedPosition ? 'fixed' : 'absolute',
});
Comment on lines +14 to 28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Portal precedence currently ignores PortalContext and can mis-handle context false

The current portal resolution has two problems:

  1. portalFromContext is effectively never used for the portal value:

    • portalProp is destructured with a default: portal: portalProp = true, so portalProp is never undefined.
    • const portal = portalProp !== undefined ? portalProp : (portalFromContext ?? true); therefore always picks portalProp, even when a PortalProvider is present.
    • This contradicts the comment (“Get portal from context, fallback to prop, then default to true”) and means PortalContext configuration doesn’t actually influence the portal prop passed to <Popover>.
  2. Context portal: false can’t properly disable portalling for UserButton:

    • With a PortalProvider setting portal: false, portalProp still defaults to true, portal becomes true, and useFixedPosition can still flip to true because portalFromContext !== undefined.

To match the comment and expected semantics (context → prop → default), you can remove the default from the destructuring and explicitly apply precedence:

-  const { defaultOpen, portal: portalProp = true } = useUserButtonContext();
-
-  // Get portal from context, fallback to prop, then default to true
-  const portalFromContext = usePortalContext();
-  const portal = portalProp !== undefined ? portalProp : (portalFromContext ?? true);
-
-  // Detect if custom portal is used (function or from context indicates intentional custom portal)
-  const useFixedPosition = typeof portal === 'function' || (portal !== false && portalFromContext !== undefined);
+  const { defaultOpen, portal: portalProp } = useUserButtonContext();
+
+  // Get portal from context, fallback to prop, then default to true
+  const portalFromContext = usePortalContext();
+  const portal = portalFromContext ?? (portalProp ?? true);
+
+  // Detect if custom portal is used (function or from context indicates intentional custom portal)
+  const useFixedPosition =
+    typeof portal === 'function' || (portal !== false && portalFromContext !== undefined);

This way:

  • PortalProvider (context) takes precedence when present.
  • A portal={...} prop on UserButton still overrides the default when no context is set.
  • Explicit portal={false} from context correctly disables portalling and keeps strategy: 'absolute'.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { defaultOpen, portal: portalProp = true } = useUserButtonContext();
// Get portal from context, fallback to prop, then default to true
const portalFromContext = usePortalContext();
const portal = portalProp !== undefined ? portalProp : (portalFromContext ?? true);
// Detect if custom portal is used (function or from context indicates intentional custom portal)
const useFixedPosition = typeof portal === 'function' || (portal !== false && portalFromContext !== undefined);
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
placement: 'bottom-end',
offset: 8,
strategy: useFixedPosition ? 'fixed' : 'absolute',
});
const { defaultOpen, portal: portalProp } = useUserButtonContext();
// Get portal from context, fallback to prop, then default to true
const portalFromContext = usePortalContext();
const portal = portalFromContext ?? (portalProp ?? true);
// Detect if custom portal is used (function or from context indicates intentional custom portal)
const useFixedPosition =
typeof portal === 'function' || (portal !== false && portalFromContext !== undefined);
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
placement: 'bottom-end',
offset: 8,
strategy: useFixedPosition ? 'fixed' : 'absolute',
});
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/UserButton/index.tsx around lines 14 to
28, the portal precedence logic is wrong and context false is ignored; remove
the default value from the portal prop destructuring (don’t set portalProp =
true), then compute portal using context first: const portal = portalFromContext
!== undefined ? portalFromContext : (portalProp !== undefined ? portalProp :
true); finally keep useFixedPosition detection as: typeof portal === 'function'
|| (portal !== false && portalFromContext !== undefined) so that a
PortalProvider can override the prop and context=false correctly disables
portalling.


const userButtonMenuId = useId();
Expand All @@ -33,6 +42,7 @@ const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(
nodeId={nodeId}
context={context}
isOpen={isOpen}
portal={portal}
>
{cloneElement(children, {
id: userButtonMenuId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useClerk } from '@clerk/shared/react';
import type { SignedInSessionResource, UserButtonProps, UserResource } from '@clerk/shared/types';
import type {
SignedInSessionResource,
UserButtonProps,
UserProfileModalProps,
UserResource,
} from '@clerk/shared/types';

import { navigateIfTaskExists } from '@/core/sessionTasks';
import { useEnvironment } from '@/ui/contexts';
Expand All @@ -19,7 +24,7 @@ type UseMultisessionActionsParams = {
userProfileUrl?: string;
signInUrl?: string;
taskUrl?: string | null;
} & Pick<UserButtonProps, 'userProfileMode' | 'appearance' | 'userProfileProps'>;
} & Pick<UserButtonProps, 'userProfileMode' | 'appearance' | 'userProfileProps' | 'portal'>;

export const useMultisessionActions = (opts: UseMultisessionActionsParams) => {
const { setActive, signOut, openUserProfile } = useClerk();
Expand All @@ -46,7 +51,10 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => {
})();
});
}
openUserProfile(opts.userProfileProps);
openUserProfile({
...opts.userProfileProps,
portal: opts.portal,
} as UserProfileModalProps);
return opts.actionCompleteCallback?.();
};

Expand All @@ -62,7 +70,8 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => {
openUserProfile({
...opts.userProfileProps,
...(__experimental_startPath && { __experimental_startPath }),
});
portal: opts.portal,
} as UserProfileModalProps);

return opts.actionCompleteCallback?.();
};
Expand Down
18 changes: 18 additions & 0 deletions packages/clerk-js/src/ui/contexts/PortalContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { PortalConfig } from '@clerk/shared/types';
import React from 'react';

const PortalContext = React.createContext<{ portal?: PortalConfig } | null>(null);

interface PortalProviderProps {
children: React.ReactNode;
portal?: PortalConfig;
}

export function PortalProvider({ children, portal }: PortalProviderProps): JSX.Element {
return <PortalContext.Provider value={{ portal }}>{children}</PortalContext.Provider>;
}

export function usePortalContext(): PortalConfig | undefined {
const context = React.useContext(PortalContext);
return context?.portal;
}
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/contexts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './CoreSessionContext';
export * from './CoreClientContext';
export * from './CoreClerkContextWrapper';
export * from './AcceptedUserInvitations';
export * from './PortalContext';
16 changes: 13 additions & 3 deletions packages/clerk-js/src/ui/elements/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createContextAndHook } from '@clerk/shared/react';
import type { MenuId } from '@clerk/shared/types';
import type { MenuId, PortalConfig } from '@clerk/shared/types';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align positioning strategy with the actual portal resolution and fix the misleading comment

The new portal integration largely makes sense, but there are two small points worth addressing:

  1. Comment vs. behavior (Lines 100–103)
    The comment says “Get portal from context, fallback to prop, then default to false”, but the code actually does:

    • Prefer asPortal prop when defined.
    • Else fall back to context.
    • Else default to false.

    This is the right behavior; the comment is just inverted. Updating it will avoid confusion.

  2. strategy may not reflect asPortal overrides (Lines 30–41, 90–103)
    useFixedPosition is derived solely from usePortalContext() in Menu, while MenuList can override portalling with its asPortal prop. This creates an edge case where:

    • Context sets portal = true.
    • A consumer passes <MenuList asPortal={false} />.

    In that case, the popover still uses strategy: 'fixed' even though portal={false}, which might be surprising in some layouts.

    Consider basing the positioning strategy on the resolved portal configuration (i.e., after combining prop + context) or allowing MenuList to influence the strategy, so the final portal decision and the positioning mode stay in lockstep.

Also applies to: 30-41, 90-103

🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/elements/Menu.tsx around lines 30-41 and 90-103 (and
update the comment at ~100-103), the comment about portal resolution is inverted
and the positioning strategy is derived only from context which can be out of
sync with MenuList's asPortal prop; fix by (1) updating the comment to reflect
the actual resolution order: prefer the asPortal prop when defined, else fall
back to context, else default false, and (2) compute a single resolvedPortal
value (prop overrides context) and use that resolvedPortal to decide the
positioning strategy (fixed vs absolute) so Menu and MenuList share the same
final portal decision; ensure MenuList either accepts a resolvedPortal prop or
re-computes using the same resolution logic so strategy and portal are always in
lockstep.

import type { Placement } from '@floating-ui/react';
import type { PropsWithChildren } from 'react';
import React, { cloneElement, isValidElement, useLayoutEffect, useRef } from 'react';

import type { Button } from '../customizables';
import { Col, descriptors, SimpleButton } from '../customizables';
import { usePortalContext } from '../contexts/PortalContext';
import type { UsePopoverReturn } from '../hooks';
import { usePopover } from '../hooks';
import type { PropsOfComponent } from '../styledSystem';
Expand All @@ -28,10 +29,15 @@ type MenuProps = PropsWithChildren<Record<never, never>> & {

export const Menu = withFloatingTree((props: MenuProps) => {
const { popoverPlacement = 'bottom-end', elementId, ...rest } = props;
const portalFromContext = usePortalContext();
const useFixedPosition =
typeof portalFromContext === 'function' || (portalFromContext !== false && portalFromContext !== undefined);

const popoverCtx = usePopover({
placement: popoverPlacement,
offset: 8,
shoudFlip: true,
strategy: useFixedPosition ? 'fixed' : 'absolute',
});

const value = React.useMemo(() => ({ value: { popoverCtx, elementId } }), [{ ...popoverCtx }, elementId]);
Expand Down Expand Up @@ -82,15 +88,19 @@ const findMenuItem = (el: Element, siblingType: 'prev' | 'next', options?: { cou
};

type MenuListProps = PropsOfComponent<typeof Col> & {
asPortal?: boolean;
asPortal?: PortalConfig;
};

export const MenuList = (props: MenuListProps) => {
const { sx, asPortal, ...rest } = props;
const { sx, asPortal: asPortalProp, ...rest } = props;
const { popoverCtx, elementId } = useMenuState();
const { floating, styles, isOpen, context, nodeId } = popoverCtx;
const containerRef = useRef<HTMLDivElement | null>(null);

// Get portal from context, fallback to prop, then default to false
const portalFromContext = usePortalContext();
const asPortal = asPortalProp !== undefined ? asPortalProp : (portalFromContext ?? false);

useLayoutEffect(() => {
const current = containerRef.current;
floating(current);
Expand Down
38 changes: 35 additions & 3 deletions packages/clerk-js/src/ui/elements/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createContextAndHook, useSafeLayoutEffect } from '@clerk/shared/react';
import type { PortalConfig } from '@clerk/shared/types';
import React, { useRef } from 'react';

import { descriptors, Flex } from '../customizables';
import { usePopover } from '../hooks';
import { useScrollLock } from '../hooks/useScrollLock';
import { usePortalContext } from '../contexts/PortalContext';
import type { ThemableCssProp } from '../styledSystem';
import { animations, mqu } from '../styledSystem';
import { withFloatingTree } from './contexts';
Expand All @@ -19,12 +21,41 @@ type ModalProps = React.PropsWithChildren<{
containerSx?: ThemableCssProp;
canCloseModal?: boolean;
style?: React.CSSProperties;
portalRoot?: HTMLElement | React.MutableRefObject<HTMLElement | null>;
portalRoot?: PortalConfig | HTMLElement | React.MutableRefObject<HTMLElement | null>;
}>;

export const Modal = withFloatingTree((props: ModalProps) => {
const { disableScrollLock, enableScrollLock } = useScrollLock();
const { handleClose, handleOpen, contentSx, containerSx, canCloseModal, id, style, portalRoot } = props;
const {
handleClose,
handleOpen,
contentSx,
containerSx,
canCloseModal,
id,
style,
portalRoot: portalRootProp,
} = props;

// Get portal from context, fallback to prop, then default to true
const portalFromContext = usePortalContext();
const portalRoot = portalRootProp !== undefined ? portalRootProp : (portalFromContext ?? true);

// Resolve portal root
const resolvePortalRoot = (): HTMLElement | null => {
if (typeof portalRoot === 'function') {
return portalRoot();
}
if (typeof portalRoot === 'boolean') {
return null;
}
if (typeof portalRoot === 'object' && 'current' in portalRoot) {
return portalRoot.current;
}
return portalRoot || null;
};

const resolvedRoot = resolvePortalRoot();
const overlayRef = useRef<HTMLDivElement>(null);
const { floating, isOpen, context, nodeId, toggle } = usePopover({
defaultOpen: true,
Expand Down Expand Up @@ -56,7 +87,8 @@ export const Modal = withFloatingTree((props: ModalProps) => {
context={context}
isOpen={isOpen}
outsideElementsInert
root={portalRoot}
portal={resolvedRoot ? () => resolvedRoot : false}
root={resolvedRoot || undefined}
>
<ModalContext.Provider value={modalCtx}>
<Flex
Expand Down
69 changes: 41 additions & 28 deletions packages/clerk-js/src/ui/elements/Popover.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { PortalConfig } from '@clerk/shared/types';
import type { FloatingContext, ReferenceType } from '@floating-ui/react';
import { FloatingFocusManager, FloatingNode, FloatingPortal } from '@floating-ui/react';
import type { PropsWithChildren } from 'react';
import React from 'react';

import { usePortalContext } from '../contexts/PortalContext';

type PopoverProps = PropsWithChildren<{
context: FloatingContext<ReferenceType>;
nodeId?: string;
Expand All @@ -14,7 +17,7 @@ type PopoverProps = PropsWithChildren<{
*/
outsideElementsInert?: boolean;
order?: Array<'reference' | 'floating' | 'content'>;
portal?: boolean;
portal?: PortalConfig;
/**
* The root element to render the portal into.
* @default document.body
Expand All @@ -30,41 +33,51 @@ export const Popover = (props: PopoverProps) => {
order = ['reference', 'content'],
nodeId,
isOpen,
portal = true,
portal: portalProp,
root,
children,
} = props;

if (portal) {
// Get portal from context, fallback to prop, then default to true
const portalFromContext = usePortalContext();
const portal = portalProp !== undefined ? portalProp : (portalFromContext ?? true);

// Resolve portal root
const resolveRoot = (): HTMLElement | null => {
if (typeof portal === 'function') {
return portal();
}
if (typeof root === 'object' && 'current' in root) {
return root.current;
}
return root || null;
};

const portalRoot = portal !== false ? resolveRoot() : null;
const shouldPortal = portal !== false && portalRoot !== null;
Comment on lines +45 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Verify portal logic consistency with documented defaults.

The portal resolution logic (lines 41-52) results in shouldPortal = false when portal={true} is specified without an explicit root element, because resolveRoot() returns null when only a boolean is passed.

However, the JSDoc for UserProfileProps.portal and UserButtonProps.portal documents @default true, suggesting portalling should be enabled by default. With the current logic, portal={true} without a root disables portalling, which seems inconsistent with the documented behavior.

Please verify:

  1. Should portal={true} use a default container (e.g., document.body)?
  2. Or should the JSDoc be updated to clarify that portal={true} requires an explicit root?

I need to verify the review comment by examining the actual codebase. Let me gather the necessary information.


🏁 Script executed:

#!/bin/bash
# Find and read the Popover.tsx file
fd -t f "Popover.tsx" packages/clerk-js/src/ui/elements

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for UserProfileProps and UserButtonProps definitions
rg -n "interface UserProfileProps|interface UserButtonProps|type UserProfileProps|type UserButtonProps" -A 10 packages/clerk-js/src

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Read the Popover.tsx file to see full context
cat packages/clerk-js/src/ui/elements/Popover.tsx

Length of output: 2090


I found the Popover.tsx file. Now I need to search for UserProfileProps and UserButtonProps to verify the JSDoc claims in the review.


🏁 Script executed:

#!/bin/bash
# Search more broadly for UserProfileProps and UserButtonProps
rg -n "UserProfileProps|UserButtonProps" packages/clerk-js/src --type=ts --type=tsx | head -20

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for portal prop documentation in prop interfaces
rg -n "portal\?" packages/clerk-js/src -B 5 -A 2 --type=ts --type=tsx | grep -E "(portal|@default)" | head -30

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find where these props might be defined
fd -t f -e "ts" -e "tsx" packages/clerk-js/src | xargs grep -l "UserProfileProps\|UserButtonProps" | head -5

Length of output: 389


Let me fix the command syntax and search again:


🏁 Script executed:

#!/bin/bash
# Search for UserProfileProps and UserButtonProps without type specification
rg -n "UserProfileProps|UserButtonProps" packages/clerk-js/src | head -20

Length of output: 1856


🏁 Script executed:

#!/bin/bash
# Search for files containing these types
fd . packages/clerk-js/src -type f | xargs grep -l "UserProfileProps\|UserButtonProps" 2>/dev/null | head -10

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Search for portal prop in all files
rg -n "portal\?" packages/clerk-js/src -B 3 -A 1 | head -50

Length of output: 2112


I can see UserProfileProps and UserButtonProps are imported from @clerk/shared/types. Let me find those definitions:


🏁 Script executed:

#!/bin/bash
# Find @clerk/shared package
find . -type d -name "shared" | grep -E "clerk|packages" | head -10

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Search for the actual type definitions in the repo
rg -n "export.*type.*UserProfileProps|export.*interface.*UserProfileProps" . | head -10

Length of output: 292


🏁 Script executed:

#!/bin/bash
# Look for @clerk/shared in package.json files
rg -n "@clerk/shared" packages/clerk-js | head -5

Length of output: 512


Now let me read the UserProfileProps definition and check the portal property documentation:


🏁 Script executed:

#!/bin/bash
# Read the UserProfileProps definition with context
sed -n '1540,1600p' packages/shared/src/types/clerk.ts

Length of output: 1960


🏁 Script executed:

#!/bin/bash
# Search for portal property documentation in the shared types
rg -n "portal" packages/shared/src/types/clerk.ts -B 2 -A 1

Length of output: 3073


🏁 Script executed:

#!/bin/bash
# Find UserButtonProps definition
rg -n "export.*type.*UserButtonProps|export.*interface.*UserButtonProps" packages/shared/src/types/clerk.ts -A 30 | head -60

Length of output: 1249


Portal default behavior in Popover contradicts JSDoc documentation.

The code behavior is inconsistent with the documented defaults. When portal={true} (the default) is used without an explicit root, the shouldPortal flag becomes false, disabling portalling entirely. However, the JSDoc for both UserProfileProps.portal and UserButtonProps.portal in packages/shared/src/types/clerk.ts documents @default true, implying portalling should be enabled by default.

Additionally, the Popover's root prop JSDoc claims @default document.body, but the resolveRoot() function returns null when root is undefined, not using document.body.

Fix required: Either:

  1. Update the implementation to use document.body as the default root when not explicitly provided (matching the JSDoc), or
  2. Update the JSDoc to clarify that portal={true} requires an explicit root element to function.


if (!isOpen) {
return <FloatingNode id={nodeId} />;
}

const content = (
<FloatingFocusManager
context={context}
initialFocus={initialFocus}
outsideElementsInert={outsideElementsInert}
order={order}
>
<>{children}</>
</FloatingFocusManager>
);

if (shouldPortal) {
return (
<FloatingNode id={nodeId}>
<FloatingPortal root={root}>
{isOpen && (
<FloatingFocusManager
context={context}
initialFocus={initialFocus}
outsideElementsInert={outsideElementsInert}
order={order}
>
<>{children}</>
</FloatingFocusManager>
)}
</FloatingPortal>
<FloatingPortal root={portalRoot}>{content}</FloatingPortal>
</FloatingNode>
);
}

return (
<FloatingNode id={nodeId}>
{isOpen && (
<FloatingFocusManager
context={context}
initialFocus={initialFocus}
order={order}
>
<>{children}</>
</FloatingFocusManager>
)}
</FloatingNode>
);
return <FloatingNode id={nodeId}>{content}</FloatingNode>;
};
Loading
Loading