@streamscloud/kit 0.89.1 → 0.90.1

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 (68) hide show
  1. package/README.md +2 -1
  2. package/dist/core/validation/form-validation-handler/form-validation-handler.svelte.d.ts +4 -2
  3. package/dist/core/validation/form-validation-handler/form-validation-handler.svelte.js +1 -1
  4. package/dist/ui/_internal/form-field-label-context/form-field-label-context.svelte.d.ts +17 -0
  5. package/dist/ui/_internal/form-field-label-context/form-field-label-context.svelte.js +22 -0
  6. package/dist/ui/_internal/form-field-label-context/index.d.ts +1 -0
  7. package/dist/ui/_internal/form-field-label-context/index.js +1 -0
  8. package/dist/ui/checkbox/cmp.checkbox.svelte +11 -2
  9. package/dist/ui/chip-group/cmp.chip-group.svelte +10 -3
  10. package/dist/ui/chip-group/cmp.chip-group.svelte.d.ts +4 -2
  11. package/dist/ui/color-picker/cmp.color-picker.svelte +15 -2
  12. package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +1 -3
  13. package/dist/ui/date-picker/cmp.date-picker.svelte +11 -6
  14. package/dist/ui/date-picker/cmp.date-picker.svelte.d.ts +2 -9
  15. package/dist/ui/date-picker/cmp.date-time-picker.svelte +19 -3
  16. package/dist/ui/date-picker/cmp.date-time-picker.svelte.d.ts +3 -5
  17. package/dist/ui/date-picker/cmp.time-picker.svelte +3 -1
  18. package/dist/ui/date-picker/cmp.time-picker.svelte.d.ts +1 -4
  19. package/dist/ui/document-tabs/types.d.ts +0 -1
  20. package/dist/ui/form-field/cmp.form-field-validatable.svelte +4 -4
  21. package/dist/ui/form-field/cmp.form-field-validatable.svelte.d.ts +4 -7
  22. package/dist/ui/form-field/cmp.form-field.svelte +31 -79
  23. package/dist/ui/form-field/cmp.form-field.svelte.d.ts +11 -10
  24. package/dist/ui/form-field/form-field-label.svelte.d.ts +6 -0
  25. package/dist/ui/form-field/form-field-label.svelte.js +19 -0
  26. package/dist/ui/form-field/index.d.ts +1 -1
  27. package/dist/ui/icon-picker/cmp.icon-picker.svelte +3 -1
  28. package/dist/ui/icon-picker/cmp.icon-picker.svelte.d.ts +3 -1
  29. package/dist/ui/input/cmp.input.svelte +5 -1
  30. package/dist/ui/input/types.d.ts +0 -3
  31. package/dist/ui/numeral-input/cmp.numeral-input.svelte +6 -2
  32. package/dist/ui/numeral-input/cmp.numeral-input.svelte.d.ts +0 -4
  33. package/dist/ui/option-pill/cmp.option-pill.svelte.d.ts +0 -2
  34. package/dist/ui/page-layout/cmp.page-layout.svelte.d.ts +1 -2
  35. package/dist/ui/pin-input/cmp.pin-input.svelte +11 -3
  36. package/dist/ui/pin-input/cmp.pin-input.svelte.d.ts +1 -1
  37. package/dist/ui/popover/cmp.popover.svelte +6 -3
  38. package/dist/ui/popover/cmp.popover.svelte.d.ts +3 -6
  39. package/dist/ui/radio/cmp.radio.svelte +12 -3
  40. package/dist/ui/rich-text-input/cmp.rich-text-input.svelte +19 -7
  41. package/dist/ui/rich-text-input/cmp.rich-text-input.svelte.d.ts +1 -2
  42. package/dist/ui/segmented-control/cmp.segmented-control.svelte +5 -3
  43. package/dist/ui/segmented-control/cmp.segmented-control.svelte.d.ts +1 -2
  44. package/dist/ui/select/cmp.input-suggest.svelte +5 -1
  45. package/dist/ui/select/cmp.input-suggest.svelte.d.ts +0 -3
  46. package/dist/ui/select/cmp.multiselect-tree.svelte +2 -1
  47. package/dist/ui/select/cmp.multiselect-tree.svelte.d.ts +1 -3
  48. package/dist/ui/select/cmp.singleselect-async.svelte +9 -3
  49. package/dist/ui/select/cmp.singleselect-async.svelte.d.ts +0 -2
  50. package/dist/ui/select/cmp.singleselect.svelte +3 -1
  51. package/dist/ui/select/cmp.singleselect.svelte.d.ts +0 -2
  52. package/dist/ui/select/multiselect-base.svelte +10 -3
  53. package/dist/ui/select/select-core.svelte.js +0 -2
  54. package/dist/ui/select/select-listbox.svelte +4 -7
  55. package/dist/ui/select/types.d.ts +2 -6
  56. package/dist/ui/slider/cmp.slider.svelte +6 -2
  57. package/dist/ui/slider/cmp.slider.svelte.d.ts +1 -0
  58. package/dist/ui/tabs/types.d.ts +0 -1
  59. package/dist/ui/textarea/cmp.textarea.svelte +5 -1
  60. package/dist/ui/textarea/cmp.textarea.svelte.d.ts +0 -2
  61. package/dist/ui/toggle/cmp.toggle.svelte +4 -2
  62. package/dist/ui/tree/cmp.tree-node-view.svelte +13 -15
  63. package/dist/ui/validatable/cmp.validatable.svelte +1 -2
  64. package/dist/ui/validatable/cmp.validatable.svelte.d.ts +1 -2
  65. package/dist/ui/video/cmp.video.svelte.d.ts +0 -1
  66. package/dist/ui/visual-select/cmp.visual-select.svelte +11 -3
  67. package/dist/ui/visual-select/cmp.visual-select.svelte.d.ts +2 -1
  68. package/package.json +3 -2
@@ -1,83 +1,30 @@
1
- <script lang="ts" module>"use strict";
2
- // HTML's interactive-content list, verbatim: exactly what makes the browser skip label activation. A wider selector (role, tabindex) skips the guard on clicks the browser still forwards.
3
- const INTERACTIVE_CONTENT = 'a[href], audio[controls], button, details, embed, iframe, img[usemap], input:not([type="hidden"]), label, select, textarea, video[controls]';
1
+ <script lang="ts" module>export {};
4
2
  </script>
5
3
 
6
- <script lang="ts">import { Icon } from '../icon';
4
+ <script lang="ts">import { provideFormFieldLabel } from '../_internal/form-field-label-context';
5
+ import { Icon } from '../icon';
7
6
  import { Tooltip } from '../tooltip';
7
+ import { createFormFieldLabel } from './form-field-label.svelte';
8
8
  import IconInfo from '@fluentui/svg-icons/icons/info_20_regular.svg?raw';
9
9
  let { label, hint, required = false, children } = $props();
10
- let rootEl = $state.raw(undefined);
11
- let swallowLabelActivation = false;
12
- // Unhandled, the click activates the labelled field (a checkbox would toggle); cancelling it on interactive content would kill a link's navigation.
13
- const swallowClick = (event) => {
14
- event.stopPropagation();
15
- // This click never reaches guardLabelActivation, so the flag would outlive the gesture and swallow a later keyboard-activated click.
16
- swallowLabelActivation = false;
17
- if (forwardsLabelActivation(event.target)) {
18
- event.preventDefault();
19
- }
20
- };
21
- const forwardsLabelActivation = (target) => {
22
- if (!(target instanceof Element)) {
23
- return false;
24
- }
25
- const interactive = target.closest(INTERACTIVE_CONTENT);
26
- return !interactive || interactive === rootEl;
27
- };
28
- // Registered at mount, so it precedes the capture-phase dismissal a Popover registers on open: by click time the popup has closed and the forwarded activation would reopen it.
29
- $effect(() => {
30
- const armLabelActivation = (event) => {
31
- swallowLabelActivation = false;
32
- if (event.button !== 0 || !rootEl || !(event.target instanceof Node) || !rootEl.contains(event.target)) {
33
- return;
34
- }
35
- swallowLabelActivation = !!rootEl.querySelector('[aria-haspopup][aria-expanded="true"]');
36
- };
37
- // A keyboard-activated click brings no pointerdown of its own and would otherwise inherit the flag from a gesture that produced no click. Only the two keys that synthesize one: any key would also disarm a pointer gesture that is still in flight.
38
- const disarmLabelActivation = (event) => {
39
- if (event.key !== 'Enter' && event.key !== ' ') {
40
- return;
41
- }
42
- swallowLabelActivation = false;
43
- };
44
- window.addEventListener('pointerdown', armLabelActivation, true);
45
- window.addEventListener('keydown', disarmLabelActivation, true);
46
- return () => {
47
- window.removeEventListener('pointerdown', armLabelActivation, true);
48
- window.removeEventListener('keydown', disarmLabelActivation, true);
49
- };
50
- });
51
- const guardLabelActivation = (event) => {
52
- const swallow = swallowLabelActivation;
53
- swallowLabelActivation = false;
54
- if (!forwardsLabelActivation(event.target)) {
55
- return;
56
- }
57
- // The label's control is the group's first radio: forwarding the click would select it instead of keeping the current choice.
58
- const radio = rootEl?.control?.getAttribute('role') === 'radio' ? rootEl.control : null;
59
- if (swallow || radio) {
60
- event.preventDefault();
61
- }
62
- radio?.closest('[role="radiogroup"]')?.querySelector('[role="radio"][tabindex="0"]')?.focus();
63
- };
10
+ const id = $props.id();
11
+ const formFieldLabel = createFormFieldLabel(id, () => !!label);
12
+ provideFormFieldLabel(formFieldLabel);
64
13
  </script>
65
14
 
66
- <!-- svelte-ignore a11y_click_events_have_key_events -->
67
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
68
- <label bind:this={rootEl} class="form-field" onclick={guardLabelActivation}>
15
+ <div class="form-field">
69
16
  {#if label}
70
- <span class="form-field__label">
71
- {#if typeof label === 'string'}
72
- {label}
73
- {:else}
74
- {@render label()}
75
- {/if}
76
- {#if required}<span class="form-field__required" aria-hidden="true">*</span>{/if}
17
+ <div class="form-field__header">
18
+ <label class="form-field__label" for={formFieldLabel.controlId} id={formFieldLabel.labelId}>
19
+ {#if typeof label === 'string'}
20
+ {label}
21
+ {:else}
22
+ {@render label()}
23
+ {/if}
24
+ {#if required}<span class="form-field__required" aria-hidden="true">*</span>{/if}
25
+ </label>
77
26
  {#if hint}
78
- <!-- svelte-ignore a11y_click_events_have_key_events -->
79
- <!-- svelte-ignore a11y_no_static_element_interactions -->
80
- <span class="form-field__hint" onclick={swallowClick}>
27
+ <span class="form-field__hint">
81
28
  {#if typeof hint === 'string'}
82
29
  <Tooltip text={hint}>
83
30
  <span class="form-field__hint-trigger" role="button" tabindex="0" aria-label={hint}>
@@ -89,21 +36,21 @@ const guardLabelActivation = (event) => {
89
36
  {/if}
90
37
  </span>
91
38
  {/if}
92
- </span>
39
+ </div>
93
40
  {/if}
94
41
 
95
- {@render children()}
96
- </label>
42
+ {@render children({ labelId: formFieldLabel.labelId })}
43
+ </div>
97
44
 
98
45
  <!--
99
46
  @component
100
- FormField — a label above an input slot. The root is a `<label>`, so the first labelable control inside is associated without id wiring; a label click never
101
- re-opens an already expanded popup field. It renders no errors — put it inside `Validatable` (or use `FormFieldValidatable`) for that.
47
+ FormField — a label above a field slot; a kit control inside is named by the label on its own, a control outside the kit takes the slot's `labelId` as its
48
+ `aria-labelledby`. Errors are `FormFieldValidatable`'s job.
102
49
 
103
50
  ### CSS Custom Properties
104
51
  | Property | Description | Default |
105
52
  |---|---|---|
106
- | `--sc-kit--form-field--gap` | Vertical gap between label and the input slot | `0.375rem` (6px) |
53
+ | `--sc-kit--form-field--gap` | Vertical gap between label and the field slot | `0.375rem` (6px) |
107
54
  | `--sc-kit--form-field--hint--color` | Hint color (inherited by the default info icon) | `--sc-kit--color--text--muted` |
108
55
  | `--sc-kit--form-field--label--color` | Label text color | `--sc-kit--color--text--primary` |
109
56
  | `--sc-kit--form-field--label--font-size` | Label font size | `--sc-kit--font-size--sm` |
@@ -122,14 +69,19 @@ re-opens an already expanded popup field. It renders no errors — put it inside
122
69
  flex-direction: column;
123
70
  gap: var(--_ff--gap);
124
71
  }
125
- .form-field__label {
126
- display: inline-flex;
72
+ .form-field__header {
73
+ display: flex;
127
74
  align-items: center;
128
75
  gap: 0.25rem;
129
76
  color: var(--_ff--label-color);
130
77
  font-size: var(--_ff--label-font-size);
131
78
  font-weight: var(--_ff--label-font-weight);
132
79
  }
80
+ .form-field__label {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: 0.25rem;
84
+ }
133
85
  .form-field__required {
134
86
  color: var(--_ff--required-color);
135
87
  }
@@ -1,25 +1,26 @@
1
+ export type FormFieldIds = {
2
+ /** For a control outside the kit — kit controls pick the label up by themselves. */
3
+ labelId: string | undefined;
4
+ };
1
5
  import type { Snippet } from 'svelte';
2
6
  type Props = {
3
- /** Label rendered above the input. String renders as text; Snippet allows extra content (e.g. tooltip icon) next to the text. */
7
+ /** Label rendered above the field. String renders as text; Snippet allows extra content (e.g. tooltip icon) next to the text. */
4
8
  label?: string | Snippet;
5
- /** Affordance pinned to the trailing edge of the label row — rendered only alongside a `label`. String renders the standard info icon — keyboard-focusable, named by the text — with that text in a tooltip; Snippet renders anything (button, link, badge). Clicks inside never reach the field. */
9
+ /** Affordance pinned to the trailing edge of the label row — rendered only alongside a `label`. String renders the standard info icon — keyboard-focusable, named by the text — with that text in a tooltip; Snippet renders anything (button, link, badge). */
6
10
  hint?: string | Snippet;
7
11
  /** Appends a danger-colored `*` to the label. Purely visual — does not enforce anything. */
8
12
  required?: boolean;
9
- /**
10
- * Input slot. The wrapping `<label>` implicitly associates with the first labelable control inside (HTML standard), so no id wiring is needed.
11
- * A control made of several fields (`SegmentedControl`, `PinInput`) is not named by it — give it its own `aria-label`.
12
- */
13
- children: Snippet;
13
+ /** Field slot. A kit control inside is named by the label on its own: a text input through `<label for>`, anything else through `aria-labelledby`. */
14
+ children: Snippet<[FormFieldIds]>;
14
15
  };
15
16
  /**
16
- * FormField — a label above an input slot. The root is a `<label>`, so the first labelable control inside is associated without id wiring; a label click never
17
- * re-opens an already expanded popup field. It renders no errors — put it inside `Validatable` (or use `FormFieldValidatable`) for that.
17
+ * FormField — a label above a field slot; a kit control inside is named by the label on its own, a control outside the kit takes the slot's `labelId` as its
18
+ * `aria-labelledby`. Errors are `FormFieldValidatable`'s job.
18
19
  *
19
20
  * ### CSS Custom Properties
20
21
  * | Property | Description | Default |
21
22
  * |---|---|---|
22
- * | `--sc-kit--form-field--gap` | Vertical gap between label and the input slot | `0.375rem` (6px) |
23
+ * | `--sc-kit--form-field--gap` | Vertical gap between label and the field slot | `0.375rem` (6px) |
23
24
  * | `--sc-kit--form-field--hint--color` | Hint color (inherited by the default info icon) | `--sc-kit--color--text--muted` |
24
25
  * | `--sc-kit--form-field--label--color` | Label text color | `--sc-kit--color--text--primary` |
25
26
  * | `--sc-kit--form-field--label--font-size` | Label font size | `--sc-kit--font-size--sm` |
@@ -0,0 +1,6 @@
1
+ import type { FormFieldLabelContext } from '../_internal/form-field-label-context';
2
+ export type FormFieldLabel = FormFieldLabelContext & {
3
+ /** The id the label's `for` points at — the first mounted text input that claimed the label, if any. */
4
+ readonly controlId: string | undefined;
5
+ };
6
+ export declare const createFormFieldLabel: (id: string, hasLabel: () => boolean) => FormFieldLabel;
@@ -0,0 +1,19 @@
1
+ import { untrack } from 'svelte';
2
+ export const createFormFieldLabel = (id, hasLabel) => {
3
+ const labelId = $derived(hasLabel() ? id : undefined);
4
+ let claimants = $state.raw([]);
5
+ return {
6
+ get labelId() {
7
+ return labelId;
8
+ },
9
+ get controlId() {
10
+ return claimants[0]?.();
11
+ },
12
+ claimControl: (controlId) => {
13
+ claimants = [...untrack(() => claimants), controlId];
14
+ return () => {
15
+ claimants = untrack(() => claimants).filter((claimant) => claimant !== controlId);
16
+ };
17
+ }
18
+ };
19
+ };
@@ -1,2 +1,2 @@
1
- export { default as FormField } from './cmp.form-field.svelte';
1
+ export { default as FormField, type FormFieldIds } from './cmp.form-field.svelte';
2
2
  export { default as FormFieldValidatable } from './cmp.form-field-validatable.svelte';
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import { Icon } from '../icon';
2
2
  import { Singleselect } from '../select';
3
3
  import { isIconPickerGroup } from './types';
4
- let { value, icons, placeholder = '', disabled = false, error = false, borderless = false, cols = 7, on } = $props();
4
+ let { value, icons, placeholder = '', disabled = false, error = false, borderless = false, cols = 7, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, on } = $props();
5
5
  const toOption = (i) => ({ value: i.value, label: i.label, disabled: i.disabled });
6
6
  const selectOptions = $derived(icons.map((item) => (isIconPickerGroup(item) ? { label: item.label, options: item.options.map(toOption) } : toOption(item))));
7
7
  const iconBySrc = $derived(new Map(icons.flatMap((item) => isIconPickerGroup(item) ? item.options.map((o) => [o.value, o.src]) : [[item.value, item.src]])));
@@ -15,6 +15,8 @@ const iconBySrc = $derived(new Map(icons.flatMap((item) => isIconPickerGroup(ite
15
15
  disabled={disabled}
16
16
  error={error}
17
17
  borderless={borderless}
18
+ aria-label={ariaLabel}
19
+ aria-labelledby={ariaLabelledby}
18
20
  on={{ change: (v) => on?.change?.(v) }}>
19
21
  {#snippet selectionSnippet({ option })}
20
22
  {@const src = iconBySrc.get(option.value)}
@@ -1,7 +1,7 @@
1
1
  import { type IconPickerItem } from './types';
2
2
  type Props = {
3
3
  value: string | null;
4
- /** Flat list of icons or a list of labelled groups. */
4
+ /** Flat list of icons or a list of labeled groups. */
5
5
  icons: IconPickerItem[];
6
6
  placeholder?: string;
7
7
  disabled?: boolean;
@@ -10,6 +10,8 @@ type Props = {
10
10
  borderless?: boolean;
11
11
  /** Tiles per row in the popover grid. @default 7 */
12
12
  cols?: number;
13
+ 'aria-label'?: string;
14
+ 'aria-labelledby'?: string;
13
15
  on?: {
14
16
  change?: (value: string | null) => void;
15
17
  };
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">import { HtmlHelper, StringHelper } from '../../core/utils';
2
+ import { claimFormFieldLabel } from '../_internal/form-field-label-context';
2
3
  import { InputClearButton } from '../_internal/input-clear-button';
3
4
  import { InputEmojiPicker } from '../_internal/input-emoji-picker';
4
5
  import { InputPasswordToggle } from '../_internal/input-password-toggle';
5
6
  import { IconSlot } from '../icon';
6
7
  const { value, type = 'text', size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, clearable = false, emoji = false, trim = true, placeholder = '', title = '', name = '', id = null, autofocus = false, maxLength = null, autocomplete, autocapitalize, inputmode, enterkeyhint, spellcheck, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, on, icon, iconPosition = 'leading', secondaryIcon, prefix, suffix } = $props();
8
+ const fallbackId = $props.id();
9
+ const controlId = $derived(id ?? fallbackId);
10
+ claimFormFieldLabel(() => controlId);
7
11
  let inputEl = $state.raw(undefined);
8
12
  let passwordRevealed = $state(false);
9
13
  const valueParsed = $derived(value ?? '');
@@ -121,7 +125,7 @@ const focusInput = () => inputEl?.focus();
121
125
  class="input__el"
122
126
  type={effectiveType}
123
127
  name={name}
124
- id={id}
128
+ id={controlId}
125
129
  placeholder={placeholder}
126
130
  disabled={disabled}
127
131
  readonly={readonly}
@@ -10,15 +10,12 @@ export type InputProps = {
10
10
  /** @default 'md' */
11
11
  size?: InputSize;
12
12
  disabled?: boolean;
13
- /** Non-editable but selectable. The value is still submitted with a form. */
14
13
  readonly?: boolean;
15
- /** Visual display mode — non-focusable, no hover/focus animations, hidden clear button. */
16
14
  inert?: boolean;
17
15
  /** Visual error state — also sets `aria-invalid="true"`. */
18
16
  error?: boolean;
19
17
  /** Strip border + background + focus underline. */
20
18
  borderless?: boolean;
21
- /** Show the default × clear button when value is non-empty. */
22
19
  clearable?: boolean;
23
20
  /** Show the emoji-picker trigger inside the input. */
24
21
  emoji?: boolean;
@@ -1,8 +1,12 @@
1
- <script lang="ts">import { InputClearButton } from '../_internal/input-clear-button';
1
+ <script lang="ts">import { claimFormFieldLabel } from '../_internal/form-field-label-context';
2
+ import { InputClearButton } from '../_internal/input-clear-button';
2
3
  import { IconSlot } from '../icon';
3
4
  import { formatNumeral, NumeralThousandGroupStyles, registerCursorTracker } from 'cleave-zen';
4
5
  import { untrack } from 'svelte';
5
6
  const { size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, placeholder = '', title = '', name = '', id = null, autofocus = false, min = null, max = null, minExclusive = false, maxExclusive = false, decimals = 2, suffix, hideResolvedValue = false, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, icon, iconPosition = 'leading', secondaryIcon, ...mode } = $props();
7
+ const fallbackId = $props.id();
8
+ const controlId = $derived(id ?? fallbackId);
9
+ claimFormFieldLabel(() => controlId);
6
10
  let inputEl = $state.raw(undefined);
7
11
  let focused = $state(false);
8
12
  let typedValue = $state(undefined);
@@ -136,7 +140,7 @@ const initInput = (node) => {
136
140
  type="text"
137
141
  inputmode="decimal"
138
142
  name={name}
139
- id={id}
143
+ id={controlId}
140
144
  placeholder={placeholder}
141
145
  disabled={disabled}
142
146
  readonly={readonly}
@@ -3,9 +3,7 @@ type BaseProps = {
3
3
  /** @default 'md' */
4
4
  size?: 'sm' | 'md' | 'lg';
5
5
  disabled?: boolean;
6
- /** Non-editable but selectable; value still submitted with a form. */
7
6
  readonly?: boolean;
8
- /** Visual display mode — non-focusable, no hover/focus animations, no clear button. */
9
7
  inert?: boolean;
10
8
  /** Visual error state — also sets `aria-invalid="true"`. */
11
9
  error?: boolean;
@@ -41,7 +39,6 @@ type BaseProps = {
41
39
  secondaryIcon?: IconProp;
42
40
  };
43
41
  type ClearableProps = BaseProps & {
44
- /** Value can be cleared to `null`. Trigger shows an × button when set. */
45
42
  clearable: true;
46
43
  value: number | null | undefined;
47
44
  on?: {
@@ -55,7 +52,6 @@ type ClearableProps = BaseProps & {
55
52
  };
56
53
  };
57
54
  type RequiredProps = BaseProps & {
58
- /** Value is required; no clear button. */
59
55
  clearable: false;
60
56
  value: number;
61
57
  on?: {
@@ -2,7 +2,6 @@ import type { OptionPillOption } from './types';
2
2
  declare function $$render<T>(): {
3
3
  props: ({
4
4
  options: OptionPillOption<T>[];
5
- /** Disable the whole group. */
6
5
  disabled?: boolean;
7
6
  /** Custom equality comparator for non-primitive values (objects with `id`, etc.). @default (a, b) => a === b */
8
7
  compare?: (a: T, b: T) => boolean;
@@ -15,7 +14,6 @@ declare function $$render<T>(): {
15
14
  };
16
15
  }) | ({
17
16
  options: OptionPillOption<T>[];
18
- /** Disable the whole group. */
19
17
  disabled?: boolean;
20
18
  /** Custom equality comparator for non-primitive values (objects with `id`, etc.). @default (a, b) => a === b */
21
19
  compare?: (a: T, b: T) => boolean;
@@ -9,9 +9,8 @@ type Props = {
9
9
  /** Editor chrome — the fixed panels-toggle + close pair. Set it to render both buttons; omit it entirely for a non-editor page. */
10
10
  editorControls?: EditorControls;
11
11
  scrollable?: boolean;
12
- /** Sets `inert` on the whole layout — header and body stop taking focus, clicks and assistive-tech interaction. */
13
12
  inert?: boolean;
14
- /** Shows a `Spinner` centered over the layout; `blocking` also makes the whole layout `inert` and dims it. @default false */
13
+ /** Shows a `Spinner` centered over the layout; `blocking` also makes the whole layout `inert` and dims it. */
15
14
  loading?: PageLoading;
16
15
  };
17
16
  /**
@@ -1,5 +1,7 @@
1
- <script lang="ts">import { PinInputLocalization } from './pin-input-localization';
2
- const { value, length = 6, mask = false, disabled = false, error = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, on } = $props();
1
+ <script lang="ts">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
2
+ import { PinInputLocalization } from './pin-input-localization';
3
+ const { value, length = 6, mask = false, disabled = false, error = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, on } = $props();
4
+ const formFieldLabel = getFormFieldLabel();
3
5
  const localization = new PinInputLocalization();
4
6
  let cells = $state([]);
5
7
  const cellValue = (i) => value[i] ?? '';
@@ -59,7 +61,13 @@ const handleFocus = (e) => {
59
61
  };
60
62
  </script>
61
63
 
62
- <div class="pin-input" class:pin-input--disabled={disabled} class:pin-input--error={error} role="group" aria-label={ariaLabel}>
64
+ <div
65
+ class="pin-input"
66
+ class:pin-input--disabled={disabled}
67
+ class:pin-input--error={error}
68
+ role="group"
69
+ aria-label={ariaLabel}
70
+ aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}>
63
71
  {#each Array(length) as _, i (i)}
64
72
  <input
65
73
  aria-label={localization.cellLabel(i + 1, length)}
@@ -7,8 +7,8 @@ type Props = {
7
7
  disabled?: boolean;
8
8
  /** Visual error state — also sets `aria-invalid="true"` on each cell. */
9
9
  error?: boolean;
10
- /** Accessible label for the cell group. Consumer-provided (no kit default). */
11
10
  'aria-label'?: string;
11
+ 'aria-labelledby'?: string;
12
12
  'aria-describedby'?: string;
13
13
  on?: {
14
14
  input?: (value: string) => void;
@@ -1,10 +1,11 @@
1
- <script lang="ts">import { Icon } from '../icon';
1
+ <script lang="ts">import { isInsideLabelOf } from '../_internal/form-field-label-context';
2
+ import { Icon } from '../icon';
2
3
  import { isIgnored } from './popover-ignore';
3
4
  import { validateTrigger } from './validate-trigger';
4
5
  import { autoUpdate, computePosition, flip, offset as offsetMiddleware, shift, size } from '@floating-ui/dom';
5
6
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
6
7
  import { tick } from 'svelte';
7
- let { position = 'bottom-start', disabled = false, fixedPosition = false, offset = 4, boundaryMargin = 8, backdrop = false, matchWidth = false, panel = true, keepOpen = false, fillContainer = false, tabbable = true, on, children, trigger, 'aria-label': ariaLabel } = $props();
8
+ let { position = 'bottom-start', disabled = false, fixedPosition = false, offset = 4, boundaryMargin = 8, backdrop = false, matchWidth = false, panel = true, keepOpen = false, fillContainer = false, tabbable = true, on, children, trigger, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby } = $props();
8
9
  let opened = $state(false);
9
10
  let triggerEl = $state.raw(undefined);
10
11
  let contentEl = $state.raw(undefined);
@@ -106,7 +107,7 @@ $effect(() => {
106
107
  if (!target) {
107
108
  return;
108
109
  }
109
- if (triggerEl?.contains(target) || contentEl?.contains(target)) {
110
+ if (triggerEl?.contains(target) || contentEl?.contains(target) || isInsideLabelOf(target, triggerEl)) {
110
111
  return;
111
112
  }
112
113
  close();
@@ -179,9 +180,11 @@ const handleBackdropClick = (e) => {
179
180
  type="button"
180
181
  class="popover__trigger"
181
182
  class:popover__trigger--fill={fillContainer}
183
+ id={id}
182
184
  disabled={disabled}
183
185
  tabindex={tabbable ? undefined : -1}
184
186
  aria-label={ariaLabel}
187
+ aria-labelledby={ariaLabelledby}
185
188
  aria-haspopup="true"
186
189
  aria-expanded={opened}
187
190
  onclick={handleTriggerClick}
@@ -27,14 +27,11 @@ type Props = {
27
27
  closed?: () => void;
28
28
  };
29
29
  children: Snippet;
30
- /**
31
- * Custom trigger content; defaults to a chevron-down icon. Renders inside the trigger `<button>`,
32
- * so content MUST be non-interactive — for a button-looking trigger use `Button` with
33
- * `type="presentational"`. Nesting an interactive element triggers a dev-only warning.
34
- */
30
+ /** Content of the trigger `<button>`, so non-interactive only — `Button type="presentational"` for a button look. @default chevron-down icon */
35
31
  trigger?: Snippet;
36
- /** Accessible name for the trigger button — required when the trigger content carries no text (an icon-only trigger). */
32
+ id?: string;
37
33
  'aria-label'?: string;
34
+ 'aria-labelledby'?: string;
38
35
  };
39
36
  /**
40
37
  * Popover — floating content anchored to a trigger element. Click trigger to open;
@@ -1,5 +1,7 @@
1
- <script lang="ts">const { selected, label, disabled = false, title, name, on } = $props();
1
+ <script lang="ts">import { getFormFieldLabel } from '../_internal/form-field-label-context';
2
+ const { selected, label, disabled = false, title, name, on } = $props();
2
3
  const ariaLabel = $derived(typeof label === 'string' ? label : name);
4
+ const formFieldLabel = getFormFieldLabel();
3
5
  const handleClick = (event) => {
4
6
  event.stopPropagation();
5
7
  if (disabled || selected) {
@@ -7,11 +9,18 @@ const handleClick = (event) => {
7
9
  }
8
10
  on?.change?.();
9
11
  };
10
- export {};
11
12
  </script>
12
13
 
13
14
  <label class="radio" class:radio--on={selected} class:radio--disabled={disabled} title={title}>
14
- <button type="button" class="radio__dot" role="radio" aria-checked={selected} aria-label={ariaLabel} disabled={disabled} onclick={handleClick}>
15
+ <button
16
+ type="button"
17
+ class="radio__dot"
18
+ role="radio"
19
+ aria-checked={selected}
20
+ aria-label={ariaLabel}
21
+ aria-labelledby={label || ariaLabel ? undefined : formFieldLabel?.labelId}
22
+ disabled={disabled}
23
+ onclick={handleClick}>
15
24
  <span class="radio__inner" class:radio__inner--visible={selected} aria-hidden="true"></span>
16
25
  </button>
17
26
  {#if typeof label === 'string'}
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">import { AppTheme } from '../../core/theme';
2
2
  import { HtmlHelper } from '../../core/utils';
3
+ import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
3
4
  import { InputEmojiPicker } from '../_internal/input-emoji-picker';
4
5
  import { HtmlBlock } from '../html-block';
5
6
  import { RichTextInputLocalization } from './rich-text-input-localization';
6
7
  import { addValidatedLinkButton } from './validated-link-button';
7
8
  import { onDestroy, onMount } from 'svelte';
8
- const { value, size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, emoji = false, autofocus = false, rows = 0, placeholder = '', options, conf, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
9
- const id = `ti-${crypto.randomUUID()}`;
9
+ const { value, size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, emoji = false, autofocus = false, rows = 0, placeholder = '', options, conf, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
10
+ const id = $props.id();
11
+ const formFieldLabel = getFormFieldLabel();
10
12
  let elementRef = $state.raw(undefined);
11
13
  let toolbarRef = $state.raw(undefined);
12
14
  let editorRef = $state.raw(null);
@@ -142,13 +144,23 @@ const onEmojiSelected = (emojiChar) => insertEmoji(emojiChar);
142
144
  class:rich-text-input--readonly={readonly}
143
145
  class:rich-text-input--inert={inert}
144
146
  class:rich-text-input--error={error}
145
- class:rich-text-input--borderless={borderless}
146
- aria-label={ariaLabel}
147
- aria-describedby={ariaDescribedby}
148
- aria-required={ariaRequired ? 'true' : undefined}>
147
+ class:rich-text-input--borderless={borderless}>
149
148
  <div class="rich-text-input__toolbar" bind:this={toolbarRef}></div>
150
149
  <HtmlBlock>
151
- <div class="rich-text-input__input" id={id} bind:this={elementRef}></div>
150
+ <div
151
+ bind:this={elementRef}
152
+ class="rich-text-input__input"
153
+ id={id}
154
+ role="textbox"
155
+ tabindex={disabled || inert ? undefined : 0}
156
+ aria-multiline="true"
157
+ aria-label={ariaLabel}
158
+ aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}
159
+ aria-describedby={ariaDescribedby}
160
+ aria-required={ariaRequired ? 'true' : undefined}
161
+ aria-invalid={error ? 'true' : undefined}
162
+ aria-readonly={readonly ? 'true' : undefined}>
163
+ </div>
152
164
  </HtmlBlock>
153
165
  {#if emoji}
154
166
  <span class="rich-text-input__emoji">
@@ -6,9 +6,7 @@ type Props = {
6
6
  /** @default 'md' — controls the editor root font size (and downstream rem-based sizing). */
7
7
  size?: 'sm' | 'md' | 'lg';
8
8
  disabled?: boolean;
9
- /** Non-editable but still visible (mirrors `disabled` in editor mode). */
10
9
  readonly?: boolean;
11
- /** Visual display mode — no focus, no hover, hidden affordances. */
12
10
  inert?: boolean;
13
11
  /** Visual error state. */
14
12
  error?: boolean;
@@ -24,6 +22,7 @@ type Props = {
24
22
  /** Escape-hatch for extra HugeRTE init options, merged AFTER kit defaults. */
25
23
  conf?: Partial<RawEditorOptions>;
26
24
  'aria-label'?: string;
25
+ 'aria-labelledby'?: string;
27
26
  'aria-describedby'?: string;
28
27
  'aria-required'?: boolean;
29
28
  on?: {
@@ -1,8 +1,9 @@
1
1
  <script lang="ts" module>export {};
2
2
  </script>
3
3
 
4
- <script lang="ts" generics="T">"use strict";
5
- const { value, segments, variant = 'tray', size = 'md', fullWidth = false, compare, 'aria-label': ariaLabel, on } = $props();
4
+ <script lang="ts" generics="T">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
5
+ const { value, segments, variant = 'tray', size = 'md', fullWidth = false, compare, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, on } = $props();
6
+ const formFieldLabel = getFormFieldLabel();
6
7
  const segmentIdPrefix = $props.id();
7
8
  const segmentEls = [];
8
9
  const isActive = (segment) => (compare ? compare(value, segment.value) : value === segment.value);
@@ -51,7 +52,8 @@ const handleKeydown = (event, from) => {
51
52
  class="segmented-control segmented-control--{variant} segmented-control--{size}"
52
53
  class:segmented-control--full={fullWidth}
53
54
  role="radiogroup"
54
- aria-label={ariaLabel}>
55
+ aria-label={ariaLabel}
56
+ aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}>
55
57
  {#each segments as segment, i (i)}
56
58
  {@const segmentLabelId = `${segmentIdPrefix}-${i}`}
57
59
  <button
@@ -1,7 +1,6 @@
1
1
  export type Segment<TValue = unknown> = {
2
2
  value: TValue;
3
3
  label: string;
4
- /** Non-interactive — click does nothing. */
5
4
  disabled?: boolean;
6
5
  /** Visual flag — colors the segment in the danger tone (validation hint). */
7
6
  error?: boolean;
@@ -23,8 +22,8 @@ declare function $$render<T>(): {
23
22
  fullWidth?: boolean;
24
23
  /** Equality for object T values. @default reference equality (`===`) */
25
24
  compare?: (a: T, b: T) => boolean;
26
- /** Accessible name of the group. */
27
25
  'aria-label'?: string;
26
+ 'aria-labelledby'?: string;
28
27
  on?: {
29
28
  change?: (value: T) => void;
30
29
  };
@@ -2,10 +2,14 @@
2
2
  </script>
3
3
 
4
4
  <script lang="ts">import { HtmlHelper } from '../../core/utils';
5
+ import { claimFormFieldLabel } from '../_internal/form-field-label-context';
5
6
  import { InputClearButton } from '../_internal/input-clear-button';
6
7
  import { IconSlot } from '../icon';
7
8
  import { default as SelectListbox } from './select-listbox.svelte';
8
9
  const { value, suggestions, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, clearable = false, trim = true, icon, name, id, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
10
+ const fallbackId = $props.id();
11
+ const controlId = $derived(id ?? fallbackId);
12
+ claimFormFieldLabel(() => controlId);
9
13
  let inputEl = $state.raw(undefined);
10
14
  let rootEl = $state.raw(undefined);
11
15
  let isOpen = $state(false);
@@ -241,7 +245,7 @@ const handlePickRow = (row) => {
241
245
  aria-describedby={ariaDescribedby}
242
246
  aria-required={ariaRequired ? 'true' : undefined}
243
247
  aria-invalid={error ? 'true' : undefined}
244
- id={id}
248
+ id={controlId}
245
249
  name={name}
246
250
  autocomplete={autocomplete}
247
251
  placeholder={placeholder}