@juspay/svelte-ui-components 4.27.6 → 4.28.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/README.md +140 -36
- package/dist/Animations/ModalAnimation.svelte +15 -1
- package/dist/Animations/OverlayAnimation.svelte +7 -1
- package/dist/AreaChart/AreaChart.svelte +366 -85
- package/dist/AspectRatio/AspectRatio.svelte +51 -0
- package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
- package/dist/AspectRatio/properties.d.ts +27 -0
- package/dist/AspectRatio/properties.js +1 -0
- package/dist/Banner/Banner.svelte +10 -4
- package/dist/Banner/properties.d.ts +8 -0
- package/dist/BarChart/BarChart.svelte +156 -49
- package/dist/Book/Book.svelte +14 -1
- package/dist/BrandLoader/BrandLoader.svelte +76 -12
- package/dist/Button/Button.svelte +56 -11
- package/dist/Calendar/Calendar.svelte +88 -61
- package/dist/Card/Card.svelte +1 -1
- package/dist/Carousel/Carousel.svelte +6 -3
- package/dist/ChatComposer/ChatComposer.svelte +54 -4
- package/dist/ChatComposer/dictationState.d.ts +15 -0
- package/dist/ChatComposer/dictationState.js +9 -0
- package/dist/ChatComposer/properties.d.ts +28 -1
- package/dist/ChatMessage/ChatMessage.svelte +27 -1
- package/dist/CheckListItem/CheckListItem.svelte +3 -3
- package/dist/Checkbox/Checkbox.svelte +84 -3
- package/dist/Checkbox/properties.d.ts +45 -13
- package/dist/ChipInput/ChipInput.svelte +70 -3
- package/dist/ChipInput/properties.d.ts +16 -0
- package/dist/Choicebox/Choicebox.svelte +156 -1
- package/dist/Choicebox/group.d.ts +63 -0
- package/dist/Choicebox/group.js +127 -0
- package/dist/Choicebox/properties.d.ts +46 -0
- package/dist/ColorPicker/ColorPicker.svelte +169 -20
- package/dist/ColorPicker/properties.d.ts +16 -0
- package/dist/Combobox/Combobox.svelte +87 -25
- package/dist/Combobox/properties.d.ts +23 -0
- package/dist/CommandMenu/CommandMenu.svelte +27 -11
- package/dist/ContextMenu/ContextMenu.svelte +31 -37
- package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
- package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
- package/dist/Draggable/Draggable.svelte +12 -7
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
- package/dist/EmptyState/EmptyState.svelte +1 -1
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
- package/dist/FileInput/FileInput.svelte +50 -1
- package/dist/FileInput/properties.d.ts +7 -0
- package/dist/FunnelChart/FunnelChart.svelte +40 -12
- package/dist/FunnelChart/properties.d.ts +7 -1
- package/dist/Gallery/Gallery.svelte +20 -13
- package/dist/Gallery/properties.d.ts +8 -0
- package/dist/GridItem/GridItem.svelte +31 -1
- package/dist/HITL/HITL.svelte +35 -9
- package/dist/Icon/Icon.svelte +19 -3
- package/dist/Input/Input.svelte +24 -10
- package/dist/Input/properties.d.ts +37 -1
- package/dist/InputButton/InputButton.svelte +86 -11
- package/dist/InputButton/properties.d.ts +10 -1
- package/dist/Label/Label.svelte +54 -0
- package/dist/Label/Label.svelte.d.ts +3 -0
- package/dist/Label/properties.d.ts +26 -0
- package/dist/Label/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +385 -94
- package/dist/ListItem/ListItem.svelte +121 -15
- package/dist/Loader/Loader.svelte +22 -2
- package/dist/LoadingDots/LoadingDots.svelte +21 -0
- package/dist/MediaUpload/MediaUpload.svelte +13 -3
- package/dist/Menu/Menu.svelte +56 -28
- package/dist/Modal/Modal.svelte +77 -42
- package/dist/Pagination/Pagination.svelte +9 -2
- package/dist/Phone/Phone.svelte +4 -0
- package/dist/PieChart/PieChart.svelte +282 -38
- package/dist/PieChart/properties.d.ts +9 -0
- package/dist/Pill/Pill.svelte +14 -5
- package/dist/Progress/Progress.svelte +20 -0
- package/dist/Radio/Radio.svelte +58 -1
- package/dist/Radio/properties.d.ts +23 -0
- package/dist/RatingGroup/RatingGroup.svelte +283 -0
- package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
- package/dist/RatingGroup/properties.d.ts +73 -0
- package/dist/RatingGroup/properties.js +1 -0
- package/dist/RelativeTime/RelativeTime.svelte +1 -1
- package/dist/SankeyChart/SankeyChart.svelte +272 -50
- package/dist/SankeyChart/properties.d.ts +9 -0
- package/dist/Scroller/Scroller.svelte +37 -2
- package/dist/Select/Select.svelte +166 -46
- package/dist/Select/properties.d.ts +7 -0
- package/dist/Separator/Separator.svelte +48 -0
- package/dist/Separator/Separator.svelte.d.ts +3 -0
- package/dist/Separator/properties.d.ts +22 -0
- package/dist/Separator/properties.js +1 -0
- package/dist/Sheet/Sheet.svelte +60 -21
- package/dist/Shimmer/Shimmer.svelte +15 -0
- package/dist/Slider/Slider.svelte +87 -6
- package/dist/Slider/properties.d.ts +26 -0
- package/dist/Snippet/Snippet.svelte +4 -0
- package/dist/Snippet/copyState.svelte.d.ts +13 -1
- package/dist/Snippet/copyState.svelte.js +27 -1
- package/dist/Snippet/properties.d.ts +7 -0
- package/dist/SoundKit/SoundKit.js +9 -8
- package/dist/SplitInput/SplitInput.svelte +68 -2
- package/dist/SplitInput/properties.d.ts +23 -0
- package/dist/StatCard/StatCard.svelte +6 -6
- package/dist/Status/Status.svelte +2 -2
- package/dist/Stepper/Step.svelte +19 -6
- package/dist/Stepper/Stepper.svelte +108 -35
- package/dist/Table/BuiltinCell.svelte +2 -2
- package/dist/Table/Table.svelte +373 -55
- package/dist/Table/properties.d.ts +134 -3
- package/dist/Tabs/Tabs.svelte +263 -65
- package/dist/Tabs/properties.d.ts +23 -0
- package/dist/TaskList/TaskList.svelte +14 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
- package/dist/Toast/Toast.svelte +27 -2
- package/dist/Toggle/Toggle.svelte +79 -7
- package/dist/Toggle/Toggle.svelte.d.ts +1 -1
- package/dist/Toggle/properties.d.ts +30 -7
- package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
- package/dist/ToolCallLog/properties.d.ts +10 -0
- package/dist/Toolbar/Toolbar.svelte +1 -1
- package/dist/Tooltip/Tooltip.svelte +20 -2
- package/dist/Tooltip/tooltip-action.js +39 -4
- package/dist/TypewriterText/TypewriterText.svelte +34 -2
- package/dist/TypewriterText/properties.d.ts +18 -5
- package/dist/_chart/ChartContainer.svelte +12 -0
- package/dist/_chart/ChartTooltip.svelte +18 -4
- package/dist/_chart/Legend.svelte +21 -0
- package/dist/_chart/Legend.svelte.d.ts +15 -0
- package/dist/_chart/geometry.d.ts +54 -1
- package/dist/_chart/geometry.js +122 -14
- package/dist/_chart/interactions.d.ts +5 -1
- package/dist/_chart/interactions.js +9 -5
- package/dist/_chart/scales.js +16 -0
- package/dist/_chart/temporal.d.ts +45 -0
- package/dist/_chart/temporal.js +116 -0
- package/dist/_field/description.d.ts +50 -0
- package/dist/_field/description.js +51 -0
- package/dist/_interaction/dismissal.d.ts +67 -0
- package/dist/_interaction/dismissal.js +137 -0
- package/dist/_interaction/focus.d.ts +73 -0
- package/dist/_interaction/focus.js +143 -0
- package/dist/assets/star.svg +5 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +21 -1
- package/dist/reduced-motion.svelte.d.ts +37 -0
- package/dist/reduced-motion.svelte.js +44 -0
- package/dist/styles/theme-dark.css +778 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils.d.ts +62 -1
- package/dist/utils.js +80 -21
- package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
- package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
- package/dist-codemod/codemod/migrate-audit.js +395 -0
- package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
- package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
- package/dist-codemod/migrate/analyze.js +804 -0
- package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
- package/dist-wc/index.js +13992 -4323
- package/package.json +13 -6
- package/dist/Modal/focus-trap.d.ts +0 -42
- package/dist/Modal/focus-trap.js +0 -57
- /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
- /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
- /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
import { getColor } from '../_chart/colors';
|
|
7
7
|
import { formatNumber } from '../_chart/format';
|
|
8
8
|
import { measureText, readCssVarPx } from '../_chart/measure';
|
|
9
|
+
import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
|
|
9
10
|
import { truncateToWidth } from '../_chart/labels';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_CHART_CORNER_RADIUS,
|
|
13
|
+
DEFAULT_CHART_MAX_HEIGHT,
|
|
14
|
+
type TooltipAnchor
|
|
15
|
+
} from '../_chart/types';
|
|
11
16
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
12
17
|
|
|
13
18
|
// ── Props ──────────────────────────────────────────────────────
|
|
@@ -39,7 +44,8 @@
|
|
|
39
44
|
disableDimOnHover = false,
|
|
40
45
|
firstColumnLabelSide = 'left',
|
|
41
46
|
lastColumnLabelSide = 'right',
|
|
42
|
-
marginX = 40
|
|
47
|
+
marginX = 40,
|
|
48
|
+
tooltipPortal = false
|
|
43
49
|
}: SankeyChartProperties = $props();
|
|
44
50
|
|
|
45
51
|
// ── State ──────────────────────────────────────────────────────
|
|
@@ -47,11 +53,30 @@
|
|
|
47
53
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
48
54
|
let chartWidth = $state(0);
|
|
49
55
|
let chartHeight = $state(0);
|
|
56
|
+
// `hoveredNode`/`hoveredLink` (pointer) and `focusedNode`/`focusedLink`
|
|
57
|
+
// (keyboard) are kept as separate state rather than one pair shared by
|
|
58
|
+
// both input sources: a pointer leaving a node/link must never erase that
|
|
59
|
+
// node/link's focus, and a late/stale pointerleave -- which real browsers
|
|
60
|
+
// fire *after* a focus event when focusing an off-screen node/link
|
|
61
|
+
// auto-scrolls the page under an unmoved cursor -- must not be able to
|
|
62
|
+
// clobber the focus highlight either. See `activeNode`/`activeLink` below
|
|
63
|
+
// for the precedence between them, and `anchor` (in Interactions) for how
|
|
64
|
+
// the tooltip re-locates the right element off of these ids alone.
|
|
50
65
|
let hoveredNode = $state<string | null>(null);
|
|
66
|
+
let focusedNode = $state<string | null>(null);
|
|
51
67
|
let hoveredLink = $state<{ source: string; target: string } | null>(null);
|
|
68
|
+
let focusedLink = $state<{ source: string; target: string } | null>(null);
|
|
52
69
|
let mouseX = $state(0);
|
|
53
70
|
let mouseY = $state(0);
|
|
54
71
|
|
|
72
|
+
// Precedence: keyboard focus wins over pointer hover, on whichever of
|
|
73
|
+
// node/link actually holds it -- a focused node stays active even while
|
|
74
|
+
// the pointer hovers an unrelated link (and vice versa). Nodes and links
|
|
75
|
+
// are mutually exclusive DOM targets, so within one tier (focus, then
|
|
76
|
+
// hover) at most one of activeNode/activeLink is ever non-null.
|
|
77
|
+
let activeNode = $derived(focusedLink !== null ? null : (focusedNode ?? hoveredNode));
|
|
78
|
+
let activeLink = $derived(focusedNode !== null ? null : (focusedLink ?? hoveredLink));
|
|
79
|
+
|
|
55
80
|
// ── Layout ─────────────────────────────────────────────────────
|
|
56
81
|
|
|
57
82
|
let format = $derived(valueFormat ?? formatNumber);
|
|
@@ -310,18 +335,18 @@
|
|
|
310
335
|
if (disableDimOnHover) {
|
|
311
336
|
return null;
|
|
312
337
|
}
|
|
313
|
-
if (
|
|
314
|
-
const connected = new SvelteSet<string>([
|
|
338
|
+
if (activeNode !== null) {
|
|
339
|
+
const connected = new SvelteSet<string>([activeNode]);
|
|
315
340
|
for (const l of links) {
|
|
316
|
-
if (l.source ===
|
|
341
|
+
if (l.source === activeNode || l.target === activeNode) {
|
|
317
342
|
connected.add(l.source);
|
|
318
343
|
connected.add(l.target);
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
346
|
return connected;
|
|
322
347
|
}
|
|
323
|
-
if (
|
|
324
|
-
return new SvelteSet([
|
|
348
|
+
if (activeLink !== null) {
|
|
349
|
+
return new SvelteSet([activeLink.source, activeLink.target]);
|
|
325
350
|
}
|
|
326
351
|
return null;
|
|
327
352
|
});
|
|
@@ -334,11 +359,11 @@
|
|
|
334
359
|
* twice on every hover state change.
|
|
335
360
|
*/
|
|
336
361
|
let hoveredLinkCache = $derived.by(() => {
|
|
337
|
-
if (
|
|
362
|
+
if (activeLink === null) {
|
|
338
363
|
return null;
|
|
339
364
|
}
|
|
340
365
|
const l = links.find(
|
|
341
|
-
(lk) => lk.source ===
|
|
366
|
+
(lk) => lk.source === activeLink!.source && lk.target === activeLink!.target
|
|
342
367
|
);
|
|
343
368
|
if (!l) {
|
|
344
369
|
return null;
|
|
@@ -350,8 +375,8 @@
|
|
|
350
375
|
});
|
|
351
376
|
|
|
352
377
|
let tooltipData = $derived.by(() => {
|
|
353
|
-
if (
|
|
354
|
-
const n = layout.nodes.find((nd) => nd.id ===
|
|
378
|
+
if (activeNode !== null) {
|
|
379
|
+
const n = layout.nodes.find((nd) => nd.id === activeNode);
|
|
355
380
|
if (!n) {
|
|
356
381
|
return null;
|
|
357
382
|
}
|
|
@@ -380,9 +405,9 @@
|
|
|
380
405
|
});
|
|
381
406
|
|
|
382
407
|
let tooltipContext = $derived.by<SankeyTooltipContext | null>(() => {
|
|
383
|
-
if (
|
|
384
|
-
const n = findNode(
|
|
385
|
-
const computed = layout.nodes.find((nd) => nd.id ===
|
|
408
|
+
if (activeNode !== null) {
|
|
409
|
+
const n = findNode(activeNode);
|
|
410
|
+
const computed = layout.nodes.find((nd) => nd.id === activeNode);
|
|
386
411
|
if (!n || !computed) {
|
|
387
412
|
return null;
|
|
388
413
|
}
|
|
@@ -402,22 +427,83 @@
|
|
|
402
427
|
});
|
|
403
428
|
|
|
404
429
|
// ── Interactions ───────────────────────────────────────────────
|
|
430
|
+
// Family-wide contract shared with BarChart/FunnelChart/DualAxisBarChart:
|
|
431
|
+
// pointer hover and keyboard focus feed SEPARATE state (see hoveredNode/
|
|
432
|
+
// focusedNode/hoveredLink/focusedLink above), read together as one "direct
|
|
433
|
+
// interaction" tier via activeNode/activeLink. Enter/Space on a focused
|
|
434
|
+
// node or link invokes the same click callback a pointer click would.
|
|
405
435
|
|
|
406
436
|
function trackMouse(e: MouseEvent) {
|
|
437
|
+
const position = pointerPositionIn(containerEl, e);
|
|
438
|
+
if (position !== null) {
|
|
439
|
+
mouseX = position.x;
|
|
440
|
+
mouseY = position.y;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Anchor from the live element's own rect (not SVG-space math), so the two
|
|
445
|
+
// nested margin/gutter transforms this chart applies never need reproducing
|
|
446
|
+
// here -- same technique BarChart/FunnelChart/DualAxisBarChart use.
|
|
447
|
+
function anchorFromElement(el: Element): TooltipAnchor | null {
|
|
407
448
|
if (containerEl === null) {
|
|
408
|
-
return;
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const r = el.getBoundingClientRect();
|
|
452
|
+
const c = containerEl.getBoundingClientRect();
|
|
453
|
+
return { x: r.left + r.width / 2 - c.left, y: r.top - c.top, side: 'top' };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Re-locates the live element for an id via its data-node-id/data-link-*
|
|
457
|
+
// attribute (added to the elements below) rather than a ref captured at
|
|
458
|
+
// enter/focus time -- `anchor` can become active through a path with no
|
|
459
|
+
// event of its own to capture from (e.g. focus blurring off a node while
|
|
460
|
+
// the pointer already rests, unmoved, over a different one), so it always
|
|
461
|
+
// needs to be able to look the current element up fresh.
|
|
462
|
+
function findNodeElement(id: string): Element | null {
|
|
463
|
+
if (containerEl === null) {
|
|
464
|
+
return null;
|
|
409
465
|
}
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
466
|
+
for (const el of containerEl.querySelectorAll('.sankey-node')) {
|
|
467
|
+
if (el.getAttribute('data-node-id') === id) {
|
|
468
|
+
return el;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
413
472
|
}
|
|
414
473
|
|
|
474
|
+
function findLinkElement(source: string, target: string): Element | null {
|
|
475
|
+
if (containerEl === null) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
for (const el of containerEl.querySelectorAll('.sankey-link')) {
|
|
479
|
+
if (
|
|
480
|
+
el.getAttribute('data-link-source') === source &&
|
|
481
|
+
el.getAttribute('data-link-target') === target
|
|
482
|
+
) {
|
|
483
|
+
return el;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let anchor = $derived.by<TooltipAnchor | null>(() => {
|
|
490
|
+
if (activeNode !== null) {
|
|
491
|
+
const el = findNodeElement(activeNode);
|
|
492
|
+
return el ? anchorFromElement(el) : null;
|
|
493
|
+
}
|
|
494
|
+
if (activeLink !== null) {
|
|
495
|
+
const el = findLinkElement(activeLink.source, activeLink.target);
|
|
496
|
+
return el ? anchorFromElement(el) : null;
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
});
|
|
500
|
+
|
|
415
501
|
function isLinkHighlighted(source: string, target: string): boolean {
|
|
416
|
-
if (
|
|
417
|
-
return source ===
|
|
502
|
+
if (activeLink !== null) {
|
|
503
|
+
return source === activeLink.source && target === activeLink.target;
|
|
418
504
|
}
|
|
419
|
-
if (
|
|
420
|
-
return source ===
|
|
505
|
+
if (activeNode !== null) {
|
|
506
|
+
return source === activeNode || target === activeNode;
|
|
421
507
|
}
|
|
422
508
|
return false;
|
|
423
509
|
}
|
|
@@ -430,18 +516,73 @@
|
|
|
430
516
|
return links.find((l) => l.source === source && l.target === target);
|
|
431
517
|
}
|
|
432
518
|
|
|
519
|
+
/** Resolved display label for a node id, from the computed layout (falls back to the id). */
|
|
520
|
+
function resolveNodeLabel(id: string): string {
|
|
521
|
+
return layout.nodes.find((n) => n.id === id)?.label ?? id;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// onnodehover mirrors the combined pointer-or-focus node (activeNode),
|
|
525
|
+
// once per actual change -- e.g. a pointer leaving a node that is still
|
|
526
|
+
// keyboard-focused must not report a hover-cleared event for a highlight
|
|
527
|
+
// that never went away.
|
|
528
|
+
let lastNotifiedNode: string | null = null;
|
|
529
|
+
function notifyNodeHover() {
|
|
530
|
+
const next = activeNode;
|
|
531
|
+
if (next === lastNotifiedNode) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
lastNotifiedNode = next;
|
|
535
|
+
const orig = next === null ? null : findNode(next);
|
|
536
|
+
onnodehover?.(orig ? { node: orig } : null);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function sameLink(
|
|
540
|
+
a: { source: string; target: string } | null,
|
|
541
|
+
b: { source: string; target: string } | null
|
|
542
|
+
): boolean {
|
|
543
|
+
if (a === null || b === null) {
|
|
544
|
+
return a === b;
|
|
545
|
+
}
|
|
546
|
+
return a.source === b.source && a.target === b.target;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// onlinkhover mirrors the combined pointer-or-focus link (activeLink),
|
|
550
|
+
// once per actual change -- same dedup rationale as notifyNodeHover.
|
|
551
|
+
let lastNotifiedLink: { source: string; target: string } | null = null;
|
|
552
|
+
function notifyLinkHover() {
|
|
553
|
+
const next = activeLink;
|
|
554
|
+
if (sameLink(next, lastNotifiedLink)) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
lastNotifiedLink = next;
|
|
558
|
+
const orig = next === null ? null : findLink(next.source, next.target);
|
|
559
|
+
onlinkhover?.(orig ? { link: orig } : null);
|
|
560
|
+
}
|
|
561
|
+
|
|
433
562
|
function handleNodeEnter(e: MouseEvent, id: string) {
|
|
434
563
|
hoveredNode = id;
|
|
435
564
|
trackMouse(e);
|
|
436
|
-
|
|
437
|
-
if (orig) {
|
|
438
|
-
onnodehover?.({ node: orig });
|
|
439
|
-
}
|
|
565
|
+
notifyNodeHover();
|
|
440
566
|
}
|
|
441
567
|
|
|
568
|
+
function handleNodeFocus(id: string) {
|
|
569
|
+
focusedNode = id;
|
|
570
|
+
notifyNodeHover();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Wired to a node's mouseleave. Clears ONLY the hover half -- never
|
|
574
|
+
// `focusedNode` -- so a late, stale mouseleave (see `hoveredNode`'s
|
|
575
|
+
// declaration comment) cannot erase a node's focus highlight.
|
|
442
576
|
function handleNodeLeave() {
|
|
443
577
|
hoveredNode = null;
|
|
444
|
-
|
|
578
|
+
notifyNodeHover();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Wired to a node's blur. Clears ONLY the focus half; a pointer that
|
|
582
|
+
// happens to still be over the node keeps it hovered.
|
|
583
|
+
function handleNodeBlur() {
|
|
584
|
+
focusedNode = null;
|
|
585
|
+
notifyNodeHover();
|
|
445
586
|
}
|
|
446
587
|
|
|
447
588
|
function handleNodeClick(id: string) {
|
|
@@ -451,18 +592,36 @@
|
|
|
451
592
|
}
|
|
452
593
|
}
|
|
453
594
|
|
|
595
|
+
function handleNodeKeydown(e: KeyboardEvent, id: string) {
|
|
596
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
597
|
+
e.preventDefault();
|
|
598
|
+
handleNodeClick(id);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
454
602
|
function handleLinkEnter(e: MouseEvent, source: string, target: string) {
|
|
455
603
|
hoveredLink = { source, target };
|
|
456
604
|
trackMouse(e);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
605
|
+
notifyLinkHover();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function handleLinkFocus(source: string, target: string) {
|
|
609
|
+
focusedLink = { source, target };
|
|
610
|
+
notifyLinkHover();
|
|
461
611
|
}
|
|
462
612
|
|
|
613
|
+
// Wired to a link's mouseleave. Clears ONLY the hover half -- never
|
|
614
|
+
// `focusedLink` -- for the same reason handleNodeLeave spares focusedNode.
|
|
463
615
|
function handleLinkLeave() {
|
|
464
616
|
hoveredLink = null;
|
|
465
|
-
|
|
617
|
+
notifyLinkHover();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Wired to a link's blur. Clears ONLY the focus half; a pointer that
|
|
621
|
+
// happens to still be over the link keeps it hovered.
|
|
622
|
+
function handleLinkBlur() {
|
|
623
|
+
focusedLink = null;
|
|
624
|
+
notifyLinkHover();
|
|
466
625
|
}
|
|
467
626
|
|
|
468
627
|
function handleLinkClick(source: string, target: string) {
|
|
@@ -471,6 +630,31 @@
|
|
|
471
630
|
onlinkclick?.({ link: orig });
|
|
472
631
|
}
|
|
473
632
|
}
|
|
633
|
+
|
|
634
|
+
function handleLinkKeydown(e: KeyboardEvent, source: string, target: string) {
|
|
635
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
636
|
+
e.preventDefault();
|
|
637
|
+
handleLinkClick(source, target);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Touch taps have no pointerleave: dismiss when a pointerdown lands
|
|
642
|
+
// outside. Clears only the hover half of each pair -- a real pointerdown
|
|
643
|
+
// outside a focused element blurs it natively (see handleNodeBlur /
|
|
644
|
+
// handleLinkBlur), matching PieChart/AreaChart/LineChart's identical
|
|
645
|
+
// hover-only scope for this effect.
|
|
646
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
647
|
+
$effect(() => {
|
|
648
|
+
if (hoveredNode === null && hoveredLink === null) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
return dismissOnOutsidePointerDown(containerEl, () => {
|
|
652
|
+
hoveredNode = null;
|
|
653
|
+
hoveredLink = null;
|
|
654
|
+
notifyNodeHover();
|
|
655
|
+
notifyLinkHover();
|
|
656
|
+
});
|
|
657
|
+
});
|
|
474
658
|
</script>
|
|
475
659
|
|
|
476
660
|
<div
|
|
@@ -503,19 +687,27 @@
|
|
|
503
687
|
{#each layout.links as link, i (i)}
|
|
504
688
|
{@const highlighted = isLinkHighlighted(link.source, link.target)}
|
|
505
689
|
{@const dimmed =
|
|
506
|
-
!disableDimOnHover && (
|
|
507
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
508
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
690
|
+
!disableDimOnHover && (activeNode !== null || activeLink !== null) && !highlighted}
|
|
509
691
|
<path
|
|
510
692
|
class="sankey-link"
|
|
693
|
+
data-link-source={link.source}
|
|
694
|
+
data-link-target={link.target}
|
|
511
695
|
d={link.path}
|
|
512
696
|
fill="none"
|
|
513
697
|
stroke={link.color ?? nodeColorMap.get(link.source) ?? getColor(0)}
|
|
514
698
|
stroke-width={Math.max(minLinkWidth, link.width)}
|
|
515
699
|
stroke-opacity={highlighted ? 0.7 : dimmed ? 0.08 : 0.4}
|
|
700
|
+
tabindex="0"
|
|
701
|
+
role="button"
|
|
702
|
+
aria-label="{resolveNodeLabel(link.source)} to {resolveNodeLabel(link.target)}: {format(
|
|
703
|
+
link.value
|
|
704
|
+
)}"
|
|
516
705
|
onmouseenter={(e) => handleLinkEnter(e, link.source, link.target)}
|
|
517
706
|
onmousemove={trackMouse}
|
|
518
707
|
onmouseleave={handleLinkLeave}
|
|
708
|
+
onfocus={() => handleLinkFocus(link.source, link.target)}
|
|
709
|
+
onblur={handleLinkBlur}
|
|
710
|
+
onkeydown={(e) => handleLinkKeydown(e, link.source, link.target)}
|
|
519
711
|
onclick={() => handleLinkClick(link.source, link.target)}
|
|
520
712
|
/>
|
|
521
713
|
{/each}
|
|
@@ -523,11 +715,10 @@
|
|
|
523
715
|
{#each layout.nodes as node, ni (ni)}
|
|
524
716
|
{@const color = nodeColorMap.get(node.id) ?? getColor(ni)}
|
|
525
717
|
{@const dimmed = connectedNodes !== null && !connectedNodes.has(node.id)}
|
|
526
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
527
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
528
718
|
<rect
|
|
529
719
|
class="sankey-node"
|
|
530
720
|
class:node-dimmed={dimmed}
|
|
721
|
+
data-node-id={node.id}
|
|
531
722
|
x={node.x}
|
|
532
723
|
y={node.y}
|
|
533
724
|
width={node.width}
|
|
@@ -535,9 +726,15 @@
|
|
|
535
726
|
rx={radius}
|
|
536
727
|
ry={radius}
|
|
537
728
|
fill={color}
|
|
729
|
+
tabindex="0"
|
|
730
|
+
role="button"
|
|
731
|
+
aria-label="{node.label}: {format(node.value)}"
|
|
538
732
|
onmouseenter={(e) => handleNodeEnter(e, node.id)}
|
|
539
733
|
onmousemove={trackMouse}
|
|
540
734
|
onmouseleave={handleNodeLeave}
|
|
735
|
+
onfocus={() => handleNodeFocus(node.id)}
|
|
736
|
+
onblur={handleNodeBlur}
|
|
737
|
+
onkeydown={(e) => handleNodeKeydown(e, node.id)}
|
|
541
738
|
onclick={() => handleNodeClick(node.id)}
|
|
542
739
|
/>
|
|
543
740
|
{/each}
|
|
@@ -570,12 +767,31 @@
|
|
|
570
767
|
</g>
|
|
571
768
|
</ChartContainer>
|
|
572
769
|
|
|
573
|
-
{#if typeof tooltipSnippet === 'function'
|
|
574
|
-
<
|
|
575
|
-
{
|
|
576
|
-
|
|
770
|
+
{#if typeof tooltipSnippet === 'function'}
|
|
771
|
+
<ChartTooltip
|
|
772
|
+
data={tooltipData}
|
|
773
|
+
{mouseX}
|
|
774
|
+
{mouseY}
|
|
775
|
+
{anchor}
|
|
776
|
+
portal={tooltipPortal}
|
|
777
|
+
originEl={containerEl}
|
|
778
|
+
unstyled
|
|
779
|
+
>
|
|
780
|
+
{#snippet content()}
|
|
781
|
+
{#if tooltipContext !== null}
|
|
782
|
+
{@render tooltipSnippet(tooltipContext)}
|
|
783
|
+
{/if}
|
|
784
|
+
{/snippet}
|
|
785
|
+
</ChartTooltip>
|
|
577
786
|
{:else}
|
|
578
|
-
<ChartTooltip
|
|
787
|
+
<ChartTooltip
|
|
788
|
+
data={tooltipData}
|
|
789
|
+
{mouseX}
|
|
790
|
+
{mouseY}
|
|
791
|
+
{anchor}
|
|
792
|
+
portal={tooltipPortal}
|
|
793
|
+
originEl={containerEl}
|
|
794
|
+
/>
|
|
579
795
|
{/if}
|
|
580
796
|
{/if}
|
|
581
797
|
</div>
|
|
@@ -590,13 +806,23 @@
|
|
|
590
806
|
pointer-events: none;
|
|
591
807
|
}
|
|
592
808
|
.sankey-link {
|
|
593
|
-
transition: stroke-opacity var(--chart-transition-duration, 0.2s)
|
|
809
|
+
transition: stroke-opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
|
|
810
|
+
var(--chart-transition-easing, var(--motion-easing, ease));
|
|
594
811
|
cursor: pointer;
|
|
595
812
|
}
|
|
813
|
+
.sankey-link:focus-visible {
|
|
814
|
+
outline: var(--sankey-focus-outline, 2px solid currentColor);
|
|
815
|
+
outline-offset: var(--sankey-focus-outline-offset, 2px);
|
|
816
|
+
}
|
|
596
817
|
.sankey-node {
|
|
597
|
-
transition: opacity var(--chart-transition-duration, 0.2s)
|
|
818
|
+
transition: opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
|
|
819
|
+
var(--chart-transition-easing, var(--motion-easing, ease));
|
|
598
820
|
cursor: pointer;
|
|
599
821
|
}
|
|
822
|
+
.sankey-node:focus-visible {
|
|
823
|
+
outline: var(--sankey-focus-outline, 2px solid currentColor);
|
|
824
|
+
outline-offset: var(--sankey-focus-outline-offset, 2px);
|
|
825
|
+
}
|
|
600
826
|
.sankey-node.node-dimmed {
|
|
601
827
|
opacity: var(--sankey-dimmed-opacity, 0.15);
|
|
602
828
|
}
|
|
@@ -613,7 +839,8 @@
|
|
|
613
839
|
stroke-width: var(--sankey-label-halo-width, 0);
|
|
614
840
|
stroke-linejoin: round;
|
|
615
841
|
pointer-events: none;
|
|
616
|
-
transition: opacity var(--chart-transition-duration, 0.2s)
|
|
842
|
+
transition: opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
|
|
843
|
+
var(--chart-transition-easing, var(--motion-easing, ease));
|
|
617
844
|
}
|
|
618
845
|
.sankey-col-label {
|
|
619
846
|
fill: var(--sankey-col-label-color, #666);
|
|
@@ -625,11 +852,6 @@
|
|
|
625
852
|
.sankey-label.node-dimmed {
|
|
626
853
|
opacity: var(--sankey-dimmed-opacity, 0.15);
|
|
627
854
|
}
|
|
628
|
-
.chart-tooltip-slot {
|
|
629
|
-
position: absolute;
|
|
630
|
-
z-index: 10;
|
|
631
|
-
pointer-events: none;
|
|
632
|
-
}
|
|
633
855
|
.chart-empty {
|
|
634
856
|
padding: var(--chart-empty-padding, 32px 24px);
|
|
635
857
|
color: var(--chart-empty-color, #9ca3af);
|
|
@@ -54,6 +54,15 @@ export type OptionalSankeyChartProperties = {
|
|
|
54
54
|
maxHeight?: number;
|
|
55
55
|
valueFormat?: (value: number) => string;
|
|
56
56
|
tooltipSnippet?: Snippet<[SankeyTooltipContext]>;
|
|
57
|
+
/**
|
|
58
|
+
* Render the tooltip into the chart's own root (its shadow root when hosted as a
|
|
59
|
+
* custom element, `document.body` otherwise) with `position: fixed`, clamped to
|
|
60
|
+
* the viewport, instead of positioned inside the chart. Use it when the chart
|
|
61
|
+
* sits inside a container with `overflow: hidden`/`scroll` that would otherwise
|
|
62
|
+
* clip the tooltip. Defaults to `false`. Has no effect on `tooltipSnippet`'s
|
|
63
|
+
* content, only on where the tooltip wrapper renders.
|
|
64
|
+
*/
|
|
65
|
+
tooltipPortal?: boolean;
|
|
57
66
|
empty?: Snippet;
|
|
58
67
|
testId?: string;
|
|
59
68
|
classes?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
+
import { prefersReducedMotion } from '../utils';
|
|
3
4
|
import type { ScrollerProperties, ScrollPosition } from './properties';
|
|
4
5
|
import Button from '../Button/Button.svelte';
|
|
5
6
|
import chevronLeft from '../assets/chevron-left.svg?raw';
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
const { clientSize } = getScrollProps(containerEl);
|
|
77
78
|
const amount = scrollAmount ?? clientSize;
|
|
78
79
|
const options: ScrollToOptions = {
|
|
79
|
-
behavior:
|
|
80
|
+
behavior: scrollBehavior()
|
|
80
81
|
};
|
|
81
82
|
if (direction === 'horizontal') {
|
|
82
83
|
options.left = delta * amount;
|
|
@@ -118,12 +119,35 @@
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Whether a scroll should animate.
|
|
124
|
+
*
|
|
125
|
+
* `smoothScroll` is the consumer's preference and the OS setting overrides it, so
|
|
126
|
+
* both are consulted. Deliberately a function called at each use rather than a value
|
|
127
|
+
* captured once: the preference can change while the component is mounted, and the
|
|
128
|
+
* three call sites below run on interaction, not on render.
|
|
129
|
+
*
|
|
130
|
+
* This has to live in script. Two of the three sites are unreachable by any
|
|
131
|
+
* stylesheet -- a `ScrollToOptions.behavior` value is not a style at all, and an
|
|
132
|
+
* inline `style.scrollBehavior` write outranks every rule in this component's own
|
|
133
|
+
* stylesheet, media query included. A `@media (prefers-reduced-motion: reduce)`
|
|
134
|
+
* block here would read correctly in review and lose at runtime.
|
|
135
|
+
*
|
|
136
|
+
* (Written without the style tag spelled out: Svelte scans a script block as raw
|
|
137
|
+
* text looking for its closing tag, and a literal style tag inside a comment here
|
|
138
|
+
* ends that scan early -- the whole component then fails to compile with
|
|
139
|
+
* "`<script>` was left open" pointing at the last line of the file.)
|
|
140
|
+
*/
|
|
141
|
+
function scrollBehavior(): ScrollBehavior {
|
|
142
|
+
return smoothScroll && !prefersReducedMotion() ? 'smooth' : 'auto';
|
|
143
|
+
}
|
|
144
|
+
|
|
121
145
|
function handleDragEnd() {
|
|
122
146
|
if (!isDragging || containerEl === null) {
|
|
123
147
|
return;
|
|
124
148
|
}
|
|
125
149
|
isDragging = false;
|
|
126
|
-
containerEl.style.scrollBehavior =
|
|
150
|
+
containerEl.style.scrollBehavior = scrollBehavior();
|
|
127
151
|
containerEl.style.userSelect = '';
|
|
128
152
|
}
|
|
129
153
|
|
|
@@ -256,6 +280,17 @@
|
|
|
256
280
|
scroll-behavior: var(--scroller-scroll-behavior, smooth);
|
|
257
281
|
}
|
|
258
282
|
|
|
283
|
+
/* The stylesheet half, for scrolls this component does not initiate -- a wheel,
|
|
284
|
+
a dragged scrollbar, an anchor jump. The script half above covers the three it
|
|
285
|
+
does initiate, and cannot be replaced by this: an inline style write and a
|
|
286
|
+
ScrollToOptions value both outrank or bypass every rule here. */
|
|
287
|
+
@media (prefers-reduced-motion: reduce) {
|
|
288
|
+
.scroller.horizontal .scroll-container,
|
|
289
|
+
.scroller.vertical .scroll-container {
|
|
290
|
+
scroll-behavior: auto;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
259
294
|
.scroller.vertical .scroll-container {
|
|
260
295
|
flex-direction: column;
|
|
261
296
|
overflow-y: auto;
|