@ims360/svelte-ivory 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/layout/drawer/Drawer.svelte +22 -15
- package/dist/components/layout/drawer/Drawer.svelte.d.ts +2 -0
- package/dist/components/layout/drawer/Drawer.svelte.d.ts.map +1 -1
- package/dist/components/layout/popover/Popover.svelte +76 -96
- package/dist/components/layout/popover/Popover.svelte.d.ts +22 -8
- package/dist/components/layout/popover/Popover.svelte.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/components/layout/drawer/Drawer.svelte +22 -15
- package/src/lib/components/layout/popover/Popover.svelte +76 -96
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
children: Snippet;
|
|
16
16
|
placement?: DrawerPlacement;
|
|
17
17
|
closeOnOutsideClick?: boolean;
|
|
18
|
+
/** Overwrites entire content of the drawer */
|
|
19
|
+
inner?: Snippet;
|
|
18
20
|
};
|
|
19
21
|
</script>
|
|
20
22
|
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
fly(e, { x: placement === 'right' ? '100%' : '-100%', duration: 200 }),
|
|
30
32
|
outTransition = (e) =>
|
|
31
33
|
fly(e, { x: placement === 'right' ? '100%' : '-100%', duration: 200 }),
|
|
34
|
+
inner,
|
|
32
35
|
...rest
|
|
33
36
|
}: DrawerProps = $props();
|
|
34
37
|
|
|
@@ -68,21 +71,25 @@
|
|
|
68
71
|
out:outTransition|global
|
|
69
72
|
{...rest}
|
|
70
73
|
>
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{title}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
{#if inner}
|
|
75
|
+
{@render inner()}
|
|
76
|
+
{:else}
|
|
77
|
+
<div class="flex flex-row items-center justify-between gap-8">
|
|
78
|
+
{#if title}
|
|
79
|
+
<Heading class="flex grow flex-row items-center gap-4">
|
|
80
|
+
{#if typeof title === 'function'}
|
|
81
|
+
{@render title()}
|
|
82
|
+
{:else}
|
|
83
|
+
{title}
|
|
84
|
+
{/if}
|
|
85
|
+
</Heading>
|
|
86
|
+
{/if}
|
|
87
|
+
<button class="group ml-auto flex justify-end" type="button" onclick={close}>
|
|
88
|
+
<X class="h-full w-auto transition-[stroke-width] group-hover:stroke-3" />
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
{@render children()}
|
|
92
|
+
{/if}
|
|
86
93
|
</div>
|
|
87
94
|
</HiddenBackground>
|
|
88
95
|
{/if}
|
|
@@ -7,6 +7,8 @@ export type DrawerProps = TransitionProps & {
|
|
|
7
7
|
children: Snippet;
|
|
8
8
|
placement?: DrawerPlacement;
|
|
9
9
|
closeOnOutsideClick?: boolean;
|
|
10
|
+
/** Overwrites entire content of the drawer */
|
|
11
|
+
inner?: Snippet;
|
|
10
12
|
};
|
|
11
13
|
declare const Drawer: import("svelte").Component<DrawerProps, {
|
|
12
14
|
close: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/drawer/Drawer.svelte.ts"],"names":[],"mappings":"AAGI,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,mBAAmB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Drawer.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/drawer/Drawer.svelte.ts"],"names":[],"mappings":"AAGI,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAwEN,QAAA,MAAM,MAAM;;;;;MAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -1,130 +1,110 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
+
import { browser } from '$app/environment';
|
|
2
3
|
import { theme } from '../../../theme.svelte';
|
|
3
4
|
import type { IvoryComponent } from '../../../types';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
import { clickOutside } from '../../../utils/attachments';
|
|
6
|
+
import {
|
|
7
|
+
autoPlacement,
|
|
8
|
+
autoUpdate,
|
|
9
|
+
computePosition,
|
|
10
|
+
flip,
|
|
11
|
+
shift,
|
|
12
|
+
type ComputePositionConfig
|
|
13
|
+
} from '@floating-ui/dom';
|
|
14
|
+
import clsx from 'clsx';
|
|
15
|
+
import { twMerge } from 'tailwind-merge';
|
|
16
|
+
|
|
17
|
+
/** Possible placements for the popover */
|
|
13
18
|
export type PopoverPlacement = ComputePositionConfig['placement'];
|
|
14
19
|
|
|
15
|
-
const ANCHOR_STYLES: Record<string, string> = {
|
|
16
|
-
// Bottom Placements
|
|
17
|
-
'bottom-start': 'top: anchor(bottom); left: anchor(left);',
|
|
18
|
-
bottom: 'top: anchor(bottom); left: anchor(center); translate: -50% 0;',
|
|
19
|
-
'bottom-end': 'top: anchor(bottom); right: anchor(right);',
|
|
20
|
-
|
|
21
|
-
// Top Placements
|
|
22
|
-
'top-start': 'bottom: anchor(top); left: anchor(left);',
|
|
23
|
-
top: 'bottom: anchor(top); left: anchor(center); translate: -50% 0;',
|
|
24
|
-
'top-end': 'bottom: anchor(top); right: anchor(right);',
|
|
25
|
-
|
|
26
|
-
// Left Placements
|
|
27
|
-
'left-start': 'right: anchor(left); top: anchor(top);',
|
|
28
|
-
left: 'right: anchor(left); top: anchor(center); translate: 0 -50%;',
|
|
29
|
-
'left-end': 'right: anchor(left); bottom: anchor(bottom);',
|
|
30
|
-
|
|
31
|
-
// Right Placements
|
|
32
|
-
'right-start': 'left: anchor(right); top: anchor(top);',
|
|
33
|
-
right: 'left: anchor(right); top: anchor(center); translate: 0 -50%;',
|
|
34
|
-
'right-end': 'left: anchor(right); bottom: anchor(bottom);'
|
|
35
|
-
};
|
|
36
|
-
|
|
37
20
|
export interface PopoverProps extends IvoryComponent<HTMLDivElement> {
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
/** The element the popover will be positioned relative to */
|
|
22
|
+
target: Element | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Where the popover should be positioned relative to the target.
|
|
25
|
+
*
|
|
26
|
+
* default: `bottom-start`
|
|
27
|
+
*/
|
|
28
|
+
placement?: PopoverPlacement;
|
|
29
|
+
/**
|
|
30
|
+
* Callback that is called when the user clicks outside the popover or the target element.
|
|
31
|
+
*/
|
|
32
|
+
onClickOutside?: (e: MouseEvent) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to place the popover automatically
|
|
35
|
+
*
|
|
36
|
+
* [Further reading](https://floating-ui.com/docs/autoPlacement)
|
|
37
|
+
*/
|
|
40
38
|
autoplacement?: boolean;
|
|
41
39
|
}
|
|
42
40
|
</script>
|
|
43
41
|
|
|
44
42
|
<script lang="ts">
|
|
45
|
-
import { onMount, tick } from 'svelte';
|
|
46
|
-
|
|
47
43
|
let {
|
|
48
44
|
class: clazz,
|
|
49
45
|
style: externalStyle,
|
|
50
46
|
target,
|
|
51
47
|
placement = 'bottom-start',
|
|
48
|
+
onClickOutside = close,
|
|
52
49
|
children,
|
|
53
|
-
|
|
54
|
-
id = pseudoRandomId(),
|
|
50
|
+
autoplacement,
|
|
55
51
|
...rest
|
|
56
52
|
}: PopoverProps = $props();
|
|
57
53
|
|
|
58
|
-
let
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
// 1. Load Polyfill
|
|
62
|
-
// We import the 'fn' version to manually control execution,
|
|
63
|
-
// ensuring it runs after the DOM is ready.
|
|
64
|
-
onMount(async () => {
|
|
65
|
-
if (!CSS.supports('position-anchor', '--foo')) {
|
|
66
|
-
await polyfill();
|
|
67
|
-
console.log('loaded polyfill');
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const anchorName = $derived(`--anchor-${id}`);
|
|
72
|
-
|
|
73
|
-
// 2. Anchor Association
|
|
74
|
-
$effect(() => {
|
|
75
|
-
if (!target) return;
|
|
76
|
-
const currentStyle = target.getAttribute('style') || '';
|
|
77
|
-
if (!currentStyle.includes(anchorName)) {
|
|
78
|
-
target.setAttribute('style', `anchor-name: ${anchorName}; ${currentStyle}`);
|
|
79
|
-
}
|
|
80
|
-
tick().then(() => polyfill());
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
$effect(() => {
|
|
84
|
-
if (!popoverEl) return;
|
|
85
|
-
|
|
86
|
-
// Use the explicit coordinates instead of position-area
|
|
87
|
-
const coords = ANCHOR_STYLES[placement] ?? ANCHOR_STYLES['bottom-start'];
|
|
88
|
-
|
|
89
|
-
// Important: We ensure position-area is NOT present
|
|
90
|
-
const polyfillStyles = `
|
|
91
|
-
position-anchor: ${anchorName};
|
|
92
|
-
${coords}
|
|
93
|
-
`;
|
|
54
|
+
let style: string = $state('');
|
|
55
|
+
let popover: HTMLDivElement | undefined = $state();
|
|
94
56
|
|
|
95
|
-
|
|
96
|
-
|
|
57
|
+
const postion = async (open: boolean) => {
|
|
58
|
+
if (!open || !popover || !browser || !target) return;
|
|
59
|
+
const { x, y } = await computePosition(target, popover, {
|
|
60
|
+
middleware: [shift(), ...(autoplacement ? [autoPlacement()] : [flip()])],
|
|
61
|
+
placement
|
|
62
|
+
});
|
|
63
|
+
style = `top: ${y}px; left: ${x}px;`;
|
|
64
|
+
};
|
|
97
65
|
|
|
98
|
-
|
|
99
|
-
});
|
|
66
|
+
let currentlyOpen = $state(false);
|
|
100
67
|
|
|
101
|
-
|
|
102
|
-
|
|
68
|
+
let cleanup: () => void = () => {};
|
|
69
|
+
export function close() {
|
|
70
|
+
currentlyOpen = false;
|
|
71
|
+
cleanup();
|
|
103
72
|
}
|
|
104
73
|
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
(
|
|
74
|
+
export function open() {
|
|
75
|
+
currentlyOpen = true;
|
|
76
|
+
if (!target || !popover) return;
|
|
77
|
+
cleanup = autoUpdate(target, popover, () => postion(true));
|
|
108
78
|
}
|
|
109
79
|
|
|
110
|
-
export
|
|
111
|
-
|
|
112
|
-
(popoverEl?.togglePopover as any)();
|
|
80
|
+
export function toggle() {
|
|
81
|
+
currentlyOpen = !currentlyOpen;
|
|
113
82
|
}
|
|
114
83
|
|
|
115
84
|
export function isOpen() {
|
|
116
85
|
return currentlyOpen;
|
|
117
86
|
}
|
|
87
|
+
|
|
88
|
+
// TODO: this is kinda hacky
|
|
89
|
+
$effect(() => {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
91
|
+
[popover, target];
|
|
92
|
+
postion(currentlyOpen);
|
|
93
|
+
});
|
|
118
94
|
</script>
|
|
119
95
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
96
|
+
<!--
|
|
97
|
+
@component
|
|
98
|
+
A popover, positions itself relative to a target element.
|
|
99
|
+
-->
|
|
100
|
+
{#if currentlyOpen}
|
|
101
|
+
<div
|
|
102
|
+
class={twMerge(clsx('absolute', theme.current.popover?.class, clazz))}
|
|
103
|
+
style={style + ' ' + externalStyle}
|
|
104
|
+
bind:this={popover}
|
|
105
|
+
{@attach clickOutside({ callback: onClickOutside, target })}
|
|
106
|
+
{...rest}
|
|
107
|
+
>
|
|
108
|
+
{@render children?.()}
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import type { IvoryComponent } from '../../../types';
|
|
2
2
|
import { type ComputePositionConfig } from '@floating-ui/dom';
|
|
3
|
-
|
|
4
|
-
type Side = 'top' | 'bottom' | 'left' | 'right';
|
|
5
|
-
type AlignedPlacement = `${Side}-${Alignment}` | Side;
|
|
3
|
+
/** Possible placements for the popover */
|
|
6
4
|
export type PopoverPlacement = ComputePositionConfig['placement'];
|
|
7
5
|
export interface PopoverProps extends IvoryComponent<HTMLDivElement> {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/** The element the popover will be positioned relative to */
|
|
7
|
+
target: Element | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Where the popover should be positioned relative to the target.
|
|
10
|
+
*
|
|
11
|
+
* default: `bottom-start`
|
|
12
|
+
*/
|
|
13
|
+
placement?: PopoverPlacement;
|
|
14
|
+
/**
|
|
15
|
+
* Callback that is called when the user clicks outside the popover or the target element.
|
|
16
|
+
*/
|
|
17
|
+
onClickOutside?: (e: MouseEvent) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Whether to place the popover automatically
|
|
20
|
+
*
|
|
21
|
+
* [Further reading](https://floating-ui.com/docs/autoPlacement)
|
|
22
|
+
*/
|
|
10
23
|
autoplacement?: boolean;
|
|
11
24
|
}
|
|
25
|
+
/** A popover, positions itself relative to a target element. */
|
|
12
26
|
declare const Popover: import("svelte").Component<PopoverProps, {
|
|
13
|
-
close: () =>
|
|
14
|
-
open: () =>
|
|
15
|
-
toggle: () =>
|
|
27
|
+
close: () => void;
|
|
28
|
+
open: () => void;
|
|
29
|
+
toggle: () => void;
|
|
16
30
|
isOpen: () => boolean;
|
|
17
31
|
}, "">;
|
|
18
32
|
type Popover = ReturnType<typeof Popover>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Popover.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/popover/Popover.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Popover.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/popover/Popover.svelte.ts"],"names":[],"mappings":"AAKI,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAMH,KAAK,qBAAqB,EAC7B,MAAM,kBAAkB,CAAC;AAI1B,0CAA0C;AAC1C,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;AAElE,MAAM,WAAW,YAAa,SAAQ,cAAc,CAAC,cAAc,CAAC;IAChE,6DAA6D;IAC7D,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACzC;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAmEL,gEAAgE;AAChE,QAAA,MAAM,OAAO;;;;;MAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
|
package/package.json
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
children: Snippet;
|
|
16
16
|
placement?: DrawerPlacement;
|
|
17
17
|
closeOnOutsideClick?: boolean;
|
|
18
|
+
/** Overwrites entire content of the drawer */
|
|
19
|
+
inner?: Snippet;
|
|
18
20
|
};
|
|
19
21
|
</script>
|
|
20
22
|
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
fly(e, { x: placement === 'right' ? '100%' : '-100%', duration: 200 }),
|
|
30
32
|
outTransition = (e) =>
|
|
31
33
|
fly(e, { x: placement === 'right' ? '100%' : '-100%', duration: 200 }),
|
|
34
|
+
inner,
|
|
32
35
|
...rest
|
|
33
36
|
}: DrawerProps = $props();
|
|
34
37
|
|
|
@@ -68,21 +71,25 @@
|
|
|
68
71
|
out:outTransition|global
|
|
69
72
|
{...rest}
|
|
70
73
|
>
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{title}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
{#if inner}
|
|
75
|
+
{@render inner()}
|
|
76
|
+
{:else}
|
|
77
|
+
<div class="flex flex-row items-center justify-between gap-8">
|
|
78
|
+
{#if title}
|
|
79
|
+
<Heading class="flex grow flex-row items-center gap-4">
|
|
80
|
+
{#if typeof title === 'function'}
|
|
81
|
+
{@render title()}
|
|
82
|
+
{:else}
|
|
83
|
+
{title}
|
|
84
|
+
{/if}
|
|
85
|
+
</Heading>
|
|
86
|
+
{/if}
|
|
87
|
+
<button class="group ml-auto flex justify-end" type="button" onclick={close}>
|
|
88
|
+
<X class="h-full w-auto transition-[stroke-width] group-hover:stroke-3" />
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
{@render children()}
|
|
92
|
+
{/if}
|
|
86
93
|
</div>
|
|
87
94
|
</HiddenBackground>
|
|
88
95
|
{/if}
|
|
@@ -1,130 +1,110 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
+
import { browser } from '$app/environment';
|
|
2
3
|
import { theme } from '$lib/theme.svelte';
|
|
3
4
|
import type { IvoryComponent } from '$lib/types';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
import { clickOutside } from '$lib/utils/attachments';
|
|
6
|
+
import {
|
|
7
|
+
autoPlacement,
|
|
8
|
+
autoUpdate,
|
|
9
|
+
computePosition,
|
|
10
|
+
flip,
|
|
11
|
+
shift,
|
|
12
|
+
type ComputePositionConfig
|
|
13
|
+
} from '@floating-ui/dom';
|
|
14
|
+
import clsx from 'clsx';
|
|
15
|
+
import { twMerge } from 'tailwind-merge';
|
|
16
|
+
|
|
17
|
+
/** Possible placements for the popover */
|
|
13
18
|
export type PopoverPlacement = ComputePositionConfig['placement'];
|
|
14
19
|
|
|
15
|
-
const ANCHOR_STYLES: Record<string, string> = {
|
|
16
|
-
// Bottom Placements
|
|
17
|
-
'bottom-start': 'top: anchor(bottom); left: anchor(left);',
|
|
18
|
-
bottom: 'top: anchor(bottom); left: anchor(center); translate: -50% 0;',
|
|
19
|
-
'bottom-end': 'top: anchor(bottom); right: anchor(right);',
|
|
20
|
-
|
|
21
|
-
// Top Placements
|
|
22
|
-
'top-start': 'bottom: anchor(top); left: anchor(left);',
|
|
23
|
-
top: 'bottom: anchor(top); left: anchor(center); translate: -50% 0;',
|
|
24
|
-
'top-end': 'bottom: anchor(top); right: anchor(right);',
|
|
25
|
-
|
|
26
|
-
// Left Placements
|
|
27
|
-
'left-start': 'right: anchor(left); top: anchor(top);',
|
|
28
|
-
left: 'right: anchor(left); top: anchor(center); translate: 0 -50%;',
|
|
29
|
-
'left-end': 'right: anchor(left); bottom: anchor(bottom);',
|
|
30
|
-
|
|
31
|
-
// Right Placements
|
|
32
|
-
'right-start': 'left: anchor(right); top: anchor(top);',
|
|
33
|
-
right: 'left: anchor(right); top: anchor(center); translate: 0 -50%;',
|
|
34
|
-
'right-end': 'left: anchor(right); bottom: anchor(bottom);'
|
|
35
|
-
};
|
|
36
|
-
|
|
37
20
|
export interface PopoverProps extends IvoryComponent<HTMLDivElement> {
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
/** The element the popover will be positioned relative to */
|
|
22
|
+
target: Element | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Where the popover should be positioned relative to the target.
|
|
25
|
+
*
|
|
26
|
+
* default: `bottom-start`
|
|
27
|
+
*/
|
|
28
|
+
placement?: PopoverPlacement;
|
|
29
|
+
/**
|
|
30
|
+
* Callback that is called when the user clicks outside the popover or the target element.
|
|
31
|
+
*/
|
|
32
|
+
onClickOutside?: (e: MouseEvent) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to place the popover automatically
|
|
35
|
+
*
|
|
36
|
+
* [Further reading](https://floating-ui.com/docs/autoPlacement)
|
|
37
|
+
*/
|
|
40
38
|
autoplacement?: boolean;
|
|
41
39
|
}
|
|
42
40
|
</script>
|
|
43
41
|
|
|
44
42
|
<script lang="ts">
|
|
45
|
-
import { onMount, tick } from 'svelte';
|
|
46
|
-
|
|
47
43
|
let {
|
|
48
44
|
class: clazz,
|
|
49
45
|
style: externalStyle,
|
|
50
46
|
target,
|
|
51
47
|
placement = 'bottom-start',
|
|
48
|
+
onClickOutside = close,
|
|
52
49
|
children,
|
|
53
|
-
|
|
54
|
-
id = pseudoRandomId(),
|
|
50
|
+
autoplacement,
|
|
55
51
|
...rest
|
|
56
52
|
}: PopoverProps = $props();
|
|
57
53
|
|
|
58
|
-
let
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
// 1. Load Polyfill
|
|
62
|
-
// We import the 'fn' version to manually control execution,
|
|
63
|
-
// ensuring it runs after the DOM is ready.
|
|
64
|
-
onMount(async () => {
|
|
65
|
-
if (!CSS.supports('position-anchor', '--foo')) {
|
|
66
|
-
await polyfill();
|
|
67
|
-
console.log('loaded polyfill');
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const anchorName = $derived(`--anchor-${id}`);
|
|
72
|
-
|
|
73
|
-
// 2. Anchor Association
|
|
74
|
-
$effect(() => {
|
|
75
|
-
if (!target) return;
|
|
76
|
-
const currentStyle = target.getAttribute('style') || '';
|
|
77
|
-
if (!currentStyle.includes(anchorName)) {
|
|
78
|
-
target.setAttribute('style', `anchor-name: ${anchorName}; ${currentStyle}`);
|
|
79
|
-
}
|
|
80
|
-
tick().then(() => polyfill());
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
$effect(() => {
|
|
84
|
-
if (!popoverEl) return;
|
|
85
|
-
|
|
86
|
-
// Use the explicit coordinates instead of position-area
|
|
87
|
-
const coords = ANCHOR_STYLES[placement] ?? ANCHOR_STYLES['bottom-start'];
|
|
88
|
-
|
|
89
|
-
// Important: We ensure position-area is NOT present
|
|
90
|
-
const polyfillStyles = `
|
|
91
|
-
position-anchor: ${anchorName};
|
|
92
|
-
${coords}
|
|
93
|
-
`;
|
|
54
|
+
let style: string = $state('');
|
|
55
|
+
let popover: HTMLDivElement | undefined = $state();
|
|
94
56
|
|
|
95
|
-
|
|
96
|
-
|
|
57
|
+
const postion = async (open: boolean) => {
|
|
58
|
+
if (!open || !popover || !browser || !target) return;
|
|
59
|
+
const { x, y } = await computePosition(target, popover, {
|
|
60
|
+
middleware: [shift(), ...(autoplacement ? [autoPlacement()] : [flip()])],
|
|
61
|
+
placement
|
|
62
|
+
});
|
|
63
|
+
style = `top: ${y}px; left: ${x}px;`;
|
|
64
|
+
};
|
|
97
65
|
|
|
98
|
-
|
|
99
|
-
});
|
|
66
|
+
let currentlyOpen = $state(false);
|
|
100
67
|
|
|
101
|
-
|
|
102
|
-
|
|
68
|
+
let cleanup: () => void = () => {};
|
|
69
|
+
export function close() {
|
|
70
|
+
currentlyOpen = false;
|
|
71
|
+
cleanup();
|
|
103
72
|
}
|
|
104
73
|
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
(
|
|
74
|
+
export function open() {
|
|
75
|
+
currentlyOpen = true;
|
|
76
|
+
if (!target || !popover) return;
|
|
77
|
+
cleanup = autoUpdate(target, popover, () => postion(true));
|
|
108
78
|
}
|
|
109
79
|
|
|
110
|
-
export
|
|
111
|
-
|
|
112
|
-
(popoverEl?.togglePopover as any)();
|
|
80
|
+
export function toggle() {
|
|
81
|
+
currentlyOpen = !currentlyOpen;
|
|
113
82
|
}
|
|
114
83
|
|
|
115
84
|
export function isOpen() {
|
|
116
85
|
return currentlyOpen;
|
|
117
86
|
}
|
|
87
|
+
|
|
88
|
+
// TODO: this is kinda hacky
|
|
89
|
+
$effect(() => {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
91
|
+
[popover, target];
|
|
92
|
+
postion(currentlyOpen);
|
|
93
|
+
});
|
|
118
94
|
</script>
|
|
119
95
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
96
|
+
<!--
|
|
97
|
+
@component
|
|
98
|
+
A popover, positions itself relative to a target element.
|
|
99
|
+
-->
|
|
100
|
+
{#if currentlyOpen}
|
|
101
|
+
<div
|
|
102
|
+
class={twMerge(clsx('absolute', theme.current.popover?.class, clazz))}
|
|
103
|
+
style={style + ' ' + externalStyle}
|
|
104
|
+
bind:this={popover}
|
|
105
|
+
{@attach clickOutside({ callback: onClickOutside, target })}
|
|
106
|
+
{...rest}
|
|
107
|
+
>
|
|
108
|
+
{@render children?.()}
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|