@sveltia/ui 0.25.3 → 0.25.5
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.
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
@see https://w3c.github.io/aria/#menuitem
|
|
5
5
|
-->
|
|
6
6
|
<script>
|
|
7
|
+
import { onMount } from 'svelte';
|
|
7
8
|
import Button from '../button/button.svelte';
|
|
8
9
|
import Icon from '../icon/icon.svelte';
|
|
9
10
|
import Popup from '../util/popup.svelte';
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
endIcon: _endIcon,
|
|
26
27
|
chevronIcon,
|
|
27
28
|
items,
|
|
29
|
+
onmouseenter,
|
|
30
|
+
onmouseleave,
|
|
31
|
+
onclick,
|
|
28
32
|
onChange,
|
|
29
33
|
onSelect,
|
|
30
34
|
...restProps
|
|
@@ -32,6 +36,7 @@
|
|
|
32
36
|
} = $props();
|
|
33
37
|
|
|
34
38
|
let isPopupOpen = $state(false);
|
|
39
|
+
let isPopupHovered = $state(false);
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* Reference to the `<button>` element.
|
|
@@ -39,7 +44,17 @@
|
|
|
39
44
|
*/
|
|
40
45
|
let buttonElement = $state();
|
|
41
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Reference to the `<button>` element.
|
|
49
|
+
* @type {HTMLDialogElement | undefined}
|
|
50
|
+
*/
|
|
51
|
+
let dialogElement = $state();
|
|
52
|
+
|
|
42
53
|
const hasItems = $derived(role === 'menuitem' && !!items);
|
|
54
|
+
|
|
55
|
+
onMount(() => {
|
|
56
|
+
dialogElement = buttonElement?.closest('dialog') ?? undefined;
|
|
57
|
+
});
|
|
43
58
|
</script>
|
|
44
59
|
|
|
45
60
|
<div role="none" class="sui menuitem {className}" {hidden}>
|
|
@@ -51,15 +66,34 @@
|
|
|
51
66
|
{disabled}
|
|
52
67
|
aria-haspopup={hasItems ? 'menu' : undefined}
|
|
53
68
|
aria-expanded={hasItems ? isPopupOpen : undefined}
|
|
54
|
-
onmouseenter={() => {
|
|
69
|
+
onmouseenter={(event) => {
|
|
55
70
|
if (hasItems) {
|
|
56
|
-
|
|
71
|
+
window.setTimeout(() => {
|
|
72
|
+
isPopupOpen = true;
|
|
73
|
+
}, 200);
|
|
57
74
|
}
|
|
75
|
+
|
|
76
|
+
onmouseenter?.(event);
|
|
58
77
|
}}
|
|
59
|
-
onmouseleave={() => {
|
|
78
|
+
onmouseleave={(event) => {
|
|
60
79
|
if (hasItems) {
|
|
61
|
-
|
|
80
|
+
window.setTimeout(() => {
|
|
81
|
+
if (!isPopupHovered) {
|
|
82
|
+
isPopupOpen = false;
|
|
83
|
+
}
|
|
84
|
+
}, 200);
|
|
62
85
|
}
|
|
86
|
+
|
|
87
|
+
onmouseleave?.(event);
|
|
88
|
+
}}
|
|
89
|
+
onclick={(event) => {
|
|
90
|
+
if (hasItems) {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
isPopupOpen = !isPopupOpen;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onclick?.(event);
|
|
63
97
|
}}
|
|
64
98
|
{onChange}
|
|
65
99
|
{onSelect}
|
|
@@ -89,8 +123,14 @@
|
|
|
89
123
|
{@render _endIcon?.()}
|
|
90
124
|
{/snippet}
|
|
91
125
|
</Button>
|
|
92
|
-
{#if hasItems}
|
|
93
|
-
<Popup
|
|
126
|
+
{#if hasItems && buttonElement && dialogElement}
|
|
127
|
+
<Popup
|
|
128
|
+
anchor={buttonElement}
|
|
129
|
+
parentDialogElement={dialogElement}
|
|
130
|
+
position="right-top"
|
|
131
|
+
bind:open={isPopupOpen}
|
|
132
|
+
bind:hovered={isPopupHovered}
|
|
133
|
+
>
|
|
94
134
|
<Menu>
|
|
95
135
|
{@render items?.()}
|
|
96
136
|
</Menu>
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
* @typedef {object} Props
|
|
14
14
|
* @property {string} [class] - The `class` attribute on the content element.
|
|
15
15
|
* @property {boolean} [open] - Whether to open the popup.
|
|
16
|
+
* @property {boolean} [hovered] - Whether the content is hovered.
|
|
16
17
|
* @property {HTMLElement} [anchor] - A reference to the anchor element that opens the popup.
|
|
17
18
|
* Typically a `<button>`.
|
|
18
19
|
* @property {HTMLElement} [content] - A reference to the content element.
|
|
19
20
|
* @property {import('../../typedefs').PopupPosition} [position] - Where to show the popup.
|
|
20
|
-
* @property {HTMLElement} [positionBaseElement] - The base element of
|
|
21
|
-
*
|
|
21
|
+
* @property {HTMLElement} [positionBaseElement] - The base element of {@link position}. If
|
|
22
|
+
* omitted, this will be {@link anchor}.
|
|
23
|
+
* @property {HTMLDialogElement} [parentDialogElement] - A reference to a dialog element that is
|
|
24
|
+
* already displayed. This should be provided for a nested popup.
|
|
22
25
|
* @property {boolean} [touchOptimized] - Whether to show the popup at the center of the screen on
|
|
23
26
|
* mobile/tablet and ignore the defined dropdown `position`.
|
|
24
27
|
* @property {import('svelte').Snippet} [children] - Primary slot content.
|
|
@@ -32,12 +35,14 @@
|
|
|
32
35
|
let {
|
|
33
36
|
/* eslint-disable prefer-const */
|
|
34
37
|
open = $bindable(false),
|
|
38
|
+
hovered = $bindable(false),
|
|
35
39
|
content = $bindable(undefined),
|
|
36
40
|
class: className,
|
|
37
41
|
showBackdrop = false,
|
|
38
42
|
anchor,
|
|
39
43
|
position = 'bottom-left',
|
|
40
44
|
positionBaseElement = undefined,
|
|
45
|
+
parentDialogElement = undefined,
|
|
41
46
|
touchOptimized = false,
|
|
42
47
|
children,
|
|
43
48
|
onOpen,
|
|
@@ -68,7 +73,7 @@
|
|
|
68
73
|
let contentType = $state();
|
|
69
74
|
/**
|
|
70
75
|
* Style to be applied to the content.
|
|
71
|
-
* @type {import('svelte/store').Writable<any
|
|
76
|
+
* @type {import('svelte/store').Writable<Record<string, any>>}
|
|
72
77
|
*/
|
|
73
78
|
let style = writable({
|
|
74
79
|
inset: undefined,
|
|
@@ -77,20 +82,21 @@
|
|
|
77
82
|
height: undefined,
|
|
78
83
|
});
|
|
79
84
|
|
|
85
|
+
/**
|
|
86
|
+
* @type {{ style: import('svelte/store').Writable<Record<string, any>>, open:
|
|
87
|
+
* import('svelte/store').Writable<boolean>, checkPosition: () => void } | undefined}
|
|
88
|
+
*/
|
|
89
|
+
let popupInstance = undefined;
|
|
90
|
+
let hoveredTimeout = 0;
|
|
91
|
+
|
|
80
92
|
/**
|
|
81
93
|
* Initialize the popup.
|
|
82
94
|
*/
|
|
83
95
|
const init = () => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
({ style, open: openStore } = activatePopup(
|
|
87
|
-
anchor,
|
|
88
|
-
dialogElement,
|
|
89
|
-
position,
|
|
90
|
-
positionBaseElement,
|
|
91
|
-
));
|
|
96
|
+
popupInstance = activatePopup(anchor, dialogElement, position, positionBaseElement);
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
style = popupInstance.style;
|
|
99
|
+
popupInstance.open.subscribe((_open) => {
|
|
94
100
|
open = _open;
|
|
95
101
|
});
|
|
96
102
|
|
|
@@ -98,12 +104,25 @@
|
|
|
98
104
|
initialized = true;
|
|
99
105
|
};
|
|
100
106
|
|
|
107
|
+
$effect(() => {
|
|
108
|
+
if (parentDialogElement && !dialogElement && content) {
|
|
109
|
+
dialogElement = parentDialogElement;
|
|
110
|
+
dialogElement.append(content);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
101
114
|
$effect(() => {
|
|
102
115
|
if (anchor && dialogElement && !initialized) {
|
|
103
116
|
init();
|
|
104
117
|
}
|
|
105
118
|
});
|
|
106
119
|
|
|
120
|
+
$effect(() => {
|
|
121
|
+
if (parentDialogElement && open) {
|
|
122
|
+
popupInstance?.checkPosition();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
107
126
|
const touch = $derived(touchOptimized && touchEnabled);
|
|
108
127
|
|
|
109
128
|
onMount(() => {
|
|
@@ -111,38 +130,10 @@
|
|
|
111
130
|
});
|
|
112
131
|
</script>
|
|
113
132
|
|
|
114
|
-
|
|
115
|
-
{...restProps}
|
|
116
|
-
bind:dialog={dialogElement}
|
|
117
|
-
role="none"
|
|
118
|
-
class="popup"
|
|
119
|
-
bind:open
|
|
120
|
-
showBackdrop={showBackdrop ?? touch}
|
|
121
|
-
lightDismiss={true}
|
|
122
|
-
keepContent={true}
|
|
123
|
-
onOpen={async (event) => {
|
|
124
|
-
onOpen?.(event);
|
|
125
|
-
|
|
126
|
-
await sleep(100);
|
|
127
|
-
|
|
128
|
-
if (!content) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const target = /** @type {HTMLElement} */ (
|
|
133
|
-
content.querySelector('[tabindex]:not([aria-disabled="true"])')
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
if (target) {
|
|
137
|
-
target.focus();
|
|
138
|
-
} else {
|
|
139
|
-
content.tabIndex = -1;
|
|
140
|
-
content.focus();
|
|
141
|
-
}
|
|
142
|
-
}}
|
|
143
|
-
>
|
|
133
|
+
{#snippet contentWrapper()}
|
|
144
134
|
<div
|
|
145
135
|
bind:this={content}
|
|
136
|
+
hidden={!open}
|
|
146
137
|
role="none"
|
|
147
138
|
class="content {className} {contentType}"
|
|
148
139
|
class:touch
|
|
@@ -152,10 +143,63 @@
|
|
|
152
143
|
style:max-width={$style.maxWidth}
|
|
153
144
|
style:max-height={$style.height}
|
|
154
145
|
style:visibility={$style.inset ? undefined : 'hidden'}
|
|
146
|
+
onmouseenter={() => {
|
|
147
|
+
hovered = true;
|
|
148
|
+
|
|
149
|
+
if (parentDialogElement) {
|
|
150
|
+
window.clearTimeout(hoveredTimeout);
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
onmouseleave={() => {
|
|
154
|
+
hovered = false;
|
|
155
|
+
|
|
156
|
+
if (parentDialogElement) {
|
|
157
|
+
hoveredTimeout = window.setTimeout(() => {
|
|
158
|
+
open = false;
|
|
159
|
+
}, 200);
|
|
160
|
+
}
|
|
161
|
+
}}
|
|
155
162
|
>
|
|
156
163
|
{@render children?.()}
|
|
157
164
|
</div>
|
|
158
|
-
|
|
165
|
+
{/snippet}
|
|
166
|
+
|
|
167
|
+
{#if parentDialogElement}
|
|
168
|
+
{@render contentWrapper()}
|
|
169
|
+
{:else}
|
|
170
|
+
<Modal
|
|
171
|
+
{...restProps}
|
|
172
|
+
bind:dialog={dialogElement}
|
|
173
|
+
role="none"
|
|
174
|
+
class="popup"
|
|
175
|
+
bind:open
|
|
176
|
+
showBackdrop={showBackdrop ?? touch}
|
|
177
|
+
lightDismiss={true}
|
|
178
|
+
keepContent={true}
|
|
179
|
+
onOpen={async (event) => {
|
|
180
|
+
onOpen?.(event);
|
|
181
|
+
|
|
182
|
+
await sleep(100);
|
|
183
|
+
|
|
184
|
+
if (!content) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const target = /** @type {HTMLElement} */ (
|
|
189
|
+
content.querySelector('[tabindex]:not([aria-disabled="true"])')
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (target) {
|
|
193
|
+
target.focus();
|
|
194
|
+
} else {
|
|
195
|
+
content.tabIndex = -1;
|
|
196
|
+
content.focus();
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
{@render contentWrapper()}
|
|
201
|
+
</Modal>
|
|
202
|
+
{/if}
|
|
159
203
|
|
|
160
204
|
<style>.content {
|
|
161
205
|
position: absolute;
|
|
@@ -13,6 +13,10 @@ declare const Popup: import("svelte").Component<import("../../typedefs").ModalPr
|
|
|
13
13
|
* - Whether to open the popup.
|
|
14
14
|
*/
|
|
15
15
|
open?: boolean | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* - Whether the content is hovered.
|
|
18
|
+
*/
|
|
19
|
+
hovered?: boolean | undefined;
|
|
16
20
|
/**
|
|
17
21
|
* - A reference to the anchor element that opens the popup.
|
|
18
22
|
* Typically a `<button>`.
|
|
@@ -27,10 +31,15 @@ declare const Popup: import("svelte").Component<import("../../typedefs").ModalPr
|
|
|
27
31
|
*/
|
|
28
32
|
position?: import("../../typedefs").PopupPosition | undefined;
|
|
29
33
|
/**
|
|
30
|
-
* - The base element of
|
|
31
|
-
*
|
|
34
|
+
* - The base element of {@link position}. If
|
|
35
|
+
* omitted, this will be {@link anchor}.
|
|
32
36
|
*/
|
|
33
37
|
positionBaseElement?: HTMLElement | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* - A reference to a dialog element that is
|
|
40
|
+
* already displayed. This should be provided for a nested popup.
|
|
41
|
+
*/
|
|
42
|
+
parentDialogElement?: HTMLDialogElement | undefined;
|
|
34
43
|
/**
|
|
35
44
|
* - Whether to show the popup at the center of the screen on
|
|
36
45
|
* mobile/tablet and ignore the defined dropdown `position`.
|
|
@@ -48,7 +57,7 @@ declare const Popup: import("svelte").Component<import("../../typedefs").ModalPr
|
|
|
48
57
|
* - Custom `Open` event handler.
|
|
49
58
|
*/
|
|
50
59
|
onOpen?: ((event: CustomEvent) => void) | undefined;
|
|
51
|
-
} & Record<string, any>, {}, "open" | "content">;
|
|
60
|
+
} & Record<string, any>, {}, "open" | "hovered" | "content">;
|
|
52
61
|
type Props = {
|
|
53
62
|
/**
|
|
54
63
|
* - The `class` attribute on the content element.
|
|
@@ -58,6 +67,10 @@ type Props = {
|
|
|
58
67
|
* - Whether to open the popup.
|
|
59
68
|
*/
|
|
60
69
|
open?: boolean | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* - Whether the content is hovered.
|
|
72
|
+
*/
|
|
73
|
+
hovered?: boolean | undefined;
|
|
61
74
|
/**
|
|
62
75
|
* - A reference to the anchor element that opens the popup.
|
|
63
76
|
* Typically a `<button>`.
|
|
@@ -72,10 +85,15 @@ type Props = {
|
|
|
72
85
|
*/
|
|
73
86
|
position?: import("../../typedefs").PopupPosition | undefined;
|
|
74
87
|
/**
|
|
75
|
-
* - The base element of
|
|
76
|
-
*
|
|
88
|
+
* - The base element of {@link position}. If
|
|
89
|
+
* omitted, this will be {@link anchor}.
|
|
77
90
|
*/
|
|
78
91
|
positionBaseElement?: HTMLElement | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* - A reference to a dialog element that is
|
|
94
|
+
* already displayed. This should be provided for a nested popup.
|
|
95
|
+
*/
|
|
96
|
+
parentDialogElement?: HTMLDialogElement | undefined;
|
|
79
97
|
/**
|
|
80
98
|
* - Whether to show the popup at the center of the screen on
|
|
81
99
|
* mobile/tablet and ignore the defined dropdown `position`.
|
|
@@ -89,11 +89,9 @@ class Popup {
|
|
|
89
89
|
? `${Math.round(intersectionRect.left)}px`
|
|
90
90
|
: 'auto';
|
|
91
91
|
|
|
92
|
-
const anchorPopup = /** @type {HTMLElement} */ (this.anchorElement.closest('.popup'));
|
|
93
|
-
|
|
94
92
|
const style = {
|
|
95
93
|
inset: [top, right, bottom, left].join(' '),
|
|
96
|
-
zIndex:
|
|
94
|
+
zIndex: 1000,
|
|
97
95
|
minWidth: `${Math.round(intersectionRect.width)}px`,
|
|
98
96
|
maxWidth: position.endsWith('-left')
|
|
99
97
|
? `${Math.round(rootBounds.width - intersectionRect.left - 8)}px`
|
package/dist/typedefs.d.ts
CHANGED
|
@@ -467,6 +467,18 @@ export type KeyboardEventHandlers = {
|
|
|
467
467
|
onkeypress?: ((event: KeyboardEvent) => void) | undefined;
|
|
468
468
|
};
|
|
469
469
|
export type MouseEventHandlers = {
|
|
470
|
+
/**
|
|
471
|
+
* - `mouseenter` event handler.
|
|
472
|
+
*/
|
|
473
|
+
onmouseenter?: ((event: MouseEvent) => void) | undefined;
|
|
474
|
+
/**
|
|
475
|
+
* - `mouseleave` event handler.
|
|
476
|
+
*/
|
|
477
|
+
onmouseleave?: ((event: MouseEvent) => void) | undefined;
|
|
478
|
+
/**
|
|
479
|
+
* - `mouseover` event handler.
|
|
480
|
+
*/
|
|
481
|
+
onmouseover?: ((event: MouseEvent) => void) | undefined;
|
|
470
482
|
/**
|
|
471
483
|
* - `mousedown` event handler.
|
|
472
484
|
*/
|
package/dist/typedefs.js
CHANGED
|
@@ -164,6 +164,9 @@
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* @typedef {object} MouseEventHandlers
|
|
167
|
+
* @property {(event: MouseEvent) => void} [onmouseenter] - `mouseenter` event handler.
|
|
168
|
+
* @property {(event: MouseEvent) => void} [onmouseleave] - `mouseleave` event handler.
|
|
169
|
+
* @property {(event: MouseEvent) => void} [onmouseover] - `mouseover` event handler.
|
|
167
170
|
* @property {(event: MouseEvent) => void} [onmousedown] - `mousedown` event handler.
|
|
168
171
|
* @property {(event: MouseEvent) => void} [onmouseup] - `mouseup` event handler.
|
|
169
172
|
* @property {(event: MouseEvent) => void} [onclick] - `click` event handler.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sveltia/ui",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@playwright/test": "^1.51.0",
|
|
50
50
|
"@sveltejs/adapter-auto": "^4.0.0",
|
|
51
|
-
"@sveltejs/kit": "^2.
|
|
51
|
+
"@sveltejs/kit": "^2.19.0",
|
|
52
52
|
"@sveltejs/package": "^2.3.10",
|
|
53
53
|
"@sveltejs/vite-plugin-svelte": "5.0.3",
|
|
54
54
|
"cspell": "^8.17.5",
|
|
55
55
|
"eslint": "^8.57.1",
|
|
56
56
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
57
|
-
"eslint-config-prettier": "^10.
|
|
57
|
+
"eslint-config-prettier": "^10.1.1",
|
|
58
58
|
"eslint-plugin-import": "^2.31.0",
|
|
59
59
|
"eslint-plugin-jsdoc": "^50.6.3",
|
|
60
60
|
"eslint-plugin-svelte": "^2.46.1",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"stylelint": "^16.15.0",
|
|
67
67
|
"stylelint-config-recommended-scss": "^14.1.0",
|
|
68
68
|
"stylelint-scss": "^6.11.1",
|
|
69
|
-
"svelte": "5.22.
|
|
69
|
+
"svelte": "5.22.6",
|
|
70
70
|
"svelte-check": "^4.1.5",
|
|
71
71
|
"svelte-i18n": "^4.0.1",
|
|
72
72
|
"svelte-preprocess": "^6.0.3",
|