Skip to content

Commit e7114c8

Browse files
authored
Add usePortal support to Tooltip component (#3320)
1 parent 8c79931 commit e7114c8

2 files changed

Lines changed: 85 additions & 25 deletions

File tree

src/lib/holocene/tooltip.stories.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
table: { category: 'Positioning' },
6969
},
7070
},
71-
} satisfies Meta<Omit<Tooltip, 'copyIconTitle'>>;
71+
} satisfies Meta<Tooltip>;
7272
</script>
7373

7474
<script lang="ts">
@@ -131,3 +131,11 @@
131131
<Button>Tooltip</Button>
132132
</Tooltip>
133133
</Story>
134+
135+
<Story name="Portal (avoids overflow clipping)">
136+
<div class="overflow-hidden rounded border border-slate-600 p-4">
137+
<Tooltip top usePortal text="This renders outside the overflow container">
138+
<Button>Hover me (portal)</Button>
139+
</Tooltip>
140+
</div>
141+
</Story>

src/lib/holocene/tooltip.svelte

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
44
import type { IconName } from '$lib/holocene/icon';
55
import Icon from '$lib/holocene/icon/icon.svelte';
6+
import Portal from '$lib/holocene/portal/portal.svelte';
7+
import type { PortalPosition } from '$lib/holocene/portal/types';
68
import type { Only } from '$lib/types/global';
79
810
type BaseProps = {
@@ -13,6 +15,8 @@
1315
class?: string;
1416
tooltipClass?: string;
1517
show?: boolean;
18+
usePortal?: boolean;
19+
scrollContainer?: string;
1620
};
1721
1822
type BasePositionProps = {
@@ -71,42 +75,90 @@
7175
export let width: number | null = null;
7276
export let tooltipClass = '';
7377
export let show = false;
78+
export let usePortal = false;
79+
export let scrollContainer: string | undefined = undefined;
80+
81+
let wrapperElement: HTMLElement | null = null;
82+
let isHovered = false;
83+
84+
$: portalPosition = ((): PortalPosition => {
85+
if (top) return 'top';
86+
if (topRight) return 'top-right';
87+
if (right) return 'right';
88+
if (bottomRight) return 'bottom-right';
89+
if (bottom) return 'bottom';
90+
if (bottomLeft) return 'bottom-left';
91+
if (left) return 'left';
92+
if (topLeft) return 'top-left';
93+
return 'top';
94+
})();
7495
</script>
7596

7697
{#if hide}
7798
<slot />
7899
{:else}
79-
<div class={merge('wrapper group relative inline-block', className)}>
100+
<!-- svelte-ignore a11y-no-static-element-interactions -->
101+
<div
102+
bind:this={wrapperElement}
103+
class={merge('wrapper group relative inline-block', className)}
104+
on:mouseenter={() => (isHovered = true)}
105+
on:mouseleave={() => (isHovered = false)}
106+
>
80107
<slot />
81-
<div
82-
class={merge(
83-
'tooltip absolute left-0 top-0 z-50 hidden translate-x-12 whitespace-nowrap text-xs opacity-0 transition-all group-hover:inline-block group-hover:opacity-95',
84-
show && 'inline-block opacity-95',
85-
)}
86-
class:left
87-
class:right
88-
class:bottom
89-
class:bottomLeft
90-
class:bottomRight
91-
class:top
92-
class:topRight
93-
class:topLeft
94-
style={width ? `white-space: pre-wrap; width: ${width}px;` : null}
95-
>
108+
109+
{#if usePortal && wrapperElement}
110+
<Portal
111+
anchor={wrapperElement}
112+
open={show || isHovered}
113+
position={portalPosition}
114+
{scrollContainer}
115+
>
116+
<div
117+
class={merge(
118+
'inline-block rounded-md bg-slate-800 px-2 py-2 text-xs text-slate-50',
119+
tooltipClass,
120+
)}
121+
style={width ? `white-space: pre-wrap; width: ${width}px;` : null}
122+
>
123+
<div class="flex gap-2">
124+
<slot name="content">
125+
{#if icon}<Icon name={icon} class="inline h-4" />{/if}
126+
<span>{text}</span>
127+
</slot>
128+
</div>
129+
</div>
130+
</Portal>
131+
{:else}
96132
<div
97133
class={merge(
98-
'inline-block rounded-md bg-slate-800 px-2 py-2 text-slate-50',
99-
tooltipClass,
134+
'tooltip absolute left-0 top-0 z-50 hidden translate-x-12 whitespace-nowrap text-xs opacity-0 transition-all group-hover:inline-block group-hover:opacity-95',
135+
show && 'inline-block opacity-95',
100136
)}
137+
class:left
138+
class:right
139+
class:bottom
140+
class:bottomLeft
141+
class:bottomRight
142+
class:top
143+
class:topRight
144+
class:topLeft
145+
style={width ? `white-space: pre-wrap; width: ${width}px;` : null}
101146
>
102-
<div class="flex gap-2">
103-
<slot name="content">
104-
{#if icon}<Icon name={icon} class="inline h-4" />{/if}
105-
<span>{text}</span>
106-
</slot>
147+
<div
148+
class={merge(
149+
'inline-block rounded-md bg-slate-800 px-2 py-2 text-slate-50',
150+
tooltipClass,
151+
)}
152+
>
153+
<div class="flex gap-2">
154+
<slot name="content">
155+
{#if icon}<Icon name={icon} class="inline h-4" />{/if}
156+
<span>{text}</span>
157+
</slot>
158+
</div>
107159
</div>
108160
</div>
109-
</div>
161+
{/if}
110162
</div>
111163
{/if}
112164

0 commit comments

Comments
 (0)