@ims360/svelte-ivory 0.2.0 → 0.2.1

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.
@@ -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 { pseudoRandomId } from '../../../utils/functions';
5
- import { merge } from '../../../utils/merge';
6
- import { type ComputePositionConfig } from '@floating-ui/dom';
7
- import polyfill from '@oddbird/css-anchor-positioning/fn';
8
-
9
- // ... (Your existing types remain unchanged)
10
- type Alignment = 'start' | 'end';
11
- type Side = 'top' | 'bottom' | 'left' | 'right';
12
- type AlignedPlacement = `${Side}-${Alignment}` | Side;
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
- target: HTMLElement | undefined;
39
- placement?: AlignedPlacement;
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
- popover = 'auto',
54
- id = pseudoRandomId(),
50
+ autoplacement,
55
51
  ...rest
56
52
  }: PopoverProps = $props();
57
53
 
58
- let popoverEl: HTMLDivElement | undefined = $state();
59
- let currentlyOpen = $state(false);
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
- const combinedStyle = `${externalStyle ? externalStyle + '; ' : ''}${polyfillStyles}`;
96
- popoverEl.setAttribute('style', combinedStyle);
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
- tick().then(() => polyfill());
99
- });
66
+ let currentlyOpen = $state(false);
100
67
 
101
- export async function close() {
102
- popoverEl?.hidePopover();
68
+ let cleanup: () => void = () => {};
69
+ export function close() {
70
+ currentlyOpen = false;
71
+ cleanup();
103
72
  }
104
73
 
105
- export async function open() {
106
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
- (popoverEl?.showPopover as any)();
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 async function toggle() {
111
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- <div
121
- {popover}
122
- class={merge('absolute m-0', theme.current.popover?.class, clazz)}
123
- bind:this={popoverEl}
124
- ontoggle={(e) => {
125
- currentlyOpen = e.newState === 'open';
126
- }}
127
- {...rest}
128
- >
129
- {@render children?.()}
130
- </div>
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
- type Alignment = 'start' | 'end';
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
- target: HTMLElement | undefined;
9
- placement?: AlignedPlacement;
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: () => Promise<void>;
14
- open: () => Promise<void>;
15
- toggle: () => Promise<void>;
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":"AAII,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAI9D,KAAK,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AACjC,KAAK,IAAI,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAChD,KAAK,gBAAgB,GAAG,GAAG,IAAI,IAAI,SAAS,EAAE,GAAG,IAAI,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;AAwBlE,MAAM,WAAW,YAAa,SAAQ,cAAc,CAAC,cAAc,CAAC;IAChE,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AA2FL,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":"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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ims360/svelte-ivory",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "keywords": [
5
5
  "svelte"
6
6
  ],
@@ -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 { pseudoRandomId } from '$lib/utils/functions';
5
- import { merge } from '$lib/utils/merge';
6
- import { type ComputePositionConfig } from '@floating-ui/dom';
7
- import polyfill from '@oddbird/css-anchor-positioning/fn';
8
-
9
- // ... (Your existing types remain unchanged)
10
- type Alignment = 'start' | 'end';
11
- type Side = 'top' | 'bottom' | 'left' | 'right';
12
- type AlignedPlacement = `${Side}-${Alignment}` | Side;
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
- target: HTMLElement | undefined;
39
- placement?: AlignedPlacement;
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
- popover = 'auto',
54
- id = pseudoRandomId(),
50
+ autoplacement,
55
51
  ...rest
56
52
  }: PopoverProps = $props();
57
53
 
58
- let popoverEl: HTMLDivElement | undefined = $state();
59
- let currentlyOpen = $state(false);
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
- const combinedStyle = `${externalStyle ? externalStyle + '; ' : ''}${polyfillStyles}`;
96
- popoverEl.setAttribute('style', combinedStyle);
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
- tick().then(() => polyfill());
99
- });
66
+ let currentlyOpen = $state(false);
100
67
 
101
- export async function close() {
102
- popoverEl?.hidePopover();
68
+ let cleanup: () => void = () => {};
69
+ export function close() {
70
+ currentlyOpen = false;
71
+ cleanup();
103
72
  }
104
73
 
105
- export async function open() {
106
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
- (popoverEl?.showPopover as any)();
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 async function toggle() {
111
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- <div
121
- {popover}
122
- class={merge('absolute m-0', theme.current.popover?.class, clazz)}
123
- bind:this={popoverEl}
124
- ontoggle={(e) => {
125
- currentlyOpen = e.newState === 'open';
126
- }}
127
- {...rest}
128
- >
129
- {@render children?.()}
130
- </div>
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}