lapikit 0.7.8 → 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 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useComponentAttrs, useElevation } from '../../utils';
|
|
3
3
|
import { ripple } from '../../animations';
|
|
4
|
+
import { tooltip as tooltipAction } from '../tooltip/tooltip-action.svelte.js';
|
|
4
5
|
import type { ButtonProps } from './btn.types.js';
|
|
5
6
|
|
|
6
7
|
let {
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
checked,
|
|
26
27
|
value,
|
|
27
28
|
label,
|
|
29
|
+
tooltip,
|
|
28
30
|
wide,
|
|
29
31
|
noRipple,
|
|
30
32
|
load,
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
|
|
45
47
|
let isInput = $derived(!!input || is === 'input');
|
|
46
48
|
let isDisabled = $derived(!!disabled);
|
|
49
|
+
let focusableDisabled = $derived(isDisabled && !!tooltip);
|
|
47
50
|
let tag = $derived<'button' | 'a' | 'label' | 'div'>(
|
|
48
51
|
isInput
|
|
49
52
|
? type === 'checkbox' || type === 'radio'
|
|
@@ -55,8 +58,17 @@
|
|
|
55
58
|
);
|
|
56
59
|
let resolvedHref = $derived(tag === 'a' && !isDisabled ? href : undefined);
|
|
57
60
|
let resolvedType = $derived(tag === 'button' ? (type ?? 'button') : undefined);
|
|
58
|
-
let resolvedDisabled = $derived(tag === 'button' ? isDisabled : undefined);
|
|
59
|
-
let resolvedTabIndex = $derived(
|
|
61
|
+
let resolvedDisabled = $derived(tag === 'button' ? isDisabled && !focusableDisabled : undefined);
|
|
62
|
+
let resolvedTabIndex = $derived(
|
|
63
|
+
tag === 'a' && isDisabled ? (focusableDisabled ? 0 : -1) : undefined
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// disabled with a tooltip: focusable, the click does nothing
|
|
67
|
+
const blockClick = (event: MouseEvent) => {
|
|
68
|
+
if (!focusableDisabled) return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
event.stopPropagation();
|
|
71
|
+
};
|
|
60
72
|
</script>
|
|
61
73
|
|
|
62
74
|
<svelte:element
|
|
@@ -78,12 +90,15 @@
|
|
|
78
90
|
data-interactive={true}
|
|
79
91
|
data-active={active}
|
|
80
92
|
data-disabled={isDisabled}
|
|
93
|
+
data-disabled-focusable={focusableDisabled || undefined}
|
|
94
|
+
onclickcapture={blockClick}
|
|
81
95
|
data-loading={loading}
|
|
82
96
|
data-block={block}
|
|
83
97
|
data-wide={wide}
|
|
84
98
|
data-icon={icon}
|
|
85
99
|
aria-busy={loading || undefined}
|
|
86
100
|
aria-disabled={isDisabled || undefined}
|
|
101
|
+
use:tooltipAction={tooltip}
|
|
87
102
|
use:ripple={{
|
|
88
103
|
component: 'btn',
|
|
89
104
|
disabled: noRipple || isDisabled
|
|
@@ -103,7 +118,8 @@
|
|
|
103
118
|
{checked}
|
|
104
119
|
{value}
|
|
105
120
|
aria-label={label || (value != null ? String(value) : undefined)}
|
|
106
|
-
disabled={isDisabled}
|
|
121
|
+
disabled={isDisabled && !focusableDisabled}
|
|
122
|
+
aria-disabled={focusableDisabled || undefined}
|
|
107
123
|
/>
|
|
108
124
|
{:else}
|
|
109
125
|
<span class="kit-btn__inner">
|
|
@@ -416,7 +432,7 @@ button.kit-btn {
|
|
|
416
432
|
color: var(--kit-btn-pressed-fg, var(--kit-btn-fg));
|
|
417
433
|
}
|
|
418
434
|
|
|
419
|
-
.kit-btn:not([data-disabled=true]):focus-visible {
|
|
435
|
+
.kit-btn:is(:not([data-disabled=true]), [data-disabled-focusable=true]):focus-visible {
|
|
420
436
|
outline: 2px solid var(--kit-color-focus);
|
|
421
437
|
outline-offset: 2px;
|
|
422
438
|
}
|
|
@@ -427,6 +443,11 @@ button.kit-btn {
|
|
|
427
443
|
user-select: none;
|
|
428
444
|
}
|
|
429
445
|
|
|
446
|
+
.kit-btn[data-disabled-focusable=true] {
|
|
447
|
+
pointer-events: auto;
|
|
448
|
+
cursor: not-allowed;
|
|
449
|
+
}
|
|
450
|
+
|
|
430
451
|
/**
|
|
431
452
|
* rounded
|
|
432
453
|
* @link https://lapikit.dev/docs/customize
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ColorProps, Component, ElevationProps, RoundedType, SizeType } from '../../@types';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { TooltipValue } from '../tooltip/tooltip.types';
|
|
3
4
|
export interface ButtonProps extends Component {
|
|
4
5
|
ref?: HTMLElement | null;
|
|
5
6
|
is?: 'button' | 'a' | 'input';
|
|
@@ -16,6 +17,8 @@ export interface ButtonProps extends Component {
|
|
|
16
17
|
type?: 'button' | 'submit' | 'reset' | 'checkbox' | 'radio';
|
|
17
18
|
value?: string | number | boolean;
|
|
18
19
|
label?: string;
|
|
20
|
+
/** text of a tooltip on hover and keyboard focus, or its options */
|
|
21
|
+
tooltip?: TooltipValue;
|
|
19
22
|
block?: boolean;
|
|
20
23
|
wide?: boolean;
|
|
21
24
|
noRipple?: boolean;
|
|
@@ -210,7 +210,7 @@ button.kit-card {
|
|
|
210
210
|
color: var(--kit-card-pressed-fg, var(--kit-card-fg));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
.kit-card:not([data-disabled=true]):focus-visible {
|
|
213
|
+
.kit-card:is(:not([data-disabled=true]), [data-disabled-focusable=true]):focus-visible {
|
|
214
214
|
outline: 2px solid var(--kit-color-focus);
|
|
215
215
|
outline-offset: 2px;
|
|
216
216
|
}
|
|
@@ -221,6 +221,11 @@ button.kit-card {
|
|
|
221
221
|
user-select: none;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
.kit-card[data-disabled-focusable=true] {
|
|
225
|
+
pointer-events: auto;
|
|
226
|
+
cursor: not-allowed;
|
|
227
|
+
}
|
|
228
|
+
|
|
224
229
|
/**
|
|
225
230
|
* rounded
|
|
226
231
|
* @link https://lapikit.dev/docs/customize
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useComponentAttrs, useElevation } from '../../utils';
|
|
3
3
|
import { ripple } from '../../animations';
|
|
4
|
+
import { tooltip as tooltipAction } from '../tooltip/tooltip-action.svelte.js';
|
|
4
5
|
import type { ChipProps } from './chip.types.js';
|
|
5
6
|
|
|
6
7
|
let {
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
checked,
|
|
27
28
|
value,
|
|
28
29
|
label,
|
|
30
|
+
tooltip,
|
|
29
31
|
noRipple,
|
|
30
32
|
load,
|
|
31
33
|
append,
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
let isDisabled = $derived(!!disabled);
|
|
47
49
|
let isReadonly = $derived(!!readonly);
|
|
48
50
|
let isLocked = $derived(isDisabled || isReadonly);
|
|
51
|
+
let focusableDisabled = $derived(isLocked && !!tooltip);
|
|
49
52
|
let hasPressHandler = $derived(
|
|
50
53
|
['onclick', 'onkeydown', 'onkeyup'].some((key) => typeof attrs.rest[key] === 'function')
|
|
51
54
|
);
|
|
@@ -64,8 +67,17 @@
|
|
|
64
67
|
let isInteractive = $derived((interactive ?? (tag !== 'div' || hasPressHandler)) && !isLocked);
|
|
65
68
|
let resolvedHref = $derived(tag === 'a' && !isLocked ? href : undefined);
|
|
66
69
|
let resolvedType = $derived(tag === 'button' ? (type ?? 'button') : undefined);
|
|
67
|
-
let resolvedDisabled = $derived(tag === 'button' ? isLocked : undefined);
|
|
68
|
-
let resolvedTabIndex = $derived(
|
|
70
|
+
let resolvedDisabled = $derived(tag === 'button' ? isLocked && !focusableDisabled : undefined);
|
|
71
|
+
let resolvedTabIndex = $derived(
|
|
72
|
+
tag === 'a' && isLocked ? (focusableDisabled ? 0 : -1) : undefined
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// disabled with a tooltip: focusable, the click does nothing
|
|
76
|
+
const blockClick = (event: MouseEvent) => {
|
|
77
|
+
if (!focusableDisabled) return;
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
event.stopPropagation();
|
|
80
|
+
};
|
|
69
81
|
</script>
|
|
70
82
|
|
|
71
83
|
<svelte:element
|
|
@@ -87,11 +99,14 @@
|
|
|
87
99
|
data-interactive={isInteractive}
|
|
88
100
|
data-active={active}
|
|
89
101
|
data-disabled={isDisabled}
|
|
102
|
+
data-disabled-focusable={(isDisabled && focusableDisabled) || undefined}
|
|
103
|
+
onclickcapture={blockClick}
|
|
90
104
|
data-readonly={isReadonly}
|
|
91
105
|
data-loading={loading}
|
|
92
106
|
data-label-style={labelStyle}
|
|
93
107
|
aria-busy={loading || undefined}
|
|
94
108
|
aria-disabled={isLocked || undefined}
|
|
109
|
+
use:tooltipAction={tooltip}
|
|
95
110
|
use:ripple={{
|
|
96
111
|
component: 'chip',
|
|
97
112
|
disabled: noRipple || !isInteractive
|
|
@@ -111,7 +126,8 @@
|
|
|
111
126
|
{checked}
|
|
112
127
|
{value}
|
|
113
128
|
aria-label={label || (value != null ? String(value) : undefined)}
|
|
114
|
-
disabled={isLocked}
|
|
129
|
+
disabled={isLocked && !focusableDisabled}
|
|
130
|
+
aria-disabled={focusableDisabled || undefined}
|
|
115
131
|
/>
|
|
116
132
|
{:else}
|
|
117
133
|
<span class="kit-chip__inner">
|
|
@@ -401,7 +417,7 @@ button.kit-chip {
|
|
|
401
417
|
color: var(--kit-chip-pressed-fg, var(--kit-chip-fg));
|
|
402
418
|
}
|
|
403
419
|
|
|
404
|
-
.kit-chip:not([data-disabled=true]):focus-visible {
|
|
420
|
+
.kit-chip:is(:not([data-disabled=true]), [data-disabled-focusable=true]):focus-visible {
|
|
405
421
|
outline: 2px solid var(--kit-color-focus);
|
|
406
422
|
outline-offset: 2px;
|
|
407
423
|
}
|
|
@@ -412,6 +428,11 @@ button.kit-chip {
|
|
|
412
428
|
user-select: none;
|
|
413
429
|
}
|
|
414
430
|
|
|
431
|
+
.kit-chip[data-disabled-focusable=true] {
|
|
432
|
+
pointer-events: auto;
|
|
433
|
+
cursor: not-allowed;
|
|
434
|
+
}
|
|
435
|
+
|
|
415
436
|
/**
|
|
416
437
|
* rounded
|
|
417
438
|
* @link https://lapikit.dev/docs/customize
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ColorProps, Component, ElevationProps, RoundedType, SizeType } from '../../@types';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { TooltipValue } from '../tooltip/tooltip.types';
|
|
3
4
|
export interface ChipProps extends Component {
|
|
4
5
|
ref?: HTMLElement | null;
|
|
5
6
|
is?: 'div' | 'a' | 'input' | 'button';
|
|
@@ -18,6 +19,8 @@ export interface ChipProps extends Component {
|
|
|
18
19
|
type?: 'button' | 'submit' | 'reset' | 'checkbox' | 'radio';
|
|
19
20
|
value?: string | number | boolean;
|
|
20
21
|
label?: string;
|
|
22
|
+
/** text of a tooltip on hover and keyboard focus, or its options */
|
|
23
|
+
tooltip?: TooltipValue;
|
|
21
24
|
noRipple?: boolean;
|
|
22
25
|
readonly?: boolean;
|
|
23
26
|
load?: Snippet;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy } from 'svelte';
|
|
3
|
+
import { DEV } from 'esm-env';
|
|
3
4
|
import {
|
|
4
5
|
clickOutside,
|
|
5
6
|
floating,
|
|
@@ -7,11 +8,17 @@
|
|
|
7
8
|
useElevation,
|
|
8
9
|
type FloatingAnchor
|
|
9
10
|
} from '../../utils';
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
DropdownProps,
|
|
13
|
+
DropdownTriggerModel,
|
|
14
|
+
DropdownTriggerProps
|
|
15
|
+
} from './dropdown.types.js';
|
|
11
16
|
|
|
12
17
|
let {
|
|
13
18
|
ref = $bindable(),
|
|
19
|
+
open = $bindable(false),
|
|
14
20
|
children,
|
|
21
|
+
trigger,
|
|
15
22
|
activator,
|
|
16
23
|
rounded = 'md',
|
|
17
24
|
position = 'bottom',
|
|
@@ -27,77 +34,147 @@
|
|
|
27
34
|
...rest
|
|
28
35
|
}: DropdownProps = $props();
|
|
29
36
|
|
|
37
|
+
$effect(() => {
|
|
38
|
+
if (DEV && activator)
|
|
39
|
+
console.warn('[lapikit] kit:dropdown: activator is removed, use the trigger snippet');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const menuId = $props.id();
|
|
43
|
+
|
|
30
44
|
let attrs = $derived(
|
|
31
45
|
useComponentAttrs('kit-dropdown-content', { className, sClass, styleAttr, sStyle, color }, rest)
|
|
32
46
|
);
|
|
33
47
|
let elevationState = $derived(useElevation(elevation));
|
|
34
48
|
|
|
35
49
|
let contentRef = $state<HTMLElement | null>(null);
|
|
36
|
-
let
|
|
37
|
-
|
|
50
|
+
let triggerRef = $state<HTMLElement | null>(null);
|
|
51
|
+
// a point opens the menu at the position of the pointer (context menu)
|
|
52
|
+
let anchor = $state<FloatingAnchor | null>(null);
|
|
38
53
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
activatorRef instanceof HTMLElement
|
|
43
|
-
? activatorRef
|
|
44
|
-
: activatorRef
|
|
45
|
-
? { x: activatorRef.clientX, y: activatorRef.clientY }
|
|
46
|
-
: null
|
|
47
|
-
);
|
|
55
|
+
const itemSelector = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
|
|
56
|
+
const focusableSelector = 'a[href], button, input, select, textarea, [tabindex]';
|
|
48
57
|
|
|
49
58
|
const clearHoverTimeout = () => {
|
|
50
59
|
if (timeoutId) clearTimeout(timeoutId);
|
|
51
60
|
timeoutId = null;
|
|
52
61
|
};
|
|
53
62
|
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
const items = () => {
|
|
64
|
+
if (!contentRef) return [];
|
|
65
|
+
const menuItems = contentRef.querySelectorAll<HTMLElement>(itemSelector);
|
|
66
|
+
const list = menuItems.length
|
|
67
|
+
? menuItems
|
|
68
|
+
: contentRef.querySelectorAll<HTMLElement>(focusableSelector);
|
|
69
|
+
return [...list].filter(
|
|
70
|
+
(item) =>
|
|
71
|
+
!item.matches(':disabled, [aria-disabled="true"], [tabindex="-1"]:not([role^="menuitem"])')
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const focusItem = (target: 'first' | 'last' | 'next' | 'previous') => {
|
|
76
|
+
const list = items();
|
|
77
|
+
if (!list.length) return;
|
|
78
|
+
const index = list.indexOf(document.activeElement as HTMLElement);
|
|
79
|
+
const next =
|
|
80
|
+
target === 'first'
|
|
81
|
+
? 0
|
|
82
|
+
: target === 'last'
|
|
83
|
+
? list.length - 1
|
|
84
|
+
: target === 'next'
|
|
85
|
+
? (index + 1) % list.length
|
|
86
|
+
: (index - 1 + list.length) % list.length;
|
|
87
|
+
list[next].focus();
|
|
58
88
|
};
|
|
59
89
|
|
|
60
|
-
const
|
|
90
|
+
const show = (target: FloatingAnchor, focus?: 'first' | 'last') => {
|
|
91
|
+
clearHoverTimeout();
|
|
92
|
+
anchor = target;
|
|
93
|
+
open = true;
|
|
94
|
+
if (focus) requestAnimationFrame(() => focusItem(focus));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const close = (restoreFocus = false) => {
|
|
61
98
|
clearHoverTimeout();
|
|
62
99
|
open = false;
|
|
100
|
+
if (restoreFocus) triggerRef?.focus();
|
|
63
101
|
};
|
|
64
102
|
|
|
65
|
-
const
|
|
66
|
-
state: 'open' | 'close',
|
|
67
|
-
element: HTMLElement | PointerEvent | null
|
|
68
|
-
) => {
|
|
103
|
+
const scheduleClose = () => {
|
|
69
104
|
if (!openOnHover) return;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
clearHoverTimeout();
|
|
73
|
-
if (element) activatorRef = element;
|
|
74
|
-
open = true;
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
timeoutId = setTimeout(() => {
|
|
79
|
-
open = false;
|
|
80
|
-
timeoutId = null;
|
|
81
|
-
}, 150);
|
|
105
|
+
clearHoverTimeout();
|
|
106
|
+
timeoutId = setTimeout(close, 150);
|
|
82
107
|
};
|
|
83
108
|
|
|
84
|
-
|
|
109
|
+
let triggerProps: DropdownTriggerProps = $derived({
|
|
110
|
+
'aria-haspopup': 'menu',
|
|
111
|
+
'aria-expanded': open,
|
|
112
|
+
'aria-controls': menuId,
|
|
113
|
+
onclick: (event: MouseEvent) => {
|
|
114
|
+
triggerRef = event.currentTarget as HTMLElement;
|
|
115
|
+
if (open) close();
|
|
116
|
+
// detail is 0 when the click comes from the keyboard (Enter, Space)
|
|
117
|
+
else show(triggerRef, event.detail === 0 ? 'first' : undefined);
|
|
118
|
+
},
|
|
119
|
+
onkeydown: (event: KeyboardEvent) => {
|
|
120
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
triggerRef = event.currentTarget as HTMLElement;
|
|
123
|
+
show(triggerRef, event.key === 'ArrowDown' ? 'first' : 'last');
|
|
124
|
+
},
|
|
125
|
+
...(openOnHover && {
|
|
126
|
+
onpointerenter: (event: PointerEvent) => {
|
|
127
|
+
if (event.pointerType === 'touch') return;
|
|
128
|
+
triggerRef = event.currentTarget as HTMLElement;
|
|
129
|
+
show(triggerRef);
|
|
130
|
+
},
|
|
131
|
+
onpointerleave: scheduleClose
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const model: DropdownTriggerModel = {
|
|
85
136
|
get open() {
|
|
86
137
|
return open;
|
|
87
138
|
},
|
|
88
|
-
close:
|
|
89
|
-
|
|
139
|
+
close: () => close(),
|
|
140
|
+
openAt: (event) => {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
triggerRef = event.currentTarget as HTMLElement;
|
|
143
|
+
show({ x: event.clientX, y: event.clientY });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const onMenuKeydown = (event: KeyboardEvent) => {
|
|
148
|
+
const keys: Record<string, 'first' | 'last' | 'next' | 'previous'> = {
|
|
149
|
+
ArrowDown: 'next',
|
|
150
|
+
ArrowUp: 'previous',
|
|
151
|
+
Home: 'first',
|
|
152
|
+
End: 'last'
|
|
153
|
+
};
|
|
154
|
+
if (keys[event.key]) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
focusItem(keys[event.key]);
|
|
157
|
+
} else if (event.key === 'Tab') {
|
|
158
|
+
close();
|
|
159
|
+
}
|
|
90
160
|
};
|
|
91
161
|
|
|
92
162
|
$effect(() => {
|
|
93
163
|
ref = contentRef;
|
|
94
164
|
});
|
|
95
165
|
|
|
166
|
+
$effect(() => {
|
|
167
|
+
if (open && !anchor) {
|
|
168
|
+
triggerRef = document.querySelector<HTMLElement>(`[aria-controls="${CSS.escape(menuId)}"]`);
|
|
169
|
+
anchor = triggerRef;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
96
173
|
$effect(() => {
|
|
97
174
|
if (!open) return;
|
|
98
175
|
|
|
99
176
|
const onKeydown = (event: KeyboardEvent) => {
|
|
100
|
-
if (event.key === 'Escape')
|
|
177
|
+
if (event.key === 'Escape') close(true);
|
|
101
178
|
};
|
|
102
179
|
|
|
103
180
|
document.addEventListener('keydown', onKeydown);
|
|
@@ -107,11 +184,12 @@
|
|
|
107
184
|
onDestroy(clearHoverTimeout);
|
|
108
185
|
</script>
|
|
109
186
|
|
|
110
|
-
{@render
|
|
187
|
+
{@render trigger?.(triggerProps, model)}
|
|
111
188
|
|
|
112
189
|
{#if open}
|
|
113
190
|
<div
|
|
114
191
|
bind:this={contentRef}
|
|
192
|
+
id={menuId}
|
|
115
193
|
popover="manual"
|
|
116
194
|
class={attrs.class}
|
|
117
195
|
style={attrs.style}
|
|
@@ -121,14 +199,15 @@
|
|
|
121
199
|
data-elevation={elevationState.base}
|
|
122
200
|
data-elevation-hover={elevationState.hover}
|
|
123
201
|
data-elevation-active={elevationState.active}
|
|
124
|
-
|
|
125
|
-
|
|
202
|
+
onpointerenter={() => openOnHover && clearHoverTimeout()}
|
|
203
|
+
onpointerleave={scheduleClose}
|
|
204
|
+
onkeydown={onMenuKeydown}
|
|
126
205
|
onclick={(event) => {
|
|
127
206
|
event.stopPropagation();
|
|
128
|
-
if (closeOnClick)
|
|
207
|
+
if (closeOnClick) close(true);
|
|
129
208
|
}}
|
|
130
209
|
use:floating={{ anchor, side: position, offset: 0 }}
|
|
131
|
-
use:clickOutside={{ exclude: [contentRef,
|
|
210
|
+
use:clickOutside={{ exclude: [contentRef, triggerRef], onClose: () => close() }}
|
|
132
211
|
{...attrs.rest}
|
|
133
212
|
>
|
|
134
213
|
{@render children?.()}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { DropdownProps } from './dropdown.types.ts';
|
|
2
|
-
declare const Dropdown: import("svelte").Component<DropdownProps, {}, "ref">;
|
|
2
|
+
declare const Dropdown: import("svelte").Component<DropdownProps, {}, "ref" | "open">;
|
|
3
3
|
type Dropdown = ReturnType<typeof Dropdown>;
|
|
4
4
|
export default Dropdown;
|
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import type { ColorProps, Component, ElevationProps, RoundedType } from '../../@types/index';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
|
|
3
|
+
/** The attributes to spread on the element that opens the menu */
|
|
4
|
+
export interface DropdownTriggerProps {
|
|
5
|
+
'aria-haspopup': 'menu';
|
|
6
|
+
'aria-expanded': boolean;
|
|
7
|
+
'aria-controls': string;
|
|
8
|
+
onclick: (event: MouseEvent) => void;
|
|
9
|
+
onkeydown: (event: KeyboardEvent) => void;
|
|
10
|
+
onpointerenter?: (event: PointerEvent) => void;
|
|
11
|
+
onpointerleave?: (event: PointerEvent) => void;
|
|
12
|
+
}
|
|
13
|
+
/** The state of the menu, for a trigger that needs more than the props */
|
|
14
|
+
export interface DropdownTriggerModel {
|
|
4
15
|
open: boolean;
|
|
5
16
|
close: () => void;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
/** opens the menu at the position of the pointer (context menu) */
|
|
18
|
+
openAt: (event: MouseEvent) => void;
|
|
19
|
+
}
|
|
9
20
|
export interface DropdownProps extends Component {
|
|
10
21
|
ref?: HTMLElement | null;
|
|
22
|
+
open?: boolean;
|
|
11
23
|
rounded?: RoundedType | 'full';
|
|
12
24
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
13
25
|
openOnHover?: boolean;
|
|
14
26
|
closeOnClick?: boolean;
|
|
15
27
|
color?: ColorProps;
|
|
16
28
|
density?: 'dense' | 'compact' | 'default' | 'comfortable' | 'spacious';
|
|
17
|
-
|
|
18
|
-
ModelDropdownProps,
|
|
19
|
-
(state: 'open' | 'close', element: HTMLElement | PointerEvent | null) => void
|
|
20
|
-
]>;
|
|
29
|
+
trigger?: Snippet<[DropdownTriggerProps, DropdownTriggerModel]>;
|
|
21
30
|
elevation?: ElevationProps;
|
|
31
|
+
/** removed in 0.8.0: use the trigger snippet */
|
|
32
|
+
activator?: never;
|
|
22
33
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
rounded = 'md',
|
|
17
17
|
color,
|
|
18
18
|
elevation,
|
|
19
|
+
label,
|
|
19
20
|
disabled = false,
|
|
20
21
|
readonly = false,
|
|
21
22
|
error = false,
|
|
@@ -76,11 +77,16 @@
|
|
|
76
77
|
data-readonly={readonly}
|
|
77
78
|
data-error={error}
|
|
78
79
|
data-focused={focused}
|
|
80
|
+
data-with-label={!!label}
|
|
79
81
|
data-with-message={hasMessage}
|
|
80
82
|
onfocusin={() => (focused = true)}
|
|
81
83
|
onfocusout={() => (focused = false)}
|
|
82
84
|
{...attrs.rest}
|
|
83
85
|
>
|
|
86
|
+
{#if label}
|
|
87
|
+
<label class="kit-field__label" for={controlId}>{label}</label>
|
|
88
|
+
{/if}
|
|
89
|
+
|
|
84
90
|
{#if prepend}
|
|
85
91
|
<div class="kit-field__prepend">{@render prepend()}</div>
|
|
86
92
|
{/if}
|
|
@@ -192,13 +198,29 @@
|
|
|
192
198
|
width: 100%;
|
|
193
199
|
font-size: var(--kit-field-font);
|
|
194
200
|
}
|
|
201
|
+
.kit-field[data-with-label=true] {
|
|
202
|
+
grid-template-areas: ". label ." "prepend box append";
|
|
203
|
+
}
|
|
195
204
|
.kit-field[data-with-message=true] {
|
|
196
205
|
grid-template-areas: "prepend box append" ". message .";
|
|
197
206
|
}
|
|
207
|
+
.kit-field[data-with-label=true][data-with-message=true] {
|
|
208
|
+
grid-template-areas: ". label ." "prepend box append" ". message .";
|
|
209
|
+
}
|
|
198
210
|
.kit-field[data-disabled=true] {
|
|
199
211
|
opacity: var(--kit-disabled-opacity, 0.55);
|
|
200
212
|
pointer-events: none;
|
|
201
213
|
}
|
|
214
|
+
.kit-field__label {
|
|
215
|
+
grid-area: label;
|
|
216
|
+
display: block;
|
|
217
|
+
padding-block-end: 0.25rem;
|
|
218
|
+
font-size: 0.875rem;
|
|
219
|
+
line-height: 1.2;
|
|
220
|
+
}
|
|
221
|
+
.kit-field[data-error=true] .kit-field__label {
|
|
222
|
+
color: var(--kit-color-error);
|
|
223
|
+
}
|
|
202
224
|
.kit-field__prepend, .kit-field__append {
|
|
203
225
|
display: flex;
|
|
204
226
|
align-items: center;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useComponentAttrs } from '../../utils';
|
|
3
|
+
import { tooltip as tooltipAction } from '../tooltip/tooltip-action.svelte.js';
|
|
3
4
|
import type { IconProps } from './icon.types.js';
|
|
4
5
|
|
|
5
6
|
let {
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
size = 'default',
|
|
17
18
|
alt = '',
|
|
18
19
|
label = undefined,
|
|
20
|
+
tooltip = undefined,
|
|
19
21
|
decorative = true,
|
|
20
22
|
loading = 'eager',
|
|
21
23
|
decoding = 'async',
|
|
@@ -35,9 +37,14 @@
|
|
|
35
37
|
let hasContent = $derived(hasChildren || !!resolvedSrc || !!resolvedName);
|
|
36
38
|
let iconClass = $derived(!hasChildren && !resolvedSrc ? resolvedName : undefined);
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
let
|
|
40
|
-
let
|
|
40
|
+
// an icon with a tooltip is not decorative: the tooltip names it
|
|
41
|
+
let tooltipLabel = $derived(typeof tooltip === 'string' ? tooltip : tooltip ? tooltip.label : '');
|
|
42
|
+
let isDecorative = $derived(decorative && !tooltipLabel);
|
|
43
|
+
let iconRole = $derived(!isDecorative && (label || alt || tooltipLabel) ? 'img' : undefined);
|
|
44
|
+
let iconAriaLabel = $derived(
|
|
45
|
+
!isDecorative ? label || alt || tooltipLabel || undefined : undefined
|
|
46
|
+
);
|
|
47
|
+
let iconAriaHidden = $derived(isDecorative ? true : undefined);
|
|
41
48
|
let imgAlt = $derived(decorative ? '' : alt || label || '');
|
|
42
49
|
let isSvgSrc = $derived(!!resolvedSrc && /\.svg($|\?)/i.test(resolvedSrc));
|
|
43
50
|
let useMaskMode = $derived(
|
|
@@ -58,6 +65,7 @@
|
|
|
58
65
|
role={iconRole}
|
|
59
66
|
aria-label={iconAriaLabel}
|
|
60
67
|
aria-hidden={iconAriaHidden}
|
|
68
|
+
use:tooltipAction={tooltip}
|
|
61
69
|
{...attrs.rest}
|
|
62
70
|
>
|
|
63
71
|
{#if hasChildren}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Component, SizeType } from '../../@types';
|
|
2
|
+
import type { TooltipValue } from '../tooltip/tooltip.types';
|
|
2
3
|
export interface IconProps extends Component {
|
|
3
4
|
ref?: HTMLElement | null;
|
|
4
5
|
is?: 'i' | 'span';
|
|
@@ -8,6 +9,8 @@ export interface IconProps extends Component {
|
|
|
8
9
|
size?: SizeType;
|
|
9
10
|
alt?: string;
|
|
10
11
|
label?: string;
|
|
12
|
+
/** text of a tooltip on hover and keyboard focus, or its options */
|
|
13
|
+
tooltip?: TooltipValue;
|
|
11
14
|
decorative?: boolean;
|
|
12
15
|
loading?: 'eager' | 'lazy';
|
|
13
16
|
decoding?: 'sync' | 'async' | 'auto';
|
|
@@ -9,17 +9,17 @@ export type { ButtonProps } from './btn/btn.types';
|
|
|
9
9
|
export type { CardProps, CardActionsProps, CardContainerProps, CardContentProps, CardMediaProps, CardTitleProps } from './card/card.types';
|
|
10
10
|
export type { ChipProps } from './chip/chip.types';
|
|
11
11
|
export type { DialogProps } from './dialog/dialog.types';
|
|
12
|
-
export type { DropdownProps,
|
|
12
|
+
export type { DropdownProps, DropdownTriggerProps, DropdownTriggerModel } from './dropdown/dropdown.types';
|
|
13
13
|
export type { FieldProps, FieldBaseProps, FieldControlProps, FieldVariant } from './field/field.types';
|
|
14
14
|
export type { IconProps } from './icon/icon.types';
|
|
15
15
|
export type { ListProps, ListItemProps, ListVariant, ListElement, ListItemElement } from './list/list.types';
|
|
16
16
|
export type { ModalProps } from './modal/modal.types';
|
|
17
|
-
export type { PopoverProps,
|
|
17
|
+
export type { PopoverProps, PopoverTriggerProps } from './popover/popover.types';
|
|
18
18
|
export type { SeparatorProps } from './separator/separator.types';
|
|
19
19
|
export type { SpacerProps } from './spacer/spacer.types';
|
|
20
20
|
export type { TextfieldProps } from './textfield/textfield.types';
|
|
21
21
|
export type { ToolbarProps } from './toolbar/toolbar.types';
|
|
22
|
-
export type { TooltipProps } from './tooltip/tooltip.types';
|
|
22
|
+
export type { TooltipProps, TooltipTriggerProps, TooltipOptions, TooltipValue } from './tooltip/tooltip.types';
|
|
23
23
|
export { default as KitApp } from './app/app.svelte';
|
|
24
24
|
export { default as KitAppbar } from './appbar/appbar.svelte';
|
|
25
25
|
export { default as KitBtn } from './btn/btn.svelte';
|