lapikit 0.7.9 → 0.8.0
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/modules/list-item.svelte +6 -1
- 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
|
@@ -166,7 +166,7 @@ button.kit-list-item {
|
|
|
166
166
|
color: var(--kit-list-item-pressed-fg, var(--kit-list-item-fg));
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
.kit-list-item:not([data-disabled=true]):focus-visible {
|
|
169
|
+
.kit-list-item:is(:not([data-disabled=true]), [data-disabled-focusable=true]):focus-visible {
|
|
170
170
|
outline: 2px solid var(--kit-color-focus);
|
|
171
171
|
outline-offset: 2px;
|
|
172
172
|
}
|
|
@@ -175,4 +175,9 @@ button.kit-list-item {
|
|
|
175
175
|
opacity: var(--kit-disabled-opacity, 0.55);
|
|
176
176
|
pointer-events: none;
|
|
177
177
|
user-select: none;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.kit-list-item[data-disabled-focusable=true] {
|
|
181
|
+
pointer-events: auto;
|
|
182
|
+
cursor: not-allowed;
|
|
178
183
|
}</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
|
+
}
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy } from 'svelte';
|
|
3
|
+
import { DEV } from 'esm-env';
|
|
3
4
|
import { floating, useComponentAttrs } from '../../utils';
|
|
4
|
-
import type { TooltipProps } from './tooltip.types.js';
|
|
5
|
+
import type { TooltipProps, TooltipTriggerProps } from './tooltip.types.js';
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
8
|
ref = $bindable(),
|
|
9
|
+
id,
|
|
8
10
|
children,
|
|
11
|
+
trigger,
|
|
9
12
|
tooltip,
|
|
10
13
|
open = $bindable(false),
|
|
11
|
-
forceMount = false,
|
|
12
14
|
label,
|
|
15
|
+
asLabel = false,
|
|
13
16
|
rounded = 'xs',
|
|
14
17
|
color,
|
|
15
18
|
location = 'bottom',
|
|
16
19
|
delayDuration = 850,
|
|
20
|
+
closeDelay = 100,
|
|
17
21
|
density = 'default',
|
|
18
22
|
variant,
|
|
19
23
|
disabled = false,
|
|
@@ -25,67 +29,96 @@
|
|
|
25
29
|
...rest
|
|
26
30
|
}: TooltipProps = $props();
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
$effect(() => {
|
|
33
|
+
if (DEV && children)
|
|
34
|
+
console.warn('[lapikit] kit:tooltip: children is removed, use the trigger snippet');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const generatedId = $props.id();
|
|
38
|
+
let tooltipId = $derived(id || generatedId);
|
|
29
39
|
|
|
30
40
|
let attrs = $derived(
|
|
31
41
|
useComponentAttrs('kit-tooltip', { className, sClass, styleAttr, sStyle, color }, rest)
|
|
32
42
|
);
|
|
33
43
|
|
|
34
|
-
let
|
|
44
|
+
let anchor = $state<HTMLElement | null>(null);
|
|
35
45
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
36
46
|
let focused = false;
|
|
37
|
-
let
|
|
47
|
+
let ariaAttribute = $derived(asLabel ? 'aria-labelledby' : 'aria-describedby');
|
|
38
48
|
|
|
39
49
|
const clearTimer = () => {
|
|
40
50
|
if (timer) clearTimeout(timer);
|
|
41
51
|
timer = null;
|
|
42
52
|
};
|
|
43
53
|
|
|
44
|
-
const
|
|
45
|
-
if (disabled) return;
|
|
46
|
-
clearTimer();
|
|
47
|
-
if (open) return;
|
|
48
|
-
timer = setTimeout(() => {
|
|
49
|
-
open = true;
|
|
50
|
-
timer = null;
|
|
51
|
-
}, delayDuration);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const handleFocusOpen = () => {
|
|
55
|
-
if (disabled) return;
|
|
56
|
-
focused = true;
|
|
54
|
+
const show = () => {
|
|
57
55
|
clearTimer();
|
|
58
56
|
open = true;
|
|
59
57
|
};
|
|
60
58
|
|
|
61
|
-
const
|
|
59
|
+
const hide = () => {
|
|
62
60
|
clearTimer();
|
|
63
61
|
open = false;
|
|
64
62
|
};
|
|
65
63
|
|
|
66
|
-
const
|
|
64
|
+
const scheduleHide = () => {
|
|
67
65
|
clearTimer();
|
|
68
66
|
if (focused) return;
|
|
69
|
-
timer = setTimeout(
|
|
67
|
+
timer = setTimeout(hide, closeDelay);
|
|
70
68
|
};
|
|
71
69
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
const isFocusVisible = (element: HTMLElement) => {
|
|
71
|
+
try {
|
|
72
|
+
return element.matches(':focus-visible');
|
|
73
|
+
} catch {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
75
76
|
};
|
|
76
77
|
|
|
78
|
+
// the trigger, when open is set without a pointer or a focus event
|
|
79
|
+
const findAnchor = () =>
|
|
80
|
+
document.querySelector<HTMLElement>(`[${ariaAttribute}~="${CSS.escape(tooltipId)}"]`);
|
|
81
|
+
|
|
82
|
+
let triggerProps: TooltipTriggerProps = $derived({
|
|
83
|
+
[ariaAttribute]: tooltipId,
|
|
84
|
+
onpointerenter: (event: PointerEvent) => {
|
|
85
|
+
if (disabled || event.pointerType === 'touch') return;
|
|
86
|
+
anchor = event.currentTarget as HTMLElement;
|
|
87
|
+
clearTimer();
|
|
88
|
+
if (open) return;
|
|
89
|
+
timer = setTimeout(show, delayDuration);
|
|
90
|
+
},
|
|
91
|
+
onpointerleave: scheduleHide,
|
|
92
|
+
onpointerdown: hide,
|
|
93
|
+
onfocus: (event: FocusEvent) => {
|
|
94
|
+
if (disabled) return;
|
|
95
|
+
anchor = event.currentTarget as HTMLElement;
|
|
96
|
+
focused = true;
|
|
97
|
+
if (isFocusVisible(event.target as HTMLElement)) show();
|
|
98
|
+
},
|
|
99
|
+
onblur: () => {
|
|
100
|
+
focused = false;
|
|
101
|
+
hide();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/** the handlers of the trigger, for the tooltip prop of kit:btn, kit:chip and kit:icon */
|
|
106
|
+
export function triggerHandlers() {
|
|
107
|
+
return triggerProps;
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
$effect(() => {
|
|
78
111
|
if (disabled) open = false;
|
|
79
112
|
});
|
|
80
113
|
|
|
81
114
|
$effect(() => {
|
|
82
|
-
|
|
115
|
+
if (open && !anchor?.isConnected) anchor = findAnchor();
|
|
83
116
|
});
|
|
84
117
|
|
|
85
118
|
$effect(() => {
|
|
86
119
|
if (!open) return;
|
|
87
120
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
88
|
-
if (event.key === 'Escape')
|
|
121
|
+
if (event.key === 'Escape') hide();
|
|
89
122
|
};
|
|
90
123
|
document.addEventListener('keydown', onKeyDown);
|
|
91
124
|
return () => document.removeEventListener('keydown', onKeyDown);
|
|
@@ -94,33 +127,22 @@
|
|
|
94
127
|
onDestroy(clearTimer);
|
|
95
128
|
</script>
|
|
96
129
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class="kit-tooltip-trigger"
|
|
101
|
-
aria-describedby={open ? tooltipId : undefined}
|
|
102
|
-
onmouseenter={handleHoverOpen}
|
|
103
|
-
onmouseleave={handleHoverClose}
|
|
104
|
-
onfocusin={handleFocusOpen}
|
|
105
|
-
onfocusout={handleFocusClose}
|
|
106
|
-
>
|
|
107
|
-
{@render children?.()}
|
|
108
|
-
</span>
|
|
109
|
-
|
|
110
|
-
{#if mounted}
|
|
130
|
+
{@render trigger?.(triggerProps)}
|
|
131
|
+
|
|
132
|
+
{#if label || tooltip}
|
|
111
133
|
<div
|
|
134
|
+
bind:this={ref}
|
|
112
135
|
id={tooltipId}
|
|
113
136
|
popover="manual"
|
|
114
137
|
role="tooltip"
|
|
115
|
-
aria-label={label}
|
|
116
138
|
class={attrs.class}
|
|
117
139
|
style={attrs.style}
|
|
118
140
|
data-density={density}
|
|
119
141
|
data-variant={variant}
|
|
120
142
|
data-rounded={rounded}
|
|
121
143
|
use:floating={{
|
|
122
|
-
anchor
|
|
123
|
-
open,
|
|
144
|
+
anchor,
|
|
145
|
+
open: open && !!anchor,
|
|
124
146
|
side: location,
|
|
125
147
|
align: 'center',
|
|
126
148
|
offset: 6,
|
|
@@ -128,7 +150,7 @@
|
|
|
128
150
|
}}
|
|
129
151
|
{...attrs.rest}
|
|
130
152
|
onmouseenter={clearTimer}
|
|
131
|
-
onmouseleave={
|
|
153
|
+
onmouseleave={scheduleHide}
|
|
132
154
|
>
|
|
133
155
|
{#if tooltip}
|
|
134
156
|
{@render tooltip()}
|
|
@@ -167,18 +189,6 @@
|
|
|
167
189
|
--kit-tooltip-arrow: 0.35rem;
|
|
168
190
|
}
|
|
169
191
|
|
|
170
|
-
.kit-tooltip-trigger {
|
|
171
|
-
display: inline-flex;
|
|
172
|
-
align-self: start;
|
|
173
|
-
justify-self: start;
|
|
174
|
-
width: fit-content;
|
|
175
|
-
max-width: max-content;
|
|
176
|
-
}
|
|
177
|
-
.kit-tooltip-trigger:focus-visible {
|
|
178
|
-
outline: 2px solid var(--kit-color-focus);
|
|
179
|
-
outline-offset: 2px;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
192
|
.kit-tooltip {
|
|
183
193
|
position: fixed;
|
|
184
194
|
inset: auto;
|
|
@@ -307,4 +317,9 @@
|
|
|
307
317
|
.kit-tooltip {
|
|
308
318
|
animation: none;
|
|
309
319
|
}
|
|
320
|
+
}
|
|
321
|
+
@media (forced-colors: active) {
|
|
322
|
+
.kit-tooltip {
|
|
323
|
+
border: 1px solid CanvasText;
|
|
324
|
+
}
|
|
310
325
|
}</style>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { TooltipProps } from './tooltip.types.ts';
|
|
2
|
-
declare const Tooltip: import("svelte").Component<TooltipProps, {
|
|
1
|
+
import type { TooltipProps, TooltipTriggerProps } from './tooltip.types.ts';
|
|
2
|
+
declare const Tooltip: import("svelte").Component<TooltipProps, {
|
|
3
|
+
triggerHandlers: () => TooltipTriggerProps;
|
|
4
|
+
}, "ref" | "open">;
|
|
3
5
|
type Tooltip = ReturnType<typeof Tooltip>;
|
|
4
6
|
export default Tooltip;
|
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
import type { ColorProps, Component, RoundedType } from '../../@types';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
|
|
3
|
+
/** The attributes to spread on the element that shows the tooltip */
|
|
4
|
+
export interface TooltipTriggerProps {
|
|
5
|
+
'aria-describedby'?: string;
|
|
6
|
+
'aria-labelledby'?: string;
|
|
7
|
+
onpointerenter: (event: PointerEvent) => void;
|
|
8
|
+
onpointerleave: (event: PointerEvent) => void;
|
|
9
|
+
onpointerdown: (event: PointerEvent) => void;
|
|
10
|
+
onfocus: (event: FocusEvent) => void;
|
|
11
|
+
onblur: (event: FocusEvent) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface TooltipProps extends Omit<Component, 'children'> {
|
|
4
14
|
ref?: HTMLElement | null;
|
|
5
15
|
open?: boolean;
|
|
6
|
-
forceMount?: boolean;
|
|
7
16
|
rounded?: RoundedType | 'full';
|
|
8
17
|
label?: string;
|
|
18
|
+
/** the tooltip names the trigger (aria-labelledby) instead of describing it */
|
|
19
|
+
asLabel?: boolean;
|
|
9
20
|
location?: 'top' | 'bottom' | 'left' | 'right';
|
|
10
21
|
color?: ColorProps;
|
|
11
22
|
delayDuration?: number;
|
|
23
|
+
closeDelay?: number;
|
|
12
24
|
variant?: 'arrow';
|
|
13
25
|
density?: 'dense' | 'compact' | 'default' | 'comfortable' | 'spacious';
|
|
14
26
|
disabled?: boolean;
|
|
15
27
|
avoidCollisions?: boolean;
|
|
28
|
+
trigger?: Snippet<[TooltipTriggerProps]>;
|
|
16
29
|
tooltip?: Snippet;
|
|
30
|
+
/** removed in 0.8.0: use the trigger snippet */
|
|
31
|
+
children?: never;
|
|
32
|
+
}
|
|
33
|
+
/** The tooltip prop of kit:btn, kit:chip and kit:icon */
|
|
34
|
+
export interface TooltipOptions {
|
|
35
|
+
label: string;
|
|
36
|
+
location?: TooltipProps['location'];
|
|
37
|
+
variant?: TooltipProps['variant'];
|
|
38
|
+
density?: TooltipProps['density'];
|
|
39
|
+
rounded?: TooltipProps['rounded'];
|
|
40
|
+
delayDuration?: number;
|
|
41
|
+
closeDelay?: number;
|
|
17
42
|
}
|
|
43
|
+
export type TooltipValue = string | TooltipOptions | null | undefined | false;
|