@ds-mo/ui 2.11.2 → 2.12.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 (226) 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.js +1 -1
  12. package/dist/components/ds-card-data-viz.js.map +1 -1
  13. package/dist/components/ds-card-setting.js +1 -1
  14. package/dist/components/ds-card-setting.js.map +1 -1
  15. package/dist/components/ds-chart-bar.js +1 -1
  16. package/dist/components/ds-chart-bar.js.map +1 -1
  17. package/dist/components/ds-chart-donut.js +1 -1
  18. package/dist/components/ds-chart-donut.js.map +1 -1
  19. package/dist/components/ds-chart-legend.js +1 -1
  20. package/dist/components/ds-chart-legend.js.map +1 -1
  21. package/dist/components/ds-chart-line.js +1 -1
  22. package/dist/components/ds-checkbox.js +1 -1
  23. package/dist/components/ds-checkbox.js.map +1 -1
  24. package/dist/components/ds-chip.js +1 -1
  25. package/dist/components/ds-chip.js.map +1 -1
  26. package/dist/components/ds-divider.js +1 -1
  27. package/dist/components/ds-empty-state.js +1 -1
  28. package/dist/components/ds-empty-state.js.map +1 -1
  29. package/dist/components/ds-field.js +1 -1
  30. package/dist/components/ds-field.js.map +1 -1
  31. package/dist/components/ds-header.js +1 -1
  32. package/dist/components/ds-header.js.map +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-scrollbar.js +1 -1
  49. package/dist/components/ds-select.js +1 -1
  50. package/dist/components/ds-select.js.map +1 -1
  51. package/dist/components/ds-shell-gradient-picker.js +1 -1
  52. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  53. package/dist/components/ds-skeleton.js +1 -1
  54. package/dist/components/ds-skeleton.js.map +1 -1
  55. package/dist/components/ds-slider.js +1 -1
  56. package/dist/components/ds-slider.js.map +1 -1
  57. package/dist/components/ds-tab-group-nav.js +1 -1
  58. package/dist/components/ds-tab-group.js +1 -1
  59. package/dist/components/ds-tab-group.js.map +1 -1
  60. package/dist/components/ds-table.js +1 -1
  61. package/dist/components/ds-table.js.map +1 -1
  62. package/dist/components/ds-tag.js +1 -1
  63. package/dist/components/ds-tag.js.map +1 -1
  64. package/dist/components/ds-text.js +1 -1
  65. package/dist/components/ds-toggle.js +1 -1
  66. package/dist/components/ds-toggle.js.map +1 -1
  67. package/dist/components/ds-tooltip-data-viz.js +1 -1
  68. package/dist/components/ds-tooltip-data-viz.js.map +1 -1
  69. package/dist/components/ds-tooltip.js +1 -1
  70. package/dist/components/ds-tooltip.js.map +1 -1
  71. package/dist/components/p-B9dHCgY-.js +2 -0
  72. package/dist/components/p-B9dHCgY-.js.map +1 -0
  73. package/dist/components/p-BPK76IsW.js +2 -0
  74. package/dist/components/p-BPK76IsW.js.map +1 -0
  75. package/dist/components/p-BQq26pt9.js.map +1 -1
  76. package/dist/components/p-BTC9Njj6.js +2 -0
  77. package/dist/components/p-BTC9Njj6.js.map +1 -0
  78. package/dist/components/p-BZIOTcnM.js +2 -0
  79. package/dist/components/p-BZIOTcnM.js.map +1 -0
  80. package/dist/components/{p-DdFUTZ3y.js → p-C3ORQUOp.js} +2 -2
  81. package/dist/components/{p-8Nt92j1-.js → p-CyFVn2Sc.js} +2 -2
  82. package/dist/components/p-DKdI8T8v.js +2 -0
  83. package/dist/components/p-DKdI8T8v.js.map +1 -0
  84. package/dist/components/{p-DOU4cbpc.js → p-DRqxVEye.js} +2 -2
  85. package/dist/components/p-DkcH8mRl.js +2 -0
  86. package/dist/components/p-DkcH8mRl.js.map +1 -0
  87. package/dist/components/p-DusYu-if.js +2 -0
  88. package/dist/components/p-DusYu-if.js.map +1 -0
  89. package/dist/components/p-EK1hOP19.js.map +1 -1
  90. package/dist/components/p-UbKTMQjv.js +2 -0
  91. package/dist/components/p-UbKTMQjv.js.map +1 -0
  92. package/dist/lib/nav/index.js +3 -0
  93. package/dist/lib/nav/index.js.map +2 -2
  94. package/dist/lib/utils/index.js +6 -0
  95. package/dist/lib/utils/index.js.map +2 -2
  96. package/dist/lib/utils/token-defaults.d.ts +3 -0
  97. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +17 -3
  98. package/dist/types/components/ButtonFilled/index.d.ts +1 -1
  99. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +66 -0
  100. package/dist/types/components/ButtonUnfilled/index.d.ts +2 -0
  101. package/dist/types/components/CardDataViz/CardDataViz.d.ts +1 -1
  102. package/dist/types/components/CardSetting/CardSetting.d.ts +1 -1
  103. package/dist/types/components/ChartDonut/ChartDonut.d.ts +7 -0
  104. package/dist/types/components/Checkbox/Checkbox.d.ts +1 -1
  105. package/dist/types/components/Chip/Chip.d.ts +18 -14
  106. package/dist/types/components/Chip/index.d.ts +1 -1
  107. package/dist/types/components/Input/Input.d.ts +1 -1
  108. package/dist/types/components/Pagination/Pagination.d.ts +1 -1
  109. package/dist/types/components/PanelNav/PanelNav.d.ts +2 -0
  110. package/dist/types/components/PanelTools/panel-tools-types.d.ts +1 -1
  111. package/dist/types/components/RadioGroup/RadioGroup.d.ts +2 -2
  112. package/dist/types/components/Select/Select.d.ts +2 -2
  113. package/dist/types/components/ShellGradientSwatch/ShellGradientSwatch.d.ts +1 -1
  114. package/dist/types/components/Slider/Slider.d.ts +1 -1
  115. package/dist/types/components/TabGroup/TabGroup.d.ts +1 -1
  116. package/dist/types/components/TabGroup/tab-item-utils.d.ts +2 -1
  117. package/dist/types/components/TabGroupNav/TabGroupNav.d.ts +1 -1
  118. package/dist/types/components/Tag/Tag.d.ts +0 -2
  119. package/dist/types/components/Tag/index.d.ts +1 -1
  120. package/dist/types/components/Toggle/Toggle.d.ts +1 -1
  121. package/dist/types/components/Tooltip/Tooltip.d.ts +25 -6
  122. package/dist/types/components/TooltipDataViz/TooltipDataViz.d.ts +18 -0
  123. package/dist/types/components.d.ts +277 -476
  124. package/dist/types/utils/token-defaults.d.ts +3 -0
  125. package/package.json +6 -3
  126. package/src/angular/index.ts +1 -6
  127. package/src/angular/proxies.ts +33 -170
  128. package/src/react/components.ts +1 -6
  129. package/src/react/ds-button-unfilled.ts +33 -0
  130. package/src/react/ds-chip.ts +2 -10
  131. package/src/wc/components/BarNav/BarNav.tsx +3 -2
  132. package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +1 -1
  133. package/src/wc/components/ButtonFilled/ButtonFilled.tsx +90 -11
  134. package/src/wc/components/ButtonFilled/index.ts +6 -1
  135. package/src/wc/components/ButtonUnfilled/ButtonUnfilled.tsx +223 -0
  136. package/src/wc/components/ButtonUnfilled/index.ts +6 -0
  137. package/src/wc/components/CardDataViz/CardDataViz.tsx +1 -2
  138. package/src/wc/components/CardSetting/CardSetting.tsx +4 -6
  139. package/src/wc/components/ChartDonut/ChartDonut.tsx +50 -3
  140. package/src/wc/components/ChartLegend/ChartLegend.tsx +3 -0
  141. package/src/wc/components/Checkbox/Checkbox.tsx +5 -5
  142. package/src/wc/components/Chip/Chip.tsx +50 -67
  143. package/src/wc/components/Chip/index.ts +1 -1
  144. package/src/wc/components/Input/Input.tsx +4 -4
  145. package/src/wc/components/Menu/Menu.tsx +7 -4
  146. package/src/wc/components/Pagination/Pagination.tsx +6 -6
  147. package/src/wc/components/PanelNav/PanelNav.tsx +77 -48
  148. package/src/wc/components/PanelTools/PanelTools.tsx +19 -16
  149. package/src/wc/components/PanelTools/panel-tools-types.ts +1 -1
  150. package/src/wc/components/PanelTools/panel-tools-utils.ts +1 -1
  151. package/src/wc/components/RadioGroup/RadioGroup.tsx +7 -7
  152. package/src/wc/components/Select/Select.tsx +5 -5
  153. package/src/wc/components/ShellGradientSwatch/ShellGradientSwatch.tsx +4 -3
  154. package/src/wc/components/Slider/Slider.tsx +7 -3
  155. package/src/wc/components/TabGroup/TabGroup.tsx +13 -7
  156. package/src/wc/components/TabGroup/tab-item-utils.ts +2 -1
  157. package/src/wc/components/TabGroupNav/TabGroupNav.tsx +14 -13
  158. package/src/wc/components/Tag/Tag.tsx +25 -24
  159. package/src/wc/components/Tag/index.ts +1 -1
  160. package/src/wc/components/Toggle/Toggle.tsx +6 -6
  161. package/src/wc/components/Tooltip/Tooltip.tsx +166 -64
  162. package/src/wc/components/TooltipDataViz/TooltipDataViz.tsx +57 -2
  163. package/src/wc/components.d.ts +277 -476
  164. package/src/wc/utils/control-density.css +42 -0
  165. package/src/wc/utils/control-inactive.css +20 -0
  166. package/src/wc/utils/interaction-fill.css +162 -0
  167. package/src/wc/utils/scroll-edge-fade.css +5 -0
  168. package/src/wc/utils/token-defaults.ts +6 -0
  169. package/dist/components/ds-accordion.js +0 -2
  170. package/dist/components/ds-accordion.js.map +0 -1
  171. package/dist/components/ds-breadcrumb.d.ts +0 -11
  172. package/dist/components/ds-breadcrumb.js +0 -2
  173. package/dist/components/ds-breadcrumb.js.map +0 -1
  174. package/dist/components/ds-button-unfilled-icon.d.ts +0 -11
  175. package/dist/components/ds-button-unfilled-icon.js +0 -2
  176. package/dist/components/ds-fade.d.ts +0 -11
  177. package/dist/components/ds-fade.js +0 -2
  178. package/dist/components/ds-fade.js.map +0 -1
  179. package/dist/components/ds-surface.d.ts +0 -11
  180. package/dist/components/ds-surface.js +0 -2
  181. package/dist/components/ds-surface.js.map +0 -1
  182. package/dist/components/ds-toast-provider.d.ts +0 -11
  183. package/dist/components/ds-toast-provider.js +0 -2
  184. package/dist/components/ds-toast-provider.js.map +0 -1
  185. package/dist/components/p-BMQbEIrQ.js +0 -2
  186. package/dist/components/p-BMQbEIrQ.js.map +0 -1
  187. package/dist/components/p-BSFgy5r6.js +0 -2
  188. package/dist/components/p-BSFgy5r6.js.map +0 -1
  189. package/dist/components/p-CBUIn8dk.js +0 -2
  190. package/dist/components/p-CBUIn8dk.js.map +0 -1
  191. package/dist/components/p-ChVwCVHy.js +0 -2
  192. package/dist/components/p-ChVwCVHy.js.map +0 -1
  193. package/dist/components/p-CpNIQXz6.js +0 -2
  194. package/dist/components/p-CpNIQXz6.js.map +0 -1
  195. package/dist/components/p-CvgHQfvT.js +0 -2
  196. package/dist/components/p-CvgHQfvT.js.map +0 -1
  197. package/dist/components/p-bDf6yBQd.js +0 -2
  198. package/dist/components/p-bDf6yBQd.js.map +0 -1
  199. package/dist/types/components/Accordion/Accordion.d.ts +0 -28
  200. package/dist/types/components/Breadcrumb/Breadcrumb.d.ts +0 -16
  201. package/dist/types/components/ButtonUnfilledIcon/ButtonUnfilledIcon.d.ts +0 -45
  202. package/dist/types/components/ButtonUnfilledIcon/index.d.ts +0 -2
  203. package/dist/types/components/Fade/Fade.d.ts +0 -22
  204. package/dist/types/components/Fade/index.d.ts +0 -2
  205. package/dist/types/components/Surface/Surface.d.ts +0 -22
  206. package/dist/types/components/Toast/Toast.d.ts +0 -16
  207. package/dist/types/components/Toast/toast-service.d.ts +0 -24
  208. package/src/react/ds-accordion.ts +0 -27
  209. package/src/react/ds-breadcrumb.ts +0 -27
  210. package/src/react/ds-button-unfilled-icon.ts +0 -33
  211. package/src/react/ds-fade.ts +0 -26
  212. package/src/react/ds-surface.ts +0 -26
  213. package/src/react/ds-toast-provider.ts +0 -26
  214. package/src/wc/components/Accordion/Accordion.tsx +0 -144
  215. package/src/wc/components/Breadcrumb/Breadcrumb.tsx +0 -61
  216. package/src/wc/components/ButtonUnfilledIcon/ButtonUnfilledIcon.tsx +0 -129
  217. package/src/wc/components/ButtonUnfilledIcon/index.ts +0 -1
  218. package/src/wc/components/Fade/Fade.tsx +0 -58
  219. package/src/wc/components/Fade/index.ts +0 -1
  220. package/src/wc/components/Surface/Surface.tsx +0 -81
  221. package/src/wc/components/Toast/Toast.tsx +0 -179
  222. package/src/wc/components/Toast/toast-service.ts +0 -67
  223. /package/dist/components/{ds-button-unfilled-icon.js.map → ds-button-unfilled.js.map} +0 -0
  224. /package/dist/components/{p-DdFUTZ3y.js.map → p-C3ORQUOp.js.map} +0 -0
  225. /package/dist/components/{p-8Nt92j1-.js.map → p-CyFVn2Sc.js.map} +0 -0
  226. /package/dist/components/{p-DOU4cbpc.js.map → p-DRqxVEye.js.map} +0 -0
@@ -15,7 +15,7 @@ export class Slider {
15
15
  @Prop() max: number = 100;
16
16
  @Prop() step: number = 1;
17
17
  @Prop() label!: string;
18
- @Prop() inactive: boolean = false;
18
+ @Prop() isInactive: boolean = false;
19
19
  @Prop() valueText: string | undefined;
20
20
  /** Associates with an external <label>. Defaults to an auto-generated ID. */
21
21
  @Prop() inputId: string | undefined;
@@ -42,7 +42,11 @@ export class Slider {
42
42
  <span class="text-body-small-emphasis">{this.value}</span>
43
43
  </div>
44
44
  <div
45
- class={{ track: true, 'track--inactive': this.inactive, 'track--at-min': pct === 0 }}
45
+ class={{
46
+ track: true,
47
+ 'ds-control-inactive': this.isInactive,
48
+ 'track--at-min': pct === 0,
49
+ }}
46
50
  style={{ '--slider-pct': String(pct) } as Record<string, string>}
47
51
  >
48
52
  <div class="track-inner">
@@ -56,7 +60,7 @@ export class Slider {
56
60
  max={this.max}
57
61
  step={this.step}
58
62
  value={this.value}
59
- disabled={this.inactive}
63
+ disabled={this.isInactive}
60
64
  class="range-input"
61
65
  aria-valuetext={this.valueText}
62
66
  onInput={this.handleInput}
@@ -64,7 +64,7 @@ export class TabGroup {
64
64
  }
65
65
 
66
66
  /**
67
- * Find the next non-disabled tab index relative to `from`, stepping by `step`.
67
+ * Find the next non-inactive tab index relative to `from`, stepping by `step`.
68
68
  * Returns `from` unchanged if no other enabled tab exists.
69
69
  */
70
70
  private findEnabled(from: number, step: 1 | -1): number {
@@ -72,7 +72,7 @@ export class TabGroup {
72
72
  const len = tabs.length;
73
73
  for (let n = 0; n < len; n++) {
74
74
  const i = (from + step * (n + 1) + len * (n + 1)) % len;
75
- if (!tabs[i]?.disabled) return i;
75
+ if (!tabs[i]?.isInactive) return i;
76
76
  }
77
77
  return from;
78
78
  }
@@ -102,10 +102,10 @@ export class TabGroup {
102
102
  }
103
103
 
104
104
  if (e.key === 'Home') {
105
- const first = tabs.findIndex(t => !t.disabled);
105
+ const first = tabs.findIndex(t => !t.isInactive);
106
106
  nextIdx = first === -1 ? null : first;
107
107
  } else if (e.key === 'End') {
108
- const lastEnabled = [...tabs].reverse().findIndex(t => !t.disabled);
108
+ const lastEnabled = [...tabs].reverse().findIndex(t => !t.isInactive);
109
109
  nextIdx = lastEnabled === -1 ? null : tabs.length - 1 - lastEnabled;
110
110
  }
111
111
 
@@ -162,14 +162,20 @@ export class TabGroup {
162
162
  tab: true,
163
163
  'tab--selected': isSelected,
164
164
  'ds-focus-ring-inset': true,
165
+ 'ds-interaction-fill': !tab.isInactive,
166
+ 'ds-interaction-fill--on-medium': bgClass === 'on-medium',
167
+ 'ds-interaction-fill--on-bold': bgClass === 'on-bold',
168
+ 'ds-interaction-fill--on-strong': bgClass === 'on-strong',
169
+ 'ds-interaction-fill--on-always-dark': bgClass === 'on-always-dark',
170
+ 'ds-control-inactive': !!tab.isInactive,
165
171
  [bgClass]: !!bgClass,
166
172
  }}
167
173
  aria-selected={isSelected}
168
- aria-disabled={tab.disabled || undefined}
174
+ aria-disabled={tab.isInactive || undefined}
169
175
  aria-controls={tab.panelId ?? undefined}
170
- disabled={tab.disabled}
176
+ disabled={tab.isInactive}
171
177
  tabIndex={isSelected ? 0 : -1}
172
- onClick={() => !tab.disabled && this.selectTab(tab.id)}
178
+ onClick={() => !tab.isInactive && this.selectTab(tab.id)}
173
179
  >
174
180
  <span class={{
175
181
  tab__label: true,
@@ -5,7 +5,8 @@ export interface TabItemDivider {
5
5
  export interface TabItemTab {
6
6
  id: string;
7
7
  label: string;
8
- disabled?: boolean;
8
+ /** Disables the tab (same meaning as control `isInactive`). */
9
+ isInactive?: boolean;
9
10
  /** id of the tabpanel this tab controls */
10
11
  panelId?: string;
11
12
  /** Show a notification dot (brand) on the tab — no count, matching panel-nav. */
@@ -91,23 +91,23 @@ export class TabGroupNav {
91
91
  @Method()
92
92
  async focusLastTab() {
93
93
  const tabs = this.selectableTabs;
94
- const last = [...tabs].reverse().find(tab => !tab.disabled);
94
+ const last = [...tabs].reverse().find(tab => !tab.isInactive);
95
95
  if (last) await this.focusTab(last.id);
96
96
  }
97
97
 
98
98
  @Method()
99
99
  async focusFirstTab() {
100
- const first = this.selectableTabs.find(tab => !tab.disabled);
100
+ const first = this.selectableTabs.find(tab => !tab.isInactive);
101
101
  if (first) await this.focusTab(first.id);
102
102
  }
103
103
 
104
104
  private syncFocusedTabId(preferred: string) {
105
105
  const tabs = this.selectableTabs;
106
- if (tabs.some(tab => tab.id === preferred && !tab.disabled)) {
106
+ if (tabs.some(tab => tab.id === preferred && !tab.isInactive)) {
107
107
  this.focusedTabId = preferred;
108
108
  return;
109
109
  }
110
- const first = tabs.find(tab => !tab.disabled);
110
+ const first = tabs.find(tab => !tab.isInactive);
111
111
  this.focusedTabId = first?.id ?? '';
112
112
  }
113
113
 
@@ -127,13 +127,13 @@ export class TabGroupNav {
127
127
  private findEnabledLinear(from: number, step: 1 | -1): number | null {
128
128
  const tabs = this.selectableTabs;
129
129
  for (let i = from + step; i >= 0 && i < tabs.length; i += step) {
130
- if (!tabs[i]?.disabled) return i;
130
+ if (!tabs[i]?.isInactive) return i;
131
131
  }
132
132
  return null;
133
133
  }
134
134
 
135
135
  /**
136
- * Find the next non-disabled tab index relative to `from`, stepping by `step`.
136
+ * Find the next non-inactive tab index relative to `from`, stepping by `step`.
137
137
  * Returns `from` unchanged if no other enabled tab exists.
138
138
  */
139
139
  private findEnabled(from: number, step: 1 | -1): number {
@@ -141,7 +141,7 @@ export class TabGroupNav {
141
141
  const len = tabs.length;
142
142
  for (let n = 0; n < len; n++) {
143
143
  const i = (from + step * (n + 1) + len * (n + 1)) % len;
144
- if (!tabs[i]?.disabled) return i;
144
+ if (!tabs[i]?.isInactive) return i;
145
145
  }
146
146
  return from;
147
147
  }
@@ -156,7 +156,7 @@ export class TabGroupNav {
156
156
  private activateFocusedTab(e: KeyboardEvent) {
157
157
  if (e.key !== 'Enter' && e.key !== ' ') return;
158
158
  const tab = this.selectableTabs.find(item => item.id === this.focusedTabId);
159
- if (!tab || tab.disabled) return;
159
+ if (!tab || tab.isInactive) return;
160
160
  e.preventDefault();
161
161
  this.selectTab(tab.id, { focus: true });
162
162
  }
@@ -221,10 +221,10 @@ export class TabGroupNav {
221
221
  }
222
222
 
223
223
  if (e.key === 'Home') {
224
- const first = tabs.findIndex(t => !t.disabled);
224
+ const first = tabs.findIndex(t => !t.isInactive);
225
225
  nextIdx = first === -1 ? null : first;
226
226
  } else if (e.key === 'End') {
227
- const lastEnabled = [...tabs].reverse().findIndex(t => !t.disabled);
227
+ const lastEnabled = [...tabs].reverse().findIndex(t => !t.isInactive);
228
228
  nextIdx = lastEnabled === -1 ? null : tabs.length - 1 - lastEnabled;
229
229
  }
230
230
 
@@ -305,14 +305,15 @@ export class TabGroupNav {
305
305
  tab: true,
306
306
  'tab--selected': isSelected,
307
307
  'ds-focus-ring-inset': true,
308
+ 'ds-control-inactive': !!tab.isInactive,
308
309
  [bgClass]: !!bgClass,
309
310
  }}
310
311
  aria-selected={isSelected}
311
- aria-disabled={tab.disabled || undefined}
312
+ aria-disabled={tab.isInactive || undefined}
312
313
  aria-controls={tab.panelId ?? undefined}
313
- disabled={tab.disabled}
314
+ disabled={tab.isInactive}
314
315
  tabIndex={this.tabIndexForTab(tab)}
315
- onClick={() => !tab.disabled && this.selectTab(tab.id)}
316
+ onClick={() => !tab.isInactive && this.selectTab(tab.id)}
316
317
  onFocus={() => {
317
318
  if (!this.selectionFollowsFocus) {
318
319
  this.focusedTabId = tab.id;
@@ -1,17 +1,25 @@
1
1
  import { Component, Prop, h, Host } from '@stencil/core';
2
2
 
3
- export type TagIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive';
4
- export type TagContrast = 'strong' | 'bold' | 'medium' | 'faint';
5
- export type TagElevation = 'none' | 'flat' | 'elevated';
6
- export type TagSize = 'md' | 'sm' | 'xs';
3
+ export type TagIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive';
4
+ export type TagContrast = 'strong' | 'bold' | 'medium' | 'faint';
5
+ export type TagSize = 'md' | 'sm' | 'xs';
7
6
 
7
+ /** Text variant per control-density recipe (md / sm / xs). */
8
8
  const TEXT_VARIANT: Record<TagSize, string> = {
9
9
  md: 'text-body-medium',
10
10
  sm: 'text-body-small',
11
11
  xs: 'text-caption',
12
12
  };
13
13
 
14
- const ICON_SIZE: Record<TagSize, number> = { md: 20, sm: 16, xs: 12 };
14
+ /**
15
+ * `ds-icon` size prop matching control-density icon metrics
16
+ * (md→20 / sm→16 / xs→12 via `--dimension-iconography-*`).
17
+ */
18
+ const ICON_SIZE: Record<TagSize, 'md' | 'sm' | 'xs'> = {
19
+ md: 'md',
20
+ sm: 'sm',
21
+ xs: 'xs',
22
+ };
15
23
 
16
24
  @Component({
17
25
  tag: 'ds-tag',
@@ -22,13 +30,11 @@ export class Tag {
22
30
  @Prop() label!: string;
23
31
  @Prop() intent: TagIntent = 'neutral';
24
32
  @Prop() contrast: TagContrast = 'faint';
25
- @Prop() elevation: TagElevation = 'none';
26
33
  @Prop() size: TagSize = 'md';
27
34
  @Prop() rounded: boolean = false;
28
35
  @Prop() maxWidth: string | number | undefined;
29
36
 
30
37
  render() {
31
- const sz = this.size.toUpperCase() as 'MD' | 'SM' | 'XS';
32
38
  const textVariant = TEXT_VARIANT[this.size];
33
39
  const iconSize = ICON_SIZE[this.size];
34
40
 
@@ -36,29 +42,24 @@ export class Tag {
36
42
  ? { maxWidth: typeof this.maxWidth === 'number' ? `${this.maxWidth}px` : this.maxWidth }
37
43
  : undefined;
38
44
 
39
- const hostCls: Record<string, boolean> = {
40
- 'tag': true,
41
- [`tag--intent-${this.intent}`]: true,
42
- [`tag--contrast-${this.contrast}`]: true,
43
- [`tag--elevation-${this.elevation}`]: true,
44
- [`tag--size-${this.size}`]: true,
45
- 'tag--rounded': this.rounded,
46
- 'tag--rounded-no-icon-left': this.rounded, // consumer overrides via slot presence hack
47
- 'tag--rounded-no-remove-right': this.rounded,
48
- [`tag--icon-left-${sz}`]: false, // toggled by slotchange
49
- [`tag--icon-right-${sz}`]: false,
50
- };
51
-
52
45
  return (
53
46
  <Host
54
- class={hostCls}
47
+ class={{
48
+ 'tag': true,
49
+ [`tag--intent-${this.intent}`]: true,
50
+ [`tag--contrast-${this.contrast}`]: true,
51
+ [`tag--size-${this.size}`]: true,
52
+ 'ds-control--md': this.size === 'md',
53
+ 'ds-control--sm': this.size === 'sm',
54
+ 'ds-control--xs': this.size === 'xs',
55
+ 'tag--rounded': this.rounded,
56
+ }}
55
57
  style={maxWidthStyle}
56
58
  >
57
- {/* Leading icon slot — consumer provides an icon element */}
58
- <span class="tag__icon-slot" style={{ fontSize: `${iconSize}px`, lineHeight: '0' }}>
59
+ {/* Leading icon — consumer provides ds-icon (or any SVG); prefer size matching tag size. */}
60
+ <span class="tag__icon-slot" data-icon-size={iconSize}>
59
61
  <slot name="icon" />
60
62
  </span>
61
- {/* Label */}
62
63
  <span class={{ 'tag__label': true, [textVariant]: true }}>
63
64
  {this.label}
64
65
  </span>
@@ -1 +1 @@
1
- export type { TagIntent, TagContrast, TagElevation, TagSize } from './Tag';
1
+ export type { TagIntent, TagContrast, TagSize } from './Tag';
@@ -7,18 +7,18 @@ import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
7
7
  })
8
8
  export class Toggle {
9
9
  @Prop({ mutable: true }) checked: boolean = false;
10
- @Prop() inactive: boolean = false;
10
+ @Prop() isInactive: boolean = false;
11
11
 
12
12
  @Event() dsChange!: EventEmitter<boolean>;
13
13
 
14
14
  private handleClick = () => {
15
- if (this.inactive) return;
15
+ if (this.isInactive) return;
16
16
  this.checked = !this.checked;
17
17
  this.dsChange.emit(this.checked);
18
18
  };
19
19
 
20
20
  private handleKeyDown = (e: KeyboardEvent) => {
21
- if (this.inactive) return;
21
+ if (this.isInactive) return;
22
22
  if (e.key === ' ' || e.key === 'Enter') {
23
23
  e.preventDefault();
24
24
  this.handleClick();
@@ -30,9 +30,9 @@ export class Toggle {
30
30
  <Host
31
31
  role="switch"
32
32
  aria-checked={String(this.checked)}
33
- aria-disabled={this.inactive || undefined}
34
- tabIndex={this.inactive ? -1 : 0}
35
- class={{ toggle: true, checked: this.checked, inactive: this.inactive }}
33
+ aria-disabled={this.isInactive || undefined}
34
+ tabIndex={this.isInactive ? -1 : 0}
35
+ class={{ toggle: true, checked: this.checked, 'ds-control-inactive': this.isInactive }}
36
36
  onClick={this.handleClick}
37
37
  onKeyDown={this.handleKeyDown}
38
38
  >
@@ -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
+ }