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.
@@ -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(tag === 'a' && isDisabled ? -1 : undefined);
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(tag === 'a' && isLocked ? -1 : undefined);
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 { DropdownProps, ModelDropdownProps } from './dropdown.types.js';
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 activatorRef = $state<HTMLElement | PointerEvent | null>(null);
37
- let open = $state(false);
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
- // a pointer event opens the menu at the position of the pointer (context menu)
41
- let anchor = $derived<FloatingAnchor | null>(
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 handleToggle = (element: HTMLElement | PointerEvent | null) => {
55
- if (element === null) return;
56
- activatorRef = element;
57
- open = !open;
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 handleClose = () => {
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 handleMouseEvent = (
66
- state: 'open' | 'close',
67
- element: HTMLElement | PointerEvent | null
68
- ) => {
103
+ const scheduleClose = () => {
69
104
  if (!openOnHover) return;
70
-
71
- if (state === 'open') {
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
- const model: ModelDropdownProps = {
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: handleClose,
89
- toggle: handleToggle
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') handleClose();
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 activator?.(model, (state, element) => handleMouseEvent(state, element))}
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
- onmouseover={() => handleMouseEvent('open', activatorRef)}
125
- onmouseleave={() => handleMouseEvent('close', activatorRef)}
202
+ onpointerenter={() => openOnHover && clearHoverTimeout()}
203
+ onpointerleave={scheduleClose}
204
+ onkeydown={onMenuKeydown}
126
205
  onclick={(event) => {
127
206
  event.stopPropagation();
128
- if (closeOnClick) handleClose();
207
+ if (closeOnClick) close(true);
129
208
  }}
130
209
  use:floating={{ anchor, side: position, offset: 0 }}
131
- use:clickOutside={{ exclude: [contentRef, activatorRef], onClose: handleClose }}
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
- export type ModelDropdownProps = {
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
- toggle: (element: HTMLElement | PointerEvent | null) => void;
7
- };
8
- export type ModelDropdownHandleProps = (state: 'open' | 'close', element: HTMLElement | PointerEvent | null) => void;
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
- activator?: Snippet<[
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;
@@ -16,6 +16,7 @@ export interface FieldBaseProps {
16
16
  rounded?: RoundedType | 'full';
17
17
  color?: ColorProps;
18
18
  elevation?: ElevationProps;
19
+ label?: string;
19
20
  disabled?: boolean;
20
21
  readonly?: boolean;
21
22
  error?: boolean;
@@ -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
- let iconRole = $derived(!decorative && (label || alt) ? 'img' : undefined);
39
- let iconAriaLabel = $derived(!decorative ? label || alt || undefined : undefined);
40
- let iconAriaHidden = $derived(decorative ? true : undefined);
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, ModelDropdownProps, ModelDropdownHandleProps } from './dropdown/dropdown.types';
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, ModelPopoverProps } from './popover/popover.types';
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';