@svgrid/grid 2.2.25 → 2.2.27
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/README.md +199 -145
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +734 -728
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +250 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2723 -2719
- package/dist/SvGrid.svelte +3333 -3333
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +287 -287
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +131 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/a11y/dismissable.d.ts +11 -0
- package/dist/a11y/dismissable.js +21 -0
- package/dist/ai.js +3 -3
- package/dist/cdn/GridMenus-BzxEYNtW.js +421 -0
- package/dist/cdn/GridMenus-Dr-nGEXn.js +417 -0
- package/dist/cdn/{SvDateTimePicker-CYC7gRER.js → SvDateTimePicker-B1sDc9Pz.js} +451 -444
- package/dist/cdn/{SvDateTimePicker-DtQKd4Ns.js → SvDateTimePicker-Bh0FTs1E.js} +114 -107
- package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-BKQsO5fZ.js} +1 -1
- package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Bgk4hDs8.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-DSkHIQDd.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-DkCc2Z7b.js} +1 -1
- package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-BrbVaELm.js} +1 -1
- package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-i9rWvSKg.js} +1 -1
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CGxxopQ4.js} +1 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D08sVmAy.js} +1 -1
- package/dist/cdn/{src-DysW9qbk.js → src-Cw8d9U8j.js} +5753 -5753
- package/dist/cdn/{src-bYfr_7aR.js → src-cgs1wcUS.js} +8102 -8102
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +30 -13
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +734 -728
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +250 -250
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3239 -3239
- package/src/SvGrid.css +2723 -2719
- package/src/SvGrid.svelte +3333 -3333
- package/src/SvGrid.types.ts +1613 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +287 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +131 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -95
- package/src/a11y/dismissable.ts +114 -94
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- 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 +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- 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 +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +157 -157
- 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/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +713 -712
- package/src/js-scroller.svelte.ts +173 -173
- 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/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +700 -700
- package/src/menus.ts +652 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- 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 +524 -524
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-scroll.test.ts +102 -0
- 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 +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- 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/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-GfCAHgkZ.js +0 -420
- package/dist/cdn/GridMenus-GiVNSeDU.js +0 -416
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvToggleButton - a button with a pressed on/off state (aria-pressed).
|
|
4
|
-
* Parity: Smart `smart-toggle-button`. Controlled via `pressed` + `onChange`.
|
|
5
|
-
*/
|
|
6
|
-
import type { Snippet } from 'svelte'
|
|
7
|
-
import SvField from './SvField.svelte'
|
|
8
|
-
import { nextEditorId, type SvEditorProps } from './editor-contract'
|
|
9
|
-
import { createToggle } from './createToggle.svelte'
|
|
10
|
-
|
|
11
|
-
type Props = SvEditorProps & {
|
|
12
|
-
pressed?: boolean
|
|
13
|
-
onChange?: (pressed: boolean) => void
|
|
14
|
-
children?: Snippet
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let {
|
|
18
|
-
pressed = false,
|
|
19
|
-
onChange,
|
|
20
|
-
disabled = false,
|
|
21
|
-
readonly = false,
|
|
22
|
-
size = 'md',
|
|
23
|
-
ariaLabel,
|
|
24
|
-
invalid = false,
|
|
25
|
-
required = false,
|
|
26
|
-
error,
|
|
27
|
-
label,
|
|
28
|
-
hint,
|
|
29
|
-
dir,
|
|
30
|
-
id,
|
|
31
|
-
children,
|
|
32
|
-
}: Props = $props()
|
|
33
|
-
|
|
34
|
-
const autoId = nextEditorId('sv-toggle')
|
|
35
|
-
const uid = $derived(id ?? autoId)
|
|
36
|
-
|
|
37
|
-
// The styled toggle is just a renderer over the headless core.
|
|
38
|
-
const t = createToggle({
|
|
39
|
-
pressed: () => pressed,
|
|
40
|
-
onChange: (v) => { if (readonly) return; onChange?.(v) },
|
|
41
|
-
disabled: () => disabled,
|
|
42
|
-
ariaLabel: () => ariaLabel,
|
|
43
|
-
id: () => uid,
|
|
44
|
-
invalid: () => invalid,
|
|
45
|
-
required: () => required,
|
|
46
|
-
error: () => error,
|
|
47
|
-
hint: () => hint,
|
|
48
|
-
})
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
52
|
-
<button
|
|
53
|
-
class="sv-toggle sv-toggle--{size}"
|
|
54
|
-
class:is-pressed={pressed}
|
|
55
|
-
class:is-invalid={invalid}
|
|
56
|
-
class:is-readonly={readonly}
|
|
57
|
-
{...t.buttonProps()}
|
|
58
|
-
>
|
|
59
|
-
{#if children}{@render children()}{/if}
|
|
60
|
-
</button>
|
|
61
|
-
</SvField>
|
|
62
|
-
|
|
63
|
-
<style>
|
|
64
|
-
.sv-toggle {
|
|
65
|
-
--_accent: var(--sg-accent, #2563eb);
|
|
66
|
-
--_border: var(--sg-border, #cbd5e1);
|
|
67
|
-
--_radius: var(--sg-radius, 8px);
|
|
68
|
-
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
69
|
-
font: inherit; font-weight: 600; line-height: 1;
|
|
70
|
-
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
71
|
-
border: 1px solid var(--_border); border-radius: var(--_radius); cursor: pointer; user-select: none;
|
|
72
|
-
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
73
|
-
}
|
|
74
|
-
.sv-toggle--sm { padding: 5px 10px; font-size: 12px; }
|
|
75
|
-
.sv-toggle--md { padding: 8px 13px; font-size: 13px; }
|
|
76
|
-
.sv-toggle--lg { padding: 11px 17px; font-size: 15px; }
|
|
77
|
-
.sv-toggle:hover:not([disabled]):not(.is-pressed) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
78
|
-
.sv-toggle.is-pressed {
|
|
79
|
-
background: var(--_accent); color: var(--sg-on-accent, #fff); border-color: var(--_accent);
|
|
80
|
-
}
|
|
81
|
-
.sv-toggle[disabled] { opacity: 0.55; cursor: not-allowed; }
|
|
82
|
-
.sv-toggle.is-readonly { cursor: default; }
|
|
83
|
-
.sv-toggle.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
84
|
-
.sv-toggle:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
85
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvToggleButton - a button with a pressed on/off state (aria-pressed).
|
|
4
|
+
* Parity: Smart `smart-toggle-button`. Controlled via `pressed` + `onChange`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Snippet } from 'svelte'
|
|
7
|
+
import SvField from './SvField.svelte'
|
|
8
|
+
import { nextEditorId, type SvEditorProps } from './editor-contract'
|
|
9
|
+
import { createToggle } from './createToggle.svelte'
|
|
10
|
+
|
|
11
|
+
type Props = SvEditorProps & {
|
|
12
|
+
pressed?: boolean
|
|
13
|
+
onChange?: (pressed: boolean) => void
|
|
14
|
+
children?: Snippet
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
pressed = false,
|
|
19
|
+
onChange,
|
|
20
|
+
disabled = false,
|
|
21
|
+
readonly = false,
|
|
22
|
+
size = 'md',
|
|
23
|
+
ariaLabel,
|
|
24
|
+
invalid = false,
|
|
25
|
+
required = false,
|
|
26
|
+
error,
|
|
27
|
+
label,
|
|
28
|
+
hint,
|
|
29
|
+
dir,
|
|
30
|
+
id,
|
|
31
|
+
children,
|
|
32
|
+
}: Props = $props()
|
|
33
|
+
|
|
34
|
+
const autoId = nextEditorId('sv-toggle')
|
|
35
|
+
const uid = $derived(id ?? autoId)
|
|
36
|
+
|
|
37
|
+
// The styled toggle is just a renderer over the headless core.
|
|
38
|
+
const t = createToggle({
|
|
39
|
+
pressed: () => pressed,
|
|
40
|
+
onChange: (v) => { if (readonly) return; onChange?.(v) },
|
|
41
|
+
disabled: () => disabled,
|
|
42
|
+
ariaLabel: () => ariaLabel,
|
|
43
|
+
id: () => uid,
|
|
44
|
+
invalid: () => invalid,
|
|
45
|
+
required: () => required,
|
|
46
|
+
error: () => error,
|
|
47
|
+
hint: () => hint,
|
|
48
|
+
})
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
52
|
+
<button
|
|
53
|
+
class="sv-toggle sv-toggle--{size}"
|
|
54
|
+
class:is-pressed={pressed}
|
|
55
|
+
class:is-invalid={invalid}
|
|
56
|
+
class:is-readonly={readonly}
|
|
57
|
+
{...t.buttonProps()}
|
|
58
|
+
>
|
|
59
|
+
{#if children}{@render children()}{/if}
|
|
60
|
+
</button>
|
|
61
|
+
</SvField>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.sv-toggle {
|
|
65
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
66
|
+
--_border: var(--sg-border, #cbd5e1);
|
|
67
|
+
--_radius: var(--sg-radius, 8px);
|
|
68
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
69
|
+
font: inherit; font-weight: 600; line-height: 1;
|
|
70
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
71
|
+
border: 1px solid var(--_border); border-radius: var(--_radius); cursor: pointer; user-select: none;
|
|
72
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
73
|
+
}
|
|
74
|
+
.sv-toggle--sm { padding: 5px 10px; font-size: 12px; }
|
|
75
|
+
.sv-toggle--md { padding: 8px 13px; font-size: 13px; }
|
|
76
|
+
.sv-toggle--lg { padding: 11px 17px; font-size: 15px; }
|
|
77
|
+
.sv-toggle:hover:not([disabled]):not(.is-pressed) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
78
|
+
.sv-toggle.is-pressed {
|
|
79
|
+
background: var(--_accent); color: var(--sg-on-accent, #fff); border-color: var(--_accent);
|
|
80
|
+
}
|
|
81
|
+
.sv-toggle[disabled] { opacity: 0.55; cursor: not-allowed; }
|
|
82
|
+
.sv-toggle.is-readonly { cursor: default; }
|
|
83
|
+
.sv-toggle.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
84
|
+
.sv-toggle:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
85
|
+
</style>
|
package/src/SvTooltip.svelte
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
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. Positioned by the shared engine (full placement
|
|
5
|
-
* matrix + flip + shift + arrow), shows after a short delay, hides on leave /
|
|
6
|
-
* blur / Escape, and is wired via `aria-describedby` (role="tooltip") onto the
|
|
7
|
-
* focusable child itself, so AT users get it announced on focus, not just
|
|
8
|
-
* sighted users on hover.
|
|
9
|
-
*
|
|
10
|
-
* Set `group` on several tooltips so scanning between them skips the re-delay,
|
|
11
|
-
* `closeDelay` + `interactive` for a hoverable tip (e.g. one with a link).
|
|
12
|
-
*
|
|
13
|
-
* ```svelte
|
|
14
|
-
* <SvTooltip text="Delete row"><button aria-label="Delete">🗑</button></SvTooltip>
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
import type { Snippet } from 'svelte'
|
|
18
|
-
import { portalToBody, popIn } from './popover'
|
|
19
|
-
import { computePosition, autoUpdate, type Placement, type ComputePositionResult } from './positioning'
|
|
20
|
-
import { getFocusable } from './a11y/focus-trap'
|
|
21
|
-
import { createTooltip } from './createTooltip.svelte'
|
|
22
|
-
|
|
23
|
-
type Props = {
|
|
24
|
-
text: string
|
|
25
|
-
/** Preferred placement; flips when there is no room. Default `top`. */
|
|
26
|
-
placement?: Placement
|
|
27
|
-
/** Show delay in ms. Default 300. */
|
|
28
|
-
delay?: number
|
|
29
|
-
/** Hide delay in ms after leave (give it room for an interactive tip). Default 0. */
|
|
30
|
-
closeDelay?: number
|
|
31
|
-
/** Delay-group id: grouped tooltips open instantly while the group is warm. */
|
|
32
|
-
group?: string
|
|
33
|
-
/** Let the pointer move onto the tip without closing it (e.g. a tip with a link). */
|
|
34
|
-
interactive?: boolean
|
|
35
|
-
disabled?: boolean
|
|
36
|
-
children?: Snippet
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let {
|
|
40
|
-
text,
|
|
41
|
-
placement = 'top',
|
|
42
|
-
delay = 300,
|
|
43
|
-
closeDelay = 0,
|
|
44
|
-
group,
|
|
45
|
-
interactive = false,
|
|
46
|
-
disabled = false,
|
|
47
|
-
children,
|
|
48
|
-
}: Props = $props()
|
|
49
|
-
|
|
50
|
-
const ARROW = 8
|
|
51
|
-
|
|
52
|
-
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
53
|
-
let tipEl = $state<HTMLDivElement | null>(null)
|
|
54
|
-
let pos = $state<ComputePositionResult>({
|
|
55
|
-
x: 0, y: 0, placement, side: 'top', align: 'center', maxWidth: 0, maxHeight: 0, arrow: { x: 12 },
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// Open state, show/close delay, delay-group and Escape-to-hide live in the
|
|
59
|
-
// shared headless core; this component owns only the DOM concerns (positioning
|
|
60
|
-
// + describedby merge). Interactive tips get a small closeDelay by default so
|
|
61
|
-
// the pointer can travel from anchor to tip.
|
|
62
|
-
const tip = createTooltip({
|
|
63
|
-
text: () => text,
|
|
64
|
-
disabled: () => disabled,
|
|
65
|
-
delay: () => delay,
|
|
66
|
-
closeDelay: () => (closeDelay || (interactive ? 120 : 0)),
|
|
67
|
-
group: () => group,
|
|
68
|
-
})
|
|
69
|
-
const tid = tip.tipId
|
|
70
|
-
|
|
71
|
-
function place() {
|
|
72
|
-
if (!anchorEl) return
|
|
73
|
-
const r = anchorEl.getBoundingClientRect()
|
|
74
|
-
const floating = { width: tipEl?.offsetWidth || 0, height: tipEl?.offsetHeight || 30 }
|
|
75
|
-
pos = computePosition(
|
|
76
|
-
{ x: r.left, y: r.top, width: r.width, height: r.height },
|
|
77
|
-
floating,
|
|
78
|
-
{ placement, offset: 8, padding: 4, arrow: { size: ARROW } },
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Reposition while open (measure the tip after it mounts, then keep in sync).
|
|
83
|
-
$effect(() => {
|
|
84
|
-
if (!tip.open || !anchorEl || !tipEl) return
|
|
85
|
-
queueMicrotask(place)
|
|
86
|
-
const stop = autoUpdate(anchorEl, tipEl, place)
|
|
87
|
-
return () => stop()
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
// The wrapper span above is never itself Tab-focusable, so putting
|
|
91
|
-
// aria-describedby only on it does nothing for keyboard/AT users - they
|
|
92
|
-
// focus the real interactive child (e.g. the <button> in the doc example),
|
|
93
|
-
// and that's the element whose accessible description actually gets
|
|
94
|
-
// announced. Mirror the id onto that child too, merging with (and later
|
|
95
|
-
// restoring) any aria-describedby it already carries.
|
|
96
|
-
$effect(() => {
|
|
97
|
-
if (!tip.open || !anchorEl) return
|
|
98
|
-
const child = getFocusable(anchorEl)[0]
|
|
99
|
-
if (!child) return
|
|
100
|
-
const before = child.getAttribute('aria-describedby')
|
|
101
|
-
const ids = new Set((before ?? '').split(/\s+/).filter(Boolean))
|
|
102
|
-
ids.add(tid)
|
|
103
|
-
child.setAttribute('aria-describedby', [...ids].join(' '))
|
|
104
|
-
return () => {
|
|
105
|
-
const remaining = (child.getAttribute('aria-describedby') ?? '')
|
|
106
|
-
.split(/\s+/)
|
|
107
|
-
.filter((id) => id && id !== tid)
|
|
108
|
-
if (remaining.length) child.setAttribute('aria-describedby', remaining.join(' '))
|
|
109
|
-
else child.removeAttribute('aria-describedby')
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
</script>
|
|
113
|
-
|
|
114
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
115
|
-
<span
|
|
116
|
-
bind:this={anchorEl}
|
|
117
|
-
class="sv-tip__anchor"
|
|
118
|
-
{...tip.anchorProps()}
|
|
119
|
-
>
|
|
120
|
-
{@render children?.()}
|
|
121
|
-
</span>
|
|
122
|
-
|
|
123
|
-
{#if tip.open}
|
|
124
|
-
<div
|
|
125
|
-
bind:this={tipEl}
|
|
126
|
-
id={tid}
|
|
127
|
-
class="sv-tip sv-tip--{pos.side}"
|
|
128
|
-
class:sv-tip--interactive={interactive}
|
|
129
|
-
role="tooltip"
|
|
130
|
-
use:portalToBody
|
|
131
|
-
use:popIn={{ up: pos.side === 'top', duration: 110, scale: 0.9 }}
|
|
132
|
-
style:position="fixed"
|
|
133
|
-
style:top={`${pos.y}px`}
|
|
134
|
-
style:left={`${pos.x}px`}
|
|
135
|
-
style:--tip-arrow-x={pos.arrow?.x != null ? `${pos.arrow.x}px` : undefined}
|
|
136
|
-
style:--tip-arrow-y={pos.arrow?.y != null ? `${pos.arrow.y}px` : undefined}
|
|
137
|
-
onpointerenter={interactive ? tip.keepOpen : undefined}
|
|
138
|
-
onpointerleave={interactive ? tip.scheduleHide : undefined}
|
|
139
|
-
>{text}<span class="sv-tip__arrow" aria-hidden="true"></span></div>
|
|
140
|
-
{/if}
|
|
141
|
-
|
|
142
|
-
<style>
|
|
143
|
-
.sv-tip__anchor { display: inline-flex; }
|
|
144
|
-
:global(.sv-tip) {
|
|
145
|
-
z-index: 2147483647; pointer-events: none; position: relative;
|
|
146
|
-
max-width: 240px; padding: 5px 9px; font-size: 12px; line-height: 1.4; font-weight: 500;
|
|
147
|
-
background: var(--sg-fg, #0f172a); color: var(--sg-bg, #fff);
|
|
148
|
-
border-radius: 6px; box-shadow: 0 6px 20px -6px rgba(15, 23, 42, 0.5); white-space: normal;
|
|
149
|
-
}
|
|
150
|
-
:global(.sv-tip--interactive) { pointer-events: auto; }
|
|
151
|
-
:global(.sv-tip__arrow) {
|
|
152
|
-
position: absolute; width: 8px; height: 8px;
|
|
153
|
-
background: var(--sg-fg, #0f172a); transform: rotate(45deg);
|
|
154
|
-
}
|
|
155
|
-
/* Arrow sits on the edge facing the anchor. top/bottom use --tip-arrow-x,
|
|
156
|
-
left/right use --tip-arrow-y. */
|
|
157
|
-
:global(.sv-tip--top .sv-tip__arrow) { bottom: -3px; inset-inline-start: var(--tip-arrow-x, 12px); }
|
|
158
|
-
:global(.sv-tip--bottom .sv-tip__arrow) { top: -3px; inset-inline-start: var(--tip-arrow-x, 12px); }
|
|
159
|
-
:global(.sv-tip--left .sv-tip__arrow) { right: -3px; top: var(--tip-arrow-y, 12px); }
|
|
160
|
-
:global(.sv-tip--right .sv-tip__arrow) { left: -3px; top: var(--tip-arrow-y, 12px); }
|
|
161
|
-
</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. Positioned by the shared engine (full placement
|
|
5
|
+
* matrix + flip + shift + arrow), shows after a short delay, hides on leave /
|
|
6
|
+
* blur / Escape, and is wired via `aria-describedby` (role="tooltip") onto the
|
|
7
|
+
* focusable child itself, so AT users get it announced on focus, not just
|
|
8
|
+
* sighted users on hover.
|
|
9
|
+
*
|
|
10
|
+
* Set `group` on several tooltips so scanning between them skips the re-delay,
|
|
11
|
+
* `closeDelay` + `interactive` for a hoverable tip (e.g. one with a link).
|
|
12
|
+
*
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <SvTooltip text="Delete row"><button aria-label="Delete">🗑</button></SvTooltip>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import type { Snippet } from 'svelte'
|
|
18
|
+
import { portalToBody, popIn } from './popover'
|
|
19
|
+
import { computePosition, autoUpdate, type Placement, type ComputePositionResult } from './positioning'
|
|
20
|
+
import { getFocusable } from './a11y/focus-trap'
|
|
21
|
+
import { createTooltip } from './createTooltip.svelte'
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
text: string
|
|
25
|
+
/** Preferred placement; flips when there is no room. Default `top`. */
|
|
26
|
+
placement?: Placement
|
|
27
|
+
/** Show delay in ms. Default 300. */
|
|
28
|
+
delay?: number
|
|
29
|
+
/** Hide delay in ms after leave (give it room for an interactive tip). Default 0. */
|
|
30
|
+
closeDelay?: number
|
|
31
|
+
/** Delay-group id: grouped tooltips open instantly while the group is warm. */
|
|
32
|
+
group?: string
|
|
33
|
+
/** Let the pointer move onto the tip without closing it (e.g. a tip with a link). */
|
|
34
|
+
interactive?: boolean
|
|
35
|
+
disabled?: boolean
|
|
36
|
+
children?: Snippet
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
text,
|
|
41
|
+
placement = 'top',
|
|
42
|
+
delay = 300,
|
|
43
|
+
closeDelay = 0,
|
|
44
|
+
group,
|
|
45
|
+
interactive = false,
|
|
46
|
+
disabled = false,
|
|
47
|
+
children,
|
|
48
|
+
}: Props = $props()
|
|
49
|
+
|
|
50
|
+
const ARROW = 8
|
|
51
|
+
|
|
52
|
+
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
53
|
+
let tipEl = $state<HTMLDivElement | null>(null)
|
|
54
|
+
let pos = $state<ComputePositionResult>({
|
|
55
|
+
x: 0, y: 0, placement, side: 'top', align: 'center', maxWidth: 0, maxHeight: 0, arrow: { x: 12 },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Open state, show/close delay, delay-group and Escape-to-hide live in the
|
|
59
|
+
// shared headless core; this component owns only the DOM concerns (positioning
|
|
60
|
+
// + describedby merge). Interactive tips get a small closeDelay by default so
|
|
61
|
+
// the pointer can travel from anchor to tip.
|
|
62
|
+
const tip = createTooltip({
|
|
63
|
+
text: () => text,
|
|
64
|
+
disabled: () => disabled,
|
|
65
|
+
delay: () => delay,
|
|
66
|
+
closeDelay: () => (closeDelay || (interactive ? 120 : 0)),
|
|
67
|
+
group: () => group,
|
|
68
|
+
})
|
|
69
|
+
const tid = tip.tipId
|
|
70
|
+
|
|
71
|
+
function place() {
|
|
72
|
+
if (!anchorEl) return
|
|
73
|
+
const r = anchorEl.getBoundingClientRect()
|
|
74
|
+
const floating = { width: tipEl?.offsetWidth || 0, height: tipEl?.offsetHeight || 30 }
|
|
75
|
+
pos = computePosition(
|
|
76
|
+
{ x: r.left, y: r.top, width: r.width, height: r.height },
|
|
77
|
+
floating,
|
|
78
|
+
{ placement, offset: 8, padding: 4, arrow: { size: ARROW } },
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reposition while open (measure the tip after it mounts, then keep in sync).
|
|
83
|
+
$effect(() => {
|
|
84
|
+
if (!tip.open || !anchorEl || !tipEl) return
|
|
85
|
+
queueMicrotask(place)
|
|
86
|
+
const stop = autoUpdate(anchorEl, tipEl, place)
|
|
87
|
+
return () => stop()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// The wrapper span above is never itself Tab-focusable, so putting
|
|
91
|
+
// aria-describedby only on it does nothing for keyboard/AT users - they
|
|
92
|
+
// focus the real interactive child (e.g. the <button> in the doc example),
|
|
93
|
+
// and that's the element whose accessible description actually gets
|
|
94
|
+
// announced. Mirror the id onto that child too, merging with (and later
|
|
95
|
+
// restoring) any aria-describedby it already carries.
|
|
96
|
+
$effect(() => {
|
|
97
|
+
if (!tip.open || !anchorEl) return
|
|
98
|
+
const child = getFocusable(anchorEl)[0]
|
|
99
|
+
if (!child) return
|
|
100
|
+
const before = child.getAttribute('aria-describedby')
|
|
101
|
+
const ids = new Set((before ?? '').split(/\s+/).filter(Boolean))
|
|
102
|
+
ids.add(tid)
|
|
103
|
+
child.setAttribute('aria-describedby', [...ids].join(' '))
|
|
104
|
+
return () => {
|
|
105
|
+
const remaining = (child.getAttribute('aria-describedby') ?? '')
|
|
106
|
+
.split(/\s+/)
|
|
107
|
+
.filter((id) => id && id !== tid)
|
|
108
|
+
if (remaining.length) child.setAttribute('aria-describedby', remaining.join(' '))
|
|
109
|
+
else child.removeAttribute('aria-describedby')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
115
|
+
<span
|
|
116
|
+
bind:this={anchorEl}
|
|
117
|
+
class="sv-tip__anchor"
|
|
118
|
+
{...tip.anchorProps()}
|
|
119
|
+
>
|
|
120
|
+
{@render children?.()}
|
|
121
|
+
</span>
|
|
122
|
+
|
|
123
|
+
{#if tip.open}
|
|
124
|
+
<div
|
|
125
|
+
bind:this={tipEl}
|
|
126
|
+
id={tid}
|
|
127
|
+
class="sv-tip sv-tip--{pos.side}"
|
|
128
|
+
class:sv-tip--interactive={interactive}
|
|
129
|
+
role="tooltip"
|
|
130
|
+
use:portalToBody
|
|
131
|
+
use:popIn={{ up: pos.side === 'top', duration: 110, scale: 0.9 }}
|
|
132
|
+
style:position="fixed"
|
|
133
|
+
style:top={`${pos.y}px`}
|
|
134
|
+
style:left={`${pos.x}px`}
|
|
135
|
+
style:--tip-arrow-x={pos.arrow?.x != null ? `${pos.arrow.x}px` : undefined}
|
|
136
|
+
style:--tip-arrow-y={pos.arrow?.y != null ? `${pos.arrow.y}px` : undefined}
|
|
137
|
+
onpointerenter={interactive ? tip.keepOpen : undefined}
|
|
138
|
+
onpointerleave={interactive ? tip.scheduleHide : undefined}
|
|
139
|
+
>{text}<span class="sv-tip__arrow" aria-hidden="true"></span></div>
|
|
140
|
+
{/if}
|
|
141
|
+
|
|
142
|
+
<style>
|
|
143
|
+
.sv-tip__anchor { display: inline-flex; }
|
|
144
|
+
:global(.sv-tip) {
|
|
145
|
+
z-index: 2147483647; pointer-events: none; position: relative;
|
|
146
|
+
max-width: 240px; padding: 5px 9px; font-size: 12px; line-height: 1.4; font-weight: 500;
|
|
147
|
+
background: var(--sg-fg, #0f172a); color: var(--sg-bg, #fff);
|
|
148
|
+
border-radius: 6px; box-shadow: 0 6px 20px -6px rgba(15, 23, 42, 0.5); white-space: normal;
|
|
149
|
+
}
|
|
150
|
+
:global(.sv-tip--interactive) { pointer-events: auto; }
|
|
151
|
+
:global(.sv-tip__arrow) {
|
|
152
|
+
position: absolute; width: 8px; height: 8px;
|
|
153
|
+
background: var(--sg-fg, #0f172a); transform: rotate(45deg);
|
|
154
|
+
}
|
|
155
|
+
/* Arrow sits on the edge facing the anchor. top/bottom use --tip-arrow-x,
|
|
156
|
+
left/right use --tip-arrow-y. */
|
|
157
|
+
:global(.sv-tip--top .sv-tip__arrow) { bottom: -3px; inset-inline-start: var(--tip-arrow-x, 12px); }
|
|
158
|
+
:global(.sv-tip--bottom .sv-tip__arrow) { top: -3px; inset-inline-start: var(--tip-arrow-x, 12px); }
|
|
159
|
+
:global(.sv-tip--left .sv-tip__arrow) { right: -3px; top: var(--tip-arrow-y, 12px); }
|
|
160
|
+
:global(.sv-tip--right .sv-tip__arrow) { left: -3px; top: var(--tip-arrow-y, 12px); }
|
|
161
|
+
</style>
|