lapikit 0.7.9 → 0.8.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.
- package/dist/components/btn/btn.svelte +25 -4
- package/dist/components/btn/btn.types.d.ts +3 -0
- package/dist/components/card/card.svelte +6 -1
- package/dist/components/chip/chip.svelte +25 -4
- package/dist/components/chip/chip.types.d.ts +3 -0
- package/dist/components/dropdown/dropdown.svelte +120 -41
- package/dist/components/dropdown/dropdown.svelte.d.ts +1 -1
- package/dist/components/dropdown/dropdown.types.d.ts +19 -8
- package/dist/components/field/field.svelte +22 -0
- package/dist/components/field/field.types.d.ts +1 -0
- package/dist/components/icon/icon.svelte +11 -3
- package/dist/components/icon/icon.types.d.ts +3 -0
- package/dist/components/index.d.ts +3 -3
- package/dist/components/list/list.context.d.ts +7 -0
- package/dist/components/list/list.context.js +4 -0
- package/dist/components/list/list.svelte +18 -4
- package/dist/components/list/modules/list-item.svelte +68 -33
- package/dist/components/popover/popover.svelte +57 -10
- package/dist/components/popover/popover.types.d.ts +10 -4
- package/dist/components/textfield/textfield.svelte +5 -12
- package/dist/components/textfield/textfield.types.d.ts +1 -5
- package/dist/components/tooltip/tooltip-action.svelte.d.ts +9 -0
- package/dist/components/tooltip/tooltip-action.svelte.js +159 -0
- package/dist/components/tooltip/tooltip.svelte +71 -56
- package/dist/components/tooltip/tooltip.svelte.d.ts +4 -2
- package/dist/components/tooltip/tooltip.types.d.ts +28 -2
- package/dist/labs/sheet/sheet.svelte +6 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useComponentAttrs } from '../../utils';
|
|
3
|
+
import { setListContext } from './list.context.js';
|
|
3
4
|
import type { ListProps } from './list.types.js';
|
|
4
5
|
|
|
5
6
|
let {
|
|
@@ -22,6 +23,16 @@
|
|
|
22
23
|
let attrs = $derived(
|
|
23
24
|
useComponentAttrs('kit-list', { className, sClass, styleAttr, sStyle, color }, rest)
|
|
24
25
|
);
|
|
26
|
+
let role = $derived(nav ? 'navigation' : is === 'ul' ? undefined : 'list');
|
|
27
|
+
let customRole = $derived(attrs.rest.role as string | undefined);
|
|
28
|
+
|
|
29
|
+
setListContext({
|
|
30
|
+
get item() {
|
|
31
|
+
if (customRole) return customRole === 'list' ? 'listitem' : undefined;
|
|
32
|
+
if (is === 'ul') return 'li';
|
|
33
|
+
return role === 'list' ? 'listitem' : undefined;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
25
36
|
</script>
|
|
26
37
|
|
|
27
38
|
<svelte:element
|
|
@@ -34,7 +45,7 @@
|
|
|
34
45
|
data-density={density}
|
|
35
46
|
data-rounded={rounded}
|
|
36
47
|
data-nav={nav}
|
|
37
|
-
role
|
|
48
|
+
{role}
|
|
38
49
|
{...attrs.rest}
|
|
39
50
|
>
|
|
40
51
|
{@render children?.()}
|
|
@@ -61,15 +72,18 @@
|
|
|
61
72
|
.kit-list[data-nav=true] :global(.kit-list-item) {
|
|
62
73
|
border-radius: var(--kit-list-item-radius);
|
|
63
74
|
}
|
|
64
|
-
.kit-list:not([data-nav=true]) :global(.kit-list-item:first-child)
|
|
75
|
+
.kit-list:not([data-nav=true]) :global(.kit-list-item:first-child:not(.kit-list-item-wrapper > *)),
|
|
76
|
+
.kit-list:not([data-nav=true]) :global(.kit-list-item-wrapper:first-child > .kit-list-item) {
|
|
65
77
|
border-start-start-radius: var(--kit-list-item-radius);
|
|
66
78
|
border-start-end-radius: var(--kit-list-item-radius);
|
|
67
79
|
}
|
|
68
|
-
.kit-list:not([data-nav=true]) :global(.kit-list-item:last-child)
|
|
80
|
+
.kit-list:not([data-nav=true]) :global(.kit-list-item:last-child:not(.kit-list-item-wrapper > *)),
|
|
81
|
+
.kit-list:not([data-nav=true]) :global(.kit-list-item-wrapper:last-child > .kit-list-item) {
|
|
69
82
|
border-end-start-radius: var(--kit-list-item-radius);
|
|
70
83
|
border-end-end-radius: var(--kit-list-item-radius);
|
|
71
84
|
}
|
|
72
|
-
.kit-list:not([data-nav=true]) :global(.kit-list-item:not(:first-child, :last-child))
|
|
85
|
+
.kit-list:not([data-nav=true]) :global(.kit-list-item:not(:first-child, :last-child)),
|
|
86
|
+
.kit-list:not([data-nav=true]) :global(.kit-list-item-wrapper:not(:first-child, :last-child) > .kit-list-item) {
|
|
73
87
|
--kit-list-item-radius: 0;
|
|
74
88
|
}
|
|
75
89
|
.kit-list:is([data-variant=outline], [data-variant=surface]) :global(.kit-list-item::before) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ripple } from '../../../animations';
|
|
3
3
|
import { useComponentAttrs, useIsInteractive } from '../../../utils';
|
|
4
|
+
import { getListContext } from '../list.context.js';
|
|
4
5
|
import type { ListItemProps } from '../list.types.js';
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
8
|
ref = $bindable(),
|
|
8
|
-
is
|
|
9
|
+
is,
|
|
9
10
|
children = undefined,
|
|
10
11
|
class: className,
|
|
11
12
|
style: styleAttr,
|
|
@@ -29,42 +30,67 @@
|
|
|
29
30
|
rest
|
|
30
31
|
)
|
|
31
32
|
);
|
|
33
|
+
const list = getListContext();
|
|
34
|
+
|
|
32
35
|
let tag = $derived(
|
|
33
|
-
href
|
|
36
|
+
href
|
|
37
|
+
? 'a'
|
|
38
|
+
: (is ?? 'div') === 'div' && typeof attrs.rest.onclick === 'function'
|
|
39
|
+
? 'button'
|
|
40
|
+
: (is ?? (list?.item === 'li' ? 'li' : 'div'))
|
|
34
41
|
);
|
|
35
42
|
let isInteractive = $derived(interactive ?? useIsInteractive(rest, tag, ['a', 'button']));
|
|
43
|
+
let wrapperTag = $derived.by(() => {
|
|
44
|
+
if (list?.item === 'li') return tag === 'li' ? undefined : 'li';
|
|
45
|
+
if (list?.item === 'listitem') return tag === 'a' || tag === 'button' ? 'div' : undefined;
|
|
46
|
+
return undefined;
|
|
47
|
+
});
|
|
36
48
|
</script>
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
{#snippet item()}
|
|
51
|
+
<svelte:element
|
|
52
|
+
this={tag}
|
|
53
|
+
bind:this={ref}
|
|
54
|
+
class={attrs.class}
|
|
55
|
+
style={attrs.style}
|
|
56
|
+
href={href && !disabled ? href : undefined}
|
|
57
|
+
type={tag === 'button' ? 'button' : undefined}
|
|
58
|
+
disabled={tag === 'button' ? disabled : undefined}
|
|
59
|
+
tabindex={href && disabled ? -1 : undefined}
|
|
60
|
+
role={tag === 'div' && list?.item === 'listitem' ? 'listitem' : undefined}
|
|
61
|
+
data-active={active}
|
|
62
|
+
data-disabled={disabled}
|
|
63
|
+
data-interactive={isInteractive}
|
|
64
|
+
aria-disabled={disabled || undefined}
|
|
65
|
+
use:ripple={{
|
|
66
|
+
component: 'list-item',
|
|
67
|
+
disabled: noRipple || disabled || !isInteractive
|
|
68
|
+
}}
|
|
69
|
+
{...attrs.rest}
|
|
70
|
+
>
|
|
71
|
+
{#if prepend}
|
|
72
|
+
<span class="kit-list-item__prepend">{@render prepend()}</span>
|
|
73
|
+
{/if}
|
|
74
|
+
|
|
75
|
+
<span class="kit-list-item__content">{@render children?.()}</span>
|
|
76
|
+
|
|
77
|
+
{#if append}
|
|
78
|
+
<span class="kit-list-item__append">{@render append()}</span>
|
|
79
|
+
{/if}
|
|
80
|
+
</svelte:element>
|
|
81
|
+
{/snippet}
|
|
82
|
+
|
|
83
|
+
{#if wrapperTag}
|
|
84
|
+
<svelte:element
|
|
85
|
+
this={wrapperTag}
|
|
86
|
+
class="kit-list-item-wrapper"
|
|
87
|
+
role={wrapperTag === 'div' ? 'listitem' : undefined}
|
|
88
|
+
>
|
|
89
|
+
{@render item()}
|
|
90
|
+
</svelte:element>
|
|
91
|
+
{:else}
|
|
92
|
+
{@render item()}
|
|
93
|
+
{/if}
|
|
68
94
|
|
|
69
95
|
<style>:where(.kit-list-item) {
|
|
70
96
|
--kit-list-item-color: initial;
|
|
@@ -96,6 +122,10 @@
|
|
|
96
122
|
--kit-list-item-on-color: var(--kit-list-on-color);
|
|
97
123
|
}
|
|
98
124
|
|
|
125
|
+
.kit-list-item-wrapper {
|
|
126
|
+
display: contents;
|
|
127
|
+
}
|
|
128
|
+
|
|
99
129
|
.kit-list-item {
|
|
100
130
|
position: relative;
|
|
101
131
|
display: flex;
|
|
@@ -166,7 +196,7 @@ button.kit-list-item {
|
|
|
166
196
|
color: var(--kit-list-item-pressed-fg, var(--kit-list-item-fg));
|
|
167
197
|
}
|
|
168
198
|
|
|
169
|
-
.kit-list-item:not([data-disabled=true]):focus-visible {
|
|
199
|
+
.kit-list-item:is(:not([data-disabled=true]), [data-disabled-focusable=true]):focus-visible {
|
|
170
200
|
outline: 2px solid var(--kit-color-focus);
|
|
171
201
|
outline-offset: 2px;
|
|
172
202
|
}
|
|
@@ -175,4 +205,9 @@ button.kit-list-item {
|
|
|
175
205
|
opacity: var(--kit-disabled-opacity, 0.55);
|
|
176
206
|
pointer-events: none;
|
|
177
207
|
user-select: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.kit-list-item[data-disabled-focusable=true] {
|
|
211
|
+
pointer-events: auto;
|
|
212
|
+
cursor: not-allowed;
|
|
178
213
|
}</style>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { DEV } from 'esm-env';
|
|
2
3
|
import { clickOutside, floating, useComponentAttrs, useElevation } from '../../utils';
|
|
3
|
-
import type {
|
|
4
|
+
import type { PopoverProps, PopoverTriggerProps } from './popover.types.js';
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
7
|
ref = $bindable(),
|
|
7
8
|
open = $bindable(false),
|
|
8
9
|
children,
|
|
10
|
+
trigger,
|
|
9
11
|
activator,
|
|
10
12
|
rounded = 'md',
|
|
11
13
|
position = 'bottom',
|
|
@@ -19,31 +21,73 @@
|
|
|
19
21
|
...rest
|
|
20
22
|
}: PopoverProps = $props();
|
|
21
23
|
|
|
24
|
+
$effect(() => {
|
|
25
|
+
if (DEV && activator)
|
|
26
|
+
console.warn('[lapikit] kit:popover: activator is removed, use the trigger snippet');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const popoverId = $props.id();
|
|
30
|
+
|
|
22
31
|
let attrs = $derived(
|
|
23
32
|
useComponentAttrs('kit-popover-content', { className, sClass, styleAttr, sStyle, color }, rest)
|
|
24
33
|
);
|
|
25
34
|
let elevationState = $derived(useElevation(elevation));
|
|
26
35
|
|
|
27
36
|
let contentRef = $state<HTMLElement | null>(null);
|
|
28
|
-
let
|
|
37
|
+
let triggerRef = $state<HTMLElement | null>(null);
|
|
29
38
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
const focusableSelector =
|
|
40
|
+
'a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
|
|
41
|
+
|
|
42
|
+
const close = (restoreFocus = false) => {
|
|
43
|
+
open = false;
|
|
44
|
+
if (restoreFocus) triggerRef?.focus();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let triggerProps: PopoverTriggerProps = $derived({
|
|
48
|
+
'aria-haspopup': 'dialog',
|
|
49
|
+
'aria-expanded': open,
|
|
50
|
+
'aria-controls': popoverId,
|
|
51
|
+
onclick: (event: MouseEvent) => {
|
|
52
|
+
triggerRef = event.currentTarget as HTMLElement;
|
|
34
53
|
open = !open;
|
|
35
54
|
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// focus leaves the popover for another element of the page
|
|
58
|
+
const onFocusOut = (event: FocusEvent) => {
|
|
59
|
+
const next = event.relatedTarget as Node | null;
|
|
60
|
+
if (!next || contentRef?.contains(next) || triggerRef?.contains(next)) return;
|
|
61
|
+
close();
|
|
36
62
|
};
|
|
37
63
|
|
|
38
64
|
$effect(() => {
|
|
39
65
|
ref = contentRef;
|
|
40
66
|
});
|
|
41
67
|
|
|
68
|
+
$effect(() => {
|
|
69
|
+
if (open && !triggerRef?.isConnected) {
|
|
70
|
+
triggerRef = document.querySelector<HTMLElement>(
|
|
71
|
+
`[aria-controls="${CSS.escape(popoverId)}"]`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
$effect(() => {
|
|
77
|
+
if (!open || !contentRef) return;
|
|
78
|
+
const content = contentRef;
|
|
79
|
+
const frame = requestAnimationFrame(() => {
|
|
80
|
+
const first = content.querySelector<HTMLElement>(focusableSelector);
|
|
81
|
+
(first ?? content).focus();
|
|
82
|
+
});
|
|
83
|
+
return () => cancelAnimationFrame(frame);
|
|
84
|
+
});
|
|
85
|
+
|
|
42
86
|
$effect(() => {
|
|
43
87
|
if (!open) return;
|
|
44
88
|
|
|
45
89
|
const onKeydown = (event: KeyboardEvent) => {
|
|
46
|
-
if (event.key === 'Escape')
|
|
90
|
+
if (event.key === 'Escape') close(true);
|
|
47
91
|
};
|
|
48
92
|
|
|
49
93
|
document.addEventListener('keydown', onKeydown);
|
|
@@ -51,22 +95,25 @@
|
|
|
51
95
|
});
|
|
52
96
|
</script>
|
|
53
97
|
|
|
54
|
-
{@render
|
|
98
|
+
{@render trigger?.(triggerProps)}
|
|
55
99
|
|
|
56
100
|
{#if open}
|
|
57
101
|
<div
|
|
58
102
|
bind:this={contentRef}
|
|
103
|
+
id={popoverId}
|
|
59
104
|
popover="manual"
|
|
60
105
|
class={attrs.class}
|
|
61
106
|
style={attrs.style}
|
|
62
107
|
role="dialog"
|
|
108
|
+
tabindex="-1"
|
|
63
109
|
data-rounded={rounded}
|
|
64
110
|
data-density={density}
|
|
65
111
|
data-elevation={elevationState.base}
|
|
66
112
|
data-elevation-hover={elevationState.hover}
|
|
67
113
|
data-elevation-active={elevationState.active}
|
|
68
|
-
|
|
69
|
-
use:
|
|
114
|
+
onfocusout={onFocusOut}
|
|
115
|
+
use:floating={{ anchor: triggerRef, side: position, offset: 6 }}
|
|
116
|
+
use:clickOutside={{ exclude: [contentRef, triggerRef], onClose: () => close() }}
|
|
70
117
|
{...attrs.rest}
|
|
71
118
|
>
|
|
72
119
|
{@render children?.()}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type { ColorProps, Component, ElevationProps, RoundedType } from '../../@types';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/** The attributes to spread on the element that opens the popover */
|
|
4
|
+
export interface PopoverTriggerProps {
|
|
5
|
+
'aria-haspopup': 'dialog';
|
|
6
|
+
'aria-expanded': boolean;
|
|
7
|
+
'aria-controls': string;
|
|
8
|
+
onclick: (event: MouseEvent) => void;
|
|
9
|
+
}
|
|
6
10
|
export interface PopoverProps extends Component {
|
|
7
11
|
ref?: HTMLElement | null;
|
|
8
12
|
open?: boolean;
|
|
9
13
|
rounded?: RoundedType | 'full';
|
|
10
14
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
11
15
|
color?: ColorProps;
|
|
12
|
-
|
|
16
|
+
trigger?: Snippet<[PopoverTriggerProps]>;
|
|
13
17
|
elevation?: ElevationProps;
|
|
14
18
|
density?: 'dense' | 'compact' | 'default' | 'comfortable' | 'spacious';
|
|
19
|
+
/** removed in 0.8.0: use the trigger snippet */
|
|
20
|
+
activator?: never;
|
|
15
21
|
}
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
ref = $bindable(),
|
|
7
7
|
value = $bindable(),
|
|
8
8
|
type = 'text',
|
|
9
|
-
|
|
10
|
-
max,
|
|
9
|
+
maxlength,
|
|
11
10
|
counter = false,
|
|
12
11
|
hideSpinButtons = false,
|
|
13
12
|
class: className,
|
|
@@ -21,6 +20,7 @@
|
|
|
21
20
|
rounded,
|
|
22
21
|
color,
|
|
23
22
|
elevation,
|
|
23
|
+
label,
|
|
24
24
|
disabled = false,
|
|
25
25
|
readonly = false,
|
|
26
26
|
error = false,
|
|
@@ -44,13 +44,6 @@
|
|
|
44
44
|
let inputRef = $state<HTMLInputElement | null>(null);
|
|
45
45
|
let text = $derived(`${value ?? ''}`);
|
|
46
46
|
|
|
47
|
-
$effect(() => {
|
|
48
|
-
if (typeof max === 'number' && max > 0 && text.length > max) {
|
|
49
|
-
const truncated = text.slice(0, max);
|
|
50
|
-
value = typeof value === 'number' ? Number(truncated) || 0 : truncated;
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
47
|
function clear() {
|
|
55
48
|
value = '';
|
|
56
49
|
inputRef?.focus();
|
|
@@ -70,6 +63,7 @@
|
|
|
70
63
|
{rounded}
|
|
71
64
|
{color}
|
|
72
65
|
{elevation}
|
|
66
|
+
{label}
|
|
73
67
|
{disabled}
|
|
74
68
|
{readonly}
|
|
75
69
|
{error}
|
|
@@ -88,7 +82,7 @@
|
|
|
88
82
|
{prependInner}
|
|
89
83
|
{appendInner}
|
|
90
84
|
dirty={text.length > 0}
|
|
91
|
-
counter={counter ? `${text.length}${
|
|
85
|
+
counter={counter ? `${text.length}${maxlength ? `/${maxlength}` : ''}` : undefined}
|
|
92
86
|
onclear={clear}
|
|
93
87
|
data-hide-spin-buttons={type === 'number' && hideSpinButtons}
|
|
94
88
|
>
|
|
@@ -101,8 +95,7 @@
|
|
|
101
95
|
class="kit-textfield__input"
|
|
102
96
|
{type}
|
|
103
97
|
size="1"
|
|
104
|
-
|
|
105
|
-
maxlength={max}
|
|
98
|
+
{maxlength}
|
|
106
99
|
/>
|
|
107
100
|
{/snippet}
|
|
108
101
|
</Field>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
2
|
import type { ClassNameType, KitClassNameType, KitStylePropertiesType, StylePropertiesType } from '../../@types';
|
|
3
3
|
import type { FieldBaseProps } from '../field/field.types';
|
|
4
|
-
type FieldOwnedAttributes = keyof FieldBaseProps | 'size' | 'type' | 'value'
|
|
4
|
+
type FieldOwnedAttributes = keyof FieldBaseProps | 'size' | 'type' | 'value';
|
|
5
5
|
export interface TextfieldProps extends FieldBaseProps, Omit<HTMLInputAttributes, FieldOwnedAttributes | 'class' | 'style' | 'children'> {
|
|
6
6
|
ref?: HTMLElement | null;
|
|
7
7
|
class?: ClassNameType;
|
|
@@ -10,10 +10,6 @@ export interface TextfieldProps extends FieldBaseProps, Omit<HTMLInputAttributes
|
|
|
10
10
|
's-style'?: KitStylePropertiesType;
|
|
11
11
|
value?: string | number;
|
|
12
12
|
type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
|
|
13
|
-
/** minimal length of the value */
|
|
14
|
-
min?: number;
|
|
15
|
-
/** maximal length of the value, also used by the counter */
|
|
16
|
-
max?: number;
|
|
17
13
|
counter?: boolean;
|
|
18
14
|
hideSpinButtons?: boolean;
|
|
19
15
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TooltipValue } from './tooltip.types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* tooltip - Shows the shared tooltip on hover and keyboard focus of the node.
|
|
4
|
+
* A node without an accessible name gets the label as aria-label, the others get it as a description.
|
|
5
|
+
*/
|
|
6
|
+
export declare function tooltip(node: HTMLElement, value: TooltipValue): {
|
|
7
|
+
update(next: TooltipValue): void;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { mount } from 'svelte';
|
|
2
|
+
const sharedId = 'kit-tooltip-shared';
|
|
3
|
+
// one tooltip for every kit:btn, kit:chip and kit:icon, mounted on the first hover or focus
|
|
4
|
+
const shared = $state({
|
|
5
|
+
id: sharedId,
|
|
6
|
+
open: false,
|
|
7
|
+
label: '',
|
|
8
|
+
location: 'bottom',
|
|
9
|
+
variant: undefined,
|
|
10
|
+
density: 'default',
|
|
11
|
+
rounded: 'xs',
|
|
12
|
+
delayDuration: 850,
|
|
13
|
+
closeDelay: 100
|
|
14
|
+
});
|
|
15
|
+
let loading = null;
|
|
16
|
+
let owner = null;
|
|
17
|
+
function load() {
|
|
18
|
+
loading ??= import('./tooltip.svelte').then(({ default: Tooltip }) => {
|
|
19
|
+
const instance = mount(Tooltip, { target: document.body, props: shared });
|
|
20
|
+
return instance.triggerHandlers;
|
|
21
|
+
});
|
|
22
|
+
return loading;
|
|
23
|
+
}
|
|
24
|
+
function normalize(value) {
|
|
25
|
+
if (!value)
|
|
26
|
+
return null;
|
|
27
|
+
return typeof value === 'string' ? { label: value } : value.label ? value : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* tooltip - Shows the shared tooltip on hover and keyboard focus of the node.
|
|
31
|
+
* A node without an accessible name gets the label as aria-label, the others get it as a description.
|
|
32
|
+
*/
|
|
33
|
+
export function tooltip(node, value) {
|
|
34
|
+
let options = normalize(value);
|
|
35
|
+
let named = false;
|
|
36
|
+
let hovered = false;
|
|
37
|
+
let listening = false;
|
|
38
|
+
function syncName() {
|
|
39
|
+
if (named)
|
|
40
|
+
node.removeAttribute('aria-label');
|
|
41
|
+
named = false;
|
|
42
|
+
if (!options)
|
|
43
|
+
return;
|
|
44
|
+
const hasName = node.hasAttribute('aria-label') ||
|
|
45
|
+
node.hasAttribute('aria-labelledby') ||
|
|
46
|
+
!!node.textContent?.trim();
|
|
47
|
+
if (hasName)
|
|
48
|
+
return;
|
|
49
|
+
node.setAttribute('aria-label', options.label);
|
|
50
|
+
named = true;
|
|
51
|
+
}
|
|
52
|
+
function describe(on) {
|
|
53
|
+
if (named)
|
|
54
|
+
return;
|
|
55
|
+
const ids = (node.getAttribute('aria-describedby') ?? '').split(' ').filter(Boolean);
|
|
56
|
+
const others = ids.filter((id) => id !== sharedId);
|
|
57
|
+
const next = on ? [...others, sharedId] : others;
|
|
58
|
+
if (next.length)
|
|
59
|
+
node.setAttribute('aria-describedby', next.join(' '));
|
|
60
|
+
else
|
|
61
|
+
node.removeAttribute('aria-describedby');
|
|
62
|
+
}
|
|
63
|
+
function activate() {
|
|
64
|
+
if (!options)
|
|
65
|
+
return;
|
|
66
|
+
owner = node;
|
|
67
|
+
Object.assign(shared, {
|
|
68
|
+
label: options.label,
|
|
69
|
+
location: options.location ?? 'bottom',
|
|
70
|
+
variant: options.variant,
|
|
71
|
+
density: options.density ?? 'default',
|
|
72
|
+
rounded: options.rounded ?? 'xs',
|
|
73
|
+
delayDuration: options.delayDuration ?? 850,
|
|
74
|
+
closeDelay: options.closeDelay ?? 100
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// the handlers read currentTarget and target, gone once the event is dispatched
|
|
78
|
+
const as = (event, target = event.target) => ({ currentTarget: node, target, pointerType: event.pointerType });
|
|
79
|
+
const onPointerEnter = (event) => {
|
|
80
|
+
hovered = true;
|
|
81
|
+
if (event.pointerType === 'touch')
|
|
82
|
+
return;
|
|
83
|
+
const fake = as(event);
|
|
84
|
+
load().then((handlers) => {
|
|
85
|
+
if (!hovered)
|
|
86
|
+
return;
|
|
87
|
+
activate();
|
|
88
|
+
handlers().onpointerenter(fake);
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const onPointerLeave = (event) => {
|
|
92
|
+
hovered = false;
|
|
93
|
+
if (owner === node)
|
|
94
|
+
loading?.then((handlers) => handlers().onpointerleave(event));
|
|
95
|
+
};
|
|
96
|
+
const onPointerDown = (event) => {
|
|
97
|
+
if (owner === node)
|
|
98
|
+
loading?.then((handlers) => handlers().onpointerdown(event));
|
|
99
|
+
};
|
|
100
|
+
const onFocusIn = (event) => {
|
|
101
|
+
if (node.contains(event.relatedTarget))
|
|
102
|
+
return;
|
|
103
|
+
describe(true);
|
|
104
|
+
const fake = as(event);
|
|
105
|
+
load().then((handlers) => {
|
|
106
|
+
if (!node.contains(document.activeElement))
|
|
107
|
+
return;
|
|
108
|
+
activate();
|
|
109
|
+
handlers().onfocus(fake);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const onFocusOut = (event) => {
|
|
113
|
+
if (node.contains(event.relatedTarget))
|
|
114
|
+
return;
|
|
115
|
+
describe(false);
|
|
116
|
+
if (owner === node)
|
|
117
|
+
loading?.then((handlers) => handlers().onblur(event));
|
|
118
|
+
};
|
|
119
|
+
function listen(on) {
|
|
120
|
+
if (on === listening)
|
|
121
|
+
return;
|
|
122
|
+
listening = on;
|
|
123
|
+
const method = on ? 'addEventListener' : 'removeEventListener';
|
|
124
|
+
node[method]('pointerenter', onPointerEnter);
|
|
125
|
+
node[method]('pointerleave', onPointerLeave);
|
|
126
|
+
node[method]('pointerdown', onPointerDown);
|
|
127
|
+
node[method]('focusin', onFocusIn);
|
|
128
|
+
node[method]('focusout', onFocusOut);
|
|
129
|
+
}
|
|
130
|
+
function release() {
|
|
131
|
+
if (owner !== node)
|
|
132
|
+
return;
|
|
133
|
+
owner = null;
|
|
134
|
+
shared.open = false;
|
|
135
|
+
}
|
|
136
|
+
listen(!!options);
|
|
137
|
+
syncName();
|
|
138
|
+
return {
|
|
139
|
+
update(next) {
|
|
140
|
+
options = normalize(next);
|
|
141
|
+
listen(!!options);
|
|
142
|
+
syncName();
|
|
143
|
+
if (!options) {
|
|
144
|
+
describe(false);
|
|
145
|
+
release();
|
|
146
|
+
}
|
|
147
|
+
else if (owner === node) {
|
|
148
|
+
activate();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
destroy() {
|
|
152
|
+
listen(false);
|
|
153
|
+
if (named)
|
|
154
|
+
node.removeAttribute('aria-label');
|
|
155
|
+
describe(false);
|
|
156
|
+
release();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|