@progress/kendo-charts 2.4.0-dev.202405201104 → 2.4.0-dev.202405211537

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.
@@ -5,7 +5,7 @@ import { Node, resolveNodeOptions } from './node';
5
5
  import { Link, resolveLinkOptions } from './link';
6
6
  import { Label, resolveLabelOptions } from './label';
7
7
  import { Title } from './title';
8
- import { BOTTOM, LEFT, RIGHT, TOP } from '../common/constants';
8
+ import { BLACK, BOTTOM, LEFT, RIGHT, TOP } from '../common/constants';
9
9
  import { Box, rectToBox } from '../core';
10
10
  import { Legend } from './legend';
11
11
  import { defined } from '../drawing-utils';
@@ -25,6 +25,7 @@ export var Sankey = (function (Observable) {
25
25
  if (options && options.data) {
26
26
  this._redraw();
27
27
  this._initResizeObserver();
28
+ this._initNavigation(element);
28
29
  }
29
30
  }
30
31
 
@@ -36,13 +37,24 @@ export var Sankey = (function (Observable) {
36
37
  this.unbind();
37
38
  this._destroySurface();
38
39
  this._destroyResizeObserver();
40
+
41
+ if (this.element) {
42
+ this.element.removeEventListener('keydown', this._keydownHandler);
43
+ this.element.removeEventListener('focus', this._focusHandler);
44
+ this.element.removeEventListener('mousedown', this._onDownHandler);
45
+ this.element.removeEventListener('touchstart', this._onDownHandler);
46
+ this.element.removeEventListener('pointerdown', this._onDownHandler);
47
+ }
48
+
49
+ this._focusState = null;
50
+
51
+ this.element = null;
39
52
  };
40
53
 
41
54
  Sankey.prototype._initElement = function _initElement (element) {
42
55
  this.element = element;
43
56
  addClass(element, [ "k-chart", "k-sankey" ]);
44
57
  element.setAttribute('role', 'graphics-document');
45
- element.tabIndex = element.getAttribute("tabindex") || 0;
46
58
 
47
59
  var ref = this.options;
48
60
  var title = ref.title;
@@ -64,6 +76,31 @@ export var Sankey = (function (Observable) {
64
76
  }
65
77
  };
66
78
 
79
+ Sankey.prototype._initNavigation = function _initNavigation (element) {
80
+ element.tabIndex = element.getAttribute("tabindex") || 0;
81
+
82
+ if (this.options.disableKeyboardNavigation) {
83
+ return;
84
+ }
85
+
86
+ this._keydownHandler = this._keydown.bind(this);
87
+ this._focusHandler = this._focus.bind(this);
88
+ this._blurHandler = this._blur.bind(this);
89
+ this._onDownHandler = this._onDown.bind(this);
90
+
91
+ element.addEventListener('keydown', this._keydownHandler);
92
+ element.addEventListener('focus', this._focusHandler);
93
+ element.addEventListener('blur', this._blurHandler);
94
+ element.addEventListener('mousedown', this._onDownHandler);
95
+ element.addEventListener('touchstart', this._onDownHandler);
96
+ element.addEventListener('pointerdown', this._onDownHandler);
97
+
98
+ this._focusState = {
99
+ node: this.columns[0][0],
100
+ link: null
101
+ };
102
+ };
103
+
67
104
  Sankey.prototype._initResizeObserver = function _initResizeObserver () {
68
105
  var this$1 = this;
69
106
 
@@ -242,16 +279,223 @@ export var Sankey = (function (Observable) {
242
279
 
243
280
  Sankey.prototype._click = function _click (ev) {
244
281
  var element = ev.element;
282
+ var dataItem = element.dataItem;
245
283
  var isLink = element.type === LINK;
246
284
  var isNode = element.type === NODE;
285
+ var focusState = this._focusState || {};
247
286
 
248
287
  if (isNode) {
288
+ var focusedNodeClicked = !focusState.link && this.sameNode(focusState.node, dataItem);
289
+
290
+ if (!focusedNodeClicked) {
291
+ this._focusState = { node: dataItem, link: null };
292
+ this._focusNode({ highlight: false });
293
+ }
294
+
249
295
  this.trigger('nodeClick', ev);
250
296
  } else if (isLink) {
297
+ var link = {
298
+ sourceId: dataItem.source.id,
299
+ targetId: dataItem.target.id,
300
+ value: dataItem.value
301
+ };
302
+ var focusedLinkClicked = this.sameLink(focusState.link, link);
303
+
304
+ if (!focusedLinkClicked) {
305
+ this._focusState = { node: dataItem.source, link: link };
306
+ this._focusLink({ highlight: false });
307
+ }
308
+
251
309
  this.trigger('linkClick', ev);
252
310
  }
253
311
  };
254
312
 
313
+ Sankey.prototype.sameNode = function sameNode (node1, node2) {
314
+ return node1 && node2 && node1.id === node2.id;
315
+ };
316
+
317
+ Sankey.prototype.sameLink = function sameLink (link1, link2) {
318
+ return link1 && link2 && link1.sourceId === link2.sourceId && link1.targetId === link2.targetId;
319
+ };
320
+
321
+ Sankey.prototype._focusNode = function _focusNode (options) {
322
+ this._cleanFocusHighlight();
323
+
324
+ var nodeData = this._focusState.node;
325
+ var node = this.models.map.get(nodeData.id);
326
+ node.focus(options);
327
+ };
328
+
329
+ Sankey.prototype._focusLink = function _focusLink (options) {
330
+ this._cleanFocusHighlight();
331
+
332
+ var linkData = this._focusState.link;
333
+ var link = this.models.map.get(((linkData.sourceId) + "-" + (linkData.targetId)));
334
+ link.focus(options);
335
+ };
336
+
337
+ Sankey.prototype._focusNextNode = function _focusNextNode (direction) {
338
+ if ( direction === void 0 ) direction = 1;
339
+
340
+ var current = this._focusState.node;
341
+
342
+ var columnIndex = this.columns.findIndex(function (column) { return column.find(function (n) { return n.id === current.id; }); });
343
+ var columnNodes = this.columns[columnIndex];
344
+ var nodeIndex = columnNodes.findIndex(function (n) { return n.id === current.id; });
345
+
346
+ var nextNode = columnNodes[nodeIndex + direction];
347
+ if (nextNode) {
348
+ this._focusState.node = nextNode;
349
+ this._focusNode();
350
+ }
351
+ };
352
+
353
+ Sankey.prototype._focusNextLink = function _focusNextLink (direction) {
354
+ if ( direction === void 0 ) direction = 1;
355
+
356
+ var node = this._focusState.node;
357
+ var link = this._focusState.link;
358
+
359
+ var sourceLinkIndex = node.sourceLinks.findIndex(function (l) { return l.sourceId === link.sourceId && l.targetId === link.targetId; });
360
+ var targetLinkIndex = node.targetLinks.findIndex(function (l) { return l.sourceId === link.sourceId && l.targetId === link.targetId; });
361
+
362
+ if (sourceLinkIndex !== -1) {
363
+ var nextLink = node.sourceLinks[sourceLinkIndex + direction];
364
+
365
+ if (nextLink) {
366
+ this._focusState.link = nextLink;
367
+ this._focusLink();
368
+ }
369
+ } else if (targetLinkIndex !== -1) {
370
+ var nextLink$1 = node.targetLinks[targetLinkIndex + direction];
371
+
372
+ if (nextLink$1) {
373
+ this._focusState.link = nextLink$1;
374
+ this._focusLink();
375
+ }
376
+ }
377
+ };
378
+
379
+ Sankey.prototype._focusSourceNode = function _focusSourceNode () {
380
+ var linkData = this._focusState.link;
381
+ var sourceNode = this.models.map.get(linkData.sourceId);
382
+ this._focusState = { node: sourceNode.options.node, link: null };
383
+ this._focusNode();
384
+ };
385
+
386
+ Sankey.prototype._focusTargetNode = function _focusTargetNode () {
387
+ var linkData = this._focusState.link;
388
+ var targetNode = this.models.map.get(linkData.targetId);
389
+ this._focusState = { node: targetNode.options.node, link: null };
390
+ this._focusNode();
391
+ };
392
+
393
+ Sankey.prototype._focusSourceLink = function _focusSourceLink () {
394
+ var nodeData = this._focusState.node;
395
+ var sourceLinks = nodeData.sourceLinks;
396
+ var linkData = sourceLinks[0];
397
+ if (linkData) {
398
+ this._focusState.link = linkData;
399
+ this._focusLink();
400
+ }
401
+ };
402
+
403
+ Sankey.prototype._focusTargetLink = function _focusTargetLink () {
404
+ var nodeData = this._focusState.node;
405
+ var targetLinks = nodeData.targetLinks;
406
+ var linkData = targetLinks[0];
407
+ if (linkData) {
408
+ this._focusState.link = linkData;
409
+ this._focusLink();
410
+ }
411
+ };
412
+
413
+ Sankey.prototype._focus = function _focus () {
414
+ if (!this._skipFocusHighlight) {
415
+ if (this._focusState.link) {
416
+ this._focusLink();
417
+ } else {
418
+ this._focusNode();
419
+ }
420
+ }
421
+
422
+ this._skipFocusHighlight = false;
423
+ };
424
+
425
+ Sankey.prototype._blur = function _blur () {
426
+ this._cleanFocusHighlight();
427
+ };
428
+
429
+ Sankey.prototype._onDown = function _onDown () {
430
+ if (!this._hasFocus()) {
431
+ this._skipFocusHighlight = true;
432
+ }
433
+ };
434
+
435
+ Sankey.prototype._hasFocus = function _hasFocus () {
436
+ return this.element.ownerDocument.activeElement === this.element;
437
+ };
438
+
439
+ Sankey.prototype._cleanFocusHighlight = function _cleanFocusHighlight () {
440
+ this.models.nodes.forEach(function (node) { return node.blur(); });
441
+ this.models.links.forEach(function (link) { return link.blur(); });
442
+ };
443
+
444
+ Sankey.prototype._keydown = function _keydown (ev) {
445
+ var handler = this['on' + ev.key];
446
+
447
+ if (handler) {
448
+ handler.call(this, ev);
449
+ }
450
+ };
451
+
452
+ Sankey.prototype.onEscape = function onEscape (ev) {
453
+ ev.preventDefault();
454
+
455
+ this._focusState = { node: this.columns[0][0], link: null };
456
+ this._focusNode();
457
+ };
458
+
459
+ Sankey.prototype.onArrowDown = function onArrowDown (ev) {
460
+ ev.preventDefault();
461
+
462
+ if (this._focusState.link) {
463
+ this._focusNextLink(1);
464
+ } else {
465
+ this._focusNextNode(1);
466
+ }
467
+ };
468
+
469
+ Sankey.prototype.onArrowUp = function onArrowUp (ev) {
470
+ ev.preventDefault();
471
+
472
+ if (this._focusState.link) {
473
+ this._focusNextLink(-1);
474
+ } else {
475
+ this._focusNextNode(-1);
476
+ }
477
+ };
478
+
479
+ Sankey.prototype.onArrowLeft = function onArrowLeft (ev) {
480
+ ev.preventDefault();
481
+
482
+ if (this._focusState.link) {
483
+ this._focusSourceNode();
484
+ } else {
485
+ this._focusTargetLink();
486
+ }
487
+ };
488
+
489
+ Sankey.prototype.onArrowRight = function onArrowRight (ev) {
490
+ ev.preventDefault();
491
+
492
+ if (this._focusState.link) {
493
+ this._focusTargetNode();
494
+ } else {
495
+ this._focusSourceLink();
496
+ }
497
+ };
498
+
255
499
  Sankey.prototype.highlightLinks = function highlightLinks (node, highlight) {
256
500
  var this$1 = this;
257
501
 
@@ -350,9 +594,13 @@ export var Sankey = (function (Observable) {
350
594
  var labels = sankeyOptions.labels;
351
595
  var nodeColors = sankeyOptions.nodeColors;
352
596
  var disableAutoLayout = sankeyOptions.disableAutoLayout;
597
+ var disableKeyboardNavigation = sankeyOptions.disableKeyboardNavigation;
353
598
  var autoLayout = !disableAutoLayout;
599
+ var padding = disableKeyboardNavigation ? 0 : highlightOptions.width / 2;
354
600
 
355
601
  var sankeyBox = new Box(0, 0, calcOptions.width, calcOptions.height);
602
+ sankeyBox.unpad(padding);
603
+
356
604
  var titleBox = this.titleBox(title, sankeyBox);
357
605
 
358
606
  var legendArea = sankeyBox.clone();
@@ -394,7 +642,7 @@ export var Sankey = (function (Observable) {
394
642
  var circularLinks = ref.circularLinks;
395
643
  if (circularLinks) {
396
644
  console.warn('Circular links detected. Kendo Sankey diagram does not support circular links.');
397
- return { sankey: { nodes: [], links: [], circularLinks: circularLinks }, legendBox: legendBox, titleBox: titleBox };
645
+ return { sankey: { nodes: [], links: [], columns: [[]], circularLinks: circularLinks }, legendBox: legendBox, titleBox: titleBox };
398
646
  }
399
647
 
400
648
  var box = new Box();
@@ -485,6 +733,7 @@ export var Sankey = (function (Observable) {
485
733
  var nodeColors = sankeyOptions.nodeColors;
486
734
  var title = sankeyOptions.title;
487
735
  var legend = sankeyOptions.legend;
736
+ var disableKeyboardNavigation = sankeyOptions.disableKeyboardNavigation;
488
737
  var ref = sankeyContext.size;
489
738
  var width = ref.width;
490
739
  var height = ref.height;
@@ -496,6 +745,13 @@ export var Sankey = (function (Observable) {
496
745
  var legendBox = ref$1.legendBox;
497
746
  var nodes = sankey.nodes;
498
747
  var links = sankey.links;
748
+ var columns = sankey.columns;
749
+
750
+ sankeyContext.columns = columns.map(function (column) {
751
+ var newColumn = column.slice();
752
+ newColumn.sort(function (a, b) { return a.y0 - b.y0; });
753
+ return newColumn;
754
+ });
499
755
 
500
756
  var visual = new drawing.Group({
501
757
  clip: drawing.Path.fromRect(new geometry.Rect([0, 0], [width, height]))
@@ -514,8 +770,19 @@ export var Sankey = (function (Observable) {
514
770
  var visualNodes = new Map();
515
771
  sankeyContext.nodesVisuals = visualNodes;
516
772
 
773
+ var models = {
774
+ nodes: [],
775
+ links: [],
776
+ map: new Map()
777
+ };
778
+ sankeyContext.models = models;
779
+
780
+ var focusHighlights = [];
781
+
517
782
  nodes.forEach(function (node, i) {
518
783
  var nodeOps = resolveNodeOptions(node, nodesOptions, nodeColors, i);
784
+ nodeOps.root = function () { return sankeyContext.element; };
785
+ nodeOps.navigatable = disableKeyboardNavigation !== true;
519
786
 
520
787
  var nodeInstance = new Node(nodeOps);
521
788
  var nodeVisual = nodeInstance.exportVisual();
@@ -532,7 +799,16 @@ export var Sankey = (function (Observable) {
532
799
  targetLinks: node.targetLinks.map(function (link) { return ({ sourceId: link.sourceId, targetId: link.targetId, value: link.value }); })});
533
800
  visualNodes.set(node.id, nodeVisual);
534
801
 
802
+ models.nodes.push(nodeInstance);
803
+ models.map.set(node.id, nodeInstance);
804
+
535
805
  visual.append(nodeVisual);
806
+
807
+ nodeInstance.createFocusHighlight();
808
+
809
+ if (nodeInstance._highlight) {
810
+ focusHighlights.push(nodeInstance._highlight);
811
+ }
536
812
  });
537
813
 
538
814
  var sortedLinks = links.slice().sort(function (a, b) { return b.value - a.value; });
@@ -546,6 +822,8 @@ export var Sankey = (function (Observable) {
546
822
  var sourceNode = visualNodes.get(source.id);
547
823
  var targetNode = visualNodes.get(target.id);
548
824
  var linkOps = resolveLinkOptions(link, linkOptions, sourceNode, targetNode);
825
+ linkOps.root = function () { return sankeyContext.element; };
826
+ linkOps.navigatable = disableKeyboardNavigation !== true;
549
827
  var linkInstance = new Link(linkOps);
550
828
  var linkVisual = linkInstance.exportVisual();
551
829
 
@@ -561,6 +839,15 @@ export var Sankey = (function (Observable) {
561
839
  sourceNode.links.push(linkVisual);
562
840
  targetNode.links.push(linkVisual);
563
841
 
842
+ models.links.push(linkInstance);
843
+ models.map.set(((source.id) + "-" + (target.id)), linkInstance);
844
+
845
+ linkInstance.createFocusHighlight();
846
+
847
+ if (linkInstance._highlight) {
848
+ focusHighlights.push(linkInstance._highlight);
849
+ }
850
+
564
851
  visual.append(linkVisual);
565
852
  });
566
853
 
@@ -581,6 +868,12 @@ export var Sankey = (function (Observable) {
581
868
  visual.append(legendVisual);
582
869
  }
583
870
 
871
+ if (focusHighlights.length !== 0) {
872
+ var focusHighlight = new drawing.Group();
873
+ focusHighlight.append.apply(focusHighlight, focusHighlights);
874
+ visual.append(focusHighlight);
875
+ }
876
+
584
877
  return visual;
585
878
  };
586
879
 
@@ -605,6 +898,12 @@ export var Sankey = (function (Observable) {
605
898
  return Sankey;
606
899
  }(Observable));
607
900
 
901
+ var highlightOptions = {
902
+ opacity: 1,
903
+ width: 2,
904
+ color: BLACK
905
+ };
906
+
608
907
  setDefaultOptions(Sankey, {
609
908
  title: {
610
909
  position: TOP, // 'top', 'bottom'
@@ -632,7 +931,15 @@ setDefaultOptions(Sankey, {
632
931
  padding: 16,
633
932
  opacity: 1,
634
933
  align: 'stretch', // 'left', 'right', 'stretch'
635
- offset: { left: 0, top: 0 }
934
+ offset: { left: 0, top: 0 },
935
+ focusHighlight: Object.assign({}, highlightOptions),
936
+ labels: {
937
+ ariaTemplate: function (ref) {
938
+ var node = ref.node;
939
+
940
+ return node.label.text;
941
+ }
942
+ }
636
943
  },
637
944
  links: {
638
945
  colorType: 'static', // 'source', 'target', 'static'
@@ -640,6 +947,14 @@ setDefaultOptions(Sankey, {
640
947
  highlight: {
641
948
  opacity: 0.8,
642
949
  inactiveOpacity: 0.2
950
+ },
951
+ focusHighlight: Object.assign({}, highlightOptions),
952
+ labels: {
953
+ ariaTemplate: function (ref) {
954
+ var link = ref.link;
955
+
956
+ return ((link.source.label.text) + " to " + (link.target.label.text));
957
+ }
643
958
  }
644
959
  },
645
960
  tooltip: {
@@ -243,7 +243,7 @@ class LegendItem extends BoxElement {
243
243
 
244
244
  if (this.options.visible) {
245
245
  const accessibilityOptions = deepExtend({
246
- ariaLabel: options.text
246
+ ariaLabel: options.accessibility.ariaLabel !== undefined ? options.accessibility.ariaLabel : options.text
247
247
  }, options.accessibility);
248
248
 
249
249
  addAccessibilityAttributesToVisual(this.visual, accessibilityOptions);
@@ -52,6 +52,11 @@ setDefaultOptions(Legend, {
52
52
  },
53
53
  position: BOTTOM,
54
54
  align: CENTER,
55
+ accessibility: {
56
+ role: 'presentation',
57
+ ariaLabel: null,
58
+ ariaRoleDescription: null
59
+ },
55
60
  border: {
56
61
  width: 0
57
62
  }
@@ -2,6 +2,66 @@ import { drawing } from '@progress/kendo-drawing';
2
2
  import { SankeyElement } from './element';
3
3
  import { deepExtend } from '../common';
4
4
  import { defined } from '../drawing-utils';
5
+ import { ARIA_ACTIVE_DESCENDANT } from '../common/constants';
6
+
7
+ const distanceToLine = (line, point) => {
8
+ const [x1, y1] = line[0];
9
+ const [x2, y2] = line[1];
10
+ const [x3, y3] = point;
11
+
12
+ return Math.abs((x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1)) / Math.sqrt(Math.pow( (x2 - x1), 2 ) + Math.pow( (y2 - y1), 2 ));
13
+ };
14
+
15
+ const bezierPoint = (p1, p2, p3, p4, t) => {
16
+ const t1 = 1 - t;
17
+ const t1t1 = t1 * t1;
18
+ const tt = t * t;
19
+ return (p1 * t1t1 * t1) + (3 * p2 * t * t1t1) + (3 * p3 * tt * t1) + (p4 * tt * t);
20
+ };
21
+
22
+ const angelBetweenTwoLines = (line1, line2) => {
23
+ const [x1, y1] = line1[0];
24
+ const [x2, y2] = line1[1];
25
+ const [x3, y3] = line2[0];
26
+ const [x4, y4] = line2[1];
27
+
28
+ const a1 = Math.atan2(y2 - y1, x2 - x1);
29
+ const a2 = Math.atan2(y4 - y3, x4 - x3);
30
+
31
+ return Math.abs(a1 - a2);
32
+ };
33
+
34
+ const calculateControlPointsOffsetX = (link) => {
35
+ const { x0, x1, y0, y1 } = link;
36
+ let xC = (x0 + x1) / 2;
37
+
38
+ const width = link.width;
39
+ const halfWidth = width / 2;
40
+
41
+ // upper curve, t = 0.5
42
+ const upperCurveMiddleLine = [[(x0 + xC) / 2, y0 - halfWidth], [(x1 + xC) / 2, y1 - halfWidth]];
43
+
44
+ // for lower curve, bezier-point at t = 0.5
45
+ // for the case t = 0.5, the bezier-point is the middle point of the curve. => ((y0 + halfWidth) + (y1 + halfWidth)) / 2
46
+ const lowerCurveMiddlePoint = [xC, bezierPoint(y0 + halfWidth, y0 + halfWidth, y1 + halfWidth, y1 + halfWidth, 0.5)];
47
+
48
+ // The actual width of the link at its middle point as can be seen on the screen.
49
+ const actualWidth = distanceToLine(upperCurveMiddleLine, lowerCurveMiddlePoint);
50
+
51
+ const upperNarrowness = (width - actualWidth) / 2;
52
+
53
+ // The line `upperCurveMiddleLine` shows the upper border of the link.
54
+ // Assumption 1: Translated to the left to the desired link width and the translate value will be the `offset`.
55
+ // Assumption 2: The translate value is a hypotenuse of a triangle.
56
+ const alpha = angelBetweenTwoLines(upperCurveMiddleLine, [[x0, y0 - halfWidth], [xC, y0 - halfWidth]]);
57
+ const a = upperNarrowness;
58
+ const b = Math.sin(alpha) * a;
59
+ const offset = Math.sqrt(a * a + b * b);
60
+ // Another option is to assume the triangle is isosceles
61
+ // => offset = Math.sqrt(2) * upperNarrowness;
62
+
63
+ return y0 - y1 > 0 ? (-1) * offset : offset;
64
+ };
5
65
 
6
66
  export class Link extends SankeyElement {
7
67
  getElement() {
@@ -13,17 +73,75 @@ export class Link extends SankeyElement {
13
73
  .moveTo(x0, y0).curveTo([xC, y0], [xC, y1], [x1, y1]);
14
74
  }
15
75
 
76
+ getLabelText(options) {
77
+ let labelTemplate = options.labels.ariaTemplate;
78
+
79
+ if (labelTemplate) {
80
+ return labelTemplate({ link: options.link });
81
+ }
82
+ }
83
+
16
84
  visualOptions() {
17
85
  const options = this.options;
18
86
  const link = this.options.link;
87
+ const ariaLabel = this.getLabelText(options);
88
+
19
89
  return {
20
90
  stroke: {
21
91
  width: options.link.width,
22
92
  color: link.color || options.color,
23
93
  opacity: defined(link.opacity) ? link.opacity : options.opacity
24
- }
94
+ },
95
+ role: 'graphics-symbol',
96
+ ariaRoleDescription: 'Link',
97
+ ariaLabel: ariaLabel
25
98
  };
26
99
  }
100
+
101
+ createFocusHighlight() {
102
+ if (!this.options.navigatable) {
103
+ return;
104
+ }
105
+ const link = this.options.link;
106
+ const { x0, x1, y0, y1 } = link;
107
+ const xC = (x0 + x1) / 2;
108
+ const halfWidth = link.width / 2;
109
+
110
+ const offset = calculateControlPointsOffsetX(link);
111
+
112
+ this._highlight = new drawing.Path({ stroke: this.options.focusHighlight, visible: false })
113
+ .moveTo(x0, y0 + halfWidth)
114
+ .lineTo(x0, y0 - halfWidth)
115
+ .curveTo([xC + offset, y0 - halfWidth], [xC + offset, y1 - halfWidth], [x1, y1 - halfWidth])
116
+ .lineTo(x1, y1 + halfWidth)
117
+ .curveTo([xC - offset, y1 + halfWidth], [xC - offset, y0 + halfWidth], [x0, y0 + halfWidth]);
118
+ }
119
+
120
+ focus(options) {
121
+ if (this._highlight) {
122
+ const { highlight = true } = options || {};
123
+ if (highlight) {
124
+ this._highlight.options.set('visible', true);
125
+ }
126
+ const id = `${this.options.link.sourceId}->${this.options.link.targetId}`;
127
+ this.visual.options.set('id', id);
128
+
129
+ if (this.options.root()) {
130
+ this.options.root().setAttribute(ARIA_ACTIVE_DESCENDANT, id);
131
+ }
132
+ }
133
+ }
134
+
135
+ blur() {
136
+ if (this._highlight) {
137
+ this._highlight.options.set('visible', false);
138
+ this.visual.options.set('id', '');
139
+
140
+ if (this.options.root()) {
141
+ this.options.root().removeAttribute(ARIA_ACTIVE_DESCENDANT);
142
+ }
143
+ }
144
+ }
27
145
  }
28
146
 
29
147
  export const resolveLinkOptions = (link, options, sourceNode, targetNode) => {
@@ -1,18 +1,29 @@
1
1
  import { geometry, drawing } from '@progress/kendo-drawing';
2
2
  import { SankeyElement } from './element';
3
3
  import { deepExtend } from '../common';
4
+ import { ARIA_ACTIVE_DESCENDANT } from '../common/constants';
4
5
 
5
6
  export class Node extends SankeyElement {
6
7
  getElement() {
7
- const options = this.options;
8
- const node = options.node;
9
- const rect = new geometry.Rect([node.x0, node.y0], [node.x1 - node.x0, node.y1 - node.y0]);
8
+ return drawing.Path.fromRect(this.getRect(), this.visualOptions());
9
+ }
10
+
11
+ getRect() {
12
+ const node = this.options.node;
13
+ return new geometry.Rect([node.x0, node.y0], [node.x1 - node.x0, node.y1 - node.y0]);
14
+ }
10
15
 
11
- return drawing.Path.fromRect(rect, this.visualOptions());
16
+ getLabelText(options) {
17
+ let labelTemplate = options.labels.ariaTemplate;
18
+
19
+ if (labelTemplate) {
20
+ return labelTemplate({ node: options.node });
21
+ }
12
22
  }
13
23
 
14
24
  visualOptions() {
15
25
  const options = deepExtend({}, this.options, this.options.node);
26
+ const ariaLabel = this.getLabelText(options);
16
27
 
17
28
  return {
18
29
  fill: {
@@ -23,9 +34,48 @@ export class Node extends SankeyElement {
23
34
  className: 'k-sankey-node',
24
35
  role: 'graphics-symbol',
25
36
  ariaRoleDescription: 'Node',
26
- ariaLabel: options.node.label.text
37
+ ariaLabel: ariaLabel
27
38
  };
28
39
  }
40
+
41
+ createFocusHighlight() {
42
+ if (!this.options.navigatable) {
43
+ return;
44
+ }
45
+
46
+ this._highlight = new drawing.Path.fromRect(this.getRect(), {
47
+ stroke: this.options.focusHighlight,
48
+ visible: false
49
+ });
50
+
51
+ return this._highlight;
52
+ }
53
+
54
+ focus(options) {
55
+ if (this._highlight) {
56
+ const { highlight = true } = options || {};
57
+ if (highlight) {
58
+ this._highlight.options.set('visible', true);
59
+ }
60
+ const id = this.options.node.id;
61
+ this.visual.options.set('id', id);
62
+
63
+ if (this.options.root()) {
64
+ this.options.root().setAttribute(ARIA_ACTIVE_DESCENDANT, id);
65
+ }
66
+ }
67
+ }
68
+
69
+ blur() {
70
+ if (this._highlight) {
71
+ this._highlight.options.set('visible', false);
72
+ this.visual.options.set('id', '');
73
+
74
+ if (this.options.root()) {
75
+ this.options.root().removeAttribute(ARIA_ACTIVE_DESCENDANT);
76
+ }
77
+ }
78
+ }
29
79
  }
30
80
 
31
81
  const nodeColor = (node, nodeColors, index) => node.color || nodeColors[index % nodeColors.length];