@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.
- package/README.md +2 -1
- package/dist/core/validation/form-validation-handler/form-validation-handler.svelte.d.ts +4 -2
- package/dist/core/validation/form-validation-handler/form-validation-handler.svelte.js +1 -1
- package/dist/ui/_internal/form-field-label-context/form-field-label-context.svelte.d.ts +15 -0
- package/dist/ui/_internal/form-field-label-context/form-field-label-context.svelte.js +20 -0
- package/dist/ui/_internal/form-field-label-context/index.d.ts +1 -0
- package/dist/ui/_internal/form-field-label-context/index.js +1 -0
- package/dist/ui/checkbox/cmp.checkbox.svelte +11 -2
- package/dist/ui/chip-group/cmp.chip-group.svelte +10 -3
- package/dist/ui/chip-group/cmp.chip-group.svelte.d.ts +4 -2
- package/dist/ui/color-picker/cmp.color-picker.svelte +12 -2
- package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +1 -3
- package/dist/ui/date-picker/cmp.date-picker.svelte +6 -4
- package/dist/ui/date-picker/cmp.date-picker.svelte.d.ts +2 -9
- package/dist/ui/date-picker/cmp.date-time-picker.svelte +19 -3
- package/dist/ui/date-picker/cmp.date-time-picker.svelte.d.ts +3 -5
- package/dist/ui/date-picker/cmp.time-picker.svelte +3 -1
- package/dist/ui/date-picker/cmp.time-picker.svelte.d.ts +1 -4
- package/dist/ui/document-tabs/types.d.ts +0 -1
- package/dist/ui/form-field/cmp.form-field-validatable.svelte +4 -4
- package/dist/ui/form-field/cmp.form-field-validatable.svelte.d.ts +4 -7
- package/dist/ui/form-field/cmp.form-field.svelte +31 -79
- package/dist/ui/form-field/cmp.form-field.svelte.d.ts +11 -10
- package/dist/ui/form-field/form-field-label.svelte.d.ts +6 -0
- package/dist/ui/form-field/form-field-label.svelte.js +19 -0
- package/dist/ui/form-field/index.d.ts +1 -1
- package/dist/ui/icon-picker/cmp.icon-picker.svelte +3 -1
- package/dist/ui/icon-picker/cmp.icon-picker.svelte.d.ts +3 -1
- package/dist/ui/input/cmp.input.svelte +5 -1
- package/dist/ui/input/types.d.ts +0 -3
- package/dist/ui/numeral-input/cmp.numeral-input.svelte +6 -2
- package/dist/ui/numeral-input/cmp.numeral-input.svelte.d.ts +0 -4
- package/dist/ui/option-pill/cmp.option-pill.svelte.d.ts +0 -2
- package/dist/ui/page-layout/cmp.page-layout.svelte.d.ts +1 -2
- package/dist/ui/pin-input/cmp.pin-input.svelte +11 -3
- package/dist/ui/pin-input/cmp.pin-input.svelte.d.ts +1 -1
- package/dist/ui/popover/cmp.popover.svelte +2 -1
- package/dist/ui/popover/cmp.popover.svelte.d.ts +2 -6
- package/dist/ui/radio/cmp.radio.svelte +12 -3
- package/dist/ui/rich-text-input/cmp.rich-text-input.svelte +19 -7
- package/dist/ui/rich-text-input/cmp.rich-text-input.svelte.d.ts +1 -2
- package/dist/ui/segmented-control/cmp.segmented-control.svelte +5 -3
- package/dist/ui/segmented-control/cmp.segmented-control.svelte.d.ts +1 -2
- package/dist/ui/select/cmp.input-suggest.svelte +5 -1
- package/dist/ui/select/cmp.input-suggest.svelte.d.ts +0 -3
- package/dist/ui/select/cmp.multiselect-tree.svelte +2 -1
- package/dist/ui/select/cmp.multiselect-tree.svelte.d.ts +1 -3
- package/dist/ui/select/cmp.singleselect-async.svelte +5 -2
- package/dist/ui/select/cmp.singleselect-async.svelte.d.ts +0 -2
- package/dist/ui/select/cmp.singleselect.svelte +3 -1
- package/dist/ui/select/cmp.singleselect.svelte.d.ts +0 -2
- package/dist/ui/select/multiselect-base.svelte +6 -2
- package/dist/ui/select/select-core.svelte.js +0 -2
- package/dist/ui/select/select-listbox.svelte +1 -5
- package/dist/ui/select/types.d.ts +2 -6
- package/dist/ui/slider/cmp.slider.svelte +6 -2
- package/dist/ui/slider/cmp.slider.svelte.d.ts +1 -0
- package/dist/ui/tabs/types.d.ts +0 -1
- package/dist/ui/textarea/cmp.textarea.svelte +5 -1
- package/dist/ui/textarea/cmp.textarea.svelte.d.ts +0 -2
- package/dist/ui/toggle/cmp.toggle.svelte +4 -2
- package/dist/ui/validatable/cmp.validatable.svelte +1 -2
- package/dist/ui/validatable/cmp.validatable.svelte.d.ts +1 -2
- package/dist/ui/video/cmp.video.svelte.d.ts +0 -1
- package/dist/ui/visual-select/cmp.visual-select.svelte +11 -3
- package/dist/ui/visual-select/cmp.visual-select.svelte.d.ts +2 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@ without it agent sessions run with no hooks and no shared skills. Re-run after c
|
|
|
15
15
|
## Checks
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm run check
|
|
18
|
+
npm run check:fix # after edits: lint:fix, then check
|
|
19
|
+
npm run check # the gate, read-only: strict svelte-check + tsc for node files + eslint + prettier --check
|
|
19
20
|
npm run lint:fix # eslint --fix + prettier --write
|
|
20
21
|
```
|
|
21
22
|
|
|
@@ -25,8 +25,10 @@ export declare class FormValidationHandler<T extends TypeToValidate<T> = TypeToV
|
|
|
25
25
|
onReset?: () => void;
|
|
26
26
|
});
|
|
27
27
|
get form(): T;
|
|
28
|
-
get errors(): {
|
|
29
|
-
|
|
28
|
+
get errors(): Readonly<{
|
|
29
|
+
[P in keyof Partial<T>]: string;
|
|
30
|
+
}>;
|
|
31
|
+
get touched(): Readonly<Record<keyof T, boolean>>;
|
|
30
32
|
get isValidating(): boolean;
|
|
31
33
|
get isSubmitting(): boolean;
|
|
32
34
|
updateField: <K extends keyof T>(field: K, value: T[K]) => void;
|
|
@@ -167,7 +167,7 @@ export class FormValidationHandler {
|
|
|
167
167
|
this.updateTouched(field, true);
|
|
168
168
|
this._isValidating = true;
|
|
169
169
|
const error = await this._validator.validateAt(this._form, field);
|
|
170
|
-
ValidationUtils.update(this.
|
|
170
|
+
ValidationUtils.update(this._errors, field, error || '');
|
|
171
171
|
this._isValidating = false;
|
|
172
172
|
};
|
|
173
173
|
stringify = (value) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type FormFieldLabelContext = {
|
|
2
|
+
readonly labelId: string | undefined;
|
|
3
|
+
/** The first mounted claimant gets the label; the returned release hands it to the next one still mounted. */
|
|
4
|
+
claimControl: (id: () => string) => () => void;
|
|
5
|
+
};
|
|
6
|
+
/** Offers a visible label to every kit control rendered inside the calling component. */
|
|
7
|
+
export declare const provideFormFieldLabel: (formFieldLabel: FormFieldLabelContext) => void;
|
|
8
|
+
/** The label of the surrounding FormField, or `undefined` outside one. */
|
|
9
|
+
export declare const getFormFieldLabel: () => FormFieldLabelContext | undefined;
|
|
10
|
+
/** For a text input: the surrounding FormField label points its `for` at this id while the input is mounted. */
|
|
11
|
+
export declare const claimFormFieldLabel: (controlId: () => string) => void;
|
|
12
|
+
/** The `aria-labelledby` of a control that is not a text input: the explicit one, else the FormField label's id — unless an explicit `aria-label` names it. */
|
|
13
|
+
export declare const resolveAriaLabelledby: (formFieldLabel: FormFieldLabelContext | undefined, ariaLabel: string | undefined, ariaLabelledby: string | undefined) => string | undefined;
|
|
14
|
+
/** Below the calling component no control is named by the surrounding FormField label — for a field that renders other fields in its own panel. */
|
|
15
|
+
export declare const stopFormFieldLabelHere: () => void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
// Svelte context despite sc-frontend-svelte's "no context API": the least invasive way for FormField to reach a kit control at any depth (see the ADR).
|
|
3
|
+
const KEY = Symbol('form-field-label');
|
|
4
|
+
/** Offers a visible label to every kit control rendered inside the calling component. */
|
|
5
|
+
export const provideFormFieldLabel = (formFieldLabel) => {
|
|
6
|
+
setContext(KEY, formFieldLabel);
|
|
7
|
+
};
|
|
8
|
+
/** The label of the surrounding FormField, or `undefined` outside one. */
|
|
9
|
+
export const getFormFieldLabel = () => getContext(KEY);
|
|
10
|
+
/** For a text input: the surrounding FormField label points its `for` at this id while the input is mounted. */
|
|
11
|
+
export const claimFormFieldLabel = (controlId) => {
|
|
12
|
+
const formFieldLabel = getFormFieldLabel();
|
|
13
|
+
$effect(() => formFieldLabel?.claimControl(controlId));
|
|
14
|
+
};
|
|
15
|
+
/** The `aria-labelledby` of a control that is not a text input: the explicit one, else the FormField label's id — unless an explicit `aria-label` names it. */
|
|
16
|
+
export const resolveAriaLabelledby = (formFieldLabel, ariaLabel, ariaLabelledby) => ariaLabelledby ?? (ariaLabel ? undefined : formFieldLabel?.labelId);
|
|
17
|
+
/** Below the calling component no control is named by the surrounding FormField label — for a field that renders other fields in its own panel. */
|
|
18
|
+
export const stopFormFieldLabelHere = () => {
|
|
19
|
+
setContext(KEY, undefined);
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { claimFormFieldLabel, getFormFieldLabel, provideFormFieldLabel, resolveAriaLabelledby, stopFormFieldLabelHere, type FormFieldLabelContext } from './form-field-label-context.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { claimFormFieldLabel, getFormFieldLabel, provideFormFieldLabel, resolveAriaLabelledby, stopFormFieldLabelHere } from './form-field-label-context.svelte';
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { getFormFieldLabel } from '../_internal/form-field-label-context';
|
|
2
|
+
import { Icon } from '../icon';
|
|
2
3
|
import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_12_regular.svg?raw';
|
|
3
4
|
import IconSubtract from '@fluentui/svg-icons/icons/subtract_16_regular.svg?raw';
|
|
4
5
|
const { checked, variant = 'default', size = 'md', disabled = false, label, title, on } = $props();
|
|
5
6
|
const isOn = $derived(checked === true);
|
|
6
7
|
const isIndeterminate = $derived(checked === 'indeterminate');
|
|
7
8
|
const ariaChecked = $derived(isIndeterminate ? 'mixed' : isOn ? 'true' : 'false');
|
|
9
|
+
const formFieldLabel = getFormFieldLabel();
|
|
8
10
|
const handleClick = (event) => {
|
|
9
11
|
event.stopPropagation();
|
|
10
12
|
if (disabled) {
|
|
@@ -21,7 +23,14 @@ const handleClick = (event) => {
|
|
|
21
23
|
class:check--indeterminate={isIndeterminate}
|
|
22
24
|
class:check--disabled={disabled}
|
|
23
25
|
title={title}>
|
|
24
|
-
<button
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class="check__box"
|
|
29
|
+
role="checkbox"
|
|
30
|
+
aria-checked={ariaChecked}
|
|
31
|
+
aria-labelledby={label ? undefined : formFieldLabel?.labelId}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
onclick={handleClick}>
|
|
25
34
|
<span class="check__mark" class:check__mark--visible={isOn || isIndeterminate} aria-hidden="true">
|
|
26
35
|
<Icon src={isIndeterminate ? IconSubtract : IconCheckmark} />
|
|
27
36
|
</span>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
<script lang="ts" generics="T">import {
|
|
1
|
+
<script lang="ts" generics="T">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
|
|
2
|
+
import { Icon } from '../icon';
|
|
2
3
|
import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_12_regular.svg?raw';
|
|
3
|
-
const { chips, variant = 'soft', size = 'md', disabled = false, compare = (a, b) => a === b, ...mode } = $props();
|
|
4
|
+
const { chips, variant = 'soft', size = 'md', disabled = false, compare = (a, b) => a === b, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...mode } = $props();
|
|
5
|
+
const formFieldLabel = getFormFieldLabel();
|
|
4
6
|
const isSelected = (chip) => {
|
|
5
7
|
if (mode.type === 'multi') {
|
|
6
8
|
return mode.value.some((v) => compare(v, chip.value));
|
|
@@ -22,7 +24,12 @@ const handleClick = (chip) => {
|
|
|
22
24
|
};
|
|
23
25
|
</script>
|
|
24
26
|
|
|
25
|
-
<div
|
|
27
|
+
<div
|
|
28
|
+
class="chip-group chip-group--{size} chip-group--{variant}"
|
|
29
|
+
class:chip-group--disabled={disabled}
|
|
30
|
+
role={mode.type === 'multi' ? 'group' : 'radiogroup'}
|
|
31
|
+
aria-label={ariaLabel}
|
|
32
|
+
aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}>
|
|
26
33
|
{#each chips as chip (chip.value)}
|
|
27
34
|
{@const selected = isSelected(chip)}
|
|
28
35
|
<button
|
|
@@ -6,10 +6,11 @@ declare function $$render<T>(): {
|
|
|
6
6
|
variant?: "soft" | "solid";
|
|
7
7
|
/** @default 'md' */
|
|
8
8
|
size?: "sm" | "md";
|
|
9
|
-
/** Disable the whole group. */
|
|
10
9
|
disabled?: boolean;
|
|
11
10
|
/** Custom equality comparator for non-primitive values (objects with `id`, etc.). @default (a, b) => a === b */
|
|
12
11
|
compare?: (a: T, b: T) => boolean;
|
|
12
|
+
'aria-label'?: string;
|
|
13
|
+
'aria-labelledby'?: string;
|
|
13
14
|
} & {
|
|
14
15
|
/** Single-select mode. */
|
|
15
16
|
type: "single";
|
|
@@ -23,10 +24,11 @@ declare function $$render<T>(): {
|
|
|
23
24
|
variant?: "soft" | "solid";
|
|
24
25
|
/** @default 'md' */
|
|
25
26
|
size?: "sm" | "md";
|
|
26
|
-
/** Disable the whole group. */
|
|
27
27
|
disabled?: boolean;
|
|
28
28
|
/** Custom equality comparator for non-primitive values (objects with `id`, etc.). @default (a, b) => a === b */
|
|
29
29
|
compare?: (a: T, b: T) => boolean;
|
|
30
|
+
'aria-label'?: string;
|
|
31
|
+
'aria-labelledby'?: string;
|
|
30
32
|
} & {
|
|
31
33
|
/** Multi-select mode. */
|
|
32
34
|
type: "multi";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">import { ColorHelper } from '../../core/utils';
|
|
2
|
+
import { getFormFieldLabel, resolveAriaLabelledby, stopFormFieldLabelHere } from '../_internal/form-field-label-context';
|
|
2
3
|
import { Icon } from '../icon';
|
|
3
4
|
import { Input } from '../input';
|
|
4
5
|
import { Popover } from '../popover';
|
|
@@ -8,7 +9,9 @@ import IconColorF from '@fluentui/svg-icons/icons/color_20_regular.svg?raw';
|
|
|
8
9
|
import IconDismiss from '@fluentui/svg-icons/icons/dismiss_20_regular.svg?raw';
|
|
9
10
|
import { colord } from 'colord';
|
|
10
11
|
import AwesomeColorPicker from 'svelte-awesome-color-picker';
|
|
11
|
-
let { value, variant = 'fill', label, size = 'md', defaultColor, clearable = false, disabled = false, placeholder, on, children, previewIcon, 'aria-label': ariaLabel } = $props();
|
|
12
|
+
let { value, variant = 'fill', label, size = 'md', defaultColor, clearable = false, disabled = false, placeholder, on, children, previewIcon, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby } = $props();
|
|
13
|
+
const formFieldLabel = getFormFieldLabel();
|
|
14
|
+
stopFormFieldLabelHere();
|
|
12
15
|
const loc = new ColorPickerLocalization();
|
|
13
16
|
const displayColor = $derived(value || defaultColor || '');
|
|
14
17
|
const textColor = $derived.by(() => {
|
|
@@ -45,7 +48,14 @@ const onColorPickerInput = ({ hex }) => {
|
|
|
45
48
|
</script>
|
|
46
49
|
|
|
47
50
|
<div class="color-picker">
|
|
48
|
-
<Popover
|
|
51
|
+
<Popover
|
|
52
|
+
position="bottom-start"
|
|
53
|
+
keepOpen
|
|
54
|
+
panel={false}
|
|
55
|
+
fixedPosition
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
aria-label={ariaLabel}
|
|
58
|
+
aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}>
|
|
49
59
|
{#snippet trigger()}
|
|
50
60
|
<div class="color-picker__trigger">
|
|
51
61
|
{#if children}
|
|
@@ -9,9 +9,7 @@ type Props = {
|
|
|
9
9
|
size?: 'sm' | 'md' | 'lg';
|
|
10
10
|
/** Default color shown in the picker UI when value is empty (does not set the value itself). */
|
|
11
11
|
defaultColor?: string;
|
|
12
|
-
/** Show a × clear button on the trigger to wipe the value. */
|
|
13
12
|
clearable?: boolean;
|
|
14
|
-
/** Visual disabled state — popover won't open. */
|
|
15
13
|
disabled?: boolean;
|
|
16
14
|
/** Placeholder shown on the swatch trigger when no value is selected. @default localized "Not set" */
|
|
17
15
|
placeholder?: string;
|
|
@@ -25,8 +23,8 @@ type Props = {
|
|
|
25
23
|
* droplet glyph. Same shape as Input's `icon` prop (Rule 9).
|
|
26
24
|
*/
|
|
27
25
|
previewIcon?: string | Snippet;
|
|
28
|
-
/** Accessible name of the trigger — otherwise it is announced by its color value only. */
|
|
29
26
|
'aria-label'?: string;
|
|
27
|
+
'aria-labelledby'?: string;
|
|
30
28
|
};
|
|
31
29
|
/**
|
|
32
30
|
* A color picker with an Input-shaped swatch trigger that opens a popover hosting a full hex/rgb picker
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
|
|
2
|
+
import { InputClearButton } from '../_internal/input-clear-button';
|
|
2
3
|
import { Icon, IconSlot } from '../icon';
|
|
3
4
|
import DatePickerCalendar from './cmp.date-picker-calendar.svelte';
|
|
4
5
|
import { DatePickerLocalization } from './date-picker-localization';
|
|
5
6
|
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
|
6
7
|
import IconCalendar from '@fluentui/svg-icons/icons/calendar_16_regular.svg?raw';
|
|
7
|
-
const { size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, min, max, locale, name, id = null, autofocus = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, icon, ...mode } = $props();
|
|
8
|
+
const { size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, min, max, locale, name, id = null, autofocus = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, icon, ...mode } = $props();
|
|
9
|
+
const formFieldLabel = getFormFieldLabel();
|
|
8
10
|
const localization = new DatePickerLocalization();
|
|
9
11
|
/** Resolved locale: explicit prop or `'en-US'` default. */
|
|
10
12
|
const resolvedLocale = $derived(locale ?? 'en-US');
|
|
@@ -134,6 +136,7 @@ $effect(() => {
|
|
|
134
136
|
aria-haspopup="dialog"
|
|
135
137
|
aria-expanded={open}
|
|
136
138
|
aria-label={ariaLabel ?? name}
|
|
139
|
+
aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}
|
|
137
140
|
aria-describedby={ariaDescribedby}
|
|
138
141
|
onclick={readonly ? undefined : toggleOpen}>
|
|
139
142
|
{#if icon}
|
|
@@ -156,8 +159,7 @@ $effect(() => {
|
|
|
156
159
|
{/if}
|
|
157
160
|
|
|
158
161
|
{#if open}
|
|
159
|
-
|
|
160
|
-
<div bind:this={panelEl} class="date-picker__panel" role="presentation" onclick={(e) => e.preventDefault()} onkeydown={() => undefined}>
|
|
162
|
+
<div bind:this={panelEl} class="date-picker__panel">
|
|
161
163
|
<DatePickerCalendar
|
|
162
164
|
selectedDate={selectedDate}
|
|
163
165
|
minDate={minDate}
|
|
@@ -5,9 +5,7 @@ type BaseProps = {
|
|
|
5
5
|
size?: 'sm' | 'md' | 'lg';
|
|
6
6
|
placeholder?: string;
|
|
7
7
|
disabled?: boolean;
|
|
8
|
-
/** Non-clickable but still visible. Trigger button is disabled (like `disabled`) but cursor stays default. */
|
|
9
8
|
readonly?: boolean;
|
|
10
|
-
/** Visual display mode — non-focusable, no hover/focus animations, hidden clear button. */
|
|
11
9
|
inert?: boolean;
|
|
12
10
|
error?: boolean;
|
|
13
11
|
/** Strip border + background + focus underline. */
|
|
@@ -16,17 +14,14 @@ type BaseProps = {
|
|
|
16
14
|
min?: LikeDate;
|
|
17
15
|
/** Latest selectable date (inclusive). */
|
|
18
16
|
max?: LikeDate;
|
|
19
|
-
/**
|
|
20
|
-
* BCP-47 locale tag. Drives month/weekday labels, trigger date formatting AND first day of week.
|
|
21
|
-
* When omitted, uses an opinionated default: `en-US` formatting + Monday-first calendar.
|
|
22
|
-
* When passed (even `'en-US'`), first day of week is derived from the locale via `Intl.Locale.getWeekInfo()`.
|
|
23
|
-
*/
|
|
17
|
+
/** Omitted: `en-US` formatting with Monday first. Passed (even `'en-US'`): first day of week comes from the locale. */
|
|
24
18
|
locale?: string;
|
|
25
19
|
/** Native form name. When set, a hidden input with the ISO value is rendered for form submission. */
|
|
26
20
|
name?: string;
|
|
27
21
|
id?: string | null;
|
|
28
22
|
autofocus?: boolean;
|
|
29
23
|
'aria-label'?: string;
|
|
24
|
+
'aria-labelledby'?: string;
|
|
30
25
|
'aria-describedby'?: string;
|
|
31
26
|
'aria-required'?: boolean;
|
|
32
27
|
/**
|
|
@@ -36,7 +31,6 @@ type BaseProps = {
|
|
|
36
31
|
icon?: IconProp;
|
|
37
32
|
};
|
|
38
33
|
type ClearableProps = BaseProps & {
|
|
39
|
-
/** Value can be cleared to `null`. Trigger shows a clear (X) button when set. */
|
|
40
34
|
clearable: true;
|
|
41
35
|
value: LikeDate | null | undefined;
|
|
42
36
|
on?: {
|
|
@@ -44,7 +38,6 @@ type ClearableProps = BaseProps & {
|
|
|
44
38
|
};
|
|
45
39
|
};
|
|
46
40
|
type RequiredProps = BaseProps & {
|
|
47
|
-
/** Value is required. No clear button. */
|
|
48
41
|
clearable: false;
|
|
49
42
|
value: LikeDate;
|
|
50
43
|
on?: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">import DatePicker from './cmp.date-picker.svelte';
|
|
2
2
|
import TimePicker from './cmp.time-picker.svelte';
|
|
3
|
-
const { value, size = 'md', datePlaceholder = '', min, step = 30, on } = $props();
|
|
3
|
+
const { value, size = 'md', datePlaceholder = '', min, step = 30, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, on } = $props();
|
|
4
4
|
const DEFAULT_TIME = '12:00';
|
|
5
5
|
const MINUTES_PER_DAY = 24 * 60;
|
|
6
6
|
const AFTER_LAST_SLOT = '24:00';
|
|
@@ -72,9 +72,25 @@ const onTimeChange = (next) => {
|
|
|
72
72
|
</script>
|
|
73
73
|
|
|
74
74
|
<div class="date-time date-time--{size}" class:date-time--date-only={!currentDate}>
|
|
75
|
-
<DatePicker
|
|
75
|
+
<DatePicker
|
|
76
|
+
clearable
|
|
77
|
+
size={size}
|
|
78
|
+
value={value}
|
|
79
|
+
placeholder={datePlaceholder}
|
|
80
|
+
min={firstAllowedDate ?? undefined}
|
|
81
|
+
aria-label={ariaLabel}
|
|
82
|
+
aria-labelledby={ariaLabelledby}
|
|
83
|
+
on={{ change: onDateChange }} />
|
|
76
84
|
{#if currentDate}
|
|
77
|
-
<TimePicker
|
|
85
|
+
<TimePicker
|
|
86
|
+
clearable={false}
|
|
87
|
+
size={size}
|
|
88
|
+
step={step}
|
|
89
|
+
min={timeMin}
|
|
90
|
+
value={timeValue}
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
aria-labelledby={ariaLabelledby}
|
|
93
|
+
on={{ change: onTimeChange }} />
|
|
78
94
|
{/if}
|
|
79
95
|
</div>
|
|
80
96
|
|
|
@@ -4,14 +4,12 @@ type Props = {
|
|
|
4
4
|
/** @default 'md' */
|
|
5
5
|
size?: 'sm' | 'md' | 'lg';
|
|
6
6
|
datePlaceholder?: string;
|
|
7
|
-
/**
|
|
8
|
-
* Earliest selectable date-time (inclusive). On its day only the `step` slots at or after it are offered and an
|
|
9
|
-
* earlier time is raised to the first of them; a day with no slot left is not selectable. A `value` before the first
|
|
10
|
-
* allowed day still renders, but its time list is empty — only picking an allowed date changes it.
|
|
11
|
-
*/
|
|
7
|
+
/** Earliest selectable date-time, inclusive: on its day only the slots from it on are offered, and a day with no slot left is disabled. */
|
|
12
8
|
min?: LikeDate;
|
|
13
9
|
/** Minute interval for the time list. @default 30 */
|
|
14
10
|
step?: number;
|
|
11
|
+
'aria-label'?: string;
|
|
12
|
+
'aria-labelledby'?: string;
|
|
15
13
|
on?: {
|
|
16
14
|
change?: (value: string | null) => void;
|
|
17
15
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">import { Singleselect } from '../select';
|
|
2
|
-
const { size = 'md', step = 30, min, placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, id, name, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
|
|
2
|
+
const { size = 'md', step = 30, min, placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, id, name, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
|
|
3
3
|
const MINUTES_PER_DAY = 24 * 60;
|
|
4
4
|
const pad = (n) => n.toString().padStart(2, '0');
|
|
5
5
|
const stepOptions = $derived.by(() => {
|
|
@@ -44,6 +44,7 @@ const emit = (next) => {
|
|
|
44
44
|
id={id}
|
|
45
45
|
name={name}
|
|
46
46
|
aria-label={ariaLabel}
|
|
47
|
+
aria-labelledby={ariaLabelledby}
|
|
47
48
|
aria-describedby={ariaDescribedby}
|
|
48
49
|
aria-required={ariaRequired}
|
|
49
50
|
on={{ change: emit }} />
|
|
@@ -62,6 +63,7 @@ const emit = (next) => {
|
|
|
62
63
|
id={id}
|
|
63
64
|
name={name}
|
|
64
65
|
aria-label={ariaLabel}
|
|
66
|
+
aria-labelledby={ariaLabelledby}
|
|
65
67
|
aria-describedby={ariaDescribedby}
|
|
66
68
|
aria-required={ariaRequired}
|
|
67
69
|
on={{ change: emit }} />
|
|
@@ -7,20 +7,18 @@ type BaseProps = {
|
|
|
7
7
|
min?: string;
|
|
8
8
|
placeholder?: string;
|
|
9
9
|
disabled?: boolean;
|
|
10
|
-
/** Focusable, value visible, but selection cannot change. */
|
|
11
10
|
readonly?: boolean;
|
|
12
|
-
/** Non-interactive, unstyled view-only mode. */
|
|
13
11
|
inert?: boolean;
|
|
14
12
|
error?: boolean;
|
|
15
13
|
borderless?: boolean;
|
|
16
14
|
id?: string;
|
|
17
15
|
name?: string;
|
|
18
16
|
'aria-label'?: string;
|
|
17
|
+
'aria-labelledby'?: string;
|
|
19
18
|
'aria-describedby'?: string;
|
|
20
19
|
'aria-required'?: boolean;
|
|
21
20
|
};
|
|
22
21
|
type ClearableProps = BaseProps & {
|
|
23
|
-
/** Value can be cleared to `null`; shows a clear (X) button when set. */
|
|
24
22
|
clearable: true;
|
|
25
23
|
value: string | null | undefined;
|
|
26
24
|
on?: {
|
|
@@ -28,7 +26,6 @@ type ClearableProps = BaseProps & {
|
|
|
28
26
|
};
|
|
29
27
|
};
|
|
30
28
|
type RequiredProps = BaseProps & {
|
|
31
|
-
/** Value is required. No clear button. */
|
|
32
29
|
clearable: false;
|
|
33
30
|
value: string;
|
|
34
31
|
on?: {
|
|
@@ -13,16 +13,16 @@ let { handler, name, label, hint, required = false, validateOn, debounceMs, disa
|
|
|
13
13
|
on={on}>
|
|
14
14
|
{#snippet children(binding, validating)}
|
|
15
15
|
<FormField label={label} hint={hint} required={required}>
|
|
16
|
-
|
|
16
|
+
{#snippet children(ids)}
|
|
17
|
+
<span class="form-field-validatable__control">{@render field(binding, validating, ids)}</span>
|
|
18
|
+
{/snippet}
|
|
17
19
|
</FormField>
|
|
18
20
|
{/snippet}
|
|
19
21
|
</Validatable>
|
|
20
22
|
|
|
21
23
|
<!--
|
|
22
24
|
@component
|
|
23
|
-
FormFieldValidatable — `FormField` and `Validatable` in one component: a label plus a validated field, with the error message
|
|
24
|
-
screen readers announce it as the field's description rather than as part of the field's name. Composing them by hand keeps that only with `Validatable` on
|
|
25
|
-
the outside and `FormField` inside its snippet.
|
|
25
|
+
FormFieldValidatable — `FormField` and `Validatable` in one component: a label plus a validated field, with the error message below the field.
|
|
26
26
|
-->
|
|
27
27
|
|
|
28
28
|
<style>.form-field-validatable__control {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FormValidationHandler } from '../../core/validation';
|
|
2
2
|
import { type ErrorPlacement, type FieldBinding, type ValidateOnEvent } from '../validatable';
|
|
3
|
+
import { type FormFieldIds } from './cmp.form-field.svelte';
|
|
3
4
|
import type { Snippet } from 'svelte';
|
|
4
5
|
declare function $$render<T extends Record<string, unknown>, K extends keyof T & string>(): {
|
|
5
6
|
props: {
|
|
@@ -27,8 +28,8 @@ declare function $$render<T extends Record<string, unknown>, K extends keyof T &
|
|
|
27
28
|
change?: (value: T[K]) => void;
|
|
28
29
|
blur?: () => void;
|
|
29
30
|
};
|
|
30
|
-
/** Receives the wired `FieldBinding<T[K]>` — spread or wire onto the kit input —
|
|
31
|
-
children: Snippet<[FieldBinding<T[K]>, boolean]>;
|
|
31
|
+
/** Receives the wired `FieldBinding<T[K]>` — spread or wire onto the kit input — the `validating` flag, and `FormField`'s ids. */
|
|
32
|
+
children: Snippet<[FieldBinding<T[K]>, boolean, FormFieldIds]>;
|
|
32
33
|
};
|
|
33
34
|
exports: {};
|
|
34
35
|
bindings: "";
|
|
@@ -49,11 +50,7 @@ interface $$IsomorphicComponent {
|
|
|
49
50
|
<T extends Record<string, unknown>, K extends keyof T & string>(internal: unknown, props: ReturnType<__sveltets_Render<T, K>['props']> & {}): ReturnType<__sveltets_Render<T, K>['exports']>;
|
|
50
51
|
z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
|
|
51
52
|
}
|
|
52
|
-
/**
|
|
53
|
-
* FormFieldValidatable — `FormField` and `Validatable` in one component: a label plus a validated field, with the error message outside the `<label>`, so
|
|
54
|
-
* screen readers announce it as the field's description rather than as part of the field's name. Composing them by hand keeps that only with `Validatable` on
|
|
55
|
-
* the outside and `FormField` inside its snippet.
|
|
56
|
-
*/
|
|
53
|
+
/** FormFieldValidatable — `FormField` and `Validatable` in one component: a label plus a validated field, with the error message below the field. */
|
|
57
54
|
declare const Cmp: $$IsomorphicComponent;
|
|
58
55
|
type Cmp<T extends Record<string, unknown>, K extends keyof T & string> = InstanceType<typeof Cmp<T, K>>;
|
|
59
56
|
export default Cmp;
|
|
@@ -1,83 +1,30 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
// HTML's interactive-content list, verbatim: exactly what makes the browser skip label activation. A wider selector (role, tabindex) skips the guard on clicks the browser still forwards.
|
|
3
|
-
const INTERACTIVE_CONTENT = 'a[href], audio[controls], button, details, embed, iframe, img[usemap], input:not([type="hidden"]), label, select, textarea, video[controls]';
|
|
1
|
+
<script lang="ts" module>export {};
|
|
4
2
|
</script>
|
|
5
3
|
|
|
6
|
-
<script lang="ts">import {
|
|
4
|
+
<script lang="ts">import { provideFormFieldLabel } from '../_internal/form-field-label-context';
|
|
5
|
+
import { Icon } from '../icon';
|
|
7
6
|
import { Tooltip } from '../tooltip';
|
|
7
|
+
import { createFormFieldLabel } from './form-field-label.svelte';
|
|
8
8
|
import IconInfo from '@fluentui/svg-icons/icons/info_20_regular.svg?raw';
|
|
9
9
|
let { label, hint, required = false, children } = $props();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const swallowClick = (event) => {
|
|
14
|
-
event.stopPropagation();
|
|
15
|
-
// This click never reaches guardLabelActivation, so the flag would outlive the gesture and swallow a later keyboard-activated click.
|
|
16
|
-
swallowLabelActivation = false;
|
|
17
|
-
if (forwardsLabelActivation(event.target)) {
|
|
18
|
-
event.preventDefault();
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
const forwardsLabelActivation = (target) => {
|
|
22
|
-
if (!(target instanceof Element)) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
const interactive = target.closest(INTERACTIVE_CONTENT);
|
|
26
|
-
return !interactive || interactive === rootEl;
|
|
27
|
-
};
|
|
28
|
-
// Registered at mount, so it precedes the capture-phase dismissal a Popover registers on open: by click time the popup has closed and the forwarded activation would reopen it.
|
|
29
|
-
$effect(() => {
|
|
30
|
-
const armLabelActivation = (event) => {
|
|
31
|
-
swallowLabelActivation = false;
|
|
32
|
-
if (event.button !== 0 || !rootEl || !(event.target instanceof Node) || !rootEl.contains(event.target)) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
swallowLabelActivation = !!rootEl.querySelector('[aria-haspopup][aria-expanded="true"]');
|
|
36
|
-
};
|
|
37
|
-
// A keyboard-activated click brings no pointerdown of its own and would otherwise inherit the flag from a gesture that produced no click. Only the two keys that synthesize one: any key would also disarm a pointer gesture that is still in flight.
|
|
38
|
-
const disarmLabelActivation = (event) => {
|
|
39
|
-
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
swallowLabelActivation = false;
|
|
43
|
-
};
|
|
44
|
-
window.addEventListener('pointerdown', armLabelActivation, true);
|
|
45
|
-
window.addEventListener('keydown', disarmLabelActivation, true);
|
|
46
|
-
return () => {
|
|
47
|
-
window.removeEventListener('pointerdown', armLabelActivation, true);
|
|
48
|
-
window.removeEventListener('keydown', disarmLabelActivation, true);
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
const guardLabelActivation = (event) => {
|
|
52
|
-
const swallow = swallowLabelActivation;
|
|
53
|
-
swallowLabelActivation = false;
|
|
54
|
-
if (!forwardsLabelActivation(event.target)) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
// The label's control is the group's first radio: forwarding the click would select it instead of keeping the current choice.
|
|
58
|
-
const radio = rootEl?.control?.getAttribute('role') === 'radio' ? rootEl.control : null;
|
|
59
|
-
if (swallow || radio) {
|
|
60
|
-
event.preventDefault();
|
|
61
|
-
}
|
|
62
|
-
radio?.closest('[role="radiogroup"]')?.querySelector('[role="radio"][tabindex="0"]')?.focus();
|
|
63
|
-
};
|
|
10
|
+
const id = $props.id();
|
|
11
|
+
const formFieldLabel = createFormFieldLabel(id, () => !!label);
|
|
12
|
+
provideFormFieldLabel(formFieldLabel);
|
|
64
13
|
</script>
|
|
65
14
|
|
|
66
|
-
|
|
67
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
68
|
-
<label bind:this={rootEl} class="form-field" onclick={guardLabelActivation}>
|
|
15
|
+
<div class="form-field">
|
|
69
16
|
{#if label}
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
{label}
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
17
|
+
<div class="form-field__header">
|
|
18
|
+
<label class="form-field__label" for={formFieldLabel.controlId} id={formFieldLabel.labelId}>
|
|
19
|
+
{#if typeof label === 'string'}
|
|
20
|
+
{label}
|
|
21
|
+
{:else}
|
|
22
|
+
{@render label()}
|
|
23
|
+
{/if}
|
|
24
|
+
{#if required}<span class="form-field__required" aria-hidden="true">*</span>{/if}
|
|
25
|
+
</label>
|
|
77
26
|
{#if hint}
|
|
78
|
-
|
|
79
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
80
|
-
<span class="form-field__hint" onclick={swallowClick}>
|
|
27
|
+
<span class="form-field__hint">
|
|
81
28
|
{#if typeof hint === 'string'}
|
|
82
29
|
<Tooltip text={hint}>
|
|
83
30
|
<span class="form-field__hint-trigger" role="button" tabindex="0" aria-label={hint}>
|
|
@@ -89,21 +36,21 @@ const guardLabelActivation = (event) => {
|
|
|
89
36
|
{/if}
|
|
90
37
|
</span>
|
|
91
38
|
{/if}
|
|
92
|
-
</
|
|
39
|
+
</div>
|
|
93
40
|
{/if}
|
|
94
41
|
|
|
95
|
-
{@render children()}
|
|
96
|
-
</
|
|
42
|
+
{@render children({ labelId: formFieldLabel.labelId })}
|
|
43
|
+
</div>
|
|
97
44
|
|
|
98
45
|
<!--
|
|
99
46
|
@component
|
|
100
|
-
FormField — a label above
|
|
101
|
-
|
|
47
|
+
FormField — a label above a field slot; a kit control inside is named by the label on its own, a control outside the kit takes the slot's `labelId` as its
|
|
48
|
+
`aria-labelledby`. Errors are `FormFieldValidatable`'s job.
|
|
102
49
|
|
|
103
50
|
### CSS Custom Properties
|
|
104
51
|
| Property | Description | Default |
|
|
105
52
|
|---|---|---|
|
|
106
|
-
| `--sc-kit--form-field--gap` | Vertical gap between label and the
|
|
53
|
+
| `--sc-kit--form-field--gap` | Vertical gap between label and the field slot | `0.375rem` (6px) |
|
|
107
54
|
| `--sc-kit--form-field--hint--color` | Hint color (inherited by the default info icon) | `--sc-kit--color--text--muted` |
|
|
108
55
|
| `--sc-kit--form-field--label--color` | Label text color | `--sc-kit--color--text--primary` |
|
|
109
56
|
| `--sc-kit--form-field--label--font-size` | Label font size | `--sc-kit--font-size--sm` |
|
|
@@ -122,14 +69,19 @@ re-opens an already expanded popup field. It renders no errors — put it inside
|
|
|
122
69
|
flex-direction: column;
|
|
123
70
|
gap: var(--_ff--gap);
|
|
124
71
|
}
|
|
125
|
-
.form-
|
|
126
|
-
display:
|
|
72
|
+
.form-field__header {
|
|
73
|
+
display: flex;
|
|
127
74
|
align-items: center;
|
|
128
75
|
gap: 0.25rem;
|
|
129
76
|
color: var(--_ff--label-color);
|
|
130
77
|
font-size: var(--_ff--label-font-size);
|
|
131
78
|
font-weight: var(--_ff--label-font-weight);
|
|
132
79
|
}
|
|
80
|
+
.form-field__label {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 0.25rem;
|
|
84
|
+
}
|
|
133
85
|
.form-field__required {
|
|
134
86
|
color: var(--_ff--required-color);
|
|
135
87
|
}
|