@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.
- package/dist/core/utils/html-helper.d.ts +7 -1
- package/dist/core/utils/html-helper.js +83 -10
- package/dist/styles/_mixins.scss +12 -0
- package/dist/ui/color-picker/cmp.color-picker.svelte +2 -2
- package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +2 -0
- package/dist/ui/dialog/cmp.dialog-container.svelte +9 -0
- package/dist/ui/dialog/cmp.dialog.svelte +1 -1
- package/dist/ui/dialog/cmp.dialog.svelte.d.ts +1 -1
- package/dist/ui/dialog/dialog-controller.d.ts +2 -0
- package/dist/ui/dialog/dialog-controller.js +3 -0
- package/dist/ui/dialog/layout/cmp.frameless-dialog.svelte +1 -1
- package/dist/ui/dialog/layout/cmp.stepper-dialog.svelte +1 -1
- package/dist/ui/file-uploader/cmp.file-uploader.svelte +4 -1
- package/dist/ui/file-uploader/file-uploader-localization.d.ts +1 -0
- package/dist/ui/file-uploader/file-uploader-localization.js +7 -0
- package/dist/ui/form-field/cmp.form-field-validatable.svelte +22 -37
- package/dist/ui/form-field/cmp.form-field-validatable.svelte.d.ts +3 -25
- package/dist/ui/form-field/cmp.form-field.svelte +23 -12
- package/dist/ui/form-field/cmp.form-field.svelte.d.ts +7 -7
- package/dist/ui/icon/cmp.icon.svelte +6 -5
- package/dist/ui/icon/cmp.icon.svelte.d.ts +1 -1
- package/dist/ui/navigation/cmp.nav-rail-item.svelte +17 -0
- package/dist/ui/navigation/cmp.nav-rail-item.svelte.d.ts +1 -1
- package/dist/ui/navigation/cmp.nav-rail.svelte +4 -4
- package/dist/ui/navigation/cmp.nav-rail.svelte.d.ts +2 -0
- package/dist/ui/navigation/nav-rail-item-localization.d.ts +3 -0
- package/dist/ui/navigation/nav-rail-item-localization.js +12 -0
- package/dist/ui/pin-input/cmp.pin-input.svelte +5 -2
- package/dist/ui/pin-input/cmp.pin-input.svelte.d.ts +1 -0
- package/dist/ui/pin-input/pin-input-localization.d.ts +3 -0
- package/dist/ui/pin-input/pin-input-localization.js +12 -0
- package/dist/ui/segmented-control/cmp.segmented-control.svelte +52 -12
- package/dist/ui/segmented-control/cmp.segmented-control.svelte.d.ts +12 -10
- package/dist/ui/table/cmp.table-draggable.svelte.d.ts +6 -0
- package/dist/ui/table/cmp.table.svelte.d.ts +4 -0
- package/dist/ui/table/table-cells/table-actions-cell.svelte +3 -1
- package/dist/ui/table/table-cells/table-by-cell.svelte +1 -1
- package/dist/ui/table/table-core.svelte +51 -7
- package/dist/ui/table/table-core.svelte.d.ts +4 -0
- package/dist/ui/table/table-localization.d.ts +1 -0
- package/dist/ui/table/table-localization.js +7 -0
- package/dist/ui/toast/cmp.toast-item.svelte +29 -4
- package/dist/ui/toast/types.d.ts +1 -1
- package/dist/ui/tooltip/cmp.tooltip.svelte +15 -4
- package/dist/ui/tooltip/cmp.tooltip.svelte.d.ts +7 -2
- package/dist/ui/validatable/cmp.validatable.svelte +6 -42
- package/dist/ui/validatable/cmp.validatable.svelte.d.ts +9 -43
- package/dist/ui/validatable/cmp.validation-error.svelte +3 -2
- package/dist/ui/validatable/cmp.validation-error.svelte.d.ts +4 -1
- package/dist/ui/validatable/field-frame.svelte +2 -2
- package/dist/ui/validatable/field-frame.svelte.d.ts +2 -0
- 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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<script lang="ts">
|
|
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) !== ''}
|
|
@@ -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
|
|
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
|
|
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 (
|
|
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 —
|
|
48
|
-
| `--sc-kit--segmented-control--color` | Default segment text color | `--sc-kit--color--text--
|
|
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
|
-
/**
|
|
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
|
|
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 (
|
|
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 —
|
|
60
|
-
* | `--sc-kit--segmented-control--color` | Default segment text color | `--sc-kit--color--text--
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
179
|
-
|
|
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;
|
|
@@ -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
|
-
|
|
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="
|
|
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
|
-
|
|
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;
|
package/dist/ui/toast/types.d.ts
CHANGED
|
@@ -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
|
|
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}
|
|
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
|
-
/**
|
|
13
|
-
|
|
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
|