@streamscloud/kit 0.89.1 → 0.90.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 (67) 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 +15 -0
  5. package/dist/ui/_internal/form-field-label-context/form-field-label-context.svelte.js +20 -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 +12 -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 +6 -4
  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 +2 -1
  38. package/dist/ui/popover/cmp.popover.svelte.d.ts +2 -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 +5 -2
  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 +6 -2
  53. package/dist/ui/select/select-core.svelte.js +0 -2
  54. package/dist/ui/select/select-listbox.svelte +1 -5
  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/validatable/cmp.validatable.svelte +1 -2
  63. package/dist/ui/validatable/cmp.validatable.svelte.d.ts +1 -2
  64. package/dist/ui/video/cmp.video.svelte.d.ts +0 -1
  65. package/dist/ui/visual-select/cmp.visual-select.svelte +11 -3
  66. package/dist/ui/visual-select/cmp.visual-select.svelte.d.ts +2 -1
  67. package/package.json +3 -2
@@ -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;
@@ -4,7 +4,7 @@ import { validateTrigger } from './validate-trigger';
4
4
  import { autoUpdate, computePosition, flip, offset as offsetMiddleware, shift, size } from '@floating-ui/dom';
5
5
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
6
6
  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();
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, 'aria-labelledby': ariaLabelledby } = $props();
8
8
  let opened = $state(false);
9
9
  let triggerEl = $state.raw(undefined);
10
10
  let contentEl = $state.raw(undefined);
@@ -182,6 +182,7 @@ const handleBackdropClick = (e) => {
182
182
  disabled={disabled}
183
183
  tabindex={tabbable ? undefined : -1}
184
184
  aria-label={ariaLabel}
185
+ aria-labelledby={ariaLabelledby}
185
186
  aria-haspopup="true"
186
187
  aria-expanded={opened}
187
188
  onclick={handleTriggerClick}
@@ -27,14 +27,10 @@ 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). */
37
32
  'aria-label'?: string;
33
+ 'aria-labelledby'?: string;
38
34
  };
39
35
  /**
40
36
  * 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}
@@ -17,15 +17,12 @@ type Props = {
17
17
  size?: 'sm' | 'md' | 'lg';
18
18
  placeholder?: string;
19
19
  disabled?: boolean;
20
- /** Non-editable but selectable; the popover never opens. */
21
20
  readonly?: boolean;
22
- /** Visual display mode — non-focusable, no hover/focus animations. */
23
21
  inert?: boolean;
24
22
  /** Visual error state — also sets `aria-invalid="true"`. */
25
23
  error?: boolean;
26
24
  /** Strip border + background + focus underline. */
27
25
  borderless?: boolean;
28
- /** Show the × clear button when the value is non-empty. */
29
26
  clearable?: boolean;
30
27
  /**
31
28
  * Strip leading / trailing whitespace when the value is committed — focus leaving the control or
@@ -4,7 +4,7 @@
4
4
  <script lang="ts" generics="T">import { default as MultiselectBase } from './multiselect-base.svelte';
5
5
  import { buildFuseIndex, defaultCompare, runFuseSearch } from './select-internals';
6
6
  import { isSelectGroup } from './types';
7
- const { options, value, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = false, groupHeader = 'toggle-all', selectionMode = 'children-only', selectedDisplay = 'checkbox', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
7
+ const { options, value, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = false, groupHeader = 'toggle-all', selectionMode = 'children-only', selectedDisplay = 'checkbox', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
8
8
  const fuse = $derived(buildFuseIndex(options));
9
9
  const loadOptionsShim = (q) => Promise.resolve(runFuseSearch(fuse, q, options));
10
10
  // Include flat roots, not only groups — else a childless root can't be picked as a create-parent.
@@ -70,6 +70,7 @@ $effect(() => {
70
70
  name={name}
71
71
  autocomplete={autocomplete}
72
72
  aria-label={ariaLabel}
73
+ aria-labelledby={ariaLabelledby}
73
74
  aria-describedby={ariaDescribedby}
74
75
  aria-required={ariaRequired}
75
76
  on={{
@@ -21,11 +21,8 @@ declare function $$render<T>(): {
21
21
  size?: "sm" | "md" | "lg";
22
22
  /** Placeholder shown only when nothing is selected and the user is not typing. @default '' */
23
23
  placeholder?: string;
24
- /** Fully disabled — no focus, no interaction, dimmed styling. @default false */
25
24
  disabled?: boolean;
26
- /** Read-only — value visible, but selection cannot change. @default false */
27
25
  readonly?: boolean;
28
- /** Inert subtree — non-interactive but unstyled. @default false */
29
26
  inert?: boolean;
30
27
  /** Visual error state (accent border + aria-invalid). @default false */
31
28
  error?: boolean;
@@ -81,6 +78,7 @@ declare function $$render<T>(): {
81
78
  name?: string;
82
79
  autocomplete?: HTMLInputAttributes["autocomplete"];
83
80
  'aria-label'?: string;
81
+ 'aria-labelledby'?: string;
84
82
  'aria-describedby'?: string;
85
83
  'aria-required'?: boolean;
86
84
  on?: {
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" module>export {};
2
2
  </script>
3
3
 
4
- <script lang="ts" generics="T">import { InputClearButton } from '../_internal/input-clear-button';
4
+ <script lang="ts" generics="T">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
5
+ import { InputClearButton } from '../_internal/input-clear-button';
5
6
  import { Spinner } from '../_internal/spinner';
6
7
  import { Icon, IconSlot } from '../icon';
7
8
  import { createSelectCore } from './select-core.svelte';
@@ -9,7 +10,8 @@ import { defaultCompare } from './select-internals';
9
10
  import { default as SelectListbox } from './select-listbox.svelte';
10
11
  import { SingleselectLocalization } from './singleselect-localization';
11
12
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_16_regular.svg?raw';
12
- const { loadOptions, debounceMs = 400, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
13
+ const { loadOptions, debounceMs = 400, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
14
+ const formFieldLabel = getFormFieldLabel();
13
15
  const localization = new SingleselectLocalization();
14
16
  const selectedOption = $derived(mode.value ?? null);
15
17
  const selectionLabel = $derived(selectedOption?.label ?? '');
@@ -101,6 +103,7 @@ const showClear = $derived(mode.clearable && hasValue && isInteractive && !core.
101
103
  aria-activedescendant={core.isOpen ? `${listboxId}-row-${core.highlight}` : undefined}
102
104
  aria-autocomplete="list"
103
105
  aria-label={ariaLabel ?? name}
106
+ aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}
104
107
  aria-describedby={ariaDescribedby}
105
108
  aria-required={ariaRequired}
106
109
  aria-invalid={error ? 'true' : undefined}
@@ -15,7 +15,6 @@ declare function $$render<T>(): {
15
15
  /** Internal — set by the sync `Singleselect` wrapper. Async typeahead is always searchable. @default true */
16
16
  searchable?: boolean;
17
17
  } & {
18
- /** Discriminator — `true` enables the X clear button and allows `value` to be `null`. */
19
18
  clearable: true;
20
19
  /** Currently selected option (pre-resolved — labels render before the first fetch lands). `null` when nothing is picked. */
21
20
  value: SelectOption<T> | null | undefined;
@@ -33,7 +32,6 @@ declare function $$render<T>(): {
33
32
  /** Internal — set by the sync `Singleselect` wrapper. Async typeahead is always searchable. @default true */
34
33
  searchable?: boolean;
35
34
  } & {
36
- /** Discriminator — `false` removes the X clear button; `value` must always be a non-null `SelectOption<T>`. */
37
35
  clearable: false;
38
36
  /** Currently selected option — non-null. */
39
37
  value: SelectOption<T>;
@@ -4,7 +4,7 @@
4
4
  <script lang="ts" generics="T">import { default as SingleselectAsync } from './cmp.singleselect-async.svelte';
5
5
  import { buildFuseIndex, defaultCompare, runFuseSearch } from './select-internals';
6
6
  import { isSelectGroup } from './types';
7
- const { options, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
7
+ const { options, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
8
8
  const fuse = $derived(buildFuseIndex(options));
9
9
  const loadOptionsShim = (q) => Promise.resolve(runFuseSearch(fuse, q, options));
10
10
  const resolvedValue = $derived.by(() => {
@@ -64,6 +64,7 @@ $effect(() => {
64
64
  name={name}
65
65
  autocomplete={autocomplete}
66
66
  aria-label={ariaLabel}
67
+ aria-labelledby={ariaLabelledby}
67
68
  aria-describedby={ariaDescribedby}
68
69
  aria-required={ariaRequired}
69
70
  on={{
@@ -97,6 +98,7 @@ $effect(() => {
97
98
  name={name}
98
99
  autocomplete={autocomplete}
99
100
  aria-label={ariaLabel}
101
+ aria-labelledby={ariaLabelledby}
100
102
  aria-describedby={ariaDescribedby}
101
103
  aria-required={ariaRequired}
102
104
  on={{
@@ -11,7 +11,6 @@ declare function $$render<T>(): {
11
11
  /** When true, typing filters the options; when false, it is a click/keyboard-only picker. @default true */
12
12
  searchable?: boolean;
13
13
  } & {
14
- /** Discriminator — `true` enables the X clear button and allows `value` to be `null`. */
15
14
  clearable: true;
16
15
  /** Current selection by value. `null` when nothing is picked. */
17
16
  value: T | null | undefined;
@@ -27,7 +26,6 @@ declare function $$render<T>(): {
27
26
  /** When true, typing filters the options; when false, it is a click/keyboard-only picker. @default true */
28
27
  searchable?: boolean;
29
28
  } & {
30
- /** Discriminator — `false` hides the X button; `value` must always be a non-null `T`. */
31
29
  clearable: false;
32
30
  /** Current selection by value — non-null. */
33
31
  value: T;