@streamscloud/kit 0.80.0 → 0.81.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 (52) hide show
  1. package/dist/core/utils/html-helper.d.ts +7 -1
  2. package/dist/core/utils/html-helper.js +83 -10
  3. package/dist/styles/_mixins.scss +12 -0
  4. package/dist/ui/color-picker/cmp.color-picker.svelte +2 -2
  5. package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +2 -0
  6. package/dist/ui/dialog/cmp.dialog-container.svelte +9 -0
  7. package/dist/ui/dialog/cmp.dialog.svelte +1 -1
  8. package/dist/ui/dialog/cmp.dialog.svelte.d.ts +1 -1
  9. package/dist/ui/dialog/dialog-controller.d.ts +2 -0
  10. package/dist/ui/dialog/dialog-controller.js +3 -0
  11. package/dist/ui/dialog/layout/cmp.frameless-dialog.svelte +1 -1
  12. package/dist/ui/dialog/layout/cmp.stepper-dialog.svelte +1 -1
  13. package/dist/ui/file-uploader/cmp.file-uploader.svelte +4 -1
  14. package/dist/ui/file-uploader/file-uploader-localization.d.ts +1 -0
  15. package/dist/ui/file-uploader/file-uploader-localization.js +7 -0
  16. package/dist/ui/form-field/cmp.form-field-validatable.svelte +22 -37
  17. package/dist/ui/form-field/cmp.form-field-validatable.svelte.d.ts +3 -25
  18. package/dist/ui/form-field/cmp.form-field.svelte +23 -12
  19. package/dist/ui/form-field/cmp.form-field.svelte.d.ts +7 -7
  20. package/dist/ui/icon/cmp.icon.svelte +6 -5
  21. package/dist/ui/icon/cmp.icon.svelte.d.ts +1 -1
  22. package/dist/ui/navigation/cmp.nav-rail-item.svelte +17 -0
  23. package/dist/ui/navigation/cmp.nav-rail-item.svelte.d.ts +1 -1
  24. package/dist/ui/navigation/cmp.nav-rail.svelte +4 -4
  25. package/dist/ui/navigation/cmp.nav-rail.svelte.d.ts +2 -0
  26. package/dist/ui/navigation/nav-rail-item-localization.d.ts +3 -0
  27. package/dist/ui/navigation/nav-rail-item-localization.js +12 -0
  28. package/dist/ui/pin-input/cmp.pin-input.svelte +5 -2
  29. package/dist/ui/pin-input/cmp.pin-input.svelte.d.ts +1 -0
  30. package/dist/ui/pin-input/pin-input-localization.d.ts +3 -0
  31. package/dist/ui/pin-input/pin-input-localization.js +12 -0
  32. package/dist/ui/segmented-control/cmp.segmented-control.svelte +52 -12
  33. package/dist/ui/segmented-control/cmp.segmented-control.svelte.d.ts +12 -10
  34. package/dist/ui/table/cmp.table-draggable.svelte.d.ts +6 -0
  35. package/dist/ui/table/cmp.table.svelte.d.ts +4 -0
  36. package/dist/ui/table/table-cells/table-actions-cell.svelte +3 -1
  37. package/dist/ui/table/table-cells/table-by-cell.svelte +1 -1
  38. package/dist/ui/table/table-core.svelte +51 -7
  39. package/dist/ui/table/table-core.svelte.d.ts +4 -0
  40. package/dist/ui/table/table-localization.d.ts +1 -0
  41. package/dist/ui/table/table-localization.js +7 -0
  42. package/dist/ui/toast/cmp.toast-item.svelte +29 -4
  43. package/dist/ui/toast/types.d.ts +1 -1
  44. package/dist/ui/tooltip/cmp.tooltip.svelte +15 -4
  45. package/dist/ui/tooltip/cmp.tooltip.svelte.d.ts +7 -2
  46. package/dist/ui/validatable/cmp.validatable.svelte +6 -42
  47. package/dist/ui/validatable/cmp.validatable.svelte.d.ts +9 -43
  48. package/dist/ui/validatable/cmp.validation-error.svelte +3 -2
  49. package/dist/ui/validatable/cmp.validation-error.svelte.d.ts +4 -1
  50. package/dist/ui/validatable/field-frame.svelte +2 -2
  51. package/dist/ui/validatable/field-frame.svelte.d.ts +2 -0
  52. package/package.json +1 -1
@@ -7,7 +7,7 @@ type Props = {
7
7
  href?: string;
8
8
  /** Highlight as the current section. Consumer-controlled. @default false */
9
9
  active?: boolean;
10
- /** Paint the tile in the danger color to signal an error in its section. @default false */
10
+ /** Paint the tile in the danger color to signal an error in its section; screen readers hear it as part of the label. @default false */
11
11
  error?: boolean;
12
12
  disabled?: boolean;
13
13
  /** Anchor target — only relevant when `href` is set. */
@@ -5,7 +5,7 @@
5
5
  import { NavRailLocalization } from './nav-rail-localization';
6
6
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
7
7
  import IconChevronUp from '@fluentui/svg-icons/icons/chevron_up_20_regular.svg?raw';
8
- const { brand, children, footerToggles } = $props();
8
+ const { brand, children, footerToggles, 'aria-label': ariaLabel } = $props();
9
9
  const localization = new NavRailLocalization();
10
10
  const SCROLL_STEP = 120;
11
11
  let listNode = $state(null);
@@ -33,7 +33,7 @@ const scrollDown = () => {
33
33
  };
34
34
  </script>
35
35
 
36
- <div class="nav-rail" role="navigation">
36
+ <div class="nav-rail" role="navigation" aria-label={ariaLabel}>
37
37
  {#if brand}
38
38
  <div class="nav-rail__brand">
39
39
  {@render brand()}
@@ -41,7 +41,7 @@ const scrollDown = () => {
41
41
  {/if}
42
42
  <div class="nav-rail__scroll-area">
43
43
  {#if canScrollUp}
44
- <button type="button" class="nav-rail__scroll-btn nav-rail__scroll-btn--up" onclick={scrollUp} aria-label={localization.scrollUp}>
44
+ <button type="button" class="nav-rail__scroll-btn nav-rail__scroll-btn--up" tabindex="-1" onclick={scrollUp} aria-label={localization.scrollUp}>
45
45
  <Icon src={IconChevronUp} size="sm" />
46
46
  </button>
47
47
  {/if}
@@ -55,7 +55,7 @@ const scrollDown = () => {
55
55
  {@render children()}
56
56
  </div>
57
57
  {#if canScrollDown}
58
- <button type="button" class="nav-rail__scroll-btn nav-rail__scroll-btn--down" onclick={scrollDown} aria-label={localization.scrollDown}>
58
+ <button type="button" class="nav-rail__scroll-btn nav-rail__scroll-btn--down" tabindex="-1" onclick={scrollDown} aria-label={localization.scrollDown}>
59
59
  <Icon src={IconChevronDown} size="sm" />
60
60
  </button>
61
61
  {/if}
@@ -13,6 +13,8 @@ type Props = {
13
13
  children: Snippet;
14
14
  /** Icon-only toggle buttons stacked at the bottom of the rail (e.g. expand panel, account). */
15
15
  footerToggles?: NavRailFooterToggle[];
16
+ /** Accessible name of the navigation landmark — needed when the page has more than one navigation. */
17
+ 'aria-label'?: string;
16
18
  };
17
19
  /**
18
20
  * NavRail — first-level icon-and-label navigation column for app shells. 70px wide by default;
@@ -0,0 +1,3 @@
1
+ export declare class NavRailItemLocalization {
2
+ get hasError(): string;
3
+ }
@@ -0,0 +1,12 @@
1
+ import { AppLocale } from '../../core/locale';
2
+ const loc = {
3
+ hasError: {
4
+ en: 'Has errors',
5
+ no: 'Har feil'
6
+ }
7
+ };
8
+ export class NavRailItemLocalization {
9
+ get hasError() {
10
+ return loc.hasError[AppLocale.current];
11
+ }
12
+ }
@@ -1,5 +1,6 @@
1
- <script lang="ts">"use strict";
2
- const { value, length = 6, mask = false, disabled = false, error = false, 'aria-label': ariaLabel, on } = $props();
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();
3
+ const localization = new PinInputLocalization();
3
4
  let cells = $state([]);
4
5
  const cellValue = (i) => value[i] ?? '';
5
6
  const emit = (next) => {
@@ -61,6 +62,8 @@ const handleFocus = (e) => {
61
62
  <div class="pin-input" class:pin-input--disabled={disabled} class:pin-input--error={error} role="group" aria-label={ariaLabel}>
62
63
  {#each Array(length) as _, i (i)}
63
64
  <input
65
+ aria-label={localization.cellLabel(i + 1, length)}
66
+ aria-describedby={ariaDescribedby}
64
67
  bind:this={cells[i]}
65
68
  class="pin-input__cell"
66
69
  class:pin-input__cell--filled={cellValue(i) !== ''}
@@ -9,6 +9,7 @@ type Props = {
9
9
  error?: boolean;
10
10
  /** Accessible label for the cell group. Consumer-provided (no kit default). */
11
11
  'aria-label'?: string;
12
+ 'aria-describedby'?: string;
12
13
  on?: {
13
14
  input?: (value: string) => void;
14
15
  change?: (value: string) => void;
@@ -0,0 +1,3 @@
1
+ export declare class PinInputLocalization {
2
+ cellLabel: (position: number, length: number) => string;
3
+ }
@@ -0,0 +1,12 @@
1
+ import { AppLocale } from '../../core/locale';
2
+ const loc = {
3
+ cellLabel: {
4
+ en: (position, length) => `Digit ${position} of ${length}`,
5
+ no: (position, length) => `Siffer ${position} av ${length}`
6
+ }
7
+ };
8
+ export class PinInputLocalization {
9
+ cellLabel = (position, length) => {
10
+ return loc.cellLabel[AppLocale.current](position, length);
11
+ };
12
+ }
@@ -2,50 +2,90 @@
2
2
  </script>
3
3
 
4
4
  <script lang="ts" generics="T">"use strict";
5
- const { value, segments, variant = 'tray', size = 'md', fullWidth = false, compare, on } = $props();
5
+ const { value, segments, variant = 'tray', size = 'md', fullWidth = false, compare, 'aria-label': ariaLabel, on } = $props();
6
+ const segmentIdPrefix = $props.id();
7
+ const segmentEls = [];
6
8
  const isActive = (segment) => (compare ? compare(value, segment.value) : value === segment.value);
9
+ const tabStopIndex = $derived.by(() => {
10
+ const activeIndex = segments.findIndex((segment) => !segment.disabled && isActive(segment));
11
+ return activeIndex >= 0 ? activeIndex : segments.findIndex((segment) => !segment.disabled);
12
+ });
7
13
  const handleClick = (segment) => {
8
14
  if (segment.disabled || isActive(segment)) {
9
15
  return;
10
16
  }
11
17
  on?.change?.(segment.value);
12
18
  };
19
+ const nextEnabled = (from, step) => {
20
+ let index = from;
21
+ do {
22
+ index = (index + step + segments.length) % segments.length;
23
+ } while (segments[index].disabled && index !== from);
24
+ return index;
25
+ };
26
+ const targetIndex = (key, from) => {
27
+ switch (key) {
28
+ case 'ArrowRight':
29
+ return nextEnabled(from, 1);
30
+ case 'ArrowLeft':
31
+ return nextEnabled(from, -1);
32
+ default:
33
+ return null;
34
+ }
35
+ };
36
+ const handleKeydown = (event, from) => {
37
+ if (event.altKey || event.ctrlKey || event.metaKey) {
38
+ return;
39
+ }
40
+ const target = targetIndex(event.key, from);
41
+ if (target === null) {
42
+ return;
43
+ }
44
+ event.preventDefault();
45
+ segmentEls[target]?.focus();
46
+ handleClick(segments[target]);
47
+ };
13
48
  </script>
14
49
 
15
- <div class="segmented-control segmented-control--{variant} segmented-control--{size}" class:segmented-control--full={fullWidth} role="radiogroup">
50
+ <div
51
+ class="segmented-control segmented-control--{variant} segmented-control--{size}"
52
+ class:segmented-control--full={fullWidth}
53
+ role="radiogroup"
54
+ aria-label={ariaLabel}>
16
55
  {#each segments as segment, i (i)}
56
+ {@const segmentLabelId = `${segmentIdPrefix}-${i}`}
17
57
  <button
58
+ bind:this={segmentEls[i]}
18
59
  type="button"
60
+ tabindex={i === tabStopIndex ? 0 : -1}
61
+ onkeydown={(e) => handleKeydown(e, i)}
19
62
  class="segmented-control__segment"
20
63
  class:segmented-control__segment--active={isActive(segment)}
21
64
  class:segmented-control__segment--disabled={segment.disabled}
22
65
  class:segmented-control__segment--error={segment.error}
23
66
  role="radio"
24
67
  aria-checked={isActive(segment)}
68
+ aria-labelledby={segmentLabelId}
25
69
  disabled={segment.disabled}
26
70
  onclick={() => handleClick(segment)}>
27
- <span class="segmented-control__label">{segment.label}</span>
71
+ <span id={segmentLabelId} class="segmented-control__label">{segment.label}</span>
28
72
  </button>
29
73
  {/each}
30
74
  </div>
31
75
 
32
76
  <!--
33
77
  @component
34
- SegmentedControl — radio-style toggle group rendered as connected pills. Two visual variants:
35
-
36
- - **`tray`** (default): inset pill — outer container is a flat tray, active segment lifts as a card with shadow.
37
- - **`outlined`**: button-row — each segment has its own border, active segment is filled with the menu-active background.
38
-
39
- Each segment carries an optional `disabled` flag (non-interactive) and an optional `error` flag (colors the label in the danger tone — useful for marking validation errors across a group picker).
78
+ SegmentedControl — a radio group rendered as connected pills, in a `tray` or `outlined` look. Keyboard follows native radio buttons: one tab stop (the
79
+ selected segment), Left / Right move and select with wrap-around.
40
80
 
41
81
  ### CSS Custom Properties
42
82
  | Property | Description | Default |
43
83
  |---|---|---|
44
- | `--sc-kit--segmented-control--height` | Control height | per size (28 / 32 px) |
84
+ | `--sc-kit--segmented-control--height` | Control height | per size (29 / 32 px) |
45
85
  | `--sc-kit--segmented-control--font-size` | Label font size | per size |
46
86
  | `--sc-kit--segmented-control--padding-inline` | Per-segment horizontal padding | per size (10 / 12 px) |
47
- | `--sc-kit--segmented-control--border-radius` | Outer radius (tray) / per-segment radius (outlined) | per size — `sm` 4px, `md` 6px |
48
- | `--sc-kit--segmented-control--color` | Default segment text color | `--sc-kit--color--text--secondary` |
87
+ | `--sc-kit--segmented-control--border-radius` | Outer radius (tray) / per-segment radius (outlined) | per size — `--sc-kit--field--border-radius--sm` / `--md` |
88
+ | `--sc-kit--segmented-control--color` | Default segment text color | `--sc-kit--color--text--muted` |
49
89
  | `--sc-kit--segmented-control--color--hover` | Hover text color | `--sc-kit--color--text--primary` |
50
90
  | `--sc-kit--segmented-control--color--active` | Active segment text color | `--sc-kit--color--text--primary` |
51
91
  | `--sc-kit--segmented-control--color--disabled` | Disabled segment text color | `--sc-kit--color--text--muted` |
@@ -11,7 +11,11 @@ declare function $$render<T>(): {
11
11
  value: T;
12
12
  /** `NoInfer<T>` ensures `T` narrows from `value` only; inline segments literals type-check against the value's T without widening — no `as const` needed at callsites. */
13
13
  segments: Segment<NoInfer<T>>[];
14
- /** @default 'tray' */
14
+ /**
15
+ * `'tray'`: flat tray, the active segment lifts as a card with a shadow. `'outlined'`: each segment has its own border, the active one is filled
16
+ * with the menu-active background.
17
+ * @default 'tray'
18
+ */
15
19
  variant?: "tray" | "outlined";
16
20
  /** @default 'md' */
17
21
  size?: "sm" | "md";
@@ -19,6 +23,8 @@ declare function $$render<T>(): {
19
23
  fullWidth?: boolean;
20
24
  /** Equality for object T values. @default reference equality (`===`) */
21
25
  compare?: (a: T, b: T) => boolean;
26
+ /** Accessible name of the group. */
27
+ 'aria-label'?: string;
22
28
  on?: {
23
29
  change?: (value: T) => void;
24
30
  };
@@ -43,21 +49,17 @@ interface $$IsomorphicComponent {
43
49
  z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
44
50
  }
45
51
  /**
46
- * SegmentedControl — radio-style toggle group rendered as connected pills. Two visual variants:
47
- *
48
- * - **`tray`** (default): inset pill — outer container is a flat tray, active segment lifts as a card with shadow.
49
- * - **`outlined`**: button-row — each segment has its own border, active segment is filled with the menu-active background.
50
- *
51
- * Each segment carries an optional `disabled` flag (non-interactive) and an optional `error` flag (colors the label in the danger tone — useful for marking validation errors across a group picker).
52
+ * SegmentedControl — a radio group rendered as connected pills, in a `tray` or `outlined` look. Keyboard follows native radio buttons: one tab stop (the
53
+ * selected segment), Left / Right move and select with wrap-around.
52
54
  *
53
55
  * ### CSS Custom Properties
54
56
  * | Property | Description | Default |
55
57
  * |---|---|---|
56
- * | `--sc-kit--segmented-control--height` | Control height | per size (28 / 32 px) |
58
+ * | `--sc-kit--segmented-control--height` | Control height | per size (29 / 32 px) |
57
59
  * | `--sc-kit--segmented-control--font-size` | Label font size | per size |
58
60
  * | `--sc-kit--segmented-control--padding-inline` | Per-segment horizontal padding | per size (10 / 12 px) |
59
- * | `--sc-kit--segmented-control--border-radius` | Outer radius (tray) / per-segment radius (outlined) | per size — `sm` 4px, `md` 6px |
60
- * | `--sc-kit--segmented-control--color` | Default segment text color | `--sc-kit--color--text--secondary` |
61
+ * | `--sc-kit--segmented-control--border-radius` | Outer radius (tray) / per-segment radius (outlined) | per size — `--sc-kit--field--border-radius--sm` / `--md` |
62
+ * | `--sc-kit--segmented-control--color` | Default segment text color | `--sc-kit--color--text--muted` |
61
63
  * | `--sc-kit--segmented-control--color--hover` | Hover text color | `--sc-kit--color--text--primary` |
62
64
  * | `--sc-kit--segmented-control--color--active` | Active segment text color | `--sc-kit--color--text--primary` |
63
65
  * | `--sc-kit--segmented-control--color--disabled` | Disabled segment text color | `--sc-kit--color--text--muted` |
@@ -25,6 +25,12 @@ declare function $$render<T extends {
25
25
  * the row background; an empty list drops the column header, leaving the toolbar and the no-items placeholder.
26
26
  */
27
27
  toolbar?: Snippet;
28
+ /** Accessible name of the table. Prefer `aria-labelledby` when a visible title exists. */
29
+ 'aria-label'?: string;
30
+ /** Id of the element whose text names the table, e.g. a heading in the `toolbar`. */
31
+ 'aria-labelledby'?: string;
32
+ /** Accessible name of a row's drag handle, e.g. `(row) => \`Move ${row.name}\``. @default localized "Reorder" */
33
+ gripLabel?: (item: T) => string;
28
34
  };
29
35
  exports: {};
30
36
  bindings: "";
@@ -41,6 +41,10 @@ declare function $$render<T extends {
41
41
  * the row background; an empty list drops the column header, leaving the toolbar and the no-items placeholder.
42
42
  */
43
43
  toolbar?: Snippet;
44
+ /** Accessible name of the table. Prefer `aria-labelledby` when a visible title exists. */
45
+ 'aria-label'?: string;
46
+ /** Id of the element whose text names the table, e.g. a heading in the `toolbar`. */
47
+ 'aria-labelledby'?: string;
44
48
  };
45
49
  exports: {};
46
50
  bindings: "";
@@ -1,11 +1,13 @@
1
1
  <script lang="ts">import { Icon } from '../../icon';
2
2
  import { Popover } from '../../popover';
3
+ import { TableLocalization } from '../table-localization';
3
4
  import IconMoreHorizontal from '@fluentui/svg-icons/icons/more_horizontal_20_regular.svg?raw';
4
5
  let { popoverPosition = 'bottom-end', children } = $props();
6
+ const localization = new TableLocalization();
5
7
  </script>
6
8
 
7
9
  <div class="table-actions-cell__dropdown-wrapper">
8
- <Popover position={popoverPosition} fillContainer>
10
+ <Popover position={popoverPosition} fillContainer aria-label={localization.actionsColumnTitle}>
9
11
  {#snippet trigger()}
10
12
  <span class="table-actions-cell__trigger">
11
13
  <Icon src={IconMoreHorizontal} />
@@ -35,7 +35,7 @@ const navigateToUser = (event, by) => {
35
35
  {@const href = getHref(by)}
36
36
  {#if href}
37
37
  <a href={href} onclick={(e) => navigateToUser(e, by)} class="table-by-cell__by-image">
38
- <ImageRound src={by.image} />
38
+ <ImageRound src={by.image} alt={by.name} />
39
39
  </a>
40
40
  {:else}
41
41
  <span class="table-by-cell__by-image">
@@ -37,8 +37,16 @@ import { TableLocalization } from './table-localization';
37
37
  import IconReorderDotsHorizontal from '@fluentui/svg-icons/icons/re_order_dots_horizontal_20_regular.svg?raw';
38
38
  import { untrack } from 'svelte';
39
39
  import { flip } from 'svelte/animate';
40
- let { model, rows, showAlternativeView = false, noItemsPlaceholderDelay, reorder, on, alternativeView, noItemsPlaceholder, toolbar, groupHeader } = $props();
40
+ let { model, rows, showAlternativeView = false, noItemsPlaceholderDelay, reorder, gripLabel, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, on, alternativeView, noItemsPlaceholder, toolbar, groupHeader } = $props();
41
41
  const localization = new TableLocalization();
42
+ const tableId = $props.id();
43
+ const handleCellClick = (event, column, item) => {
44
+ if (!column.onClick) {
45
+ return;
46
+ }
47
+ event.stopPropagation();
48
+ column.onClick(item);
49
+ };
42
50
  $effect(() => {
43
51
  const selection = model.selection;
44
52
  return untrack(() => {
@@ -117,12 +125,17 @@ const rowState = (row) => {
117
125
  {@render alternativeView()}
118
126
  {/if}
119
127
  {:else}
120
- <table class="table" class:table--sticky-header={model.styles?.stickyHeader === true || !!toolbar} class:table--fixed={model.styles?.layout === 'fixed'}>
128
+ <table
129
+ class="table"
130
+ aria-label={ariaLabel}
131
+ aria-labelledby={ariaLabelledby}
132
+ class:table--sticky-header={model.styles?.stickyHeader === true || !!toolbar}
133
+ class:table--fixed={model.styles?.layout === 'fixed'}>
121
134
  {#if showColumnHeader}
122
135
  <thead class="table__head">
123
136
  <tr>
124
137
  {#if reorder}
125
- <th class="table__th table__drag-handle">&nbsp;</th>
138
+ <th class="table__th table__drag-handle"><span class="table__visually-hidden">{localization.reorderColumnTitle}</span></th>
126
139
  {/if}
127
140
  {#each model.columns as column (column)}
128
141
  <TableHideableCell column={column}>
@@ -162,12 +175,16 @@ const rowState = (row) => {
162
175
  {:else}
163
176
  {#if reorder}
164
177
  <td class="table__td table__drag-handle" class:table__td--flush={state?.flushBottom}>
165
- <div class="table__drag-handle-grip" {@attach reorder.handleAttachment}>
178
+ <div
179
+ class="table__drag-handle-grip"
180
+ aria-label={gripLabel?.(row.item) ?? localization.reorderColumnTitle}
181
+ {@attach reorder.handleAttachment}>
166
182
  <Icon src={IconReorderDotsHorizontal} />
167
183
  </div>
168
184
  </td>
169
185
  {/if}
170
- {#each model.columns as column (column)}
186
+ {#each model.columns as column, columnIndex (column)}
187
+ {@const cellId = `${tableId}-${row.index}-${columnIndex}`}
171
188
  <TableHideableCell column={column}>
172
189
  <td
173
190
  class="table__td"
@@ -175,8 +192,11 @@ const rowState = (row) => {
175
192
  class:table__td--clickable={!!column.onClick}
176
193
  class:table__td--flush={state?.flushBottom}
177
194
  style={columnCellStyle(column)}
178
- onclick={() => column.onClick && column.onClick(row.item)}>
179
- <div class="table__cell" class:table__cell--reveal-on-row-hover={column.revealOnRowHover}>
195
+ onclick={(e) => handleCellClick(e, column, row.item)}>
196
+ {#if column.onClick && column.type !== 'button'}
197
+ <button type="button" class="table__cell-activator" aria-labelledby={cellId}></button>
198
+ {/if}
199
+ <div id={cellId} class="table__cell" class:table__cell--reveal-on-row-hover={column.revealOnRowHover}>
180
200
  <TableCell model={model} item={row.item} column={column} itemIndex={row.index} on={{ itemsReordered: on?.itemsReordered }} />
181
201
  </div>
182
202
  </td>
@@ -369,11 +389,35 @@ Not exported from `index.ts` — for kit-internal use only.
369
389
  text-align: center;
370
390
  }
371
391
  .table__td--clickable {
392
+ position: relative;
372
393
  cursor: pointer;
373
394
  }
374
395
  .table__td--flush {
375
396
  border-bottom: none;
376
397
  }
398
+ .table__cell-activator {
399
+ position: absolute;
400
+ inset: 0;
401
+ padding: 0;
402
+ background: transparent;
403
+ border: none;
404
+ pointer-events: none;
405
+ }
406
+ .table__cell-activator:focus-visible {
407
+ outline: 2px solid var(--sc-kit--color--border--focus);
408
+ outline-offset: -2px;
409
+ }
410
+ .table__visually-hidden {
411
+ position: absolute;
412
+ width: 1px;
413
+ height: 1px;
414
+ margin: -1px;
415
+ padding: 0;
416
+ overflow: hidden;
417
+ clip-path: inset(50%);
418
+ white-space: nowrap;
419
+ border: 0;
420
+ }
377
421
  .table__cell {
378
422
  padding: var(--_table--cell--padding-block) var(--_table--cell--padding-inline);
379
423
  }
@@ -34,6 +34,10 @@ declare function $$render<T extends {
34
34
  showAlternativeView?: boolean;
35
35
  noItemsPlaceholderDelay?: number;
36
36
  reorder?: TableRowReorder;
37
+ /** Accessible name of a row's drag handle. */
38
+ gripLabel?: (item: T) => string;
39
+ 'aria-label'?: string;
40
+ 'aria-labelledby'?: string;
37
41
  on?: {
38
42
  selectionChanged?: (e: T[]) => void;
39
43
  itemsSelected?: (e: T[]) => void;
@@ -2,4 +2,5 @@ export declare class TableLocalization {
2
2
  get noItemsFoundTitle(): string;
3
3
  get noItemsFoundBody(): string;
4
4
  get actionsColumnTitle(): string;
5
+ get reorderColumnTitle(): string;
5
6
  }
@@ -9,6 +9,9 @@ export class TableLocalization {
9
9
  get actionsColumnTitle() {
10
10
  return loc.actionsColumnTitle[AppLocale.current];
11
11
  }
12
+ get reorderColumnTitle() {
13
+ return loc.reorderColumnTitle[AppLocale.current];
14
+ }
12
15
  }
13
16
  const loc = {
14
17
  noItemsFoundTitle: {
@@ -22,5 +25,9 @@ const loc = {
22
25
  actionsColumnTitle: {
23
26
  en: 'Actions',
24
27
  no: 'Handlinger'
28
+ },
29
+ reorderColumnTitle: {
30
+ en: 'Reorder',
31
+ no: 'Endre rekkefølge'
25
32
  }
26
33
  };
@@ -8,6 +8,7 @@ let dragging = $state(false);
8
8
  let pointerId = null;
9
9
  let startX = 0;
10
10
  const SWIPE_THRESHOLD = 80;
11
+ const INTERACTIVE_CONTENT = 'button, a, input, textarea, select';
11
12
  const handlePointerDown = (event) => {
12
13
  if (!toast.swipeable) {
13
14
  return;
@@ -15,6 +16,10 @@ const handlePointerDown = (event) => {
15
16
  if (event.button !== 0) {
16
17
  return;
17
18
  }
19
+ // Pointer capture retargets the click to the toast, so a button inside would never receive it.
20
+ if (event.target instanceof Element && event.target.closest(INTERACTIVE_CONTENT)) {
21
+ return;
22
+ }
18
23
  pointerId = event.pointerId;
19
24
  startX = event.clientX;
20
25
  dragging = true;
@@ -42,14 +47,18 @@ const handleClose = (event) => {
42
47
  event.stopPropagation();
43
48
  on.dismiss();
44
49
  };
50
+ const contentId = $props.id();
51
+ let activatorEl = $state.raw(undefined);
45
52
  const handleBodyClick = (event) => {
46
53
  if (!toast.onClick) {
47
54
  return;
48
55
  }
49
56
  const target = event.target;
50
- if (target?.closest('button, a, input, textarea, select')) {
57
+ const interactive = target?.closest(INTERACTIVE_CONTENT);
58
+ if (interactive && interactive !== activatorEl) {
51
59
  return;
52
60
  }
61
+ event.stopPropagation();
53
62
  toast.onClick();
54
63
  };
55
64
  </script>
@@ -60,8 +69,7 @@ const handleBodyClick = (event) => {
60
69
  class="toast toast--{toast.variant}"
61
70
  class:toast--dragging={dragging}
62
71
  class:toast--clickable={!!toast.onClick}
63
- role="status"
64
- aria-live="polite"
72
+ role="group"
65
73
  style:transform="translateX({dragX}px)"
66
74
  style:opacity={dragging && Math.abs(dragX) > 0 ? Math.max(0.3, 1 - Math.abs(dragX) / 200) : 1}
67
75
  onclick={handleBodyClick}
@@ -70,7 +78,10 @@ const handleBodyClick = (event) => {
70
78
  onpointerup={handlePointerUp}
71
79
  onpointercancel={handlePointerUp}>
72
80
  <div class="toast__indicator" aria-hidden="true"></div>
73
- <div class="toast__body">
81
+ {#if toast.onClick}
82
+ <button bind:this={activatorEl} type="button" class="toast__activator" aria-labelledby={contentId}></button>
83
+ {/if}
84
+ <div id={contentId} class="toast__body">
74
85
  <div class="toast__message">
75
86
  {#if typeof toast.message === 'string'}
76
87
  {toast.message}
@@ -101,6 +112,7 @@ const handleBodyClick = (event) => {
101
112
  --_toast--shadow: var(--sc-kit--toast--shadow, var(--sc-kit--shadow--lg));
102
113
  --_toast--indicator-color: var(--sc-kit--toast--indicator-color, var(--sc-kit--color--accent));
103
114
  pointer-events: auto;
115
+ position: relative;
104
116
  display: flex;
105
117
  align-items: flex-start;
106
118
  gap: var(--_toast--gap);
@@ -142,6 +154,19 @@ const handleBodyClick = (event) => {
142
154
  background: var(--_toast--indicator-color);
143
155
  border-radius: 0.125rem;
144
156
  }
157
+ .toast__activator {
158
+ position: absolute;
159
+ inset: 0;
160
+ padding: 0;
161
+ background: transparent;
162
+ border: none;
163
+ border-radius: inherit;
164
+ pointer-events: none;
165
+ }
166
+ .toast__activator:focus-visible {
167
+ outline: 2px solid var(--sc-kit--color--border--focus);
168
+ outline-offset: 2px;
169
+ }
145
170
  .toast__body {
146
171
  flex: 1;
147
172
  min-width: 0;
@@ -12,7 +12,7 @@ export type ToastrOptions = {
12
12
  closable?: boolean;
13
13
  /** Swipe-to-dismiss enabled. @default true */
14
14
  swipeable?: boolean;
15
- /** Fires when the toast body is clicked (excludes close button and other interactive children). Does NOT auto-dismiss — call `Toastr.dismiss(id)` from the callback if needed. */
15
+ /** Fires when the toast body is clicked or its keyboard-focusable activator is pressed (excludes close button and other interactive children). Does NOT auto-dismiss — call `Toastr.dismiss(id)` from the callback if needed. */
16
16
  onClick?: () => void;
17
17
  };
18
18
  export type Toast = {
@@ -10,7 +10,7 @@ let visible = $state(false);
10
10
  let resolvedPlacement = $state(placement);
11
11
  let showTimer;
12
12
  let stopAutoUpdate;
13
- const id = `tt-${Math.random().toString(36).slice(2, 9)}`;
13
+ const descriptionId = $props.id();
14
14
  const position = async () => {
15
15
  if (!triggerEl || !bubbleEl) {
16
16
  return;
@@ -66,17 +66,17 @@ $effect(() => () => {
66
66
  bind:this={triggerEl}
67
67
  class="tooltip"
68
68
  class:tooltip--fill-height={fillHeight}
69
- aria-describedby={visible ? id : undefined}
70
69
  onmouseenter={show}
71
70
  onmouseleave={hide}
72
71
  onfocusin={show}
73
72
  onfocusout={hide}
74
73
  role="presentation">
75
- {@render children()}
74
+ {@render children({ 'aria-describedby': descriptionId })}
76
75
  </span>
76
+ <span id={descriptionId} class="tooltip__description" aria-hidden="true">{text}</span>
77
77
 
78
78
  {#if visible}
79
- <span bind:this={bubbleEl} id={id} class="tooltip__bubble" role="tooltip" data-placement={resolvedPlacement}>
79
+ <span bind:this={bubbleEl} class="tooltip__bubble" role="tooltip" aria-hidden="true" data-placement={resolvedPlacement}>
80
80
  {text}
81
81
  <span bind:this={arrowEl} class="tooltip__arrow" aria-hidden="true"></span>
82
82
  </span>
@@ -111,6 +111,17 @@ or plain text. Bubble flips and shifts on viewport collision; an arrow points to
111
111
  .tooltip--fill-height {
112
112
  block-size: 100%;
113
113
  }
114
+ .tooltip__description {
115
+ position: absolute;
116
+ width: 1px;
117
+ height: 1px;
118
+ margin: -1px;
119
+ padding: 0;
120
+ overflow: hidden;
121
+ clip-path: inset(50%);
122
+ white-space: nowrap;
123
+ border: 0;
124
+ }
114
125
  .tooltip__bubble {
115
126
  --_tt--background: var(--sc-kit--tooltip--background, var(--sc-kit--color--bg--tooltip));
116
127
  --_tt--color: var(--sc-kit--tooltip--color, var(--sc-kit--color--text--tooltip));
@@ -9,8 +9,13 @@ type Props = {
9
9
  delay?: number;
10
10
  /** Stretch the trigger to the parent's full height (content stays vertically centered). @default false */
11
11
  fillHeight?: boolean;
12
- /** Wrapped element — anything passed becomes the tooltip trigger (icon, button, link, plain text). */
13
- children: Snippet;
12
+ /**
13
+ * Wrapped element — anything passed becomes the tooltip trigger (icon, button, link, plain text). Receives
14
+ * `aria-describedby` pointing at the tooltip text: spread it onto an interactive trigger so screen readers announce the text.
15
+ */
16
+ children: Snippet<[{
17
+ 'aria-describedby': string;
18
+ }]>;
14
19
  };
15
20
  /**
16
21
  * Tooltip — hover/focus help text positioned with Floating UI. Wraps a single child element