@streamscloud/kit 0.89.0 → 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/utils/html-helper.d.ts +10 -0
- package/dist/core/utils/html-helper.js +37 -0
- package/dist/core/utils/string-helper.d.ts +10 -0
- package/dist/core/utils/string-helper.js +14 -0
- 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 +18 -2
- package/dist/ui/input/cmp.input.svelte.d.ts +3 -0
- 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 +16 -16
- package/dist/ui/textarea/cmp.textarea.svelte.d.ts +4 -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
|
@@ -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
|
}
|
|
@@ -1,25 +1,26 @@
|
|
|
1
|
+
export type FormFieldIds = {
|
|
2
|
+
/** For a control outside the kit — kit controls pick the label up by themselves. */
|
|
3
|
+
labelId: string | undefined;
|
|
4
|
+
};
|
|
1
5
|
import type { Snippet } from 'svelte';
|
|
2
6
|
type Props = {
|
|
3
|
-
/** Label rendered above the
|
|
7
|
+
/** Label rendered above the field. String renders as text; Snippet allows extra content (e.g. tooltip icon) next to the text. */
|
|
4
8
|
label?: string | Snippet;
|
|
5
|
-
/** Affordance pinned to the trailing edge of the label row — rendered only alongside a `label`. String renders the standard info icon — keyboard-focusable, named by the text — with that text in a tooltip; Snippet renders anything (button, link, badge).
|
|
9
|
+
/** Affordance pinned to the trailing edge of the label row — rendered only alongside a `label`. String renders the standard info icon — keyboard-focusable, named by the text — with that text in a tooltip; Snippet renders anything (button, link, badge). */
|
|
6
10
|
hint?: string | Snippet;
|
|
7
11
|
/** Appends a danger-colored `*` to the label. Purely visual — does not enforce anything. */
|
|
8
12
|
required?: boolean;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
* A control made of several fields (`SegmentedControl`, `PinInput`) is not named by it — give it its own `aria-label`.
|
|
12
|
-
*/
|
|
13
|
-
children: Snippet;
|
|
13
|
+
/** Field slot. A kit control inside is named by the label on its own: a text input through `<label for>`, anything else through `aria-labelledby`. */
|
|
14
|
+
children: Snippet<[FormFieldIds]>;
|
|
14
15
|
};
|
|
15
16
|
/**
|
|
16
|
-
* FormField — a label above
|
|
17
|
-
*
|
|
17
|
+
* FormField — a label above a field slot; a kit control inside is named by the label on its own, a control outside the kit takes the slot's `labelId` as its
|
|
18
|
+
* `aria-labelledby`. Errors are `FormFieldValidatable`'s job.
|
|
18
19
|
*
|
|
19
20
|
* ### CSS Custom Properties
|
|
20
21
|
* | Property | Description | Default |
|
|
21
22
|
* |---|---|---|
|
|
22
|
-
* | `--sc-kit--form-field--gap` | Vertical gap between label and the
|
|
23
|
+
* | `--sc-kit--form-field--gap` | Vertical gap between label and the field slot | `0.375rem` (6px) |
|
|
23
24
|
* | `--sc-kit--form-field--hint--color` | Hint color (inherited by the default info icon) | `--sc-kit--color--text--muted` |
|
|
24
25
|
* | `--sc-kit--form-field--label--color` | Label text color | `--sc-kit--color--text--primary` |
|
|
25
26
|
* | `--sc-kit--form-field--label--font-size` | Label font size | `--sc-kit--font-size--sm` |
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FormFieldLabelContext } from '../_internal/form-field-label-context';
|
|
2
|
+
export type FormFieldLabel = FormFieldLabelContext & {
|
|
3
|
+
/** The id the label's `for` points at — the first mounted text input that claimed the label, if any. */
|
|
4
|
+
readonly controlId: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
export declare const createFormFieldLabel: (id: string, hasLabel: () => boolean) => FormFieldLabel;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
export const createFormFieldLabel = (id, hasLabel) => {
|
|
3
|
+
const labelId = $derived(hasLabel() ? id : undefined);
|
|
4
|
+
let claimants = $state.raw([]);
|
|
5
|
+
return {
|
|
6
|
+
get labelId() {
|
|
7
|
+
return labelId;
|
|
8
|
+
},
|
|
9
|
+
get controlId() {
|
|
10
|
+
return claimants[0]?.();
|
|
11
|
+
},
|
|
12
|
+
claimControl: (controlId) => {
|
|
13
|
+
claimants = [...untrack(() => claimants), controlId];
|
|
14
|
+
return () => {
|
|
15
|
+
claimants = untrack(() => claimants).filter((claimant) => claimant !== controlId);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { default as FormField } from './cmp.form-field.svelte';
|
|
1
|
+
export { default as FormField, type FormFieldIds } from './cmp.form-field.svelte';
|
|
2
2
|
export { default as FormFieldValidatable } from './cmp.form-field-validatable.svelte';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">import { Icon } from '../icon';
|
|
2
2
|
import { Singleselect } from '../select';
|
|
3
3
|
import { isIconPickerGroup } from './types';
|
|
4
|
-
let { value, icons, placeholder = '', disabled = false, error = false, borderless = false, cols = 7, on } = $props();
|
|
4
|
+
let { value, icons, placeholder = '', disabled = false, error = false, borderless = false, cols = 7, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, on } = $props();
|
|
5
5
|
const toOption = (i) => ({ value: i.value, label: i.label, disabled: i.disabled });
|
|
6
6
|
const selectOptions = $derived(icons.map((item) => (isIconPickerGroup(item) ? { label: item.label, options: item.options.map(toOption) } : toOption(item))));
|
|
7
7
|
const iconBySrc = $derived(new Map(icons.flatMap((item) => isIconPickerGroup(item) ? item.options.map((o) => [o.value, o.src]) : [[item.value, item.src]])));
|
|
@@ -15,6 +15,8 @@ const iconBySrc = $derived(new Map(icons.flatMap((item) => isIconPickerGroup(ite
|
|
|
15
15
|
disabled={disabled}
|
|
16
16
|
error={error}
|
|
17
17
|
borderless={borderless}
|
|
18
|
+
aria-label={ariaLabel}
|
|
19
|
+
aria-labelledby={ariaLabelledby}
|
|
18
20
|
on={{ change: (v) => on?.change?.(v) }}>
|
|
19
21
|
{#snippet selectionSnippet({ option })}
|
|
20
22
|
{@const src = iconBySrc.get(option.value)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type IconPickerItem } from './types';
|
|
2
2
|
type Props = {
|
|
3
3
|
value: string | null;
|
|
4
|
-
/** Flat list of icons or a list of
|
|
4
|
+
/** Flat list of icons or a list of labeled groups. */
|
|
5
5
|
icons: IconPickerItem[];
|
|
6
6
|
placeholder?: string;
|
|
7
7
|
disabled?: boolean;
|
|
@@ -10,6 +10,8 @@ type Props = {
|
|
|
10
10
|
borderless?: boolean;
|
|
11
11
|
/** Tiles per row in the popover grid. @default 7 */
|
|
12
12
|
cols?: number;
|
|
13
|
+
'aria-label'?: string;
|
|
14
|
+
'aria-labelledby'?: string;
|
|
13
15
|
on?: {
|
|
14
16
|
change?: (value: string | null) => void;
|
|
15
17
|
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
<script lang="ts">import { HtmlHelper } from '../../core/utils';
|
|
1
|
+
<script lang="ts">import { HtmlHelper, StringHelper } from '../../core/utils';
|
|
2
|
+
import { claimFormFieldLabel } from '../_internal/form-field-label-context';
|
|
2
3
|
import { InputClearButton } from '../_internal/input-clear-button';
|
|
3
4
|
import { InputEmojiPicker } from '../_internal/input-emoji-picker';
|
|
4
5
|
import { InputPasswordToggle } from '../_internal/input-password-toggle';
|
|
5
6
|
import { IconSlot } from '../icon';
|
|
6
7
|
const { value, type = 'text', size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, clearable = false, emoji = false, trim = true, placeholder = '', title = '', name = '', id = null, autofocus = false, maxLength = null, autocomplete, autocapitalize, inputmode, enterkeyhint, spellcheck, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, on, icon, iconPosition = 'leading', secondaryIcon, prefix, suffix } = $props();
|
|
8
|
+
const fallbackId = $props.id();
|
|
9
|
+
const controlId = $derived(id ?? fallbackId);
|
|
10
|
+
claimFormFieldLabel(() => controlId);
|
|
7
11
|
let inputEl = $state.raw(undefined);
|
|
8
12
|
let passwordRevealed = $state(false);
|
|
9
13
|
const valueParsed = $derived(value ?? '');
|
|
@@ -25,6 +29,9 @@ const handleInput = () => {
|
|
|
25
29
|
if (!inputEl) {
|
|
26
30
|
return;
|
|
27
31
|
}
|
|
32
|
+
if (type !== 'password') {
|
|
33
|
+
HtmlHelper.cleanElementValue(inputEl, StringHelper.toSingleLineText);
|
|
34
|
+
}
|
|
28
35
|
on?.input?.(inputEl.value);
|
|
29
36
|
};
|
|
30
37
|
const applyTrim = () => {
|
|
@@ -71,6 +78,11 @@ const handleBlur = () => {
|
|
|
71
78
|
}
|
|
72
79
|
on?.blur?.();
|
|
73
80
|
};
|
|
81
|
+
const handlePaste = (event) => {
|
|
82
|
+
if (type !== 'password') {
|
|
83
|
+
HtmlHelper.pasteCleanedText(event, StringHelper.toSingleLineText);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
74
86
|
const handleKeydown = (event) => {
|
|
75
87
|
// Input only — the native `change` Enter fires next announces the commit; emitting it here doubles it.
|
|
76
88
|
if (event.key === 'Enter' && !event.isComposing) {
|
|
@@ -113,7 +125,7 @@ const focusInput = () => inputEl?.focus();
|
|
|
113
125
|
class="input__el"
|
|
114
126
|
type={effectiveType}
|
|
115
127
|
name={name}
|
|
116
|
-
id={
|
|
128
|
+
id={controlId}
|
|
117
129
|
placeholder={placeholder}
|
|
118
130
|
disabled={disabled}
|
|
119
131
|
readonly={readonly}
|
|
@@ -133,6 +145,7 @@ const focusInput = () => inputEl?.focus();
|
|
|
133
145
|
aria-required={ariaRequired ? 'true' : undefined}
|
|
134
146
|
oninput={handleInput}
|
|
135
147
|
onchange={handleChange}
|
|
148
|
+
onpaste={handlePaste}
|
|
136
149
|
onblur={handleBlur}
|
|
137
150
|
onfocus={() => on?.focus?.()}
|
|
138
151
|
onkeydown={handleKeydown} />
|
|
@@ -173,6 +186,9 @@ Leading / trailing whitespace is stripped at every commit point — `change`, bl
|
|
|
173
186
|
value the consumer receives always matches what the field shows. Typing is untouched. Pass
|
|
174
187
|
`trim={false}` to opt out; `type="password"` never trims.
|
|
175
188
|
|
|
189
|
+
Entered text is cleaned as it arrives — typed, pasted, dropped or autofilled: line breaks and tabs
|
|
190
|
+
become spaces, other control characters are dropped. `type="password"` is never cleaned.
|
|
191
|
+
|
|
176
192
|
Two snippet slots widen the chrome: `prefix` for a full-height segmented leading addon (see
|
|
177
193
|
`WebsiteInput`), and `suffix` for a free-form trailing addon rendered before any trailing icons
|
|
178
194
|
or the clear button (see `HandleInput`'s availability pill).
|
|
@@ -10,6 +10,9 @@ import type { InputProps } from './types';
|
|
|
10
10
|
* value the consumer receives always matches what the field shows. Typing is untouched. Pass
|
|
11
11
|
* `trim={false}` to opt out; `type="password"` never trims.
|
|
12
12
|
*
|
|
13
|
+
* Entered text is cleaned as it arrives — typed, pasted, dropped or autofilled: line breaks and tabs
|
|
14
|
+
* become spaces, other control characters are dropped. `type="password"` is never cleaned.
|
|
15
|
+
*
|
|
13
16
|
* Two snippet slots widen the chrome: `prefix` for a full-height segmented leading addon (see
|
|
14
17
|
* `WebsiteInput`), and `suffix` for a free-form trailing addon rendered before any trailing icons
|
|
15
18
|
* or the clear button (see `HandleInput`'s availability pill).
|
package/dist/ui/input/types.d.ts
CHANGED
|
@@ -10,15 +10,12 @@ export type InputProps = {
|
|
|
10
10
|
/** @default 'md' */
|
|
11
11
|
size?: InputSize;
|
|
12
12
|
disabled?: boolean;
|
|
13
|
-
/** Non-editable but selectable. The value is still submitted with a form. */
|
|
14
13
|
readonly?: boolean;
|
|
15
|
-
/** Visual display mode — non-focusable, no hover/focus animations, hidden clear button. */
|
|
16
14
|
inert?: boolean;
|
|
17
15
|
/** Visual error state — also sets `aria-invalid="true"`. */
|
|
18
16
|
error?: boolean;
|
|
19
17
|
/** Strip border + background + focus underline. */
|
|
20
18
|
borderless?: boolean;
|
|
21
|
-
/** Show the default × clear button when value is non-empty. */
|
|
22
19
|
clearable?: boolean;
|
|
23
20
|
/** Show the emoji-picker trigger inside the input. */
|
|
24
21
|
emoji?: boolean;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { claimFormFieldLabel } from '../_internal/form-field-label-context';
|
|
2
|
+
import { InputClearButton } from '../_internal/input-clear-button';
|
|
2
3
|
import { IconSlot } from '../icon';
|
|
3
4
|
import { formatNumeral, NumeralThousandGroupStyles, registerCursorTracker } from 'cleave-zen';
|
|
4
5
|
import { untrack } from 'svelte';
|
|
5
6
|
const { size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, placeholder = '', title = '', name = '', id = null, autofocus = false, min = null, max = null, minExclusive = false, maxExclusive = false, decimals = 2, suffix, hideResolvedValue = false, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, icon, iconPosition = 'leading', secondaryIcon, ...mode } = $props();
|
|
7
|
+
const fallbackId = $props.id();
|
|
8
|
+
const controlId = $derived(id ?? fallbackId);
|
|
9
|
+
claimFormFieldLabel(() => controlId);
|
|
6
10
|
let inputEl = $state.raw(undefined);
|
|
7
11
|
let focused = $state(false);
|
|
8
12
|
let typedValue = $state(undefined);
|
|
@@ -136,7 +140,7 @@ const initInput = (node) => {
|
|
|
136
140
|
type="text"
|
|
137
141
|
inputmode="decimal"
|
|
138
142
|
name={name}
|
|
139
|
-
id={
|
|
143
|
+
id={controlId}
|
|
140
144
|
placeholder={placeholder}
|
|
141
145
|
disabled={disabled}
|
|
142
146
|
readonly={readonly}
|
|
@@ -3,9 +3,7 @@ type BaseProps = {
|
|
|
3
3
|
/** @default 'md' */
|
|
4
4
|
size?: 'sm' | 'md' | 'lg';
|
|
5
5
|
disabled?: boolean;
|
|
6
|
-
/** Non-editable but selectable; value still submitted with a form. */
|
|
7
6
|
readonly?: boolean;
|
|
8
|
-
/** Visual display mode — non-focusable, no hover/focus animations, no clear button. */
|
|
9
7
|
inert?: boolean;
|
|
10
8
|
/** Visual error state — also sets `aria-invalid="true"`. */
|
|
11
9
|
error?: boolean;
|
|
@@ -41,7 +39,6 @@ type BaseProps = {
|
|
|
41
39
|
secondaryIcon?: IconProp;
|
|
42
40
|
};
|
|
43
41
|
type ClearableProps = BaseProps & {
|
|
44
|
-
/** Value can be cleared to `null`. Trigger shows an × button when set. */
|
|
45
42
|
clearable: true;
|
|
46
43
|
value: number | null | undefined;
|
|
47
44
|
on?: {
|
|
@@ -55,7 +52,6 @@ type ClearableProps = BaseProps & {
|
|
|
55
52
|
};
|
|
56
53
|
};
|
|
57
54
|
type RequiredProps = BaseProps & {
|
|
58
|
-
/** Value is required; no clear button. */
|
|
59
55
|
clearable: false;
|
|
60
56
|
value: number;
|
|
61
57
|
on?: {
|
|
@@ -2,7 +2,6 @@ import type { OptionPillOption } from './types';
|
|
|
2
2
|
declare function $$render<T>(): {
|
|
3
3
|
props: ({
|
|
4
4
|
options: OptionPillOption<T>[];
|
|
5
|
-
/** Disable the whole group. */
|
|
6
5
|
disabled?: boolean;
|
|
7
6
|
/** Custom equality comparator for non-primitive values (objects with `id`, etc.). @default (a, b) => a === b */
|
|
8
7
|
compare?: (a: T, b: T) => boolean;
|
|
@@ -15,7 +14,6 @@ declare function $$render<T>(): {
|
|
|
15
14
|
};
|
|
16
15
|
}) | ({
|
|
17
16
|
options: OptionPillOption<T>[];
|
|
18
|
-
/** Disable the whole group. */
|
|
19
17
|
disabled?: boolean;
|
|
20
18
|
/** Custom equality comparator for non-primitive values (objects with `id`, etc.). @default (a, b) => a === b */
|
|
21
19
|
compare?: (a: T, b: T) => boolean;
|
|
@@ -9,9 +9,8 @@ type Props = {
|
|
|
9
9
|
/** Editor chrome — the fixed panels-toggle + close pair. Set it to render both buttons; omit it entirely for a non-editor page. */
|
|
10
10
|
editorControls?: EditorControls;
|
|
11
11
|
scrollable?: boolean;
|
|
12
|
-
/** Sets `inert` on the whole layout — header and body stop taking focus, clicks and assistive-tech interaction. */
|
|
13
12
|
inert?: boolean;
|
|
14
|
-
/** Shows a `Spinner` centered over the layout; `blocking` also makes the whole layout `inert` and dims it.
|
|
13
|
+
/** Shows a `Spinner` centered over the layout; `blocking` also makes the whole layout `inert` and dims it. */
|
|
15
14
|
loading?: PageLoading;
|
|
16
15
|
};
|
|
17
16
|
/**
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
|
|
1
|
+
<script lang="ts">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
|
|
2
|
+
import { PinInputLocalization } from './pin-input-localization';
|
|
3
|
+
const { value, length = 6, mask = false, disabled = false, error = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, on } = $props();
|
|
4
|
+
const formFieldLabel = getFormFieldLabel();
|
|
3
5
|
const localization = new PinInputLocalization();
|
|
4
6
|
let cells = $state([]);
|
|
5
7
|
const cellValue = (i) => value[i] ?? '';
|
|
@@ -59,7 +61,13 @@ const handleFocus = (e) => {
|
|
|
59
61
|
};
|
|
60
62
|
</script>
|
|
61
63
|
|
|
62
|
-
<div
|
|
64
|
+
<div
|
|
65
|
+
class="pin-input"
|
|
66
|
+
class:pin-input--disabled={disabled}
|
|
67
|
+
class:pin-input--error={error}
|
|
68
|
+
role="group"
|
|
69
|
+
aria-label={ariaLabel}
|
|
70
|
+
aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}>
|
|
63
71
|
{#each Array(length) as _, i (i)}
|
|
64
72
|
<input
|
|
65
73
|
aria-label={localization.cellLabel(i + 1, length)}
|
|
@@ -7,8 +7,8 @@ type Props = {
|
|
|
7
7
|
disabled?: boolean;
|
|
8
8
|
/** Visual error state — also sets `aria-invalid="true"` on each cell. */
|
|
9
9
|
error?: boolean;
|
|
10
|
-
/** Accessible label for the cell group. Consumer-provided (no kit default). */
|
|
11
10
|
'aria-label'?: string;
|
|
11
|
+
'aria-labelledby'?: string;
|
|
12
12
|
'aria-describedby'?: string;
|
|
13
13
|
on?: {
|
|
14
14
|
input?: (value: string) => void;
|
|
@@ -4,7 +4,7 @@ import { validateTrigger } from './validate-trigger';
|
|
|
4
4
|
import { autoUpdate, computePosition, flip, offset as offsetMiddleware, shift, size } from '@floating-ui/dom';
|
|
5
5
|
import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
|
|
6
6
|
import { tick } from 'svelte';
|
|
7
|
-
let { position = 'bottom-start', disabled = false, fixedPosition = false, offset = 4, boundaryMargin = 8, backdrop = false, matchWidth = false, panel = true, keepOpen = false, fillContainer = false, tabbable = true, on, children, trigger, 'aria-label': ariaLabel } = $props();
|
|
7
|
+
let { position = 'bottom-start', disabled = false, fixedPosition = false, offset = 4, boundaryMargin = 8, backdrop = false, matchWidth = false, panel = true, keepOpen = false, fillContainer = false, tabbable = true, on, children, trigger, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby } = $props();
|
|
8
8
|
let opened = $state(false);
|
|
9
9
|
let triggerEl = $state.raw(undefined);
|
|
10
10
|
let contentEl = $state.raw(undefined);
|
|
@@ -182,6 +182,7 @@ const handleBackdropClick = (e) => {
|
|
|
182
182
|
disabled={disabled}
|
|
183
183
|
tabindex={tabbable ? undefined : -1}
|
|
184
184
|
aria-label={ariaLabel}
|
|
185
|
+
aria-labelledby={ariaLabelledby}
|
|
185
186
|
aria-haspopup="true"
|
|
186
187
|
aria-expanded={opened}
|
|
187
188
|
onclick={handleTriggerClick}
|
|
@@ -27,14 +27,10 @@ type Props = {
|
|
|
27
27
|
closed?: () => void;
|
|
28
28
|
};
|
|
29
29
|
children: Snippet;
|
|
30
|
-
/**
|
|
31
|
-
* Custom trigger content; defaults to a chevron-down icon. Renders inside the trigger `<button>`,
|
|
32
|
-
* so content MUST be non-interactive — for a button-looking trigger use `Button` with
|
|
33
|
-
* `type="presentational"`. Nesting an interactive element triggers a dev-only warning.
|
|
34
|
-
*/
|
|
30
|
+
/** Content of the trigger `<button>`, so non-interactive only — `Button type="presentational"` for a button look. @default chevron-down icon */
|
|
35
31
|
trigger?: Snippet;
|
|
36
|
-
/** Accessible name for the trigger button — required when the trigger content carries no text (an icon-only trigger). */
|
|
37
32
|
'aria-label'?: string;
|
|
33
|
+
'aria-labelledby'?: string;
|
|
38
34
|
};
|
|
39
35
|
/**
|
|
40
36
|
* Popover — floating content anchored to a trigger element. Click trigger to open;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts">import { getFormFieldLabel } from '../_internal/form-field-label-context';
|
|
2
|
+
const { selected, label, disabled = false, title, name, on } = $props();
|
|
2
3
|
const ariaLabel = $derived(typeof label === 'string' ? label : name);
|
|
4
|
+
const formFieldLabel = getFormFieldLabel();
|
|
3
5
|
const handleClick = (event) => {
|
|
4
6
|
event.stopPropagation();
|
|
5
7
|
if (disabled || selected) {
|
|
@@ -7,11 +9,18 @@ const handleClick = (event) => {
|
|
|
7
9
|
}
|
|
8
10
|
on?.change?.();
|
|
9
11
|
};
|
|
10
|
-
export {};
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
14
|
<label class="radio" class:radio--on={selected} class:radio--disabled={disabled} title={title}>
|
|
14
|
-
<button
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
class="radio__dot"
|
|
18
|
+
role="radio"
|
|
19
|
+
aria-checked={selected}
|
|
20
|
+
aria-label={ariaLabel}
|
|
21
|
+
aria-labelledby={label || ariaLabel ? undefined : formFieldLabel?.labelId}
|
|
22
|
+
disabled={disabled}
|
|
23
|
+
onclick={handleClick}>
|
|
15
24
|
<span class="radio__inner" class:radio__inner--visible={selected} aria-hidden="true"></span>
|
|
16
25
|
</button>
|
|
17
26
|
{#if typeof label === 'string'}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script lang="ts">import { AppTheme } from '../../core/theme';
|
|
2
2
|
import { HtmlHelper } from '../../core/utils';
|
|
3
|
+
import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
|
|
3
4
|
import { InputEmojiPicker } from '../_internal/input-emoji-picker';
|
|
4
5
|
import { HtmlBlock } from '../html-block';
|
|
5
6
|
import { RichTextInputLocalization } from './rich-text-input-localization';
|
|
6
7
|
import { addValidatedLinkButton } from './validated-link-button';
|
|
7
8
|
import { onDestroy, onMount } from 'svelte';
|
|
8
|
-
const { value, size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, emoji = false, autofocus = false, rows = 0, placeholder = '', options, conf, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
|
|
9
|
-
const id =
|
|
9
|
+
const { value, size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, emoji = false, autofocus = false, rows = 0, placeholder = '', options, conf, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
|
|
10
|
+
const id = $props.id();
|
|
11
|
+
const formFieldLabel = getFormFieldLabel();
|
|
10
12
|
let elementRef = $state.raw(undefined);
|
|
11
13
|
let toolbarRef = $state.raw(undefined);
|
|
12
14
|
let editorRef = $state.raw(null);
|
|
@@ -142,13 +144,23 @@ const onEmojiSelected = (emojiChar) => insertEmoji(emojiChar);
|
|
|
142
144
|
class:rich-text-input--readonly={readonly}
|
|
143
145
|
class:rich-text-input--inert={inert}
|
|
144
146
|
class:rich-text-input--error={error}
|
|
145
|
-
class:rich-text-input--borderless={borderless}
|
|
146
|
-
aria-label={ariaLabel}
|
|
147
|
-
aria-describedby={ariaDescribedby}
|
|
148
|
-
aria-required={ariaRequired ? 'true' : undefined}>
|
|
147
|
+
class:rich-text-input--borderless={borderless}>
|
|
149
148
|
<div class="rich-text-input__toolbar" bind:this={toolbarRef}></div>
|
|
150
149
|
<HtmlBlock>
|
|
151
|
-
<div
|
|
150
|
+
<div
|
|
151
|
+
bind:this={elementRef}
|
|
152
|
+
class="rich-text-input__input"
|
|
153
|
+
id={id}
|
|
154
|
+
role="textbox"
|
|
155
|
+
tabindex={disabled || inert ? undefined : 0}
|
|
156
|
+
aria-multiline="true"
|
|
157
|
+
aria-label={ariaLabel}
|
|
158
|
+
aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}
|
|
159
|
+
aria-describedby={ariaDescribedby}
|
|
160
|
+
aria-required={ariaRequired ? 'true' : undefined}
|
|
161
|
+
aria-invalid={error ? 'true' : undefined}
|
|
162
|
+
aria-readonly={readonly ? 'true' : undefined}>
|
|
163
|
+
</div>
|
|
152
164
|
</HtmlBlock>
|
|
153
165
|
{#if emoji}
|
|
154
166
|
<span class="rich-text-input__emoji">
|
|
@@ -6,9 +6,7 @@ type Props = {
|
|
|
6
6
|
/** @default 'md' — controls the editor root font size (and downstream rem-based sizing). */
|
|
7
7
|
size?: 'sm' | 'md' | 'lg';
|
|
8
8
|
disabled?: boolean;
|
|
9
|
-
/** Non-editable but still visible (mirrors `disabled` in editor mode). */
|
|
10
9
|
readonly?: boolean;
|
|
11
|
-
/** Visual display mode — no focus, no hover, hidden affordances. */
|
|
12
10
|
inert?: boolean;
|
|
13
11
|
/** Visual error state. */
|
|
14
12
|
error?: boolean;
|
|
@@ -24,6 +22,7 @@ type Props = {
|
|
|
24
22
|
/** Escape-hatch for extra HugeRTE init options, merged AFTER kit defaults. */
|
|
25
23
|
conf?: Partial<RawEditorOptions>;
|
|
26
24
|
'aria-label'?: string;
|
|
25
|
+
'aria-labelledby'?: string;
|
|
27
26
|
'aria-describedby'?: string;
|
|
28
27
|
'aria-required'?: boolean;
|
|
29
28
|
on?: {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts" module>export {};
|
|
2
2
|
</script>
|
|
3
3
|
|
|
4
|
-
<script lang="ts" generics="T">
|
|
5
|
-
const { value, segments, variant = 'tray', size = 'md', fullWidth = false, compare, 'aria-label': ariaLabel, on } = $props();
|
|
4
|
+
<script lang="ts" generics="T">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
|
|
5
|
+
const { value, segments, variant = 'tray', size = 'md', fullWidth = false, compare, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, on } = $props();
|
|
6
|
+
const formFieldLabel = getFormFieldLabel();
|
|
6
7
|
const segmentIdPrefix = $props.id();
|
|
7
8
|
const segmentEls = [];
|
|
8
9
|
const isActive = (segment) => (compare ? compare(value, segment.value) : value === segment.value);
|
|
@@ -51,7 +52,8 @@ const handleKeydown = (event, from) => {
|
|
|
51
52
|
class="segmented-control segmented-control--{variant} segmented-control--{size}"
|
|
52
53
|
class:segmented-control--full={fullWidth}
|
|
53
54
|
role="radiogroup"
|
|
54
|
-
aria-label={ariaLabel}
|
|
55
|
+
aria-label={ariaLabel}
|
|
56
|
+
aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}>
|
|
55
57
|
{#each segments as segment, i (i)}
|
|
56
58
|
{@const segmentLabelId = `${segmentIdPrefix}-${i}`}
|
|
57
59
|
<button
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export type Segment<TValue = unknown> = {
|
|
2
2
|
value: TValue;
|
|
3
3
|
label: string;
|
|
4
|
-
/** Non-interactive — click does nothing. */
|
|
5
4
|
disabled?: boolean;
|
|
6
5
|
/** Visual flag — colors the segment in the danger tone (validation hint). */
|
|
7
6
|
error?: boolean;
|
|
@@ -23,8 +22,8 @@ declare function $$render<T>(): {
|
|
|
23
22
|
fullWidth?: boolean;
|
|
24
23
|
/** Equality for object T values. @default reference equality (`===`) */
|
|
25
24
|
compare?: (a: T, b: T) => boolean;
|
|
26
|
-
/** Accessible name of the group. */
|
|
27
25
|
'aria-label'?: string;
|
|
26
|
+
'aria-labelledby'?: string;
|
|
28
27
|
on?: {
|
|
29
28
|
change?: (value: T) => void;
|
|
30
29
|
};
|