@ims360/svelte-ivory 0.3.3 → 0.3.4

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.
@@ -33,6 +33,8 @@
33
33
  style="min-width: {target?.getBoundingClientRect().width}px;"
34
34
  class="py-3"
35
35
  placement="top"
36
+ autoplacement
37
+ popover="manual"
36
38
  >
37
39
  <div class="bg-surface-50-950 flex w-full flex-col gap-2 rounded p-4 shadow-lg">
38
40
  {#each requirements as requirement (requirement.re)}
@@ -1,39 +1,36 @@
1
1
  <script lang="ts" module>
2
- import { browser } from '$app/environment';
3
2
  import { theme } from '../../../theme.svelte';
4
3
  import type { IvoryComponent } from '../../../types';
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';
4
+ import { pseudoRandomId } from '../../../utils/functions';
14
5
  import clsx from 'clsx';
15
6
  import { twMerge } from 'tailwind-merge';
16
7
 
17
8
  /** Possible placements for the popover */
18
- export type PopoverPlacement = ComputePositionConfig['placement'];
9
+ export type PopoverPlacement =
10
+ | 'top'
11
+ | 'top-start'
12
+ | 'top-end'
13
+ | 'right'
14
+ | 'right-start'
15
+ | 'right-end'
16
+ | 'bottom'
17
+ | 'bottom-start'
18
+ | 'bottom-end'
19
+ | 'left'
20
+ | 'left-start'
21
+ | 'left-end';
19
22
 
20
23
  export interface PopoverProps extends IvoryComponent<HTMLDivElement> {
21
24
  /** The element the popover will be positioned relative to */
22
- target: Element | undefined;
25
+ target: HTMLElement | undefined;
23
26
  /**
24
27
  * Where the popover should be positioned relative to the target.
25
28
  *
26
29
  * default: `bottom-start`
27
30
  */
28
31
  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
32
  /**
34
33
  * Whether to place the popover automatically
35
- *
36
- * [Further reading](https://floating-ui.com/docs/autoPlacement)
37
34
  */
38
35
  autoplacement?: boolean;
39
36
  }
@@ -45,66 +42,139 @@
45
42
  style: externalStyle,
46
43
  target,
47
44
  placement = 'bottom-start',
48
- onClickOutside = close,
49
45
  children,
50
46
  autoplacement,
47
+ popover = 'auto',
51
48
  ...rest
52
49
  }: PopoverProps = $props();
53
50
 
54
- let style: string = $state('');
55
- let popover: HTMLDivElement | undefined = $state();
51
+ let popoverEl: HTMLDivElement | undefined = $state();
52
+ const anchorName = `--${pseudoRandomId('anchor-')}`;
53
+
54
+ const getStyles = (
55
+ placement: PopoverPlacement,
56
+ anchorName: string,
57
+ autoplacement?: boolean
58
+ ) => {
59
+ let styles = `position-anchor: ${anchorName}; margin: 0; inset: auto;`;
60
+ if (autoplacement) {
61
+ styles += ` position-try: flip-block, flip-inline;`;
62
+ }
63
+
64
+ switch (placement) {
65
+ case 'top':
66
+ return (
67
+ styles +
68
+ ` bottom: anchor(${anchorName} top); left: anchor(${anchorName} center); translate: -50% 0;`
69
+ );
70
+ case 'top-start':
71
+ return (
72
+ styles +
73
+ ` bottom: anchor(${anchorName} top); left: anchor(${anchorName} start);`
74
+ );
75
+ case 'top-end':
76
+ return (
77
+ styles + ` bottom: anchor(${anchorName} top); right: anchor(${anchorName} end);`
78
+ );
56
79
 
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;`;
80
+ case 'bottom':
81
+ return (
82
+ styles +
83
+ ` top: anchor(${anchorName} bottom); left: anchor(${anchorName} center); translate: -50% 0;`
84
+ );
85
+ case 'bottom-start':
86
+ return (
87
+ styles +
88
+ ` top: anchor(${anchorName} bottom); left: anchor(${anchorName} start);`
89
+ );
90
+ case 'bottom-end':
91
+ return (
92
+ styles + ` top: anchor(${anchorName} bottom); right: anchor(${anchorName} end);`
93
+ );
94
+
95
+ case 'left':
96
+ return (
97
+ styles +
98
+ ` right: anchor(${anchorName} left); top: anchor(${anchorName} center); translate: 0 -50%;`
99
+ );
100
+ case 'left-start':
101
+ return (
102
+ styles + ` right: anchor(${anchorName} left); top: anchor(${anchorName} top);`
103
+ );
104
+ case 'left-end':
105
+ return (
106
+ styles +
107
+ ` right: anchor(${anchorName} left); bottom: anchor(${anchorName} bottom);`
108
+ );
109
+
110
+ case 'right':
111
+ return (
112
+ styles +
113
+ ` left: anchor(${anchorName} right); top: anchor(${anchorName} center); translate: 0 -50%;`
114
+ );
115
+ case 'right-start':
116
+ return (
117
+ styles + ` left: anchor(${anchorName} right); top: anchor(${anchorName} top);`
118
+ );
119
+ case 'right-end':
120
+ return (
121
+ styles +
122
+ ` left: anchor(${anchorName} right); bottom: anchor(${anchorName} bottom);`
123
+ );
124
+
125
+ default:
126
+ return (
127
+ styles +
128
+ ` top: anchor(${anchorName} bottom); left: anchor(${anchorName} start);`
129
+ );
130
+ }
64
131
  };
65
132
 
66
133
  let currentlyOpen = $state(false);
134
+ let style = $derived(getStyles(placement, anchorName, autoplacement));
135
+
136
+ $effect(() => {
137
+ if (target) {
138
+ target.style.setProperty('anchor-name', anchorName);
139
+ return () => {
140
+ target.style.removeProperty('anchor-name');
141
+ };
142
+ }
143
+ });
144
+
145
+ $effect(() => {
146
+ if (!popoverEl) return;
147
+
148
+ const handleToggle = (e: ToggleEvent) => {
149
+ const newState = e.newState === 'open';
150
+ currentlyOpen = newState;
151
+ };
152
+
153
+ popoverEl.addEventListener('toggle', handleToggle);
154
+ return () => popoverEl?.removeEventListener('toggle', handleToggle);
155
+ });
67
156
 
68
- let cleanup: () => void = () => {};
69
157
  export function close() {
70
- currentlyOpen = false;
71
- cleanup();
158
+ popoverEl?.hidePopover();
72
159
  }
73
160
 
74
161
  export function open() {
75
- currentlyOpen = true;
76
- if (!target || !popover) return;
77
- cleanup = autoUpdate(target, popover, () => postion(true));
162
+ if (popoverEl) popoverEl.showPopover();
78
163
  }
79
164
 
80
165
  export function toggle() {
81
- currentlyOpen = !currentlyOpen;
166
+ if (currentlyOpen) close();
167
+ else open();
82
168
  }
83
169
 
170
+ // Now this is reactive!
84
171
  export function isOpen() {
85
172
  return currentlyOpen;
86
173
  }
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
- });
94
174
  </script>
95
175
 
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
- >
176
+ <div bind:this={popoverEl} {style} {popover} class="bg-transparent">
177
+ <div class={twMerge(clsx(theme.current.popover?.class, clazz))} style={externalStyle} {...rest}>
108
178
  {@render children?.()}
109
179
  </div>
110
- {/if}
180
+ </div>
@@ -1,28 +1,20 @@
1
1
  import type { IvoryComponent } from '../../../types';
2
- import { type ComputePositionConfig } from '@floating-ui/dom';
3
2
  /** Possible placements for the popover */
4
- export type PopoverPlacement = ComputePositionConfig['placement'];
3
+ export type PopoverPlacement = 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end';
5
4
  export interface PopoverProps extends IvoryComponent<HTMLDivElement> {
6
5
  /** The element the popover will be positioned relative to */
7
- target: Element | undefined;
6
+ target: HTMLElement | undefined;
8
7
  /**
9
8
  * Where the popover should be positioned relative to the target.
10
9
  *
11
10
  * default: `bottom-start`
12
11
  */
13
12
  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
13
  /**
19
14
  * Whether to place the popover automatically
20
- *
21
- * [Further reading](https://floating-ui.com/docs/autoPlacement)
22
15
  */
23
16
  autoplacement?: boolean;
24
17
  }
25
- /** A popover, positions itself relative to a target element. */
26
18
  declare const Popover: import("svelte").Component<PopoverProps, {
27
19
  close: () => void;
28
20
  open: () => void;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"Popover.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/popover/Popover.svelte.ts"],"names":[],"mappings":"AAII,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD,0CAA0C;AAC1C,MAAM,MAAM,gBAAgB,GACtB,KAAK,GACL,WAAW,GACX,SAAS,GACT,OAAO,GACP,aAAa,GACb,WAAW,GACX,QAAQ,GACR,cAAc,GACd,YAAY,GACZ,MAAM,GACN,YAAY,GACZ,UAAU,CAAC;AAEjB,MAAM,WAAW,YAAa,SAAQ,cAAc,CAAC,cAAc,CAAC;IAChE,6DAA6D;IAC7D,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAqJL,QAAA,MAAM,OAAO;;;;;MAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ims360/svelte-ivory",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "keywords": [
5
5
  "svelte"
6
6
  ],
@@ -71,7 +71,6 @@
71
71
  "test:unit": "vitest"
72
72
  },
73
73
  "dependencies": {
74
- "@floating-ui/dom": "^1.7.4",
75
74
  "@lucide/svelte": "^0.553.0",
76
75
  "@oddbird/css-anchor-positioning": "^0.8.0",
77
76
  "@tailwindcss/forms": "^0.5.10",
@@ -33,6 +33,8 @@
33
33
  style="min-width: {target?.getBoundingClientRect().width}px;"
34
34
  class="py-3"
35
35
  placement="top"
36
+ autoplacement
37
+ popover="manual"
36
38
  >
37
39
  <div class="bg-surface-50-950 flex w-full flex-col gap-2 rounded p-4 shadow-lg">
38
40
  {#each requirements as requirement (requirement.re)}
@@ -1,39 +1,36 @@
1
1
  <script lang="ts" module>
2
- import { browser } from '$app/environment';
3
2
  import { theme } from '$lib/theme.svelte';
4
3
  import type { IvoryComponent } from '$lib/types';
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';
4
+ import { pseudoRandomId } from '$lib/utils/functions';
14
5
  import clsx from 'clsx';
15
6
  import { twMerge } from 'tailwind-merge';
16
7
 
17
8
  /** Possible placements for the popover */
18
- export type PopoverPlacement = ComputePositionConfig['placement'];
9
+ export type PopoverPlacement =
10
+ | 'top'
11
+ | 'top-start'
12
+ | 'top-end'
13
+ | 'right'
14
+ | 'right-start'
15
+ | 'right-end'
16
+ | 'bottom'
17
+ | 'bottom-start'
18
+ | 'bottom-end'
19
+ | 'left'
20
+ | 'left-start'
21
+ | 'left-end';
19
22
 
20
23
  export interface PopoverProps extends IvoryComponent<HTMLDivElement> {
21
24
  /** The element the popover will be positioned relative to */
22
- target: Element | undefined;
25
+ target: HTMLElement | undefined;
23
26
  /**
24
27
  * Where the popover should be positioned relative to the target.
25
28
  *
26
29
  * default: `bottom-start`
27
30
  */
28
31
  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
32
  /**
34
33
  * Whether to place the popover automatically
35
- *
36
- * [Further reading](https://floating-ui.com/docs/autoPlacement)
37
34
  */
38
35
  autoplacement?: boolean;
39
36
  }
@@ -45,66 +42,139 @@
45
42
  style: externalStyle,
46
43
  target,
47
44
  placement = 'bottom-start',
48
- onClickOutside = close,
49
45
  children,
50
46
  autoplacement,
47
+ popover = 'auto',
51
48
  ...rest
52
49
  }: PopoverProps = $props();
53
50
 
54
- let style: string = $state('');
55
- let popover: HTMLDivElement | undefined = $state();
51
+ let popoverEl: HTMLDivElement | undefined = $state();
52
+ const anchorName = `--${pseudoRandomId('anchor-')}`;
53
+
54
+ const getStyles = (
55
+ placement: PopoverPlacement,
56
+ anchorName: string,
57
+ autoplacement?: boolean
58
+ ) => {
59
+ let styles = `position-anchor: ${anchorName}; margin: 0; inset: auto;`;
60
+ if (autoplacement) {
61
+ styles += ` position-try: flip-block, flip-inline;`;
62
+ }
63
+
64
+ switch (placement) {
65
+ case 'top':
66
+ return (
67
+ styles +
68
+ ` bottom: anchor(${anchorName} top); left: anchor(${anchorName} center); translate: -50% 0;`
69
+ );
70
+ case 'top-start':
71
+ return (
72
+ styles +
73
+ ` bottom: anchor(${anchorName} top); left: anchor(${anchorName} start);`
74
+ );
75
+ case 'top-end':
76
+ return (
77
+ styles + ` bottom: anchor(${anchorName} top); right: anchor(${anchorName} end);`
78
+ );
56
79
 
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;`;
80
+ case 'bottom':
81
+ return (
82
+ styles +
83
+ ` top: anchor(${anchorName} bottom); left: anchor(${anchorName} center); translate: -50% 0;`
84
+ );
85
+ case 'bottom-start':
86
+ return (
87
+ styles +
88
+ ` top: anchor(${anchorName} bottom); left: anchor(${anchorName} start);`
89
+ );
90
+ case 'bottom-end':
91
+ return (
92
+ styles + ` top: anchor(${anchorName} bottom); right: anchor(${anchorName} end);`
93
+ );
94
+
95
+ case 'left':
96
+ return (
97
+ styles +
98
+ ` right: anchor(${anchorName} left); top: anchor(${anchorName} center); translate: 0 -50%;`
99
+ );
100
+ case 'left-start':
101
+ return (
102
+ styles + ` right: anchor(${anchorName} left); top: anchor(${anchorName} top);`
103
+ );
104
+ case 'left-end':
105
+ return (
106
+ styles +
107
+ ` right: anchor(${anchorName} left); bottom: anchor(${anchorName} bottom);`
108
+ );
109
+
110
+ case 'right':
111
+ return (
112
+ styles +
113
+ ` left: anchor(${anchorName} right); top: anchor(${anchorName} center); translate: 0 -50%;`
114
+ );
115
+ case 'right-start':
116
+ return (
117
+ styles + ` left: anchor(${anchorName} right); top: anchor(${anchorName} top);`
118
+ );
119
+ case 'right-end':
120
+ return (
121
+ styles +
122
+ ` left: anchor(${anchorName} right); bottom: anchor(${anchorName} bottom);`
123
+ );
124
+
125
+ default:
126
+ return (
127
+ styles +
128
+ ` top: anchor(${anchorName} bottom); left: anchor(${anchorName} start);`
129
+ );
130
+ }
64
131
  };
65
132
 
66
133
  let currentlyOpen = $state(false);
134
+ let style = $derived(getStyles(placement, anchorName, autoplacement));
135
+
136
+ $effect(() => {
137
+ if (target) {
138
+ target.style.setProperty('anchor-name', anchorName);
139
+ return () => {
140
+ target.style.removeProperty('anchor-name');
141
+ };
142
+ }
143
+ });
144
+
145
+ $effect(() => {
146
+ if (!popoverEl) return;
147
+
148
+ const handleToggle = (e: ToggleEvent) => {
149
+ const newState = e.newState === 'open';
150
+ currentlyOpen = newState;
151
+ };
152
+
153
+ popoverEl.addEventListener('toggle', handleToggle);
154
+ return () => popoverEl?.removeEventListener('toggle', handleToggle);
155
+ });
67
156
 
68
- let cleanup: () => void = () => {};
69
157
  export function close() {
70
- currentlyOpen = false;
71
- cleanup();
158
+ popoverEl?.hidePopover();
72
159
  }
73
160
 
74
161
  export function open() {
75
- currentlyOpen = true;
76
- if (!target || !popover) return;
77
- cleanup = autoUpdate(target, popover, () => postion(true));
162
+ if (popoverEl) popoverEl.showPopover();
78
163
  }
79
164
 
80
165
  export function toggle() {
81
- currentlyOpen = !currentlyOpen;
166
+ if (currentlyOpen) close();
167
+ else open();
82
168
  }
83
169
 
170
+ // Now this is reactive!
84
171
  export function isOpen() {
85
172
  return currentlyOpen;
86
173
  }
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
- });
94
174
  </script>
95
175
 
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
- >
176
+ <div bind:this={popoverEl} {style} {popover} class="bg-transparent">
177
+ <div class={twMerge(clsx(theme.current.popover?.class, clazz))} style={externalStyle} {...rest}>
108
178
  {@render children?.()}
109
179
  </div>
110
- {/if}
180
+ </div>