@ds-mo/ui 2.11.2 → 2.13.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.
Files changed (262) hide show
  1. package/README.md +13 -13
  2. package/dist/.build-stamp +1 -1
  3. package/dist/components/ds-app-shell.js +1 -1
  4. package/dist/components/ds-banner.js +1 -1
  5. package/dist/components/ds-banner.js.map +1 -1
  6. package/dist/components/ds-bar-nav.js +1 -1
  7. package/dist/components/ds-bar-nav.js.map +1 -1
  8. package/dist/components/ds-button-filled.js +1 -1
  9. package/dist/components/{ds-accordion.d.ts → ds-button-unfilled.d.ts} +4 -4
  10. package/dist/components/ds-button-unfilled.js +2 -0
  11. package/dist/components/{ds-card-data-viz.d.ts → ds-card-data-viz-donut.d.ts} +4 -4
  12. package/dist/components/ds-card-data-viz-donut.js +2 -0
  13. package/dist/components/ds-card-data-viz-donut.js.map +1 -0
  14. package/dist/components/ds-card-setting.js +1 -1
  15. package/dist/components/ds-card-setting.js.map +1 -1
  16. package/dist/components/{ds-fade.d.ts → ds-card.d.ts} +4 -4
  17. package/dist/components/ds-card.js +2 -0
  18. package/dist/components/ds-card.js.map +1 -0
  19. package/dist/components/ds-chart-bar.js +1 -1
  20. package/dist/components/ds-chart-bar.js.map +1 -1
  21. package/dist/components/ds-chart-donut.js +1 -1
  22. package/dist/components/ds-chart-donut.js.map +1 -1
  23. package/dist/components/ds-chart-legend.js +1 -1
  24. package/dist/components/ds-chart-legend.js.map +1 -1
  25. package/dist/components/ds-chart-line.js +1 -1
  26. package/dist/components/ds-checkbox.js +1 -1
  27. package/dist/components/ds-checkbox.js.map +1 -1
  28. package/dist/components/ds-chip.js +1 -1
  29. package/dist/components/ds-chip.js.map +1 -1
  30. package/dist/components/ds-empty-state.js +1 -1
  31. package/dist/components/ds-empty-state.js.map +1 -1
  32. package/dist/components/ds-field.js +1 -1
  33. package/dist/components/ds-icon.js +1 -1
  34. package/dist/components/ds-input.js +1 -1
  35. package/dist/components/ds-input.js.map +1 -1
  36. package/dist/components/ds-loader.js +1 -1
  37. package/dist/components/ds-loader.js.map +1 -1
  38. package/dist/components/ds-menu.js +1 -1
  39. package/dist/components/ds-modal.js +1 -1
  40. package/dist/components/ds-pagination.js +1 -1
  41. package/dist/components/ds-pagination.js.map +1 -1
  42. package/dist/components/ds-panel-nav.js +1 -1
  43. package/dist/components/ds-panel-nav.js.map +1 -1
  44. package/dist/components/ds-panel-tools.js +1 -1
  45. package/dist/components/ds-panel-tools.js.map +1 -1
  46. package/dist/components/ds-radio-group.js +1 -1
  47. package/dist/components/ds-radio-group.js.map +1 -1
  48. package/dist/components/ds-select.js +1 -1
  49. package/dist/components/ds-select.js.map +1 -1
  50. package/dist/components/ds-shell-gradient-picker.js +1 -1
  51. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  52. package/dist/components/ds-skeleton.js +1 -1
  53. package/dist/components/ds-skeleton.js.map +1 -1
  54. package/dist/components/ds-slider.js +1 -1
  55. package/dist/components/ds-slider.js.map +1 -1
  56. package/dist/components/ds-tab-group-nav.js +1 -1
  57. package/dist/components/ds-tab-group.js +1 -1
  58. package/dist/components/ds-tab-group.js.map +1 -1
  59. package/dist/components/ds-table.js +1 -1
  60. package/dist/components/ds-table.js.map +1 -1
  61. package/dist/components/ds-tag.js +1 -1
  62. package/dist/components/ds-tag.js.map +1 -1
  63. package/dist/components/ds-text.js +1 -1
  64. package/dist/components/ds-toggle.js +1 -1
  65. package/dist/components/ds-toggle.js.map +1 -1
  66. package/dist/components/ds-tooltip-data-viz.js +1 -1
  67. package/dist/components/ds-tooltip-data-viz.js.map +1 -1
  68. package/dist/components/ds-tooltip.js +1 -1
  69. package/dist/components/ds-tooltip.js.map +1 -1
  70. package/dist/components/p-1m8r0wFg.js +2 -0
  71. package/dist/components/p-1m8r0wFg.js.map +1 -0
  72. package/dist/components/p-3FLHei19.js +2 -0
  73. package/dist/components/p-3FLHei19.js.map +1 -0
  74. package/dist/components/p-B2_D4IDN.js +2 -0
  75. package/dist/components/p-B2_D4IDN.js.map +1 -0
  76. package/dist/components/p-B5NcWtpp.js +2 -0
  77. package/dist/components/p-B5NcWtpp.js.map +1 -0
  78. package/dist/components/{p-8Nt92j1-.js → p-B82KZX9D.js} +2 -2
  79. package/dist/components/p-BQq26pt9.js.map +1 -1
  80. package/dist/components/p-B_g8X0eh.js +2 -0
  81. package/dist/components/p-B_g8X0eh.js.map +1 -0
  82. package/dist/components/{p-DdFUTZ3y.js → p-Be3PHv34.js} +2 -2
  83. package/dist/components/p-CQmf2CeW.js +2 -0
  84. package/dist/components/p-CQmf2CeW.js.map +1 -0
  85. package/dist/components/{p-DOU4cbpc.js → p-CtwNUCnp.js} +2 -2
  86. package/dist/components/p-CuRjYmBk.js +2 -0
  87. package/dist/components/p-CuRjYmBk.js.map +1 -0
  88. package/dist/components/p-CwARVsZQ.js +2 -0
  89. package/dist/components/p-CwARVsZQ.js.map +1 -0
  90. package/dist/components/p-DohXlNCJ.js +2 -0
  91. package/dist/components/p-DohXlNCJ.js.map +1 -0
  92. package/dist/components/p-DuFm-ieS.js +2 -0
  93. package/dist/components/p-DuFm-ieS.js.map +1 -0
  94. package/dist/components/p-EK1hOP19.js.map +1 -1
  95. package/dist/lib/nav/index.js +3 -0
  96. package/dist/lib/nav/index.js.map +2 -2
  97. package/dist/lib/utils/control-width.d.ts +4 -0
  98. package/dist/lib/utils/index.d.ts +2 -0
  99. package/dist/lib/utils/index.js +20 -1
  100. package/dist/lib/utils/index.js.map +3 -3
  101. package/dist/lib/utils/token-defaults.d.ts +7 -0
  102. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +21 -3
  103. package/dist/types/components/ButtonFilled/index.d.ts +1 -1
  104. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +70 -0
  105. package/dist/types/components/ButtonUnfilled/index.d.ts +2 -0
  106. package/dist/types/components/Card/Card.d.ts +20 -0
  107. package/dist/types/components/Card/index.d.ts +2 -0
  108. package/dist/types/components/CardDataVizDonut/CardDataVizDonut.d.ts +26 -0
  109. package/dist/types/components/CardDataVizDonut/index.d.ts +2 -0
  110. package/dist/types/components/CardSetting/CardSetting.d.ts +7 -2
  111. package/dist/types/components/ChartDonut/ChartDonut.d.ts +31 -1
  112. package/dist/types/components/Checkbox/Checkbox.d.ts +1 -1
  113. package/dist/types/components/Chip/Chip.d.ts +18 -14
  114. package/dist/types/components/Chip/index.d.ts +1 -1
  115. package/dist/types/components/Input/Input.d.ts +1 -1
  116. package/dist/types/components/Pagination/Pagination.d.ts +1 -1
  117. package/dist/types/components/PanelNav/PanelNav.d.ts +2 -0
  118. package/dist/types/components/PanelTools/panel-tools-types.d.ts +1 -1
  119. package/dist/types/components/RadioGroup/RadioGroup.d.ts +2 -2
  120. package/dist/types/components/Select/Select.d.ts +34 -2
  121. package/dist/types/components/Select/index.d.ts +2 -0
  122. package/dist/types/components/ShellGradientSwatch/ShellGradientSwatch.d.ts +1 -1
  123. package/dist/types/components/Slider/Slider.d.ts +1 -1
  124. package/dist/types/components/TabGroup/TabGroup.d.ts +1 -1
  125. package/dist/types/components/TabGroup/tab-item-utils.d.ts +2 -1
  126. package/dist/types/components/TabGroupNav/TabGroupNav.d.ts +1 -1
  127. package/dist/types/components/Tag/Tag.d.ts +0 -2
  128. package/dist/types/components/Tag/index.d.ts +1 -1
  129. package/dist/types/components/Toggle/Toggle.d.ts +1 -1
  130. package/dist/types/components/Tooltip/Tooltip.d.ts +25 -6
  131. package/dist/types/components/TooltipDataViz/TooltipDataViz.d.ts +18 -0
  132. package/dist/types/components.d.ts +474 -576
  133. package/dist/types/utils/control-width.d.ts +5 -0
  134. package/dist/types/utils/index.d.ts +2 -0
  135. package/dist/types/utils/token-defaults.d.ts +7 -0
  136. package/package.json +7 -4
  137. package/src/angular/index.ts +3 -9
  138. package/src/angular/proxies.ts +62 -215
  139. package/src/react/components.ts +3 -9
  140. package/src/react/ds-button-unfilled.ts +33 -0
  141. package/src/react/ds-card-data-viz-donut.ts +27 -0
  142. package/src/react/{ds-fade.ts → ds-card.ts} +7 -7
  143. package/src/react/ds-chip.ts +2 -10
  144. package/src/wc/components/BarNav/BarNav.tsx +3 -2
  145. package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +1 -1
  146. package/src/wc/components/ButtonFilled/ButtonFilled.tsx +97 -11
  147. package/src/wc/components/ButtonFilled/index.ts +7 -1
  148. package/src/wc/components/ButtonUnfilled/ButtonUnfilled.tsx +230 -0
  149. package/src/wc/components/ButtonUnfilled/index.ts +7 -0
  150. package/src/wc/components/Card/Card.tsx +79 -0
  151. package/src/wc/components/Card/index.ts +1 -0
  152. package/src/wc/components/{CardDataViz/CardDataViz.tsx → CardDataVizDonut/CardDataVizDonut.tsx} +47 -41
  153. package/src/wc/components/CardDataVizDonut/index.ts +1 -0
  154. package/src/wc/components/CardSetting/CardSetting.tsx +21 -36
  155. package/src/wc/components/ChartDonut/ChartDonut.tsx +159 -11
  156. package/src/wc/components/ChartLegend/ChartLegend.tsx +3 -0
  157. package/src/wc/components/Checkbox/Checkbox.tsx +5 -5
  158. package/src/wc/components/Chip/Chip.tsx +50 -67
  159. package/src/wc/components/Chip/index.ts +1 -1
  160. package/src/wc/components/Input/Input.tsx +4 -4
  161. package/src/wc/components/Menu/Menu.tsx +7 -4
  162. package/src/wc/components/Pagination/Pagination.tsx +6 -6
  163. package/src/wc/components/PanelNav/PanelNav.tsx +77 -48
  164. package/src/wc/components/PanelTools/PanelTools.tsx +19 -16
  165. package/src/wc/components/PanelTools/panel-tools-types.ts +1 -1
  166. package/src/wc/components/PanelTools/panel-tools-utils.ts +1 -1
  167. package/src/wc/components/RadioGroup/RadioGroup.tsx +7 -7
  168. package/src/wc/components/Select/Select.tsx +145 -64
  169. package/src/wc/components/Select/index.ts +1 -0
  170. package/src/wc/components/ShellGradientSwatch/ShellGradientSwatch.tsx +4 -3
  171. package/src/wc/components/Slider/Slider.tsx +7 -3
  172. package/src/wc/components/TabGroup/TabGroup.tsx +13 -7
  173. package/src/wc/components/TabGroup/tab-item-utils.ts +2 -1
  174. package/src/wc/components/TabGroupNav/TabGroupNav.tsx +14 -13
  175. package/src/wc/components/Tag/Tag.tsx +25 -24
  176. package/src/wc/components/Tag/index.ts +1 -1
  177. package/src/wc/components/Toggle/Toggle.tsx +6 -6
  178. package/src/wc/components/Tooltip/Tooltip.tsx +166 -64
  179. package/src/wc/components/TooltipDataViz/TooltipDataViz.tsx +57 -2
  180. package/src/wc/components.d.ts +474 -579
  181. package/src/wc/utils/control-density.css +42 -0
  182. package/src/wc/utils/control-inactive.css +20 -0
  183. package/src/wc/utils/control-width.css +28 -0
  184. package/src/wc/utils/control-width.ts +10 -0
  185. package/src/wc/utils/index.ts +2 -0
  186. package/src/wc/utils/interaction-fill.css +162 -0
  187. package/src/wc/utils/scroll-edge-fade.css +5 -0
  188. package/src/wc/utils/token-defaults.ts +10 -0
  189. package/dist/components/ds-accordion.js +0 -2
  190. package/dist/components/ds-accordion.js.map +0 -1
  191. package/dist/components/ds-breadcrumb.d.ts +0 -11
  192. package/dist/components/ds-breadcrumb.js +0 -2
  193. package/dist/components/ds-breadcrumb.js.map +0 -1
  194. package/dist/components/ds-button-unfilled-icon.d.ts +0 -11
  195. package/dist/components/ds-button-unfilled-icon.js +0 -2
  196. package/dist/components/ds-card-data-viz.js +0 -2
  197. package/dist/components/ds-card-data-viz.js.map +0 -1
  198. package/dist/components/ds-fade.js +0 -2
  199. package/dist/components/ds-fade.js.map +0 -1
  200. package/dist/components/ds-header.d.ts +0 -11
  201. package/dist/components/ds-header.js +0 -2
  202. package/dist/components/ds-header.js.map +0 -1
  203. package/dist/components/ds-scrollbar.d.ts +0 -11
  204. package/dist/components/ds-scrollbar.js +0 -2
  205. package/dist/components/ds-scrollbar.js.map +0 -1
  206. package/dist/components/ds-surface.d.ts +0 -11
  207. package/dist/components/ds-surface.js +0 -2
  208. package/dist/components/ds-surface.js.map +0 -1
  209. package/dist/components/ds-toast-provider.d.ts +0 -11
  210. package/dist/components/ds-toast-provider.js +0 -2
  211. package/dist/components/ds-toast-provider.js.map +0 -1
  212. package/dist/components/p-BMQbEIrQ.js +0 -2
  213. package/dist/components/p-BMQbEIrQ.js.map +0 -1
  214. package/dist/components/p-BSFgy5r6.js +0 -2
  215. package/dist/components/p-BSFgy5r6.js.map +0 -1
  216. package/dist/components/p-CBUIn8dk.js +0 -2
  217. package/dist/components/p-CBUIn8dk.js.map +0 -1
  218. package/dist/components/p-ChVwCVHy.js +0 -2
  219. package/dist/components/p-ChVwCVHy.js.map +0 -1
  220. package/dist/components/p-CpNIQXz6.js +0 -2
  221. package/dist/components/p-CpNIQXz6.js.map +0 -1
  222. package/dist/components/p-CvgHQfvT.js +0 -2
  223. package/dist/components/p-CvgHQfvT.js.map +0 -1
  224. package/dist/components/p-bDf6yBQd.js +0 -2
  225. package/dist/components/p-bDf6yBQd.js.map +0 -1
  226. package/dist/types/components/Accordion/Accordion.d.ts +0 -28
  227. package/dist/types/components/Breadcrumb/Breadcrumb.d.ts +0 -16
  228. package/dist/types/components/ButtonUnfilledIcon/ButtonUnfilledIcon.d.ts +0 -45
  229. package/dist/types/components/ButtonUnfilledIcon/index.d.ts +0 -2
  230. package/dist/types/components/CardDataViz/CardDataViz.d.ts +0 -22
  231. package/dist/types/components/CardDataViz/index.d.ts +0 -2
  232. package/dist/types/components/Fade/Fade.d.ts +0 -22
  233. package/dist/types/components/Fade/index.d.ts +0 -2
  234. package/dist/types/components/Header/Header.d.ts +0 -7
  235. package/dist/types/components/Scrollbar/Scrollbar.d.ts +0 -27
  236. package/dist/types/components/Surface/Surface.d.ts +0 -22
  237. package/dist/types/components/Toast/Toast.d.ts +0 -16
  238. package/dist/types/components/Toast/toast-service.d.ts +0 -24
  239. package/src/react/ds-accordion.ts +0 -27
  240. package/src/react/ds-breadcrumb.ts +0 -27
  241. package/src/react/ds-button-unfilled-icon.ts +0 -33
  242. package/src/react/ds-card-data-viz.ts +0 -26
  243. package/src/react/ds-header.ts +0 -26
  244. package/src/react/ds-scrollbar.ts +0 -26
  245. package/src/react/ds-surface.ts +0 -26
  246. package/src/react/ds-toast-provider.ts +0 -26
  247. package/src/wc/components/Accordion/Accordion.tsx +0 -144
  248. package/src/wc/components/Breadcrumb/Breadcrumb.tsx +0 -61
  249. package/src/wc/components/ButtonUnfilledIcon/ButtonUnfilledIcon.tsx +0 -129
  250. package/src/wc/components/ButtonUnfilledIcon/index.ts +0 -1
  251. package/src/wc/components/CardDataViz/index.ts +0 -1
  252. package/src/wc/components/Fade/Fade.tsx +0 -58
  253. package/src/wc/components/Fade/index.ts +0 -1
  254. package/src/wc/components/Header/Header.tsx +0 -33
  255. package/src/wc/components/Scrollbar/Scrollbar.tsx +0 -189
  256. package/src/wc/components/Surface/Surface.tsx +0 -81
  257. package/src/wc/components/Toast/Toast.tsx +0 -179
  258. package/src/wc/components/Toast/toast-service.ts +0 -67
  259. /package/dist/components/{ds-button-unfilled-icon.js.map → ds-button-unfilled.js.map} +0 -0
  260. /package/dist/components/{p-8Nt92j1-.js.map → p-B82KZX9D.js.map} +0 -0
  261. /package/dist/components/{p-DdFUTZ3y.js.map → p-Be3PHv34.js.map} +0 -0
  262. /package/dist/components/{p-DOU4cbpc.js.map → p-CtwNUCnp.js.map} +0 -0
@@ -1,12 +1,39 @@
1
- import { Component, Prop, State, Element, Watch, h, Host } from '@stencil/core';
1
+ import { Component, Prop, Element, Watch, h, Host } from '@stencil/core';
2
2
  import { resolveCssLengthPx, resolveCssTimeMs, TOKEN_CSS_LENGTHS, TOKEN_DEFAULTS } from '../../utils';
3
3
 
4
4
  export type TooltipSide = 'top' | 'right' | 'bottom' | 'left';
5
5
  export type TooltipAlign = 'start' | 'center' | 'end';
6
+ export type TooltipSize = 'md' | 'sm' | 'xs';
7
+
8
+ /** Body text per control-density size (regular weight — not emphasis). */
9
+ const TEXT_VARIANT: Record<TooltipSize, string> = {
10
+ md: 'text-body-medium',
11
+ sm: 'text-body-small',
12
+ xs: 'text-caption',
13
+ };
14
+
15
+ const LINE_HEIGHT: Record<TooltipSize, string> = {
16
+ md: 'var(--typography-lineheight-md)',
17
+ sm: 'var(--typography-lineheight-sm)',
18
+ xs: 'var(--typography-lineheight-xs)',
19
+ };
20
+
21
+ const FALLBACK_HEIGHT: Record<TooltipSize, string> = {
22
+ md: TOKEN_DEFAULTS.size400,
23
+ sm: TOKEN_DEFAULTS.size300,
24
+ xs: TOKEN_DEFAULTS.size200,
25
+ };
6
26
 
7
27
  let lastDismissedAt = 0;
8
28
  let tooltipIdCounter = 0;
29
+ /** Currently open tooltip — force-cleared on warm handoff so tips don't cross-fade. */
30
+ let activeTooltip: Tooltip | null = null;
9
31
 
32
+ /**
33
+ * Imperative body portal for the popup.
34
+ * Stencil must not own the portaled node — moving a VDOM child to `document.body`
35
+ * causes recreate → re-portal → @State loops that freeze the page.
36
+ */
10
37
  @Component({
11
38
  tag: 'ds-tooltip',
12
39
  styleUrl: 'Tooltip.css',
@@ -16,47 +43,67 @@ export class Tooltip {
16
43
  @Element() el!: HTMLElement;
17
44
 
18
45
  @Prop() label!: string;
46
+
47
+ /** Control density (height, padding, type). */
48
+ @Prop() size: TooltipSize = 'md';
49
+
19
50
  @Prop() side: TooltipSide = 'top';
20
51
  @Prop() align: TooltipAlign = 'center';
21
52
  /** Gap between anchor and tooltip — number (px) or TokoMo length. */
22
53
  @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;
23
54
  /** Cross-axis offset — number (px) or TokoMo length. */
24
55
  @Prop() alignOffset: number | string = 0;
25
- /** Show delay — number (ms) or TokoMo time token / `var(--effect-animation-delay-*)`. */
26
- @Prop() delay: number | string = TOKEN_DEFAULTS.animationDelayMedium1;
56
+ /**
57
+ * Hover show delay before the tooltip appears.
58
+ * Default: `--effect-animation-delay-medium-3` (1000ms). Accepts a number (ms)
59
+ * or a TokoMo time token / `var(--effect-animation-delay-*)`. Applies to the
60
+ * initial hover/focus show only — after a recent dismiss, reopen is instant.
61
+ * Prefer the default; override only for denser chrome or rare/destructive actions.
62
+ */
63
+ @Prop() delay: number | string = TOKEN_DEFAULTS.animationDelayMedium3;
27
64
  @Prop() shortcutKey: string | undefined;
28
65
  @Prop() shortcutKeyPosition: 'start' | 'end' = 'end';
29
66
 
30
- @State() private visible: boolean = false;
31
- @State() private closing: boolean = false;
32
- @State() private pos: { x: number; y: number } = { x: 0, y: 0 };
33
-
34
67
  private tooltipId = `ds-tooltip-${++tooltipIdCounter}`;
35
68
  private delayTimer: ReturnType<typeof setTimeout> | null = null;
36
69
  private closeTimer: ReturnType<typeof setTimeout> | null = null;
37
70
  private anchor: HTMLElement | null = null;
38
- private tooltipEl: HTMLElement | null = null;
71
+ private popupEl: HTMLElement | null = null;
72
+ private skipEnterAnimation = false;
73
+ private isOpen = false;
39
74
  private mouseEnterHandler: () => void = () => this.show();
40
75
  private mouseLeaveHandler: () => void = () => this.hide();
76
+ /** focusin/focusout so nested focus targets (e.g. ds-button-* inner button) still open the tip. */
41
77
  private focusHandler: () => void = () => this.show();
42
- private blurHandler: () => void = () => this.hide();
78
+ private blurHandler: (e: FocusEvent) => void = (e) => {
79
+ const next = e.relatedTarget as Node | null;
80
+ if (next && this.anchor?.contains(next)) return;
81
+ this.hide();
82
+ };
43
83
 
44
84
  componentDidLoad() {
45
85
  this.bindAnchor();
46
86
  }
47
87
 
48
88
  disconnectedCallback() {
89
+ if (activeTooltip === this) activeTooltip = null;
49
90
  this.unbindAnchor();
50
91
  this.clearTimers();
92
+ this.destroyPopup();
51
93
  }
52
94
 
53
95
  @Watch('label')
96
+ @Watch('size')
54
97
  @Watch('side')
55
98
  @Watch('align')
56
99
  @Watch('sideOffset')
57
100
  @Watch('alignOffset')
58
- onPositionPropsChange() {
59
- if (this.visible) this.calculatePosition();
101
+ @Watch('shortcutKey')
102
+ @Watch('shortcutKeyPosition')
103
+ onContentOrPositionChange() {
104
+ if (!this.popupEl || !this.isOpen) return;
105
+ this.renderPopupContent();
106
+ this.calculatePosition();
60
107
  }
61
108
 
62
109
  private get viewportPadPx(): number {
@@ -72,7 +119,7 @@ export class Tooltip {
72
119
  }
73
120
 
74
121
  private get hoverDelayMs(): number {
75
- return resolveCssTimeMs(this.delay, TOKEN_DEFAULTS.animationDelayMedium1);
122
+ return resolveCssTimeMs(this.delay, TOKEN_DEFAULTS.animationDelayMedium3);
76
123
  }
77
124
 
78
125
  private get instantReopenMs(): number {
@@ -91,7 +138,8 @@ export class Tooltip {
91
138
  }
92
139
 
93
140
  private get tooltipFallbackHeightPx(): number {
94
- return resolveCssLengthPx(TOKEN_DEFAULTS.size300, TOKEN_DEFAULTS.size300);
141
+ const token = FALLBACK_HEIGHT[this.size];
142
+ return resolveCssLengthPx(token, token);
95
143
  }
96
144
 
97
145
  private bindAnchor() {
@@ -103,8 +151,8 @@ export class Tooltip {
103
151
  child.setAttribute('aria-describedby', this.tooltipId);
104
152
  child.addEventListener('mouseenter', this.mouseEnterHandler);
105
153
  child.addEventListener('mouseleave', this.mouseLeaveHandler);
106
- child.addEventListener('focus', this.focusHandler);
107
- child.addEventListener('blur', this.blurHandler);
154
+ child.addEventListener('focusin', this.focusHandler);
155
+ child.addEventListener('focusout', this.blurHandler);
108
156
  }
109
157
 
110
158
  private unbindAnchor() {
@@ -112,8 +160,8 @@ export class Tooltip {
112
160
  this.anchor.removeAttribute('aria-describedby');
113
161
  this.anchor.removeEventListener('mouseenter', this.mouseEnterHandler);
114
162
  this.anchor.removeEventListener('mouseleave', this.mouseLeaveHandler);
115
- this.anchor.removeEventListener('focus', this.focusHandler);
116
- this.anchor.removeEventListener('blur', this.blurHandler);
163
+ this.anchor.removeEventListener('focusin', this.focusHandler);
164
+ this.anchor.removeEventListener('focusout', this.blurHandler);
117
165
  this.anchor = null;
118
166
  }
119
167
 
@@ -122,39 +170,114 @@ export class Tooltip {
122
170
  if (this.closeTimer) { clearTimeout(this.closeTimer); this.closeTimer = null; }
123
171
  }
124
172
 
173
+ private destroyPopup() {
174
+ if (!this.popupEl) return;
175
+ this.popupEl.remove();
176
+ this.popupEl = null;
177
+ }
178
+
179
+ /** Drop the open tip immediately (warm handoff to another tooltip). */
180
+ private hideInstant() {
181
+ this.clearTimers();
182
+ lastDismissedAt = Date.now();
183
+ if (activeTooltip === this) activeTooltip = null;
184
+ this.destroyPopup();
185
+ this.isOpen = false;
186
+ this.skipEnterAnimation = false;
187
+ }
188
+
125
189
  private show() {
126
190
  this.clearTimers();
127
- const instant = Date.now() - lastDismissedAt < this.instantReopenMs;
191
+ let instant = Date.now() - lastDismissedAt < this.instantReopenMs;
192
+ if (activeTooltip && activeTooltip !== this) {
193
+ activeTooltip.hideInstant();
194
+ instant = true;
195
+ }
196
+ activeTooltip = this;
197
+ this.skipEnterAnimation = instant;
128
198
  if (instant) {
129
- this.closing = false;
130
- this.visible = true;
131
- requestAnimationFrame(() => this.calculatePosition());
199
+ this.mountPopup();
132
200
  return;
133
201
  }
134
202
  this.delayTimer = setTimeout(() => {
135
203
  this.delayTimer = null;
136
- this.closing = false;
137
- this.visible = true;
138
- requestAnimationFrame(() => this.calculatePosition());
204
+ this.mountPopup();
139
205
  }, this.hoverDelayMs);
140
206
  }
141
207
 
142
208
  private hide() {
143
209
  this.clearTimers();
144
- if (!this.visible) return;
210
+ if (!this.popupEl && !this.isOpen) return;
145
211
  lastDismissedAt = Date.now();
146
- this.closing = true;
212
+ if (activeTooltip === this) activeTooltip = null;
213
+ if (!this.popupEl) {
214
+ this.isOpen = false;
215
+ return;
216
+ }
217
+ this.popupEl.classList.remove('tooltip-popup--instant');
218
+ this.popupEl.classList.add('tooltip-popup--closing');
147
219
  this.closeTimer = setTimeout(() => {
148
220
  this.closeTimer = null;
149
- this.visible = false;
150
- this.closing = false;
221
+ this.destroyPopup();
222
+ this.isOpen = false;
223
+ this.skipEnterAnimation = false;
151
224
  }, this.fadeOutMs);
152
225
  }
153
226
 
227
+ private mountPopup() {
228
+ if (!this.popupEl) {
229
+ const tip = document.createElement('div');
230
+ tip.id = this.tooltipId;
231
+ tip.setAttribute('role', 'tooltip');
232
+ // `sc-ds-tooltip` is required for Stencil scoped CSS to match outside the host.
233
+ tip.className = 'tooltip-popup sc-ds-tooltip';
234
+ Object.assign(tip.style, {
235
+ position: 'fixed',
236
+ left: '0',
237
+ top: '0',
238
+ zIndex: 'var(--dimension-z-index-tooltip)',
239
+ pointerEvents: 'none',
240
+ });
241
+ document.body.appendChild(tip);
242
+ this.popupEl = tip;
243
+ }
244
+
245
+ this.popupEl.classList.toggle('tooltip-popup--instant', this.skipEnterAnimation);
246
+ this.popupEl.classList.remove('tooltip-popup--closing');
247
+ this.renderPopupContent();
248
+ this.calculatePosition();
249
+ this.isOpen = true;
250
+ }
251
+
252
+ private renderPopupContent() {
253
+ if (!this.popupEl) return;
254
+ const textVariant = TEXT_VARIANT[this.size];
255
+ const lineHeight = LINE_HEIGHT[this.size];
256
+ const density =
257
+ this.size === 'md' ? 'ds-control--md' : this.size === 'sm' ? 'ds-control--sm' : 'ds-control--xs';
258
+ // Every node needs `sc-ds-tooltip` so scoped selectors (`.foo.sc-ds-tooltip`) match.
259
+ const sc = 'sc-ds-tooltip';
260
+
261
+ const startKey =
262
+ this.shortcutKey && this.shortcutKeyPosition === 'start'
263
+ ? `<div class="key-hint ${sc}" aria-hidden="true"><span class="text-caption-emphasis ${sc}">${escapeHtml(this.shortcutKey)}</span></div>`
264
+ : '';
265
+ const endKey =
266
+ this.shortcutKey && this.shortcutKeyPosition === 'end'
267
+ ? `<div class="key-hint ${sc}" aria-hidden="true"><span class="text-caption-emphasis ${sc}">${escapeHtml(this.shortcutKey)}</span></div>`
268
+ : '';
269
+
270
+ this.popupEl.innerHTML =
271
+ `<div class="tooltip-inner ${density} ${sc}">` +
272
+ startKey +
273
+ `<span class="tooltip-label ${textVariant} ${sc}" style="line-height:${lineHeight}">${escapeHtml(this.label)}</span>` +
274
+ endKey +
275
+ `</div>`;
276
+ }
277
+
154
278
  private calculatePosition() {
155
- if (!this.anchor) return;
156
- const tip = this.el.querySelector('.tooltip-popup') as HTMLElement | null;
157
- if (!tip) return;
279
+ if (!this.anchor || !this.popupEl) return;
280
+ const tip = this.popupEl;
158
281
  const a = this.anchor.getBoundingClientRect();
159
282
  const tw = tip.offsetWidth || this.tooltipFallbackWidthPx;
160
283
  const th = tip.offsetHeight || this.tooltipFallbackHeightPx;
@@ -190,46 +313,25 @@ export class Tooltip {
190
313
  break;
191
314
  }
192
315
 
193
- this.pos = {
194
- x: Math.min(Math.max(x, vpPad), window.innerWidth - tw - vpPad),
195
- y: Math.min(Math.max(y, vpPad), window.innerHeight - th - vpPad),
196
- };
316
+ const nextX = Math.min(Math.max(x, vpPad), window.innerWidth - tw - vpPad);
317
+ const nextY = Math.min(Math.max(y, vpPad), window.innerHeight - th - vpPad);
318
+ tip.style.transform = `translate(${Math.round(nextX)}px, ${Math.round(nextY)}px)`;
197
319
  }
198
320
 
199
321
  render() {
200
322
  return (
201
323
  <Host style={{ display: 'contents' }}>
202
324
  <slot />
203
- {(this.visible || this.closing) && (
204
- <div
205
- id={this.tooltipId}
206
- class={{ 'tooltip-popup': true, 'tooltip-popup--closing': this.closing }}
207
- role="tooltip"
208
- style={{
209
- position: 'fixed',
210
- left: '0',
211
- top: '0',
212
- transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,
213
- zIndex: '10000',
214
- pointerEvents: 'none',
215
- }}
216
- >
217
- <div class="tooltip-inner">
218
- {this.shortcutKey && this.shortcutKeyPosition === 'start' && (
219
- <div class="key-hint" aria-hidden="true">
220
- <span class="text-caption-emphasis">{this.shortcutKey}</span>
221
- </div>
222
- )}
223
- <span class="text-body-small">{this.label}</span>
224
- {this.shortcutKey && this.shortcutKeyPosition === 'end' && (
225
- <div class="key-hint" aria-hidden="true">
226
- <span class="text-caption-emphasis">{this.shortcutKey}</span>
227
- </div>
228
- )}
229
- </div>
230
- </div>
231
- )}
232
325
  </Host>
233
326
  );
234
327
  }
235
328
  }
329
+
330
+ function escapeHtml(value: string): string {
331
+ return value
332
+ .replace(/&/g, '&amp;')
333
+ .replace(/</g, '&lt;')
334
+ .replace(/>/g, '&gt;')
335
+ .replace(/"/g, '&quot;')
336
+ .replace(/'/g, '&#39;');
337
+ }
@@ -1,4 +1,5 @@
1
1
  import { Component, Prop, State, Element, Watch, h, Host } from '@stencil/core';
2
+ import { resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
2
3
 
3
4
  const CURSOR_OFFSET_PX = 12;
4
5
  const VIEWPORT_PAD_PX = 8;
@@ -16,6 +17,9 @@ export type TooltipDataVizAlign = 'top' | 'bottom';
16
17
  * with `x`/`y` as pixel coordinates within that wrapper. Defaults to sitting below-right
17
18
  * of the anchor (matching cursor-following tooltips), flipping to whichever side/edge
18
19
  * keeps it on-screen.
20
+ *
21
+ * Mount (or remount) when a hover session starts so `delay` applies once per hover;
22
+ * keep the instance mounted while the cursor moves so tracking stays instant.
19
23
  */
20
24
  @Component({
21
25
  tag: 'ds-tooltip-data-viz',
@@ -31,19 +35,67 @@ export class TooltipDataViz {
31
35
  @Prop() x: number = 0;
32
36
  @Prop() y: number = 0;
33
37
 
38
+ /**
39
+ * Show delay after mount before the callout appears.
40
+ * Default: `--effect-animation-delay-instant` (0ms). Accepts a number (ms)
41
+ * or a TokoMo time token / `var(--effect-animation-delay-*)`. Charts need
42
+ * immediate feedback while scrubbing; prefer the default. Mount once per
43
+ * hover session so any non-zero delay runs once, then track `x`/`y` instantly.
44
+ */
45
+ @Prop() delay: number | string = TOKEN_DEFAULTS.animationDelayInstant;
46
+
34
47
  @State() private side: TooltipDataVizSide = 'right';
35
48
  @State() private align: TooltipDataVizAlign = 'bottom';
49
+ @State() private visible: boolean = false;
50
+
51
+ private delayTimer: ReturnType<typeof setTimeout> | null = null;
36
52
 
37
53
  componentDidLoad() {
54
+ this.scheduleShow();
38
55
  this.calculatePlacement();
39
56
  }
40
57
 
58
+ disconnectedCallback() {
59
+ this.clearDelayTimer();
60
+ }
61
+
41
62
  @Watch('x')
42
63
  @Watch('y')
43
64
  onAnchorChange() {
44
65
  requestAnimationFrame(() => this.calculatePlacement());
45
66
  }
46
67
 
68
+ @Watch('delay')
69
+ onDelayChange() {
70
+ if (!this.visible) this.scheduleShow();
71
+ }
72
+
73
+ private get showDelayMs(): number {
74
+ return resolveCssTimeMs(this.delay, TOKEN_DEFAULTS.animationDelayInstant);
75
+ }
76
+
77
+ private clearDelayTimer() {
78
+ if (this.delayTimer) {
79
+ clearTimeout(this.delayTimer);
80
+ this.delayTimer = null;
81
+ }
82
+ }
83
+
84
+ private scheduleShow() {
85
+ this.clearDelayTimer();
86
+ this.visible = false;
87
+ const ms = this.showDelayMs;
88
+ if (ms <= 0) {
89
+ this.visible = true;
90
+ return;
91
+ }
92
+ this.delayTimer = setTimeout(() => {
93
+ this.delayTimer = null;
94
+ this.visible = true;
95
+ requestAnimationFrame(() => this.calculatePlacement());
96
+ }, ms);
97
+ }
98
+
47
99
  private calculatePlacement() {
48
100
  const rect = this.el.getBoundingClientRect();
49
101
  if (!rect.width || !rect.height) return;
@@ -61,10 +113,13 @@ export class TooltipDataViz {
61
113
 
62
114
  return (
63
115
  <Host
64
- class="tooltip-data-viz"
116
+ class={{
117
+ 'tooltip-data-viz': true,
118
+ 'tooltip-data-viz--visible': this.visible,
119
+ }}
65
120
  style={{ left: `${this.x}px`, top: `${this.y}px`, transform: `translate(${translateX}, ${translateY})` }}
66
121
  >
67
- <div class="tooltip-data-viz__item">
122
+ <div class="tooltip-data-viz__item ds-control--md">
68
123
  <span class="tooltip-data-viz__label">
69
124
  <ds-text as="span" variant="text-body-medium" color="var(--color-foreground-on-translucent-background-secondary)">
70
125
  {this.label}