@svgrid/grid 2.2.0 → 2.2.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/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvColorInput.svelte +187 -187
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvDrawer.svelte +250 -250
- package/dist/SvGrid.svelte +3131 -3131
- package/dist/SvGridBoard.svelte +2339 -2339
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridDropdown.svelte +689 -689
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +117 -117
- package/dist/SvNumberInput.svelte +131 -131
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPopover.svelte +139 -139
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvTooltip.svelte +127 -127
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +377 -377
- package/dist/SvTreeSelect.svelte +230 -230
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/chart-export.js +8 -8
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/SvCarousel.svelte +141 -141
- package/src/SvColorInput.svelte +187 -187
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvDrawer.svelte +250 -250
- package/src/SvGrid.controller.svelte.ts +3158 -3158
- package/src/SvGrid.svelte +3131 -3131
- package/src/SvGrid.types.ts +1489 -1489
- package/src/SvGridBoard.svelte +2339 -2339
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridDropdown.svelte +689 -689
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +117 -117
- package/src/SvNumberInput.svelte +131 -131
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPopover.svelte +139 -139
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvTooltip.svelte +127 -127
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +377 -377
- package/src/SvTreeSelect.svelte +230 -230
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/build-api.ts +870 -870
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +179 -179
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +162 -162
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +202 -202
- package/src/createPopoverSelect.svelte.ts +206 -206
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/flex-render.ts +3 -3
- package/src/index.ts +623 -623
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/menus.ts +639 -639
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +501 -501
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +527 -527
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +159 -159
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
package/src/SvScrollArea.svelte
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvScrollArea - a scroll container with the kit's themed custom scrollbars
|
|
4
|
-
* (Firefox `scrollbar-color` + Chromium `::-webkit-scrollbar`) consuming the
|
|
5
|
-
* `--sg-scrollbar-*` tokens, so any panel matches the grid/demos without
|
|
6
|
-
* hand-rolling scrollbar CSS. The fallbacks derive from `--sg-fg` so it also
|
|
7
|
-
* adapts to light/dark when no preset defines the tokens.
|
|
8
|
-
*
|
|
9
|
-
* ```svelte
|
|
10
|
-
* <SvScrollArea maxHeight="240px">…long content…</SvScrollArea>
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
import type { Snippet } from 'svelte'
|
|
14
|
-
|
|
15
|
-
type Props = {
|
|
16
|
-
maxHeight?: string
|
|
17
|
-
height?: string
|
|
18
|
-
/** Scroll horizontally instead of vertically. */
|
|
19
|
-
horizontal?: boolean
|
|
20
|
-
/** Accessible name for the scrollable region, announced by screen readers. */
|
|
21
|
-
ariaLabel?: string
|
|
22
|
-
children?: Snippet
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let { maxHeight, height, horizontal = false, ariaLabel = 'Scrollable content', children }: Props = $props()
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
29
|
-
<div
|
|
30
|
-
class="sv-scroll"
|
|
31
|
-
class:is-x={horizontal}
|
|
32
|
-
style:max-height={maxHeight}
|
|
33
|
-
style:height={height}
|
|
34
|
-
tabindex="0"
|
|
35
|
-
role="region"
|
|
36
|
-
aria-label={ariaLabel}
|
|
37
|
-
>
|
|
38
|
-
{@render children?.()}
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<style>
|
|
42
|
-
.sv-scroll {
|
|
43
|
-
overflow: auto; overscroll-behavior: contain; min-height: 0;
|
|
44
|
-
scrollbar-width: thin;
|
|
45
|
-
scrollbar-color: var(--sg-scrollbar-thumb, color-mix(in srgb, var(--sg-fg, #0f172a) 26%, transparent)) var(--sg-scrollbar-bg, transparent);
|
|
46
|
-
}
|
|
47
|
-
.sv-scroll.is-x { overflow: auto hidden; }
|
|
48
|
-
.sv-scroll::-webkit-scrollbar { width: 12px; height: 12px; }
|
|
49
|
-
.sv-scroll::-webkit-scrollbar-track {
|
|
50
|
-
background: var(--sg-scrollbar-bg, transparent);
|
|
51
|
-
box-shadow: inset 1px 0 0 0 var(--sg-scrollbar-border, transparent);
|
|
52
|
-
}
|
|
53
|
-
.sv-scroll::-webkit-scrollbar-thumb {
|
|
54
|
-
background: var(--sg-scrollbar-thumb, color-mix(in srgb, var(--sg-fg, #0f172a) 26%, transparent));
|
|
55
|
-
border-radius: var(--sg-scrollbar-thumb-radius, 6px);
|
|
56
|
-
border: 3px solid transparent; background-clip: padding-box;
|
|
57
|
-
}
|
|
58
|
-
.sv-scroll::-webkit-scrollbar-thumb:hover { background: var(--sg-scrollbar-thumb-hover, color-mix(in srgb, var(--sg-fg, #0f172a) 38%, transparent)); }
|
|
59
|
-
.sv-scroll::-webkit-scrollbar-thumb:active { background: var(--sg-scrollbar-thumb-active, color-mix(in srgb, var(--sg-fg, #0f172a) 50%, transparent)); }
|
|
60
|
-
.sv-scroll::-webkit-scrollbar-corner { background: var(--sg-scrollbar-bg, transparent); }
|
|
61
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvScrollArea - a scroll container with the kit's themed custom scrollbars
|
|
4
|
+
* (Firefox `scrollbar-color` + Chromium `::-webkit-scrollbar`) consuming the
|
|
5
|
+
* `--sg-scrollbar-*` tokens, so any panel matches the grid/demos without
|
|
6
|
+
* hand-rolling scrollbar CSS. The fallbacks derive from `--sg-fg` so it also
|
|
7
|
+
* adapts to light/dark when no preset defines the tokens.
|
|
8
|
+
*
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <SvScrollArea maxHeight="240px">…long content…</SvScrollArea>
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import type { Snippet } from 'svelte'
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
maxHeight?: string
|
|
17
|
+
height?: string
|
|
18
|
+
/** Scroll horizontally instead of vertically. */
|
|
19
|
+
horizontal?: boolean
|
|
20
|
+
/** Accessible name for the scrollable region, announced by screen readers. */
|
|
21
|
+
ariaLabel?: string
|
|
22
|
+
children?: Snippet
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let { maxHeight, height, horizontal = false, ariaLabel = 'Scrollable content', children }: Props = $props()
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
29
|
+
<div
|
|
30
|
+
class="sv-scroll"
|
|
31
|
+
class:is-x={horizontal}
|
|
32
|
+
style:max-height={maxHeight}
|
|
33
|
+
style:height={height}
|
|
34
|
+
tabindex="0"
|
|
35
|
+
role="region"
|
|
36
|
+
aria-label={ariaLabel}
|
|
37
|
+
>
|
|
38
|
+
{@render children?.()}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.sv-scroll {
|
|
43
|
+
overflow: auto; overscroll-behavior: contain; min-height: 0;
|
|
44
|
+
scrollbar-width: thin;
|
|
45
|
+
scrollbar-color: var(--sg-scrollbar-thumb, color-mix(in srgb, var(--sg-fg, #0f172a) 26%, transparent)) var(--sg-scrollbar-bg, transparent);
|
|
46
|
+
}
|
|
47
|
+
.sv-scroll.is-x { overflow: auto hidden; }
|
|
48
|
+
.sv-scroll::-webkit-scrollbar { width: 12px; height: 12px; }
|
|
49
|
+
.sv-scroll::-webkit-scrollbar-track {
|
|
50
|
+
background: var(--sg-scrollbar-bg, transparent);
|
|
51
|
+
box-shadow: inset 1px 0 0 0 var(--sg-scrollbar-border, transparent);
|
|
52
|
+
}
|
|
53
|
+
.sv-scroll::-webkit-scrollbar-thumb {
|
|
54
|
+
background: var(--sg-scrollbar-thumb, color-mix(in srgb, var(--sg-fg, #0f172a) 26%, transparent));
|
|
55
|
+
border-radius: var(--sg-scrollbar-thumb-radius, 6px);
|
|
56
|
+
border: 3px solid transparent; background-clip: padding-box;
|
|
57
|
+
}
|
|
58
|
+
.sv-scroll::-webkit-scrollbar-thumb:hover { background: var(--sg-scrollbar-thumb-hover, color-mix(in srgb, var(--sg-fg, #0f172a) 38%, transparent)); }
|
|
59
|
+
.sv-scroll::-webkit-scrollbar-thumb:active { background: var(--sg-scrollbar-thumb-active, color-mix(in srgb, var(--sg-fg, #0f172a) 50%, transparent)); }
|
|
60
|
+
.sv-scroll::-webkit-scrollbar-corner { background: var(--sg-scrollbar-bg, transparent); }
|
|
61
|
+
</style>
|
package/src/SvTooltip.svelte
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvTooltip - a small hover/focus tooltip anchored to its child, portalled to
|
|
4
|
-
* <body> so it is never clipped. Shows after a short delay, hides on leave /
|
|
5
|
-
* blur / Escape, and is wired via `aria-describedby` (role="tooltip") onto the
|
|
6
|
-
* focusable child itself, so AT users get it announced on focus, not just
|
|
7
|
-
* sighted users on hover.
|
|
8
|
-
*
|
|
9
|
-
* ```svelte
|
|
10
|
-
* <SvTooltip text="Delete row"><button aria-label="Delete">🗑</button></SvTooltip>
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
import type { Snippet } from 'svelte'
|
|
14
|
-
import { portalToBody, popIn } from './popover'
|
|
15
|
-
import { getFocusable } from './a11y/focus-trap'
|
|
16
|
-
import { createTooltip } from './createTooltip.svelte'
|
|
17
|
-
|
|
18
|
-
type Props = {
|
|
19
|
-
text: string
|
|
20
|
-
placement?: 'top' | 'bottom'
|
|
21
|
-
/** Show delay in ms. Default 300. */
|
|
22
|
-
delay?: number
|
|
23
|
-
disabled?: boolean
|
|
24
|
-
children?: Snippet
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let { text, placement = 'top', delay = 300, disabled = false, children }: Props = $props()
|
|
28
|
-
|
|
29
|
-
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
30
|
-
let tipEl = $state<HTMLDivElement | null>(null)
|
|
31
|
-
let pos = $state({ top: 0, left: 0, arrowX: 12 })
|
|
32
|
-
|
|
33
|
-
// Open state, show-delay and Escape-to-hide live in the shared headless core;
|
|
34
|
-
// this component owns only the DOM concerns (positioning + describedby merge).
|
|
35
|
-
const tip = createTooltip({ text: () => text, disabled: () => disabled, delay: () => delay })
|
|
36
|
-
const tid = tip.tipId
|
|
37
|
-
|
|
38
|
-
function place() {
|
|
39
|
-
if (!anchorEl) return
|
|
40
|
-
const r = anchorEl.getBoundingClientRect()
|
|
41
|
-
const tipH = tipEl?.offsetHeight ?? 30
|
|
42
|
-
const tipW = tipEl?.offsetWidth ?? 0
|
|
43
|
-
const top = placement === 'top' ? r.top - tipH - 8 : r.bottom + 8
|
|
44
|
-
let left = r.left + r.width / 2 - tipW / 2
|
|
45
|
-
left = Math.max(4, Math.min(left, window.innerWidth - tipW - 4))
|
|
46
|
-
// Keep the arrow under the anchor's center even when the tip is clamped.
|
|
47
|
-
const arrowX = Math.max(10, Math.min(tipW - 10, r.left + r.width / 2 - left))
|
|
48
|
-
pos = { top, left, arrowX }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Reposition while open (and on scroll/resize). Escape-to-hide is the core's.
|
|
52
|
-
$effect(() => {
|
|
53
|
-
if (!tip.open) return
|
|
54
|
-
queueMicrotask(place)
|
|
55
|
-
const rp = () => place()
|
|
56
|
-
window.addEventListener('scroll', rp, true)
|
|
57
|
-
window.addEventListener('resize', rp)
|
|
58
|
-
return () => {
|
|
59
|
-
window.removeEventListener('scroll', rp, true)
|
|
60
|
-
window.removeEventListener('resize', rp)
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
// The wrapper span above is never itself Tab-focusable, so putting
|
|
65
|
-
// aria-describedby only on it does nothing for keyboard/AT users - they
|
|
66
|
-
// focus the real interactive child (e.g. the <button> in the doc example),
|
|
67
|
-
// and that's the element whose accessible description actually gets
|
|
68
|
-
// announced. Mirror the id onto that child too, merging with (and later
|
|
69
|
-
// restoring) any aria-describedby it already carries.
|
|
70
|
-
$effect(() => {
|
|
71
|
-
if (!tip.open || !anchorEl) return
|
|
72
|
-
const child = getFocusable(anchorEl)[0]
|
|
73
|
-
if (!child) return
|
|
74
|
-
const before = child.getAttribute('aria-describedby')
|
|
75
|
-
const ids = new Set((before ?? '').split(/\s+/).filter(Boolean))
|
|
76
|
-
ids.add(tid)
|
|
77
|
-
child.setAttribute('aria-describedby', [...ids].join(' '))
|
|
78
|
-
return () => {
|
|
79
|
-
const remaining = (child.getAttribute('aria-describedby') ?? '')
|
|
80
|
-
.split(/\s+/)
|
|
81
|
-
.filter((id) => id && id !== tid)
|
|
82
|
-
if (remaining.length) child.setAttribute('aria-describedby', remaining.join(' '))
|
|
83
|
-
else child.removeAttribute('aria-describedby')
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
</script>
|
|
87
|
-
|
|
88
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
89
|
-
<span
|
|
90
|
-
bind:this={anchorEl}
|
|
91
|
-
class="sv-tip__anchor"
|
|
92
|
-
{...tip.anchorProps()}
|
|
93
|
-
>
|
|
94
|
-
{@render children?.()}
|
|
95
|
-
</span>
|
|
96
|
-
|
|
97
|
-
{#if tip.open}
|
|
98
|
-
<div
|
|
99
|
-
bind:this={tipEl}
|
|
100
|
-
id={tid}
|
|
101
|
-
class="sv-tip"
|
|
102
|
-
role="tooltip"
|
|
103
|
-
use:portalToBody
|
|
104
|
-
use:popIn={{ up: placement === 'top', duration: 110, scale: 0.9 }}
|
|
105
|
-
class:sv-tip--bottom={placement === 'bottom'}
|
|
106
|
-
style:position="fixed"
|
|
107
|
-
style:top={`${pos.top}px`}
|
|
108
|
-
style:left={`${pos.left}px`}
|
|
109
|
-
style:--tip-arrow-x={`${pos.arrowX}px`}
|
|
110
|
-
>{text}<span class="sv-tip__arrow" aria-hidden="true"></span></div>
|
|
111
|
-
{/if}
|
|
112
|
-
|
|
113
|
-
<style>
|
|
114
|
-
.sv-tip__anchor { display: inline-flex; }
|
|
115
|
-
:global(.sv-tip) {
|
|
116
|
-
z-index: 2147483647; pointer-events: none; position: relative;
|
|
117
|
-
max-width: 240px; padding: 5px 9px; font-size: 12px; line-height: 1.4; font-weight: 500;
|
|
118
|
-
background: var(--sg-fg, #0f172a); color: var(--sg-bg, #fff);
|
|
119
|
-
border-radius: 6px; box-shadow: 0 6px 20px -6px rgba(15, 23, 42, 0.5); white-space: normal;
|
|
120
|
-
}
|
|
121
|
-
:global(.sv-tip__arrow) {
|
|
122
|
-
position: absolute; inset-inline-start: var(--tip-arrow-x, 12px); width: 8px; height: 8px;
|
|
123
|
-
background: var(--sg-fg, #0f172a); transform: translateX(-50%) rotate(45deg);
|
|
124
|
-
}
|
|
125
|
-
:global(.sv-tip:not(.sv-tip--bottom) .sv-tip__arrow) { bottom: -3px; }
|
|
126
|
-
:global(.sv-tip--bottom .sv-tip__arrow) { top: -3px; }
|
|
127
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvTooltip - a small hover/focus tooltip anchored to its child, portalled to
|
|
4
|
+
* <body> so it is never clipped. Shows after a short delay, hides on leave /
|
|
5
|
+
* blur / Escape, and is wired via `aria-describedby` (role="tooltip") onto the
|
|
6
|
+
* focusable child itself, so AT users get it announced on focus, not just
|
|
7
|
+
* sighted users on hover.
|
|
8
|
+
*
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <SvTooltip text="Delete row"><button aria-label="Delete">🗑</button></SvTooltip>
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import type { Snippet } from 'svelte'
|
|
14
|
+
import { portalToBody, popIn } from './popover'
|
|
15
|
+
import { getFocusable } from './a11y/focus-trap'
|
|
16
|
+
import { createTooltip } from './createTooltip.svelte'
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
text: string
|
|
20
|
+
placement?: 'top' | 'bottom'
|
|
21
|
+
/** Show delay in ms. Default 300. */
|
|
22
|
+
delay?: number
|
|
23
|
+
disabled?: boolean
|
|
24
|
+
children?: Snippet
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { text, placement = 'top', delay = 300, disabled = false, children }: Props = $props()
|
|
28
|
+
|
|
29
|
+
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
30
|
+
let tipEl = $state<HTMLDivElement | null>(null)
|
|
31
|
+
let pos = $state({ top: 0, left: 0, arrowX: 12 })
|
|
32
|
+
|
|
33
|
+
// Open state, show-delay and Escape-to-hide live in the shared headless core;
|
|
34
|
+
// this component owns only the DOM concerns (positioning + describedby merge).
|
|
35
|
+
const tip = createTooltip({ text: () => text, disabled: () => disabled, delay: () => delay })
|
|
36
|
+
const tid = tip.tipId
|
|
37
|
+
|
|
38
|
+
function place() {
|
|
39
|
+
if (!anchorEl) return
|
|
40
|
+
const r = anchorEl.getBoundingClientRect()
|
|
41
|
+
const tipH = tipEl?.offsetHeight ?? 30
|
|
42
|
+
const tipW = tipEl?.offsetWidth ?? 0
|
|
43
|
+
const top = placement === 'top' ? r.top - tipH - 8 : r.bottom + 8
|
|
44
|
+
let left = r.left + r.width / 2 - tipW / 2
|
|
45
|
+
left = Math.max(4, Math.min(left, window.innerWidth - tipW - 4))
|
|
46
|
+
// Keep the arrow under the anchor's center even when the tip is clamped.
|
|
47
|
+
const arrowX = Math.max(10, Math.min(tipW - 10, r.left + r.width / 2 - left))
|
|
48
|
+
pos = { top, left, arrowX }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Reposition while open (and on scroll/resize). Escape-to-hide is the core's.
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!tip.open) return
|
|
54
|
+
queueMicrotask(place)
|
|
55
|
+
const rp = () => place()
|
|
56
|
+
window.addEventListener('scroll', rp, true)
|
|
57
|
+
window.addEventListener('resize', rp)
|
|
58
|
+
return () => {
|
|
59
|
+
window.removeEventListener('scroll', rp, true)
|
|
60
|
+
window.removeEventListener('resize', rp)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// The wrapper span above is never itself Tab-focusable, so putting
|
|
65
|
+
// aria-describedby only on it does nothing for keyboard/AT users - they
|
|
66
|
+
// focus the real interactive child (e.g. the <button> in the doc example),
|
|
67
|
+
// and that's the element whose accessible description actually gets
|
|
68
|
+
// announced. Mirror the id onto that child too, merging with (and later
|
|
69
|
+
// restoring) any aria-describedby it already carries.
|
|
70
|
+
$effect(() => {
|
|
71
|
+
if (!tip.open || !anchorEl) return
|
|
72
|
+
const child = getFocusable(anchorEl)[0]
|
|
73
|
+
if (!child) return
|
|
74
|
+
const before = child.getAttribute('aria-describedby')
|
|
75
|
+
const ids = new Set((before ?? '').split(/\s+/).filter(Boolean))
|
|
76
|
+
ids.add(tid)
|
|
77
|
+
child.setAttribute('aria-describedby', [...ids].join(' '))
|
|
78
|
+
return () => {
|
|
79
|
+
const remaining = (child.getAttribute('aria-describedby') ?? '')
|
|
80
|
+
.split(/\s+/)
|
|
81
|
+
.filter((id) => id && id !== tid)
|
|
82
|
+
if (remaining.length) child.setAttribute('aria-describedby', remaining.join(' '))
|
|
83
|
+
else child.removeAttribute('aria-describedby')
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
89
|
+
<span
|
|
90
|
+
bind:this={anchorEl}
|
|
91
|
+
class="sv-tip__anchor"
|
|
92
|
+
{...tip.anchorProps()}
|
|
93
|
+
>
|
|
94
|
+
{@render children?.()}
|
|
95
|
+
</span>
|
|
96
|
+
|
|
97
|
+
{#if tip.open}
|
|
98
|
+
<div
|
|
99
|
+
bind:this={tipEl}
|
|
100
|
+
id={tid}
|
|
101
|
+
class="sv-tip"
|
|
102
|
+
role="tooltip"
|
|
103
|
+
use:portalToBody
|
|
104
|
+
use:popIn={{ up: placement === 'top', duration: 110, scale: 0.9 }}
|
|
105
|
+
class:sv-tip--bottom={placement === 'bottom'}
|
|
106
|
+
style:position="fixed"
|
|
107
|
+
style:top={`${pos.top}px`}
|
|
108
|
+
style:left={`${pos.left}px`}
|
|
109
|
+
style:--tip-arrow-x={`${pos.arrowX}px`}
|
|
110
|
+
>{text}<span class="sv-tip__arrow" aria-hidden="true"></span></div>
|
|
111
|
+
{/if}
|
|
112
|
+
|
|
113
|
+
<style>
|
|
114
|
+
.sv-tip__anchor { display: inline-flex; }
|
|
115
|
+
:global(.sv-tip) {
|
|
116
|
+
z-index: 2147483647; pointer-events: none; position: relative;
|
|
117
|
+
max-width: 240px; padding: 5px 9px; font-size: 12px; line-height: 1.4; font-weight: 500;
|
|
118
|
+
background: var(--sg-fg, #0f172a); color: var(--sg-bg, #fff);
|
|
119
|
+
border-radius: 6px; box-shadow: 0 6px 20px -6px rgba(15, 23, 42, 0.5); white-space: normal;
|
|
120
|
+
}
|
|
121
|
+
:global(.sv-tip__arrow) {
|
|
122
|
+
position: absolute; inset-inline-start: var(--tip-arrow-x, 12px); width: 8px; height: 8px;
|
|
123
|
+
background: var(--sg-fg, #0f172a); transform: translateX(-50%) rotate(45deg);
|
|
124
|
+
}
|
|
125
|
+
:global(.sv-tip:not(.sv-tip--bottom) .sv-tip__arrow) { bottom: -3px; }
|
|
126
|
+
:global(.sv-tip--bottom .sv-tip__arrow) { top: -3px; }
|
|
127
|
+
</style>
|