diff --git a/.changeset/tooltip-above-modal.md b/.changeset/tooltip-above-modal.md new file mode 100644 index 00000000000..11809b73cf0 --- /dev/null +++ b/.changeset/tooltip-above-modal.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Fix tooltips rendering behind modals (for example on the organization profile Security page). Tooltips now layer above modal content, and pressing Escape or clicking outside while a tooltip is open inside a modal closes only the tooltip instead of also dismissing the modal. diff --git a/packages/ui/src/elements/Tooltip.tsx b/packages/ui/src/elements/Tooltip.tsx index 1fcc207a298..7b0a4a76d4b 100644 --- a/packages/ui/src/elements/Tooltip.tsx +++ b/packages/ui/src/elements/Tooltip.tsx @@ -1,12 +1,15 @@ +import { usePortalRoot } from '@clerk/shared/react'; import type { Placement, UseFloatingReturn, UseInteractionsReturn } from '@floating-ui/react'; import { autoUpdate, flip, + FloatingNode, FloatingPortal, offset, shift, useDismiss, useFloating, + useFloatingNodeId, useFocus, useHover, useInteractions, @@ -16,11 +19,10 @@ import { } from '@floating-ui/react'; import * as React from 'react'; -import { usePortalRoot } from '@clerk/shared/react'; - import { Box, descriptors, type LocalizationKey, Span, Text, useAppearance } from '../customizables'; import { usePrefersReducedMotion } from '../hooks'; import type { ThemableCssProp } from '../styledSystem'; +import { withFloatingTree } from './contexts'; interface TooltipOptions { initialOpen?: boolean; @@ -34,6 +36,7 @@ interface UseTooltipReturn extends UseFloatingReturn, UseInteractionsReturn { setOpen: (open: boolean) => void; isMounted: boolean; transitionStyles: React.CSSProperties; + nodeId: string | undefined; } export function useTooltip({ @@ -51,7 +54,9 @@ export function useTooltip({ const { animations: layoutAnimations } = useAppearance().parsedOptions; const isMotionSafe = !prefersReducedMotion && layoutAnimations === true; + const nodeId = useFloatingNodeId(); const data = useFloating({ + nodeId, placement, open, onOpenChange: setOpen, @@ -104,11 +109,12 @@ export function useTooltip({ open, setOpen, isMounted, + nodeId, ...interactions, ...data, transitionStyles, }), - [open, setOpen, interactions, data, isMounted, transitionStyles], + [open, setOpen, interactions, data, isMounted, transitionStyles, nodeId], ); } @@ -126,12 +132,12 @@ export const useTooltipContext = (): UseTooltipReturn => { return context; }; -function Root({ children, ...options }: { children: React.ReactNode } & TooltipOptions) { +const Root = withFloatingTree(({ children, ...options }: { children: React.ReactNode } & TooltipOptions) => { // This can accept any props as options, e.g. `placement`, // or other positioning options. const tooltip = useTooltip(options); return {children}; -} +}); type TriggerProps = React.HTMLProps & { sx?: ThemableCssProp; @@ -203,41 +209,44 @@ const Content = React.forwardRef< } return ( - - + + ({ - paddingBlock: t.space.$1, - paddingInline: t.space.$1x5, - borderRadius: t.radii.$md, - backgroundColor: t.colors.$black, - color: t.colors.$white, - maxWidth: t.sizes.$60, - }), - sx, - ]} + ref={ref} + elementDescriptor={descriptors.tooltip} + style={{ + ...context.floatingStyles, + ...style, + }} + {...context.getFloatingProps(props)} + sx={t => ({ zIndex: t.zIndices.$tooltip })} > - + ({ + paddingBlock: t.space.$1, + paddingInline: t.space.$1x5, + borderRadius: t.radii.$md, + backgroundColor: t.colors.$black, + color: t.colors.$white, + maxWidth: t.sizes.$60, + }), + sx, + ]} + > + + - - + + ); }); diff --git a/packages/ui/src/foundations/zIndices.ts b/packages/ui/src/foundations/zIndices.ts index 950e76a86dd..8c08516547f 100644 --- a/packages/ui/src/foundations/zIndices.ts +++ b/packages/ui/src/foundations/zIndices.ts @@ -4,4 +4,5 @@ export const zIndices = Object.freeze({ fab: '9000', modal: '10000', dropdown: '11000', + tooltip: '12000', } as const);