Skip to content

Commit f9afa48

Browse files
fix(ui): Ensure tooltips render above dialog content (#9093)
1 parent 3995e8a commit f9afa48

3 files changed

Lines changed: 52 additions & 37 deletions

File tree

.changeset/tooltip-above-modal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/ui': patch
3+
---
4+
5+
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.

packages/ui/src/elements/Tooltip.tsx

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import { usePortalRoot } from '@clerk/shared/react';
12
import type { Placement, UseFloatingReturn, UseInteractionsReturn } from '@floating-ui/react';
23
import {
34
autoUpdate,
45
flip,
6+
FloatingNode,
57
FloatingPortal,
68
offset,
79
shift,
810
useDismiss,
911
useFloating,
12+
useFloatingNodeId,
1013
useFocus,
1114
useHover,
1215
useInteractions,
@@ -16,11 +19,10 @@ import {
1619
} from '@floating-ui/react';
1720
import * as React from 'react';
1821

19-
import { usePortalRoot } from '@clerk/shared/react';
20-
2122
import { Box, descriptors, type LocalizationKey, Span, Text, useAppearance } from '../customizables';
2223
import { usePrefersReducedMotion } from '../hooks';
2324
import type { ThemableCssProp } from '../styledSystem';
25+
import { withFloatingTree } from './contexts';
2426

2527
interface TooltipOptions {
2628
initialOpen?: boolean;
@@ -34,6 +36,7 @@ interface UseTooltipReturn extends UseFloatingReturn, UseInteractionsReturn {
3436
setOpen: (open: boolean) => void;
3537
isMounted: boolean;
3638
transitionStyles: React.CSSProperties;
39+
nodeId: string | undefined;
3740
}
3841

3942
export function useTooltip({
@@ -51,7 +54,9 @@ export function useTooltip({
5154
const { animations: layoutAnimations } = useAppearance().parsedOptions;
5255
const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
5356

57+
const nodeId = useFloatingNodeId();
5458
const data = useFloating({
59+
nodeId,
5560
placement,
5661
open,
5762
onOpenChange: setOpen,
@@ -104,11 +109,12 @@ export function useTooltip({
104109
open,
105110
setOpen,
106111
isMounted,
112+
nodeId,
107113
...interactions,
108114
...data,
109115
transitionStyles,
110116
}),
111-
[open, setOpen, interactions, data, isMounted, transitionStyles],
117+
[open, setOpen, interactions, data, isMounted, transitionStyles, nodeId],
112118
);
113119
}
114120

@@ -126,12 +132,12 @@ export const useTooltipContext = (): UseTooltipReturn => {
126132
return context;
127133
};
128134

129-
function Root({ children, ...options }: { children: React.ReactNode } & TooltipOptions) {
135+
const Root = withFloatingTree(({ children, ...options }: { children: React.ReactNode } & TooltipOptions) => {
130136
// This can accept any props as options, e.g. `placement`,
131137
// or other positioning options.
132138
const tooltip = useTooltip(options);
133139
return <TooltipContext.Provider value={tooltip}>{children}</TooltipContext.Provider>;
134-
}
140+
});
135141

136142
type TriggerProps = React.HTMLProps<HTMLElement> & {
137143
sx?: ThemableCssProp;
@@ -203,41 +209,44 @@ const Content = React.forwardRef<
203209
}
204210

205211
return (
206-
<FloatingPortal root={effectiveRoot}>
207-
<Box
208-
ref={ref}
209-
elementDescriptor={descriptors.tooltip}
210-
style={{
211-
...context.floatingStyles,
212-
...style,
213-
}}
214-
{...context.getFloatingProps(props)}
215-
>
212+
<FloatingNode id={context.nodeId}>
213+
<FloatingPortal root={effectiveRoot}>
216214
<Box
217-
elementDescriptor={descriptors.tooltipContent}
218-
style={context.transitionStyles}
219-
sx={[
220-
t => ({
221-
paddingBlock: t.space.$1,
222-
paddingInline: t.space.$1x5,
223-
borderRadius: t.radii.$md,
224-
backgroundColor: t.colors.$black,
225-
color: t.colors.$white,
226-
maxWidth: t.sizes.$60,
227-
}),
228-
sx,
229-
]}
215+
ref={ref}
216+
elementDescriptor={descriptors.tooltip}
217+
style={{
218+
...context.floatingStyles,
219+
...style,
220+
}}
221+
{...context.getFloatingProps(props)}
222+
sx={t => ({ zIndex: t.zIndices.$tooltip })}
230223
>
231-
<Text
232-
elementDescriptor={descriptors.tooltipText}
233-
localizationKey={text}
234-
variant='body'
235-
colorScheme='inherit'
236-
sx={textSx}
237-
/>
224+
<Box
225+
elementDescriptor={descriptors.tooltipContent}
226+
style={context.transitionStyles}
227+
sx={[
228+
t => ({
229+
paddingBlock: t.space.$1,
230+
paddingInline: t.space.$1x5,
231+
borderRadius: t.radii.$md,
232+
backgroundColor: t.colors.$black,
233+
color: t.colors.$white,
234+
maxWidth: t.sizes.$60,
235+
}),
236+
sx,
237+
]}
238+
>
239+
<Text
240+
elementDescriptor={descriptors.tooltipText}
241+
localizationKey={text}
242+
variant='body'
243+
colorScheme='inherit'
244+
sx={textSx}
245+
/>
246+
</Box>
238247
</Box>
239-
</Box>
240-
</FloatingPortal>
248+
</FloatingPortal>
249+
</FloatingNode>
241250
);
242251
});
243252

packages/ui/src/foundations/zIndices.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export const zIndices = Object.freeze({
44
fab: '9000',
55
modal: '10000',
66
dropdown: '11000',
7+
tooltip: '12000',
78
} as const);

0 commit comments

Comments
 (0)