@juspay/svelte-ui-components 4.51.0 → 4.52.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 (95) hide show
  1. package/dist/Animations/tokenizedFly.js +9 -2
  2. package/dist/AreaChart/AreaChart.svelte +41 -13
  3. package/dist/AreaChart/properties.d.ts +12 -0
  4. package/dist/AttachmentChipRow/AttachmentChipRow.svelte +7 -6
  5. package/dist/BarChart/BarChart.svelte +54 -6
  6. package/dist/BarChart/properties.d.ts +12 -0
  7. package/dist/Calendar/Calendar.svelte +124 -51
  8. package/dist/Card/Card.svelte +5 -0
  9. package/dist/ChatSuggestions/ChatSuggestions.svelte +1 -0
  10. package/dist/CheckListItem/CheckListItem.svelte +27 -5
  11. package/dist/ColorPicker/ColorPicker.svelte +40 -9
  12. package/dist/ColorPicker/properties.d.ts +8 -0
  13. package/dist/Combobox/Combobox.svelte +34 -2
  14. package/dist/CommandMenu/CommandMenu.svelte +35 -8
  15. package/dist/CommandMenu/properties.d.ts +6 -0
  16. package/dist/DateRangePicker/DateRangePicker.svelte +5 -5
  17. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +41 -7
  18. package/dist/DualAxisBarChart/properties.d.ts +4 -0
  19. package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +104 -10
  20. package/dist/FileDropzoneTrigger/properties.d.ts +14 -4
  21. package/dist/FileInput/FileInput.svelte +38 -9
  22. package/dist/FileInput/context.d.ts +20 -0
  23. package/dist/FileInput/context.js +8 -0
  24. package/dist/FileInput/properties.d.ts +25 -1
  25. package/dist/FunnelChart/FunnelChart.svelte +20 -1
  26. package/dist/FunnelChart/properties.d.ts +4 -0
  27. package/dist/Gallery/Gallery.svelte +72 -27
  28. package/dist/HITL/HITL.svelte +12 -3
  29. package/dist/LineChart/LineChart.svelte +105 -33
  30. package/dist/LineChart/properties.d.ts +12 -0
  31. package/dist/MediaPlayer/MediaPlayer.svelte +172 -2
  32. package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +1 -1
  33. package/dist/MediaPlayer/properties.d.ts +31 -0
  34. package/dist/Menu/Menu.svelte +6 -4
  35. package/dist/Modal/Modal.svelte +15 -12
  36. package/dist/Modal/properties.d.ts +29 -1
  37. package/dist/Pagination/Pagination.svelte +1 -0
  38. package/dist/PieChart/PieChart.svelte +51 -29
  39. package/dist/PieChart/properties.d.ts +4 -0
  40. package/dist/Pill/Pill.svelte +5 -0
  41. package/dist/Radio/Radio.svelte +11 -2
  42. package/dist/Radio/properties.d.ts +7 -2
  43. package/dist/SankeyChart/SankeyChart.svelte +339 -211
  44. package/dist/SankeyChart/properties.d.ts +4 -0
  45. package/dist/Scroller/Scroller.svelte +236 -4
  46. package/dist/Scroller/properties.d.ts +8 -0
  47. package/dist/Select/Select.svelte +43 -5
  48. package/dist/Select/properties.d.ts +17 -1
  49. package/dist/Sheet/Sheet.svelte +31 -4
  50. package/dist/Sheet/properties.d.ts +6 -0
  51. package/dist/SplitInput/SplitInput.svelte +17 -0
  52. package/dist/SplitInput/field-name.d.ts +29 -0
  53. package/dist/SplitInput/field-name.js +50 -0
  54. package/dist/SplitInput/properties.d.ts +18 -4
  55. package/dist/Status/Status.svelte +24 -12
  56. package/dist/Status/properties.d.ts +11 -0
  57. package/dist/Table/BuiltinCell.svelte +13 -5
  58. package/dist/Table/BuiltinCell.svelte.d.ts +1 -0
  59. package/dist/Table/Table.svelte +78 -20
  60. package/dist/Table/a11y.d.ts +7 -0
  61. package/dist/Table/a11y.js +71 -0
  62. package/dist/Table/cellData.js +3 -0
  63. package/dist/Table/properties.d.ts +13 -5
  64. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +4 -0
  65. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +48 -9
  66. package/dist/Toast/Toast.svelte +40 -4
  67. package/dist/Toggle/Toggle.svelte +2 -0
  68. package/dist/Toggle/properties.d.ts +2 -0
  69. package/dist/_chart/Axis.svelte +10 -0
  70. package/dist/_chart/ChartContainer.svelte +32 -1
  71. package/dist/_chart/ChartTooltip.svelte +18 -3
  72. package/dist/_chart/LabelPlate.svelte +114 -0
  73. package/dist/_chart/LabelPlate.svelte.d.ts +7 -0
  74. package/dist/_chart/a11y.d.ts +29 -0
  75. package/dist/_chart/a11y.js +44 -0
  76. package/dist/_chart/measure.d.ts +2 -0
  77. package/dist/_chart/measure.js +4 -0
  78. package/dist/_chart/tooltipPosition.d.ts +6 -0
  79. package/dist/_chart/tooltipPosition.js +30 -2
  80. package/dist/_chart/types.d.ts +22 -0
  81. package/dist/_interaction/focusable.d.ts +47 -0
  82. package/dist/_interaction/focusable.js +139 -0
  83. package/dist/assets/captions-on.svg +3 -0
  84. package/dist/assets/captions.svg +5 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +1 -1
  87. package/dist/styles/theme-dark.css +6 -1
  88. package/dist/types.d.ts +2 -0
  89. package/dist/utils.d.ts +18 -2
  90. package/dist/utils.js +73 -28
  91. package/dist-codemod/codemod/migrate-audit.js +9 -7
  92. package/dist-codemod/codemod/migration-assets/legacy-palette.css +9 -1
  93. package/dist-codemod/codemod/migration-assets-layout.js +61 -0
  94. package/dist-wc/index.js +5653 -4148
  95. package/package.json +4 -2
@@ -3,7 +3,7 @@
3
3
  import { onMount, onDestroy, tick } from 'svelte';
4
4
  import ModalAnimation from '../Animations/ModalAnimation.svelte';
5
5
  import OverlayAnimation from '../Animations/OverlayAnimation.svelte';
6
- import { createDebouncer, lockBodyScroll, unlockBodyScroll } from '../utils';
6
+ import { createDebouncer, acquireScrollLock, resolveScrollContainer } from '../utils';
7
7
  import { focusEntryPoint, focusTrapTabTarget, getActiveElement } from '../_interaction/focus';
8
8
  import { registerDismissible } from '../_interaction/dismissal';
9
9
  import Button from '../Button/Button.svelte';
@@ -46,8 +46,10 @@
46
46
  overlayFadeIn = false,
47
47
  usePortal = false,
48
48
  lockScroll = true,
49
+ scrollContainer,
49
50
  autoDismissAfter = null,
50
51
  ariaLabel,
52
+ ariaDescribedby,
51
53
  role,
52
54
  overlayAriaLabel
53
55
  }: ModalProperties = $props();
@@ -64,10 +66,13 @@
64
66
 
65
67
  let dismissTimer: ReturnType<typeof setTimeout> | null = null;
66
68
 
67
- // Fix [major]: plain const so the debouncer closure retains its internal lastCallTime state
68
- // across re-renders. $derived would recreate the debouncer on every reactive re-evaluation,
69
- // resetting the timer and breaking debounce correctness.
70
- const debounce = createDebouncer(debounceTime);
69
+ // One plain const, so the debouncer keeps its lastCallTime across re-renders and every
70
+ // dismissal path (overlay click, Escape, back-press) measures against the same history.
71
+ // $derived would rebuild it on every change and forget when it last fired. The delay is
72
+ // passed as a getter instead of the value: a value is captured when this line runs, so a
73
+ // `debounceTime` changed while the modal is open would never be seen. Reading the prop
74
+ // inside the closure picks up the current one on each dismissal.
75
+ const debounce = createDebouncer(() => debounceTime);
71
76
 
72
77
  /**
73
78
  * Where a portalled node is allowed to land. Svelte scopes a custom element's
@@ -233,7 +238,7 @@
233
238
  // prop changed while the modal was open: turned off, the count never comes back
234
239
  // down and the page stays frozen; turned on, this modal releases a hold another
235
240
  // component is still relying on. Release exactly what was taken.
236
- let heldScrollLock = false;
241
+ let releaseScrollLock: (() => void) | null = null;
237
242
 
238
243
  // Set once in onMount, cleared once in onDestroy -- registerDismissible's
239
244
  // own release function already tolerates a repeat call, but this still
@@ -249,8 +254,7 @@
249
254
  onEscape: handleEscape
250
255
  });
251
256
  if (lockScroll) {
252
- lockBodyScroll();
253
- heldScrollLock = true;
257
+ releaseScrollLock = acquireScrollLock(resolveScrollContainer(modalContent, scrollContainer));
254
258
  }
255
259
  if (typeof autoDismissAfter === 'number') {
256
260
  dismissTimer = setTimeout(() => onclose?.(), autoDismissAfter);
@@ -280,10 +284,8 @@
280
284
  clearTimeout(dismissTimer);
281
285
  }
282
286
  if (typeof window !== 'undefined') {
283
- if (heldScrollLock) {
284
- unlockBodyScroll();
285
- heldScrollLock = false;
286
- }
287
+ releaseScrollLock?.();
288
+ releaseScrollLock = null;
287
289
  if (supportHardwareBackPress) {
288
290
  if (!backPressed) {
289
291
  history.back();
@@ -328,6 +330,7 @@
328
330
  role={role ?? null}
329
331
  aria-modal={role != null ? 'true' : null}
330
332
  aria-label={ariaLabel ?? null}
333
+ aria-describedby={ariaDescribedby ?? null}
331
334
  tabindex="-1"
332
335
  >
333
336
  {#if (typeof header?.leftImage === 'string' && header.leftImage.length > 0) || (typeof header?.text === 'string' && header.text.length > 0) || (typeof header?.rightImage === 'string' && header.rightImage.length > 0)}
@@ -1,3 +1,4 @@
1
+ import type { ScrollContainer } from '../types';
1
2
  import type { ButtonProperties } from '../Button/properties';
2
3
  import type { ModalTransition } from '../types';
3
4
  import type { Snippet } from 'svelte';
@@ -19,6 +20,11 @@ export type ModalEntryAnimation = 'fade' | 'slide-up' | 'slide-down';
19
20
  export type ModalRole = 'dialog' | 'alertdialog';
20
21
  export type ModalProperties = OptionalModalProperties & ModalEventProperties;
21
22
  export type OptionalModalProperties = {
23
+ /** Scroll owner, resolved once per open span. Omit for the overlay's ownerDocument.body.
24
+ * Supply the embedded scroll element or a getter; explicit null means no lock.
25
+ * Changing ownership while open takes effect on the next open, and teardown
26
+ * always releases the originally acquired container. */
27
+ scrollContainer?: ScrollContainer;
22
28
  size?: ModalSize;
23
29
  align?: ModalAlign;
24
30
  showOverlay?: boolean;
@@ -40,6 +46,13 @@ export type OptionalModalProperties = {
40
46
  primaryButton?: ButtonProperties;
41
47
  secondaryButton?: ButtonProperties;
42
48
  };
49
+ /**
50
+ * Milliseconds during which a repeat dismissal (overlay click, Escape, hardware
51
+ * back-press) is dropped after one has fired. One window is shared by all three
52
+ * paths. Read when each dismissal arrives, so a value changed while the modal
53
+ * is open applies to the next one, without forgetting when the last fired.
54
+ * Default: 700.
55
+ */
43
56
  debounceTime?: number;
44
57
  leftImageTestId?: string;
45
58
  /** Accessible name for the left header image's role="button" wrapper (e.g. a back control). Rendered as aria-label. */
@@ -63,7 +76,7 @@ export type OptionalModalProperties = {
63
76
  overlayFadeIn?: boolean;
64
77
  /** When true, mounts the overlay at document.body to escape clipping/stacking contexts. Default: false. */
65
78
  usePortal?: boolean;
66
- /** When true, locks document.body scroll while the modal is mounted, restoring it on unmount. Default: true (matches prior unconditional behavior). Set false for a non-blocking modal that should allow background scroll. */
79
+ /** When true, locks the selected scrollContainer (default: ownerDocument.body) while the modal is mounted, restoring it on unmount. Default: true (matches prior unconditional behavior). Set false for a non-blocking modal that should allow background scroll. */
67
80
  lockScroll?: boolean;
68
81
  /** Milliseconds after mount to automatically fire onclose, e.g. for a transient success/confirmation modal. null (default) disables auto-dismiss. */
69
82
  autoDismissAfter?: number | null;
@@ -75,6 +88,21 @@ export type OptionalModalProperties = {
75
88
  * accessible name at all.
76
89
  */
77
90
  ariaLabel?: string;
91
+ /**
92
+ * `id` of the element that describes the modal, rendered as `aria-describedby`
93
+ * on the modal content panel (the element carrying `role`/`aria-modal` when
94
+ * `role` is set). An `alertdialog`'s message belongs here: without it a screen
95
+ * reader announces the name and role but not what the dialog is asking.
96
+ * Default: unset -- no `aria-describedby` is rendered, so an existing consumer
97
+ * sees no DOM/ARIA change.
98
+ *
99
+ * The browser resolves the id inside the panel's own root, so the described
100
+ * element must live in the same tree as the panel. In the web-component build
101
+ * that means content supplied through the `content` snippet property, not
102
+ * light-DOM content passed through the default `<slot>`, which sits outside
103
+ * the shadow root and cannot be reached by id.
104
+ */
105
+ ariaDescribedby?: string;
78
106
  /**
79
107
  * ARIA role for the modal content panel. Default: unset -- the panel
80
108
  * renders no `role` and no `aria-modal` at all, byte-for-byte matching
@@ -127,6 +127,7 @@
127
127
 
128
128
  <style>
129
129
  .pagination {
130
+ flex-wrap: wrap;
130
131
  display: var(--pagination-display, flex);
131
132
  gap: var(--pagination-gap, 4px);
132
133
  align-items: var(--pagination-align-items, center);
@@ -3,6 +3,8 @@
3
3
  import { DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
4
4
  import type { PieChartProperties } from './properties';
5
5
  import ChartContainer from '../_chart/ChartContainer.svelte';
6
+ import LabelPlate from '../_chart/LabelPlate.svelte';
7
+ import { defaultChartName, resolveChartName } from '../_chart/a11y';
6
8
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
7
9
  import Legend from '../_chart/Legend.svelte';
8
10
  import DeltaIndicator from '../DeltaIndicator/DeltaIndicator.svelte';
@@ -38,6 +40,8 @@
38
40
  empty,
39
41
  onsliceclick,
40
42
  onslicehover,
43
+ ariaLabel,
44
+ ariaDescription,
41
45
  testId,
42
46
  classes,
43
47
  tooltipPortal = false,
@@ -62,6 +66,7 @@
62
66
  let chartHeight = $state(0);
63
67
  let hoveredIndex = $state<number | null>(null);
64
68
  let focusedIndex = $state<number | null>(null);
69
+ let focusedSlice = $state(false);
65
70
  let programmaticIndex = $state<number | null>(null);
66
71
  let mouseX = $state(0);
67
72
  let mouseY = $state(0);
@@ -135,6 +140,13 @@
135
140
  `${format(v)}\u00A0${pctFormat(v)}`
136
141
  );
137
142
  let isEmpty = $derived(data.length === 0 || total === 0);
143
+ let chartName = $derived(
144
+ resolveChartName(ariaLabel, defaultChartName('pie', { seriesNames: data.map((d) => d.label) }))
145
+ );
146
+ let hasMarks = $derived.by(() => !isEmpty && slices.length > 0);
147
+ // A center snippet can contain a real action even when the data is empty.
148
+ // Keep that child exposed; the empty slice geometry itself stays out of Tab.
149
+ let interactiveDrawing = $derived(hasMarks || typeof center === 'function');
138
150
 
139
151
  // When semiCircle is true the effective aspect ratio is driven by:
140
152
  // 1. The explicit `aspectRatio` prop (highest priority — always wins).
@@ -386,21 +398,20 @@
386
398
  };
387
399
  });
388
400
 
389
- // ── Assistive-tech status region ───────────────────────────────
390
- // Mirrors the tooltip's exact text (value AND percentage) into a live region
391
- // every slice is described-by, so a screen reader user gets the same detail a
392
- // pointer hover shows without needing pointer hover -- and, because it reads
393
- // activeIndex rather than hoveredIndex, it also announces the declarative
394
- // highlightedIndex prop and the imperative ChartHighlightAPI, neither of
395
- // which fires a focus event of its own.
401
+ // A focused slice is spoken once, by its complete name. The shared status
402
+ // region is not a point description: that gave every unfocused slice the
403
+ // last-announced slice's text, and echoed it on focus. Keep the live region
404
+ // for pointer/legend and programmatic highlights, which lack that name.
396
405
  const statusId = `pie-status-${instanceId}`;
397
- let statusText = $derived.by(() => {
398
- if (activeIndex === null) {
399
- return '';
400
- }
401
- const s = sliceAt(activeIndex);
402
- return s === null ? '' : `${s.label}: ${format(s.value)} (${pctFormat(s.value)})`;
403
- });
406
+ function sliceAriaLabel(index: number): string {
407
+ const slice = sliceAt(index);
408
+ return slice === null
409
+ ? ''
410
+ : `${slice.label}: ${format(slice.value)} (${pctFormat(slice.value)})`;
411
+ }
412
+ let statusText = $derived(
413
+ activeIndex === null || focusedSlice ? '' : sliceAriaLabel(activeIndex)
414
+ );
404
415
 
405
416
  // ── Interactions ───────────────────────────────────────────────
406
417
  // Family-wide contract shared with BarChart/FunnelChart/DualAxisBarChart:
@@ -457,8 +468,9 @@
457
468
  }
458
469
 
459
470
  // Wired to focus on both slices and legend rows.
460
- function handleFocus(i: number) {
471
+ function handleFocus(i: number, slice = false) {
461
472
  focusedIndex = i;
473
+ focusedSlice = slice;
462
474
  notifyHoverChange();
463
475
  }
464
476
 
@@ -467,6 +479,7 @@
467
479
  // hovered.
468
480
  function handleBlur() {
469
481
  focusedIndex = null;
482
+ focusedSlice = false;
470
483
  notifyHoverChange();
471
484
  }
472
485
 
@@ -539,6 +552,9 @@
539
552
  aspectRatio={effectiveAspectRatio}
540
553
  {maxHeight}
541
554
  {minHeight}
555
+ ariaLabel={chartName}
556
+ {ariaDescription}
557
+ interactive={interactiveDrawing}
542
558
  >
543
559
  <g transform="translate({cx}, {cy})">
544
560
  {#each slices as slice (slice.index)}
@@ -548,28 +564,18 @@
548
564
  class:dimmed={activeIndex !== null && activeIndex !== slice.index}
549
565
  d={slice.path}
550
566
  fill={slice.color}
551
- tabindex="0"
567
+ tabindex={hasMarks ? 0 : null}
552
568
  role="button"
553
- aria-label="{slice.label}: {format(slice.value)}"
554
- aria-describedby={statusId}
569
+ aria-hidden={hasMarks ? null : 'true'}
570
+ aria-label={sliceAriaLabel(slice.index)}
555
571
  onmouseenter={(e) => handlePointerEnter(e, slice.index)}
556
572
  onmousemove={trackMouse}
557
573
  onmouseleave={handlePointerLeave}
558
- onfocus={() => handleFocus(slice.index)}
574
+ onfocus={() => handleFocus(slice.index, true)}
559
575
  onblur={handleBlur}
560
576
  onkeydown={(e) => handleKeydown(e, slice.index)}
561
577
  onclick={() => onsliceclick?.({ index: slice.index, slice: data[slice.index] })}
562
578
  />
563
- {#if visibleSliceLabels.has(slice.index)}
564
- <text
565
- class="slice-label"
566
- class:label-outside={labelPosition === 'outside'}
567
- x={slice.labelX}
568
- y={slice.labelY}
569
- text-anchor="middle"
570
- dominant-baseline="middle">{visibleSliceLabels.get(slice.index)}</text
571
- >
572
- {/if}
573
579
  {/each}
574
580
 
575
581
  {#if innerR > 0 && typeof center === 'function' && centerBoxSize > 0}
@@ -584,6 +590,22 @@
584
590
  </div>
585
591
  </foreignObject>
586
592
  {/if}
593
+ <!-- Labels and their plates paint after every slice and center surface. -->
594
+ {#each slices as slice (slice.index)}
595
+ {#if visibleSliceLabels.has(slice.index)}
596
+ <LabelPlate>
597
+ <text
598
+ class="slice-label"
599
+ aria-hidden="true"
600
+ class:label-outside={labelPosition === 'outside'}
601
+ x={slice.labelX}
602
+ y={slice.labelY}
603
+ text-anchor="middle"
604
+ dominant-baseline="middle">{visibleSliceLabels.get(slice.index)}</text
605
+ >
606
+ </LabelPlate>
607
+ {/if}
608
+ {/each}
587
609
  </g>
588
610
  </ChartContainer>
589
611
 
@@ -40,6 +40,10 @@ export type OptionalPieChartProperties = {
40
40
  tooltipPortal?: boolean;
41
41
  center?: Snippet;
42
42
  empty?: Snippet;
43
+ /** Accessible name of the drawing; omit to derive one from the plotted data. */
44
+ ariaLabel?: string;
45
+ /** Longer chart description, resolved inside the drawing's own DOM/shadow root. */
46
+ ariaDescription?: string;
43
47
  testId?: string;
44
48
  classes?: string;
45
49
  semiCircle?: boolean;
@@ -39,6 +39,11 @@
39
39
  * once per mounted instance is what a consumer needs to act on, and it costs
40
40
  * nothing on subsequent updates.
41
41
  */
42
+ // Reading `attrs` once is the point of this block: it is a dev notice about what the consumer
43
+ // passed at mount, not state. Rendering reads `attrs` live in the spread on the root element
44
+ // below, so an `attrs` that changes afterwards is still applied; only the notice is not
45
+ // repeated. The ignore is on this one block, whose only reference is that read.
46
+ // svelte-ignore state_referenced_locally
42
47
  if (import.meta.env?.DEV === true) {
43
48
  for (const message of discardedAttrWarnings('Pill', PILL_MANAGED_ATTRS, attrs)) {
44
49
  console.warn(message);
@@ -23,7 +23,16 @@
23
23
  rejected. `describeField` composes the reference from the messages that are
24
24
  actually rendered, so aria-describedby never points at an id that is not in
25
25
  the DOM -- which passes an attribute assertion and resolves to nothing in a
26
- real reader. */
26
+ real reader.
27
+
28
+ `aria-invalid` is deliberately NOT put on the input. WAI-ARIA 1.2 supports it on
29
+ radiogroup (and checkbox, textbox, ...) but not on role="radio", where the
30
+ attribute is deprecated, so emitting it was an unsupported attribute that the
31
+ compiler flagged and some assistive technology ignores. The error is reachable
32
+ the way FileInput's is -- aria-describedby on the control plus role="alert" on
33
+ the message -- and the state is exposed as `data-invalid` for styling. A group
34
+ is invalid as a whole, so that state belongs on the consumer's
35
+ role="radiogroup" wrapper (see docs/Radio.md). */
27
36
  const fieldUid = $props.id();
28
37
  const field = $derived(
29
38
  describeField(fieldUid, { error: errorMessage, info: infoMessage, invalid })
@@ -49,7 +58,6 @@
49
58
  <input
50
59
  type="radio"
51
60
  aria-describedby={field.describedBy}
52
- aria-invalid={field.ariaInvalid}
53
61
  class="radio-input"
54
62
  {name}
55
63
  {value}
@@ -59,6 +67,7 @@
59
67
  form={typeof form === 'string' ? form : null}
60
68
  data-state={checked ? 'checked' : 'unchecked'}
61
69
  data-disabled={disabled ? '' : null}
70
+ data-invalid={field.ariaInvalid === null ? null : ''}
62
71
  onchange={handleChange}
63
72
  />
64
73
  <span class="radio-indicator" class:checked class:disabled>
@@ -6,7 +6,10 @@ export type MandatoryRadioProperties = {
6
6
  export type OptionalRadioProperties = {
7
7
  /**
8
8
  * Text shown, and announced, when the control is in error. Linked to the
9
- * control through `aria-describedby`, and sets `aria-invalid` while present.
9
+ * control through `aria-describedby` and announced through `role="alert"`;
10
+ * the input carries `data-invalid` while present. It does not set
11
+ * `aria-invalid`, which WAI-ARIA 1.2 does not support on `role="radio"` --
12
+ * put that on the `role="radiogroup"` that wraps the group.
10
13
  */
11
14
  errorMessage?: string | null;
12
15
  /**
@@ -17,7 +20,9 @@ export type OptionalRadioProperties = {
17
20
  infoMessage?: string | null;
18
21
  /**
19
22
  * Marks the control invalid without supplying a message, for a consumer
20
- * driving validity from a server or its own rules.
23
+ * driving validity from a server or its own rules. Sets `data-invalid` on
24
+ * the input; the announced group state is the wrapping `role="radiogroup"`'s
25
+ * `aria-invalid`.
21
26
  */
22
27
  invalid?: boolean;
23
28
  selectedValue?: string;