@juspay/svelte-ui-components 4.27.5 → 4.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +140 -36
  2. package/dist/Animations/ModalAnimation.svelte +15 -1
  3. package/dist/Animations/OverlayAnimation.svelte +7 -1
  4. package/dist/AreaChart/AreaChart.svelte +366 -85
  5. package/dist/AspectRatio/AspectRatio.svelte +51 -0
  6. package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
  7. package/dist/AspectRatio/properties.d.ts +27 -0
  8. package/dist/AspectRatio/properties.js +1 -0
  9. package/dist/Banner/Banner.svelte +10 -4
  10. package/dist/Banner/properties.d.ts +8 -0
  11. package/dist/BarChart/BarChart.svelte +156 -49
  12. package/dist/Book/Book.svelte +14 -1
  13. package/dist/BrandLoader/BrandLoader.svelte +76 -12
  14. package/dist/Button/Button.svelte +56 -11
  15. package/dist/Calendar/Calendar.svelte +88 -61
  16. package/dist/Card/Card.svelte +1 -1
  17. package/dist/Carousel/Carousel.svelte +6 -3
  18. package/dist/ChatComposer/ChatComposer.svelte +54 -4
  19. package/dist/ChatComposer/dictationState.d.ts +15 -0
  20. package/dist/ChatComposer/dictationState.js +9 -0
  21. package/dist/ChatComposer/properties.d.ts +28 -1
  22. package/dist/ChatMessage/ChatMessage.svelte +27 -1
  23. package/dist/CheckListItem/CheckListItem.svelte +3 -3
  24. package/dist/Checkbox/Checkbox.svelte +84 -3
  25. package/dist/Checkbox/properties.d.ts +45 -13
  26. package/dist/ChipInput/ChipInput.svelte +70 -3
  27. package/dist/ChipInput/properties.d.ts +16 -0
  28. package/dist/Choicebox/Choicebox.svelte +156 -1
  29. package/dist/Choicebox/group.d.ts +63 -0
  30. package/dist/Choicebox/group.js +127 -0
  31. package/dist/Choicebox/properties.d.ts +46 -0
  32. package/dist/ColorPicker/ColorPicker.svelte +169 -20
  33. package/dist/ColorPicker/properties.d.ts +16 -0
  34. package/dist/Combobox/Combobox.svelte +87 -25
  35. package/dist/Combobox/properties.d.ts +23 -0
  36. package/dist/CommandMenu/CommandMenu.svelte +27 -11
  37. package/dist/ContextMenu/ContextMenu.svelte +31 -37
  38. package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
  39. package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
  40. package/dist/Draggable/Draggable.svelte +12 -7
  41. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
  42. package/dist/EmptyState/EmptyState.svelte +1 -1
  43. package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
  44. package/dist/FileInput/FileInput.svelte +50 -1
  45. package/dist/FileInput/properties.d.ts +7 -0
  46. package/dist/FunnelChart/FunnelChart.svelte +40 -12
  47. package/dist/FunnelChart/properties.d.ts +7 -1
  48. package/dist/Gallery/Gallery.svelte +20 -13
  49. package/dist/Gallery/properties.d.ts +8 -0
  50. package/dist/GridItem/GridItem.svelte +31 -1
  51. package/dist/HITL/HITL.svelte +35 -9
  52. package/dist/Icon/Icon.svelte +19 -3
  53. package/dist/Input/Input.svelte +24 -10
  54. package/dist/Input/properties.d.ts +37 -1
  55. package/dist/InputButton/InputButton.svelte +86 -11
  56. package/dist/InputButton/properties.d.ts +10 -1
  57. package/dist/Label/Label.svelte +54 -0
  58. package/dist/Label/Label.svelte.d.ts +3 -0
  59. package/dist/Label/properties.d.ts +26 -0
  60. package/dist/Label/properties.js +1 -0
  61. package/dist/LineChart/LineChart.svelte +385 -94
  62. package/dist/ListItem/ListItem.svelte +121 -15
  63. package/dist/Loader/Loader.svelte +22 -2
  64. package/dist/LoadingDots/LoadingDots.svelte +21 -0
  65. package/dist/MediaUpload/MediaUpload.svelte +13 -3
  66. package/dist/Menu/Menu.svelte +56 -28
  67. package/dist/Modal/Modal.svelte +77 -42
  68. package/dist/Pagination/Pagination.svelte +9 -2
  69. package/dist/Phone/Phone.svelte +4 -0
  70. package/dist/PieChart/PieChart.svelte +282 -38
  71. package/dist/PieChart/properties.d.ts +9 -0
  72. package/dist/Pill/Pill.svelte +14 -5
  73. package/dist/Progress/Progress.svelte +20 -0
  74. package/dist/Radio/Radio.svelte +58 -1
  75. package/dist/Radio/properties.d.ts +23 -0
  76. package/dist/RatingGroup/RatingGroup.svelte +283 -0
  77. package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
  78. package/dist/RatingGroup/properties.d.ts +73 -0
  79. package/dist/RatingGroup/properties.js +1 -0
  80. package/dist/RelativeTime/RelativeTime.svelte +1 -1
  81. package/dist/SankeyChart/SankeyChart.svelte +272 -50
  82. package/dist/SankeyChart/properties.d.ts +9 -0
  83. package/dist/Scroller/Scroller.svelte +37 -2
  84. package/dist/Select/Select.svelte +166 -46
  85. package/dist/Select/properties.d.ts +7 -0
  86. package/dist/Separator/Separator.svelte +48 -0
  87. package/dist/Separator/Separator.svelte.d.ts +3 -0
  88. package/dist/Separator/properties.d.ts +22 -0
  89. package/dist/Separator/properties.js +1 -0
  90. package/dist/Sheet/Sheet.svelte +60 -21
  91. package/dist/Shimmer/Shimmer.svelte +15 -0
  92. package/dist/Slider/Slider.svelte +87 -6
  93. package/dist/Slider/properties.d.ts +26 -0
  94. package/dist/Snippet/Snippet.svelte +4 -0
  95. package/dist/Snippet/copyState.svelte.d.ts +13 -1
  96. package/dist/Snippet/copyState.svelte.js +27 -1
  97. package/dist/Snippet/properties.d.ts +7 -0
  98. package/dist/SoundKit/SoundKit.js +9 -8
  99. package/dist/SplitInput/SplitInput.svelte +68 -2
  100. package/dist/SplitInput/properties.d.ts +23 -0
  101. package/dist/StatCard/StatCard.svelte +6 -6
  102. package/dist/Status/Status.svelte +2 -2
  103. package/dist/Stepper/Step.svelte +19 -6
  104. package/dist/Stepper/Stepper.svelte +108 -35
  105. package/dist/Table/BuiltinCell.svelte +2 -2
  106. package/dist/Table/Table.svelte +373 -55
  107. package/dist/Table/properties.d.ts +134 -3
  108. package/dist/Tabs/Tabs.svelte +263 -65
  109. package/dist/Tabs/properties.d.ts +23 -0
  110. package/dist/TaskList/TaskList.svelte +14 -5
  111. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
  112. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
  113. package/dist/Toast/Toast.svelte +27 -2
  114. package/dist/Toggle/Toggle.svelte +79 -7
  115. package/dist/Toggle/Toggle.svelte.d.ts +1 -1
  116. package/dist/Toggle/properties.d.ts +30 -7
  117. package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
  118. package/dist/ToolCallLog/properties.d.ts +10 -0
  119. package/dist/Toolbar/Toolbar.svelte +1 -1
  120. package/dist/Tooltip/Tooltip.svelte +20 -2
  121. package/dist/Tooltip/tooltip-action.js +39 -4
  122. package/dist/TypewriterText/TypewriterText.svelte +34 -2
  123. package/dist/TypewriterText/properties.d.ts +18 -5
  124. package/dist/_chart/ChartContainer.svelte +12 -0
  125. package/dist/_chart/ChartTooltip.svelte +18 -4
  126. package/dist/_chart/Legend.svelte +21 -0
  127. package/dist/_chart/Legend.svelte.d.ts +15 -0
  128. package/dist/_chart/geometry.d.ts +54 -1
  129. package/dist/_chart/geometry.js +122 -14
  130. package/dist/_chart/interactions.d.ts +5 -1
  131. package/dist/_chart/interactions.js +9 -5
  132. package/dist/_chart/scales.js +16 -0
  133. package/dist/_chart/temporal.d.ts +45 -0
  134. package/dist/_chart/temporal.js +116 -0
  135. package/dist/_field/description.d.ts +50 -0
  136. package/dist/_field/description.js +51 -0
  137. package/dist/_interaction/dismissal.d.ts +67 -0
  138. package/dist/_interaction/dismissal.js +137 -0
  139. package/dist/_interaction/focus.d.ts +73 -0
  140. package/dist/_interaction/focus.js +143 -0
  141. package/dist/assets/star.svg +5 -0
  142. package/dist/index.d.ts +13 -1
  143. package/dist/index.js +21 -1
  144. package/dist/reduced-motion.svelte.d.ts +37 -0
  145. package/dist/reduced-motion.svelte.js +44 -0
  146. package/dist/styles/theme-dark.css +778 -0
  147. package/dist/types.d.ts +4 -2
  148. package/dist/utils.d.ts +62 -1
  149. package/dist/utils.js +80 -21
  150. package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
  151. package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
  152. package/dist-codemod/codemod/migrate-audit.js +395 -0
  153. package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
  154. package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
  155. package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
  156. package/dist-codemod/migrate/analyze.js +804 -0
  157. package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
  158. package/dist-wc/index.js +13992 -4323
  159. package/package.json +13 -6
  160. package/dist/Modal/focus-trap.d.ts +0 -42
  161. package/dist/Modal/focus-trap.js +0 -57
  162. /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
  163. /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
  164. /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { closestInEventPath } from '../_interaction/dismissal';
2
3
  import type { DraggableProperties, DragPosition } from './properties';
3
4
 
4
5
  let {
@@ -26,18 +27,22 @@
26
27
  baseY: number;
27
28
  } | null = $state(null);
28
29
 
29
- function isInteractive(target: EventTarget | null): boolean {
30
+ // Searched over the event's composed path rather than from `event.target`: a
31
+ // `<sui-button>` nested inside a Draggable retargets to its host, so a plain
32
+ // `closest('button')` misses the real control and the press starts a drag
33
+ // instead of activating it.
34
+ function isInteractive(event: Event): boolean {
30
35
  return (
31
- target instanceof Element &&
32
- target.closest('input, textarea, select, button, a, [contenteditable="true"]') !== null
36
+ closestInEventPath(event, 'input, textarea, select, button, a, [contenteditable="true"]') !==
37
+ null
33
38
  );
34
39
  }
35
40
 
36
- function withinHandle(target: EventTarget | null): boolean {
41
+ function withinHandle(event: Event): boolean {
37
42
  if (typeof handle !== 'string' || handle.length === 0) {
38
- return !isInteractive(target);
43
+ return !isInteractive(event);
39
44
  }
40
- return target instanceof Element && target.closest(handle) !== null;
45
+ return closestInEventPath(event, handle) !== null;
41
46
  }
42
47
 
43
48
  function clamp(node: HTMLElement, nextX: number, nextY: number): DragPosition {
@@ -61,7 +66,7 @@
61
66
  }
62
67
 
63
68
  function startDrag(event: PointerEvent & { currentTarget: HTMLElement }): void {
64
- if (disabled || !withinHandle(event.target)) {
69
+ if (disabled || !withinHandle(event)) {
65
70
  return;
66
71
  }
67
72
  event.preventDefault();
@@ -94,7 +94,11 @@
94
94
  if (axisSeries.length === 0) {
95
95
  return [0, 1];
96
96
  }
97
- const allValues = axisSeries.flatMap((s) => s.data);
97
+ // Non-finite values (NaN/Infinity) mark gap points; exclude them so one
98
+ // bad point cannot poison the axis extent (Infinity in particular passes
99
+ // right through a Math.max comparison). Matches the LineChart/AreaChart
100
+ // call-site filter.
101
+ const allValues = axisSeries.flatMap((s) => s.data).filter((v) => Number.isFinite(v));
98
102
  if (allValues.length === 0) {
99
103
  return [0, 1];
100
104
  }
@@ -651,7 +655,8 @@
651
655
  }
652
656
 
653
657
  .bar-shape {
654
- transition: opacity var(--chart-transition-duration, 0.2s) ease;
658
+ transition: opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
659
+ var(--chart-transition-easing, var(--motion-easing, ease));
655
660
  cursor: pointer;
656
661
  }
657
662
 
@@ -672,8 +677,10 @@
672
677
 
673
678
  .line-dot {
674
679
  transition:
675
- r var(--chart-transition-duration, 0.2s) ease,
676
- opacity var(--chart-transition-duration, 0.2s) ease;
680
+ r var(--chart-transition-duration, var(--motion-duration, 0.2s))
681
+ var(--chart-transition-easing, var(--motion-easing, ease)),
682
+ opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
683
+ var(--chart-transition-easing, var(--motion-easing, ease));
677
684
  cursor: pointer;
678
685
  }
679
686
 
@@ -111,7 +111,7 @@
111
111
  .empty-state-description {
112
112
  font-size: var(--empty-state-description-font-size, 14px);
113
113
  color: var(--empty-state-description-color, inherit);
114
- opacity: var(--empty-state-description-opacity, 0.6);
114
+ opacity: var(--empty-state-description-opacity, 0.8);
115
115
  max-width: var(--empty-state-description-max-width, 360px);
116
116
  margin-top: 4px;
117
117
  }
@@ -81,7 +81,7 @@
81
81
 
82
82
  .file-dropzone-trigger-caption {
83
83
  margin: var(--file-dropzone-trigger-caption-margin, 0);
84
- color: var(--file-dropzone-trigger-caption-color, #64748b);
84
+ color: var(--file-dropzone-trigger-caption-color, #f1f5f9);
85
85
  font-size: var(--file-dropzone-trigger-caption-font-size, 0.85em);
86
86
  }
87
87
  </style>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { describeField } from '../_field/description';
2
3
  import type { FileInputProperties } from './properties';
3
4
 
4
5
  let {
@@ -10,9 +11,23 @@
10
11
  testId,
11
12
  classes,
12
13
  onfiles,
13
- onerror
14
+ onerror,
15
+ errorMessage,
16
+ infoMessage,
17
+ invalid
14
18
  }: FileInputProperties = $props();
15
19
 
20
+ /* The drop zone is a `role="button"` with no linked text, so a rejected
21
+ file (wrong type, too large) had nowhere to be announced. It takes
22
+ `aria-describedby` but deliberately not `aria-invalid`: ARIA does not
23
+ support that attribute on `button`, and an unsupported attribute is not a
24
+ harmless extra -- it is one a reader is entitled to ignore. The error text
25
+ itself is the announcement, through `role="alert"`. */
26
+ const fieldUid = $props.id();
27
+ const field = $derived(
28
+ describeField(fieldUid, { error: errorMessage, info: infoMessage, invalid })
29
+ );
30
+
16
31
  let dragOver = $state(false);
17
32
  let inputEl: HTMLInputElement | null = $state(null);
18
33
  let pickerBusy = false;
@@ -143,6 +158,7 @@
143
158
  role="button"
144
159
  tabindex={disabled ? -1 : 0}
145
160
  aria-disabled={disabled}
161
+ aria-describedby={field.describedBy}
146
162
  ondragover={handleDragOver}
147
163
  ondragleave={handleDragLeave}
148
164
  ondrop={handleDrop}
@@ -166,7 +182,40 @@
166
182
  {@render trigger({ openFilePicker, dragOver, disabled })}
167
183
  </div>
168
184
 
185
+ {#if field.showsError}
186
+ <div
187
+ id={field.errorId}
188
+ role="alert"
189
+ class="field-error"
190
+ data-pw={typeof testId === 'string' ? `${testId}-error-message` : null}
191
+ testID={typeof testId === 'string' ? `${testId}-error-message` : null}
192
+ >
193
+ {errorMessage}
194
+ </div>
195
+ {/if}
196
+ {#if field.showsInfo}
197
+ <div
198
+ id={field.infoId}
199
+ class="field-info"
200
+ data-pw={typeof testId === 'string' ? `${testId}-info-message` : null}
201
+ testID={typeof testId === 'string' ? `${testId}-info-message` : null}
202
+ >
203
+ {infoMessage}
204
+ </div>
205
+ {/if}
206
+
169
207
  <style>
208
+ .field-error {
209
+ color: var(--file-input-error-color, #b3261e);
210
+ font-size: var(--file-input-error-font-size, 12px);
211
+ margin-top: var(--file-input-error-margin-top, 4px);
212
+ }
213
+ .field-info {
214
+ color: var(--file-input-info-color, #5f6368);
215
+ font-size: var(--file-input-info-font-size, 12px);
216
+ margin-top: var(--file-input-info-margin-top, 4px);
217
+ }
218
+
170
219
  .file-input {
171
220
  display: var(--file-input-display, inline-flex);
172
221
  flex-direction: var(--file-input-flex-direction, column);
@@ -13,6 +13,13 @@ export type FileInputProperties = {
13
13
  disabled?: boolean;
14
14
  testId?: string;
15
15
  classes?: string;
16
+ /** Text shown, and announced, when the control is in error. Linked through
17
+ * `aria-describedby`, and sets `aria-invalid` while present. */
18
+ errorMessage?: string | null;
19
+ /** Persistent helper text describing the control, linked the same way. */
20
+ infoMessage?: string | null;
21
+ /** Marks the control invalid without supplying a message. */
22
+ invalid?: boolean;
16
23
  onfiles?: (files: File[]) => void;
17
24
  onerror?: (message: string) => void;
18
25
  };
@@ -56,6 +56,17 @@
56
56
  const MARGIN_BOTTOM = 8;
57
57
  const MARGIN_LEFT = 8;
58
58
 
59
+ /**
60
+ * Funnel bars are fixed structural columns (one per stage, always drawn),
61
+ * not sparse samples that can be omitted like a LineChart/AreaChart gap
62
+ * point — so a non-finite stage value is treated as a 0 contribution
63
+ * (this file's own computeStackedValues precedent in geometry.ts for
64
+ * value-accumulating charts), not excluded from the dataset.
65
+ */
66
+ function safeValue(value: number): number {
67
+ return Number.isFinite(value) ? value : 0;
68
+ }
69
+
59
70
  let innerWidth = $derived(Math.max(0, chartWidth - MARGIN_LEFT - MARGIN_RIGHT));
60
71
  let innerHeight = $derived(Math.max(0, chartHeight - MARGIN_TOP - MARGIN_BOTTOM));
61
72
 
@@ -65,8 +76,14 @@
65
76
  }
66
77
  let max = 0;
67
78
  for (const stage of data) {
68
- if (stage.value > max) {
69
- max = stage.value;
79
+ // A non-finite value contributes nothing to the max (the same "0
80
+ // contribution" treatment computeStackedValues gives a non-finite or
81
+ // negative point) rather than a gap: `Infinity > max` is true, so an
82
+ // unguarded comparison lets one bad stage collapse every bar to the
83
+ // 2px floor.
84
+ const value = safeValue(stage.value);
85
+ if (value > max) {
86
+ max = value;
70
87
  }
71
88
  }
72
89
  return max === 0 ? 1 : max;
@@ -123,7 +140,10 @@
123
140
  * Clamped to at least 2px so zero-value stages remain visible.
124
141
  */
125
142
  function barHeight(stageValue: number, expandPixels: number = 0): number {
126
- return Math.max(2, (stageValue / maxValue) * barAreaHeight + expandPixels);
143
+ // Sanitize before the division: an unguarded NaN/Infinity here reaches
144
+ // barY and connectorPoints (both call this), producing "NaN" SVG
145
+ // coordinates instead of the 2px-floor bar a 0 contribution renders as.
146
+ return Math.max(2, (safeValue(stageValue) / maxValue) * barAreaHeight + expandPixels);
127
147
  }
128
148
 
129
149
  /**
@@ -166,11 +186,15 @@
166
186
  * otherwise renders `"<value> | <pct>%"`.
167
187
  */
168
188
  function formatLabel(stage: FunnelStage): string {
189
+ // Feeds the in-bar label, tooltip, and aria-label alike: sanitizing once
190
+ // here keeps all three reading "0" for a non-finite stage instead of
191
+ // only some of them being patched and the rest still leaking "NaN".
192
+ const value = safeValue(stage.value);
169
193
  if (typeof valueFormat === 'function') {
170
- return valueFormat(stage.value, maxValue);
194
+ return valueFormat(value, maxValue);
171
195
  }
172
- const pct = formatPercent(stage.value, maxValue);
173
- return `${formatNumber(stage.value)} | ${pct}`;
196
+ const pct = formatPercent(value, maxValue);
197
+ return `${formatNumber(value)} | ${pct}`;
174
198
  }
175
199
 
176
200
  // ── Label fitting ──────────────────────────────────────────────
@@ -189,7 +213,7 @@
189
213
  if (bh >= fullSize.height + 4 && stageColumnWidth >= fullSize.width + 8) {
190
214
  return full;
191
215
  }
192
- const compact = formatPercent(stage.value, maxValue);
216
+ const compact = formatPercent(safeValue(stage.value), maxValue);
193
217
  const compactSize = measureText(compact, valueFont);
194
218
  if (bh >= compactSize.height + 4 && stageColumnWidth >= compactSize.width + 8) {
195
219
  return compact;
@@ -407,9 +431,12 @@
407
431
 
408
432
  .funnel-bar {
409
433
  transition:
410
- opacity var(--chart-transition-duration, 0.2s) ease,
411
- y var(--chart-transition-duration, 0.2s) ease,
412
- height var(--chart-transition-duration, 0.2s) ease;
434
+ opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
435
+ var(--chart-transition-easing, var(--motion-easing, ease)),
436
+ y var(--chart-transition-duration, var(--motion-duration, 0.2s))
437
+ var(--chart-transition-easing, var(--motion-easing, ease)),
438
+ height var(--chart-transition-duration, var(--motion-duration, 0.2s))
439
+ var(--chart-transition-easing, var(--motion-easing, ease));
413
440
  cursor: pointer;
414
441
  }
415
442
 
@@ -436,12 +463,13 @@
436
463
  }
437
464
 
438
465
  .funnel-connector {
439
- transition: d var(--chart-transition-duration, 0.2s) ease;
466
+ transition: d var(--chart-transition-duration, var(--motion-duration, 0.2s))
467
+ var(--chart-transition-easing, var(--motion-easing, ease));
440
468
  }
441
469
 
442
470
  .chart-empty {
443
471
  padding: var(--chart-empty-padding, 32px 24px);
444
- color: var(--chart-empty-color, light-dark(#9ca3af, #6b7280));
472
+ color: var(--chart-empty-color, light-dark(#6b7280, #9ca3af));
445
473
  text-align: center;
446
474
  }
447
475
  </style>
@@ -63,7 +63,13 @@ export type OptionalFunnelChartProperties = {
63
63
  maxHeight?: number;
64
64
  /** Lower bound (px) on the rendered chart height (defaults to `0`). */
65
65
  minHeight?: number;
66
- /** Render the tooltip into document.body (position:fixed) so scroll/overflow ancestors never clip it. */
66
+ /**
67
+ * Render the tooltip into the chart's own root (its shadow root when hosted as a
68
+ * custom element, `document.body` otherwise) with `position: fixed`, clamped to
69
+ * the viewport, instead of positioned inside the chart. Use it when the chart
70
+ * sits inside a container with `overflow: hidden`/`scroll` that would otherwise
71
+ * clip the tooltip.
72
+ */
67
73
  tooltipPortal?: boolean;
68
74
  /** Value for the `data-pw` attribute on the chart root element. */
69
75
  testId?: string;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getActiveElement } from '../_interaction/focus';
3
+ import { lockBodyScroll, unlockBodyScroll } from '../utils';
2
4
  import { tick } from 'svelte';
3
5
  import type { Snippet } from 'svelte';
4
6
  import { fade } from 'svelte/transition';
@@ -35,7 +37,8 @@
35
37
  onclose,
36
38
  onchange,
37
39
  onkeydown,
38
- classes
40
+ classes,
41
+ lightboxTransitionDuration
39
42
  }: GalleryProperties = $props();
40
43
 
41
44
  let lightboxDiv: HTMLDivElement | null = $state(null);
@@ -54,16 +57,20 @@
54
57
  let showItemActions = $derived(showEditButton || showDeleteButton);
55
58
 
56
59
  function lightboxAction(node: HTMLElement) {
57
- if (openerElement === null && document.activeElement instanceof HTMLElement) {
58
- openerElement = document.activeElement;
60
+ const opener = getActiveElement(node);
61
+ if (openerElement === null && opener instanceof HTMLElement) {
62
+ openerElement = opener;
59
63
  }
60
- document.body.style.overflow = 'hidden';
64
+ // Reference-counted: a lightbox opened from inside another scroll-locking
65
+ // surface (Modal, Sheet, CommandMenu) must not restore scroll out from
66
+ // under it on close. See lockBodyScroll's own doc comment in utils.ts.
67
+ lockBodyScroll();
61
68
  tick().then(() => {
62
69
  node.focus();
63
70
  });
64
71
  return {
65
72
  destroy() {
66
- document.body.style.overflow = '';
73
+ unlockBodyScroll();
67
74
  if (openerElement !== null) {
68
75
  openerElement.focus();
69
76
  openerElement = null;
@@ -93,7 +100,7 @@
93
100
  activeIndex = index;
94
101
  onchange?.(activeIndex);
95
102
  await tick();
96
- if (lightboxDiv !== null && !lightboxDiv.contains(document.activeElement)) {
103
+ if (lightboxDiv !== null && !lightboxDiv.contains(getActiveElement(lightboxDiv))) {
97
104
  lightboxDiv.focus();
98
105
  }
99
106
  }
@@ -138,11 +145,11 @@
138
145
  }
139
146
  if (
140
147
  event.shiftKey &&
141
- (document.activeElement === first || document.activeElement === lightboxDiv)
148
+ (getActiveElement(lightboxDiv) === first || getActiveElement(lightboxDiv) === lightboxDiv)
142
149
  ) {
143
150
  event.preventDefault();
144
151
  last.focus();
145
- } else if (!event.shiftKey && document.activeElement === last) {
152
+ } else if (!event.shiftKey && getActiveElement(lightboxDiv) === last) {
146
153
  event.preventDefault();
147
154
  first.focus();
148
155
  }
@@ -266,7 +273,7 @@
266
273
  use:lightboxAction
267
274
  onclick={handleBackdropClick}
268
275
  onkeydown={handleLightboxKeydown}
269
- transition:fade={{ duration: 200 }}
276
+ transition:fade={{ duration: lightboxTransitionDuration ?? 200 }}
270
277
  >
271
278
  <div class="lightbox-close" bind:this={closeButtonWrap}>
272
279
  <Button ariaLabel="Close gallery" onclick={closeLightbox}>
@@ -450,13 +457,13 @@
450
457
  color: var(--gallery-list-title-color, inherit);
451
458
  font-size: var(--gallery-list-title-font-size, 14px);
452
459
  font-weight: var(--gallery-list-title-font-weight, 500);
453
- font-family: var(--gallery-list-title-font-family);
460
+ font-family: var(--gallery-list-title-font-family, inherit);
454
461
  }
455
462
 
456
463
  .list-caption {
457
464
  color: var(--gallery-list-caption-color, inherit);
458
465
  font-size: var(--gallery-list-caption-font-size, 12px);
459
- font-family: var(--gallery-list-caption-font-family);
466
+ font-family: var(--gallery-list-caption-font-family, inherit);
460
467
  }
461
468
 
462
469
  .gallery-item-actions {
@@ -542,7 +549,7 @@
542
549
  .lightbox-caption {
543
550
  color: var(--gallery-lightbox-caption-color, #ffffff);
544
551
  font-size: var(--gallery-lightbox-caption-font-size, 14px);
545
- font-family: var(--gallery-lightbox-caption-font-family);
552
+ font-family: var(--gallery-lightbox-caption-font-family, inherit);
546
553
  text-align: center;
547
554
  max-width: var(--gallery-lightbox-image-width, 85vw);
548
555
  }
@@ -554,7 +561,7 @@
554
561
  transform: translateX(-50%);
555
562
  color: var(--gallery-lightbox-counter-color, #ffffff);
556
563
  font-size: var(--gallery-lightbox-counter-font-size, 13px);
557
- font-family: var(--gallery-lightbox-counter-font-family);
564
+ font-family: var(--gallery-lightbox-counter-font-family, inherit);
558
565
  }
559
566
 
560
567
  .lightbox-close,
@@ -27,6 +27,14 @@ export type OptionalGalleryProperties = {
27
27
  itemFooter?: Snippet<[GalleryImage, number]>;
28
28
  testId?: string;
29
29
  classes?: string;
30
+ /**
31
+ * Duration (ms) of the lightbox's `fade` transition. A CSS custom property
32
+ * cannot reach a Svelte transition directive's parameters, so this prop is
33
+ * the token-contract equivalent for Gallery's motion — same shape as
34
+ * Toast's `inAnimationDuration`/`outAnimationDuration`. Defaults to the
35
+ * library's existing 200ms so omitting it renders identically to before.
36
+ */
37
+ lightboxTransitionDuration?: number | null;
30
38
  };
31
39
  export type GalleryEventProperties = {
32
40
  onimageclick?: (index: number, event: MouseEvent) => void;
@@ -16,12 +16,27 @@
16
16
  showLoader = !showLoader;
17
17
  onclick?.(event);
18
18
  }
19
+
20
+ // The tile carries role="button" and sits in the tab order, so Enter and Space have
21
+ // to do what a click does. Clicking the element itself rather than calling the
22
+ // handler keeps one activation path, and gives `onclick` the MouseEvent its type
23
+ // promises instead of a synthesized stand-in.
24
+ function handleKeydown(event: KeyboardEvent): void {
25
+ onkeydown?.(event);
26
+ if (event.key !== 'Enter' && event.key !== ' ') {
27
+ return;
28
+ }
29
+ event.preventDefault();
30
+ if (event.currentTarget instanceof HTMLElement) {
31
+ event.currentTarget.click();
32
+ }
33
+ }
19
34
  </script>
20
35
 
21
36
  <div
22
37
  class="container {classes ?? ''}"
23
38
  onclick={handleClick}
24
- {onkeydown}
39
+ onkeydown={handleKeydown}
25
40
  role="button"
26
41
  tabindex="0"
27
42
  data-pw={typeof testId === 'string' ? testId : null}
@@ -132,4 +147,19 @@
132
147
  clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0);
133
148
  }
134
149
  }
150
+ /* An indefinite animation is the case this preference exists for: it never
151
+ ends, so a user who asked the OS to minimise motion gets a permanent loop.
152
+ The animation stops, but the element must still read as "busy" -- a guard
153
+ that leaves nothing on screen, or leaves a frame that means something else,
154
+ is worse than the motion it removed. This block lives in the component's own
155
+ <style> because that is the only stylesheet that reaches inside the shadow
156
+ root a custom-element consumer gets. */
157
+ @media (prefers-reduced-motion: reduce) {
158
+ .grid-body-loader::before {
159
+ /* clip-path is what the animation drives; clearing it reveals the whole
160
+ border ring, which is the frame the animation ends on anyway. */
161
+ animation: none;
162
+ clip-path: none;
163
+ }
164
+ }
135
165
  </style>
@@ -450,7 +450,8 @@
450
450
  width: 100%;
451
451
  max-width: var(--hitl-max-width, 100%);
452
452
  margin: var(--hitl-margin, 0);
453
- animation: hitl-slide-in 0.3s ease-out forwards;
453
+ animation: hitl-slide-in var(--hitl-slide-in-animation-duration, var(--motion-duration, 0.3s))
454
+ var(--hitl-slide-in-animation-easing, var(--motion-easing, ease-out)) forwards;
454
455
  contain: layout style;
455
456
  }
456
457
 
@@ -465,7 +466,7 @@
465
466
  font-size: var(--hitl-badge-font-size, 0.6875rem);
466
467
  font-weight: var(--hitl-badge-font-weight, 600);
467
468
  letter-spacing: var(--hitl-badge-letter-spacing, 0.06em);
468
- color: var(--hitl-badge-color, #858585);
469
+ color: var(--hitl-badge-color, #595959);
469
470
  text-transform: uppercase;
470
471
  }
471
472
 
@@ -485,7 +486,7 @@
485
486
  padding: var(--hitl-description-padding, 0.25rem 0);
486
487
  font-size: var(--hitl-description-font-size, 0.8125rem);
487
488
  line-height: var(--hitl-description-line-height, inherit);
488
- color: var(--hitl-description-color, #858585);
489
+ color: var(--hitl-description-color, #5c5c5c);
489
490
  }
490
491
 
491
492
  .confirmation-body {
@@ -505,7 +506,7 @@
505
506
  font-size: var(--hitl-param-label-font-size, 0.6875rem);
506
507
  font-weight: var(--hitl-param-label-font-weight, 600);
507
508
  letter-spacing: var(--hitl-param-label-letter-spacing, 0.04em);
508
- color: var(--hitl-param-label-color, #858585);
509
+ color: var(--hitl-param-label-color, #595959);
509
510
  word-spacing: var(--hitl-param-label-word-spacing, normal);
510
511
  text-transform: uppercase;
511
512
  }
@@ -552,7 +553,12 @@
552
553
  overflow: hidden;
553
554
  --progress-track-background: transparent;
554
555
  --progress-bar-background: transparent;
555
- --progress-bar-transition: width 0.1s linear;
556
+ /* Named so a consumer can reach it. HITL sets the Progress child's token
557
+ directly, which beats anything declared at :root, so a `<sui-hitl>`
558
+ consumer had no way to slow or stop the countdown sweep -- custom
559
+ properties are the only thing that crosses a shadow boundary, and this
560
+ one was spoken for. */
561
+ --progress-bar-transition: var(--hitl-countdown-transition, width 0.1s linear);
556
562
  --progress-track-height: 100%;
557
563
  --progress-container-padding: 0;
558
564
  }
@@ -573,17 +579,24 @@
573
579
  background: var(--hitl-completion-background, #f4f4f5);
574
580
  border-radius: var(--hitl-border-radius, 0.5rem);
575
581
  padding: var(--hitl-completion-padding, 1rem);
576
- animation: hitl-fade-in 0.3s ease-in-out;
582
+ animation: hitl-fade-in var(--hitl-completion-animation-duration, var(--motion-duration, 0.3s))
583
+ var(--hitl-completion-animation-easing, var(--motion-easing, ease-in-out));
577
584
  }
578
585
 
579
586
  .completion-icon {
580
587
  display: inline-flex;
581
588
  width: var(--hitl-completion-icon-size, 1.25rem);
582
589
  height: var(--hitl-completion-icon-size, 1.25rem);
583
- color: var(--hitl-approved-color, #16a34a);
590
+ color: var(--hitl-approved-color, #166534);
584
591
  }
585
592
 
586
- .completion-icon svg {
593
+ /* :global() rather than the plain scoped selector: sui-hitl's wrapper mirrors
594
+ this file's default SVG into a <slot> fallback so the icon survives when a
595
+ consumer slots nothing, and that markup is written in a different .svelte
596
+ file -- Svelte's scoped-style hash is only ever added to elements written in
597
+ THIS file's own template, so an unscoped selector is the only one that also
598
+ matches the mirrored copy. Same idiom as Checkbox.wc.svelte's `.icon :global(svg)`. */
599
+ .completion-icon :global(svg) {
587
600
  width: 100%;
588
601
  height: 100%;
589
602
  }
@@ -595,7 +608,7 @@
595
608
  .completion-text {
596
609
  font-size: var(--hitl-completion-font-size, 0.875rem);
597
610
  font-weight: var(--hitl-completion-font-weight, 600);
598
- color: var(--hitl-approved-color, #16a34a);
611
+ color: var(--hitl-approved-color, light-dark(#15803d, #4ade80));
599
612
  }
600
613
 
601
614
  .completion-text.halted {
@@ -623,4 +636,17 @@
623
636
  opacity: 1;
624
637
  }
625
638
  }
639
+
640
+ /* Two entrance animations: a slide-in and a fade-in. `animation: none` is safe
641
+ for both -- an entrance animates FROM an offset TO the resting state, so
642
+ removing it shows the resting state immediately, which is the frame the
643
+ animation was travelling towards anyway. That is the opposite of the timed
644
+ loaders in Button and ListItem, where the base rule is the FINISHED state and
645
+ stopping the animation would claim completion that has not happened. */
646
+ @media (prefers-reduced-motion: reduce) {
647
+ .hitl-container,
648
+ .hitl-completion {
649
+ animation: none;
650
+ }
651
+ }
626
652
  </style>
@@ -3,14 +3,30 @@
3
3
  import type { IconProperties } from './properties';
4
4
 
5
5
  let { icon, svg, text, onclick, onkeydown, classes, testId }: IconProperties = $props();
6
+
7
+ // A decorative icon is not a control. Without a click handler it keeps no button
8
+ // role and stays out of the tab order, rather than being a focusable no-op that
9
+ // assistive technology announces as a button.
10
+ const interactive = $derived(typeof onclick === 'function');
11
+
12
+ function handleKeydown(event: KeyboardEvent): void {
13
+ onkeydown?.(event);
14
+ if (!interactive || (event.key !== 'Enter' && event.key !== ' ')) {
15
+ return;
16
+ }
17
+ event.preventDefault();
18
+ if (event.currentTarget instanceof HTMLElement) {
19
+ event.currentTarget.click();
20
+ }
21
+ }
6
22
  </script>
7
23
 
8
24
  <div
9
25
  class="icon-container {classes ?? ''}"
10
26
  {onclick}
11
- {onkeydown}
12
- role="button"
13
- tabindex="0"
27
+ onkeydown={handleKeydown}
28
+ role={interactive ? 'button' : null}
29
+ tabindex={interactive ? 0 : null}
14
30
  data-pw={typeof testId === 'string' ? testId : null}
15
31
  testID={typeof testId === 'string' ? testId : null}
16
32
  >