Skip to content

Commit 7610cab

Browse files
committed
feat: Popover component and doc
1 parent ade1f6e commit 7610cab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1740
-93
lines changed

.changeset/wet-rice-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-5-ui-lib': patch
3+
---
4+
5+
feat: Popover compoents and page

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,10 @@
367367
"types": "./dist/pagination/PaginationItem.svelte.d.ts",
368368
"svelte": "./dist/pagination/PaginationItem.svelte"
369369
},
370+
"./Popover.svelte": {
371+
"types": "./dist/popover/Popover.svelte.d.ts",
372+
"svelte": "./dist/popover/Popover.svelte"
373+
},
370374
"./Progressbar.svelte": {
371375
"types": "./dist/progress/Progressbar.svelte.d.ts",
372376
"svelte": "./dist/progress/Progressbar.svelte"

src/generatedFileList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const fileList = [
6464
"src/lib/nav/Navbar.svelte",
6565
"src/lib/pagination/Pagination.svelte",
6666
"src/lib/pagination/PaginationItem.svelte",
67+
"src/lib/popover/Popover.svelte",
6768
"src/lib/progress/Progressbar.svelte",
6869
"src/lib/rating/AdvancedRating.svelte",
6970
"src/lib/rating/Heart.svelte",

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './kbd';
2121
export * from './list-group';
2222
export * from './nav';
2323
export * from './pagination';
24+
export * from './popover';
2425
export * from './progress';
2526
export * from './rating';
2627
export * from './sidebar';

src/lib/list-group/ListgroupItem.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
<Icon class="me-2.5 h-5 w-5" />
4848
{/if}
4949
{#if name}
50-
{name}
50+
{name}
5151
{:else}
5252
{@render children()}
5353
{/if}

src/lib/popover/Popover.svelte

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
<script lang="ts">
2+
import { type PopoverProps as Props, popover } from '.';
3+
import { onDestroy } from 'svelte';
4+
import { type ParamsType } from '$lib/types';
5+
import { fade } from 'svelte/transition';
6+
import { linear } from 'svelte/easing';
7+
8+
let { children, titleSlot, color = 'default', arrow = true, offset = 0, triggeredBy, position = 'top', class: className, reference, transition = fade, params, ...restProps }: Props = $props();
9+
10+
let { base, title, h3, arrowBase } = $derived(popover({ color, arrow, position }));
11+
12+
const defaultParams: ParamsType = { duration: 100, easing: linear };
13+
14+
let tooltipElement: HTMLElement | null = $state(null);
15+
let triggerElement: HTMLElement | null = null;
16+
let referenceElement: HTMLElement | null = null;
17+
let arrowEl: HTMLElement | null = $state(null);
18+
let visible = $state(false);
19+
let positioned = $state(false);
20+
21+
// Change the type to accommodate both browser and Node.js environments
22+
let hideTimeoutId: ReturnType<typeof setTimeout> | undefined;
23+
24+
const showTooltip = () => {
25+
visible = true;
26+
setTimeout(() => {
27+
positionTooltip();
28+
positioned = true;
29+
}, 0);
30+
};
31+
32+
const hideTooltip = () => {
33+
visible = false;
34+
positioned = false;
35+
};
36+
37+
const onPopoverEnter = () => {
38+
if (hideTimeoutId !== undefined) {
39+
clearTimeout(hideTimeoutId);
40+
hideTimeoutId = undefined;
41+
}
42+
};
43+
44+
// Handle mouse leave on popover
45+
const onPopoverLeave = () => {
46+
hideTooltip();
47+
};
48+
49+
const onTriggerLeave = () => {
50+
// Set a timeout to hide the tooltip, allowing time to move to the popover
51+
hideTimeoutId = setTimeout(hideTooltip, 100);
52+
};
53+
54+
const positionTooltip = () => {
55+
if (!tooltipElement || !triggerElement) return;
56+
const triggerRect = triggerElement.getBoundingClientRect();
57+
const referenceRect = reference && referenceElement ? referenceElement.getBoundingClientRect() : triggerRect;
58+
const tooltipRect = tooltipElement.getBoundingClientRect();
59+
const arrowRect = arrow && arrowEl ? arrowEl.getBoundingClientRect() : null;
60+
61+
const scrollX = window.scrollX || document.documentElement.scrollLeft;
62+
const scrollY = window.scrollY || document.documentElement.scrollTop;
63+
64+
let top, left, arrowTop, arrowLeft;
65+
66+
switch (position) {
67+
case 'top':
68+
top = referenceRect.top + scrollY - tooltipRect.height - 10 - offset;
69+
left = referenceRect.left + scrollX + referenceRect.width / 2 - tooltipRect.width / 2;
70+
if (arrowRect && offset === 0) {
71+
arrowTop = tooltipRect.height - 5;
72+
arrowLeft = tooltipRect.width / 2 - arrowRect.width / 2;
73+
}
74+
break;
75+
case 'top-start':
76+
top = referenceRect.top + scrollY - tooltipRect.height - 10 - offset;
77+
left = referenceRect.left + scrollX;
78+
if (arrowRect && offset === 0) {
79+
arrowTop = tooltipRect.height - 5;
80+
arrowLeft = referenceRect.width / 2 - arrowRect.width / 2;
81+
}
82+
break;
83+
case 'top-end':
84+
top = referenceRect.top + scrollY - tooltipRect.height - 10 - offset;
85+
left = referenceRect.right + scrollX - tooltipRect.width;
86+
if (arrowRect && offset === 0) {
87+
arrowTop = tooltipRect.height - 5;
88+
arrowLeft = tooltipRect.width - referenceRect.width / 2 - arrowRect.width / 2;
89+
}
90+
break;
91+
case 'bottom':
92+
top = referenceRect.bottom + scrollY + 10 + offset;
93+
left = referenceRect.left + scrollX + referenceRect.width / 2 - tooltipRect.width / 2;
94+
if (arrowRect && offset === 0) {
95+
arrowTop = -arrowRect.height + 9;
96+
arrowLeft = tooltipRect.width / 2 - arrowRect.width / 2;
97+
}
98+
break;
99+
case 'bottom-start':
100+
top = referenceRect.bottom + scrollY + 10 + offset;
101+
left = referenceRect.left + scrollX;
102+
if (arrowRect && offset === 0) {
103+
arrowTop = -arrowRect.height + 9;
104+
arrowLeft = referenceRect.width / 2 - arrowRect.width / 2;
105+
}
106+
break;
107+
case 'bottom-end':
108+
top = referenceRect.bottom + scrollY + 10 + offset;
109+
left = referenceRect.right + scrollX - tooltipRect.width;
110+
if (arrowRect && offset === 0) {
111+
arrowTop = -arrowRect.height + 9;
112+
arrowLeft = tooltipRect.width - referenceRect.width / 2 - arrowRect.width / 2;
113+
}
114+
break;
115+
case 'left':
116+
top = referenceRect.top + scrollY + referenceRect.height / 2 - tooltipRect.height / 2;
117+
left = referenceRect.left + scrollX - tooltipRect.width - 10 - offset;
118+
if (arrowRect && offset === 0) {
119+
arrowTop = tooltipRect.height / 2 - arrowRect.height / 2;
120+
arrowLeft = tooltipRect.width - 5;
121+
}
122+
break;
123+
case 'left-start':
124+
top = referenceRect.top + scrollY;
125+
left = referenceRect.left + scrollX - tooltipRect.width - 10 - offset;
126+
if (arrowRect && offset === 0) {
127+
arrowTop = arrowRect.height;
128+
arrowLeft = tooltipRect.width - 5;
129+
}
130+
break;
131+
case 'left-end':
132+
top = referenceRect.bottom + scrollY - tooltipRect.height;
133+
left = referenceRect.left + scrollX - tooltipRect.width - 10 - offset;
134+
if (arrowRect && offset === 0) {
135+
arrowTop = tooltipRect.height - arrowRect.height * 2;
136+
arrowLeft = tooltipRect.width - 5;
137+
}
138+
break;
139+
case 'right':
140+
top = referenceRect.top + scrollY + referenceRect.height / 2 - tooltipRect.height / 2;
141+
left = referenceRect.right + scrollX + 10 + offset;
142+
if (arrowRect && offset === 0) {
143+
arrowTop = tooltipRect.height / 2 - arrowRect.height / 2;
144+
arrowLeft = -arrowRect.width / 2 + 2;
145+
}
146+
break;
147+
case 'right-start':
148+
top = referenceRect.top + scrollY;
149+
left = referenceRect.right + scrollX + 10 + offset;
150+
if (arrowRect && offset === 0) {
151+
arrowTop = arrowRect.height;
152+
arrowLeft = -arrowRect.width / 2 + 2;
153+
}
154+
break;
155+
case 'right-end':
156+
top = referenceRect.bottom + scrollY - tooltipRect.height;
157+
left = referenceRect.right + scrollX + 10 + offset;
158+
if (arrowRect && offset === 0) {
159+
arrowTop = tooltipRect.height - arrowRect.height * 2;
160+
arrowLeft = -arrowRect.width / 2 + 2;
161+
}
162+
break;
163+
}
164+
165+
tooltipElement.style.top = `${top}px`;
166+
tooltipElement.style.left = `${left}px`;
167+
168+
if (arrowEl && arrowRect) {
169+
arrowEl.style.top = `${arrowTop}px`;
170+
arrowEl.style.left = `${arrowLeft}px`;
171+
}
172+
};
173+
174+
$effect(() => {
175+
triggerElement = document.querySelector(triggeredBy);
176+
referenceElement = reference ? document.querySelector(reference) : triggerElement;
177+
178+
if (triggerElement) {
179+
triggerElement.addEventListener('mouseenter', showTooltip);
180+
triggerElement.addEventListener('mouseleave', onTriggerLeave);
181+
}
182+
183+
const handlePositionUpdate = () => {
184+
if (visible) {
185+
positionTooltip();
186+
}
187+
};
188+
189+
window.addEventListener('resize', handlePositionUpdate);
190+
window.addEventListener('scroll', handlePositionUpdate, true);
191+
192+
onDestroy(() => {
193+
if (triggerElement) {
194+
triggerElement.removeEventListener('mouseenter', showTooltip);
195+
triggerElement.removeEventListener('mouseleave', onTriggerLeave);
196+
}
197+
window.removeEventListener('resize', handlePositionUpdate);
198+
window.removeEventListener('scroll', handlePositionUpdate, true);
199+
if (hideTimeoutId !== undefined) {
200+
clearTimeout(hideTimeoutId);
201+
}
202+
});
203+
});
204+
</script>
205+
206+
{#if transition && visible}
207+
<div transition:transition={params || defaultParams} role="tooltip" bind:this={tooltipElement} class={`${base({ className })} ${positioned ? 'visible opacity-100' : 'invisible opacity-0'} transition-opacity duration-200`} onmouseenter={onPopoverEnter} onmouseleave={onPopoverLeave} {...restProps}>
208+
{#if typeof titleSlot === 'string'}
209+
<div class={title()}>
210+
<h3 class={h3()}>{titleSlot}</h3>
211+
</div>
212+
{:else if titleSlot}
213+
{@render titleSlot()}
214+
{/if}
215+
{@render children()}
216+
{#if arrow}<div bind:this={arrowEl} class={arrowBase({ arrow, position })}></div>{/if}
217+
</div>
218+
{:else if visible}
219+
<div role="tooltip" bind:this={tooltipElement} class={`${base({ className })} ${positioned ? 'visible opacity-100' : 'invisible opacity-0'} transition-opacity duration-200`} onmouseenter={onPopoverEnter} onmouseleave={onPopoverLeave} {...restProps}>
220+
{#if typeof titleSlot === 'string'}
221+
<div class={title()}>
222+
<h3 class={h3()}>{titleSlot}</h3>
223+
</div>
224+
{:else if titleSlot}
225+
{@render titleSlot()}
226+
{/if}
227+
{@render children()}
228+
{#if arrow}<div bind:this={arrowEl} class={arrowBase({ arrow, position })}></div>{/if}
229+
</div>
230+
{/if}
231+
232+
<!--
233+
@component
234+
[Go to docs](https://svelte-5-ui-lib.codewithshin.com/)
235+
## Props
236+
@prop children
237+
@prop titleSlot
238+
@prop color = 'default'
239+
@prop arrow = true
240+
@prop offset = 0
241+
@prop triggeredBy
242+
@prop position = 'top'
243+
@prop class: className
244+
@prop reference
245+
@prop transition = fade
246+
@prop params
247+
@prop ...restProps
248+
-->

0 commit comments

Comments
 (0)