@nectary/components 4.9.1 → 4.10.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/avatar/index.js +1 -1
- package/avatar/types.d.ts +19 -1
- package/button/index.js +23 -1
- package/button/types.d.ts +3 -0
- package/button/utils.d.ts +2 -1
- package/button/utils.js +2 -1
- package/card-v2/index.js +1 -1
- package/checkbox/index.js +40 -0
- package/checkbox/types.d.ts +4 -0
- package/date-picker/index.js +31 -1
- package/date-picker/types.d.ts +2 -0
- package/icon/generated-icon-type.d.ts +1 -1
- package/input/index.js +66 -3
- package/input/types.d.ts +3 -1
- package/package.json +3 -3
- package/radio/index.js +32 -1
- package/radio/types.d.ts +6 -1
- package/select-menu/index.js +35 -0
- package/select-menu/types.d.ts +2 -0
- package/textarea/index.js +33 -0
- package/textarea/types.d.ts +2 -0
- package/utils/form.d.ts +8 -0
- package/utils/form.js +36 -0
package/input/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context, defineCustomElement, getAttribute, getBooleanAttribute, getLiteralAttribute, getReactEventHandler, isAttrEqual, isAttrTrue, isElementFocused, NectaryElement, setClass, subscribeContext, updateAttribute, updateBooleanAttribute, updateLiteralAttribute } from '../utils';
|
|
2
|
+
import { requestSubmitForm, setFormValue } from '../utils/form';
|
|
2
3
|
import { DEFAULT_SIZE, sizeValues } from '../utils/size';
|
|
3
4
|
const templateHTML = '<style>:host{all:initial;display:inline-block;vertical-align:middle}#wrapper{position:relative;display:flex;flex-direction:row;align-items:center;box-sizing:border-box;border-radius:var(--sinch-local-shape-radius);width:100%;height:var(--sinch-local-size);background-color:var(--sinch-comp-input-color-default-background-initial);--sinch-local-size:var(--sinch-comp-input-size-container-m);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-m);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-m)}:host([data-size="l"])>#wrapper{--sinch-local-size:var(--sinch-comp-input-size-container-l);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-l);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-l)}:host([data-size="m"])>#wrapper{--sinch-local-size:var(--sinch-comp-input-size-container-m);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-m);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-m)}:host([data-size="s"])>#wrapper{--sinch-local-size:var(--sinch-comp-input-size-container-s);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-s);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-s)}#input-wrapper{position:relative;flex:1;flex-basis:0;min-width:0;align-self:stretch}#input{all:initial;width:100%;height:100%;padding:0 12px;box-sizing:border-box;font:var(--sinch-comp-input-font-input);color:var(--sinch-comp-input-color-default-text-initial)}#input::placeholder{font:var(--sinch-comp-input-font-placeholder)!important;color:var(--sinch-comp-input-color-default-text-placeholder);opacity:1}#input:disabled{color:var(--sinch-comp-input-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-input-color-disabled-text-initial)}#input-mask{display:none;position:absolute;inset:0;padding:0 12px;pointer-events:none;color:var(--sinch-comp-input-color-default-text-placeholder);white-space:pre;height:fit-content;margin:auto 0;overflow:hidden}#border{position:absolute;border:1px solid var(--sinch-comp-input-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([disabled]) #border{border-color:var(--sinch-comp-input-color-disabled-border-initial)}#input-wrapper:focus-within+#border{border-color:var(--sinch-comp-input-color-default-border-focus);border-width:2px}#input-mask,:host([mask]) #input{font:var(--sinch-sys-font-body-monospace-m)}:host([mask]) #input-mask{display:block}:host([invalid]:not([disabled])) #input-wrapper:not(:focus-within)+#border{border-color:var(--sinch-comp-input-color-invalid-border-initial)}#input[type=password]:not(:placeholder-shown){font-size:1.5em;letter-spacing:.1em}#icon-wrapper{position:relative;height:100%}#icon{position:absolute;display:flex;align-items:center;left:12px;top:0;bottom:0;pointer-events:none;--sinch-global-color-icon:var(--sinch-comp-input-color-default-icon-initial)}:host([disabled]) #icon{--sinch-global-color-icon:var(--sinch-comp-input-color-disabled-icon-initial)}#icon-wrapper.empty{display:none}#icon-wrapper.empty~#input-wrapper>#input,#icon-wrapper.empty~#input-wrapper>#input-mask{padding-left:12px}#icon-wrapper:not(.empty)~#input-wrapper>#input,#icon-wrapper:not(.empty)~#input-wrapper>#input-mask{padding-left:calc(var(--sinch-global-size-icon) + 20px)}#right{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-right:4px}#right.empty{display:none}#left{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-left:4px}#left.empty{display:none}</style><div id="wrapper"><div id="left"><slot name="left"></slot></div><div id="icon-wrapper"><div id="icon"><slot name="icon"></slot></div></div><div id="input-wrapper"><div id="input-mask"></div><input id="input" type="text"/></div><div id="border"></div><div id="right"><slot name="right"></slot></div></div>';
|
|
4
5
|
import { deleteContentBackward, deleteContentForward, getMaskSymbols, inputTypes, insertText, beginMaskedComposition, endMaskedComposition, splitValueAndMask, getMergedValueSliced, insertFromPaste } from './utils';
|
|
@@ -22,12 +23,15 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
22
23
|
#controller = null;
|
|
23
24
|
#sizeContext;
|
|
24
25
|
#maskSymbols = null;
|
|
26
|
+
#internals;
|
|
27
|
+
static formAssociated = true;
|
|
25
28
|
constructor() {
|
|
26
29
|
super();
|
|
27
30
|
const shadowRoot = this.attachShadow({
|
|
28
31
|
delegatesFocus: true
|
|
29
32
|
});
|
|
30
33
|
shadowRoot.appendChild(template.content.cloneNode(true));
|
|
34
|
+
this.#internals = this.attachInternals();
|
|
31
35
|
this.#$input = shadowRoot.querySelector('#input');
|
|
32
36
|
this.#$inputMask = shadowRoot.querySelector('#input-mask');
|
|
33
37
|
this.#$iconSlot = shadowRoot.querySelector('slot[name="icon"]');
|
|
@@ -43,12 +47,14 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
43
47
|
connectedCallback() {
|
|
44
48
|
super.connectedCallback();
|
|
45
49
|
this.setAttribute('role', 'textbox');
|
|
50
|
+
this.#internals.role = 'textbox';
|
|
46
51
|
if (this.#controller === null) {
|
|
47
52
|
this.#controller = new AbortController();
|
|
48
53
|
}
|
|
49
54
|
const options = {
|
|
50
55
|
signal: this.#controller.signal
|
|
51
56
|
};
|
|
57
|
+
this.#$input.addEventListener('keydown', this.#onKeyDown, options);
|
|
52
58
|
this.#$input.addEventListener('input', this.#onInput, options);
|
|
53
59
|
this.#$input.addEventListener('cut', this.#onCut, options);
|
|
54
60
|
this.#$input.addEventListener('copy', this.#onCopy, options);
|
|
@@ -80,11 +86,53 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
80
86
|
this.#controller.abort();
|
|
81
87
|
this.#controller = null;
|
|
82
88
|
}
|
|
89
|
+
formAssociatedCallback() {
|
|
90
|
+
setFormValue(this.#internals, this.#$input.value);
|
|
91
|
+
}
|
|
92
|
+
formResetCallback() {
|
|
93
|
+
this.#$input.value = '';
|
|
94
|
+
setFormValue(this.#internals, '');
|
|
95
|
+
}
|
|
96
|
+
formStateRestoreCallback(state) {
|
|
97
|
+
if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (state !== null) {
|
|
101
|
+
const value = typeof state === 'string' ? state : state.get(this.name);
|
|
102
|
+
this.#$input.value = value?.toString() ?? '';
|
|
103
|
+
setFormValue(this.#internals, value?.toString() ?? '');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
#onKeyDown = e => {
|
|
107
|
+
const form = this.#internals.form;
|
|
108
|
+
if (form === null) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (form.disabled === true) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (e.key === 'Enter') {
|
|
115
|
+
const submitSelectors = ['sinch-button[form-type="submit"]'];
|
|
116
|
+
const formSubmitters = Array.from(form.querySelectorAll(submitSelectors.join(',')));
|
|
117
|
+
const formSubmitter = formSubmitters.find(submitter => !submitter.disabled) ?? null;
|
|
118
|
+
if (formSubmitter !== null) {
|
|
119
|
+
requestSubmitForm(form, formSubmitter);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
83
123
|
static get observedAttributes() {
|
|
84
|
-
return ['type', 'value', 'placeholder', 'mask', 'invalid', 'disabled', 'size', 'autocomplete', 'autofocus', 'data-size', 'aria-label'];
|
|
124
|
+
return ['name', 'type', 'value', 'placeholder', 'mask', 'invalid', 'disabled', 'size', 'autocomplete', 'autofocus', 'data-size', 'aria-label'];
|
|
85
125
|
}
|
|
86
126
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
87
127
|
switch (name) {
|
|
128
|
+
case 'name':
|
|
129
|
+
{
|
|
130
|
+
if (isAttrEqual(oldVal, newVal)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
updateAttribute(this.#$input, 'name', newVal);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
88
136
|
case 'type':
|
|
89
137
|
{
|
|
90
138
|
updateLiteralAttribute(this.#$input, inputTypes, 'type', newVal);
|
|
@@ -107,6 +155,7 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
107
155
|
placeholder
|
|
108
156
|
} = splitValueAndMask(nextVal, this.#maskSymbols);
|
|
109
157
|
this.#$input.value = value;
|
|
158
|
+
setFormValue(this.#internals, value);
|
|
110
159
|
this.#$inputMask.textContent = placeholder;
|
|
111
160
|
if (isElementFocused(this.#$input)) {
|
|
112
161
|
this.#setSelectionRange(this.#selectionEnd, this.#selectionEnd);
|
|
@@ -115,6 +164,7 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
115
164
|
}
|
|
116
165
|
if (nextVal !== prevVal) {
|
|
117
166
|
this.#$input.value = nextVal;
|
|
167
|
+
setFormValue(this.#internals, nextVal);
|
|
118
168
|
if (isElementFocused(this.#$input)) {
|
|
119
169
|
this.#setSelectionRange(this.#selectionEnd, this.#selectionEnd);
|
|
120
170
|
}
|
|
@@ -139,6 +189,7 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
139
189
|
const isInvalid = isAttrTrue(newVal);
|
|
140
190
|
this.ariaInvalid = isInvalid.toString();
|
|
141
191
|
this.#$input.ariaInvalid = this.ariaInvalid;
|
|
192
|
+
this.#internals.ariaInvalid = this.ariaInvalid;
|
|
142
193
|
updateBooleanAttribute(this, name, isInvalid);
|
|
143
194
|
break;
|
|
144
195
|
}
|
|
@@ -180,10 +231,17 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
180
231
|
case 'aria-label':
|
|
181
232
|
{
|
|
182
233
|
this.#$input.ariaLabel = newVal;
|
|
234
|
+
this.#internals.ariaLabel = newVal;
|
|
183
235
|
break;
|
|
184
236
|
}
|
|
185
237
|
}
|
|
186
238
|
}
|
|
239
|
+
set name(value) {
|
|
240
|
+
updateAttribute(this, 'name', value);
|
|
241
|
+
}
|
|
242
|
+
get name() {
|
|
243
|
+
return getAttribute(this, 'name', '');
|
|
244
|
+
}
|
|
187
245
|
set type(value) {
|
|
188
246
|
updateAttribute(this, 'type', value);
|
|
189
247
|
}
|
|
@@ -411,11 +469,14 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
411
469
|
}
|
|
412
470
|
const nextValue = this.#$input.value;
|
|
413
471
|
const prevValue = this.value;
|
|
472
|
+
setFormValue(this.#internals, nextValue);
|
|
414
473
|
if (prevValue !== nextValue) {
|
|
415
474
|
const nextSelectionStart = this.#$input.selectionStart;
|
|
416
475
|
const nextSelectionEnd = this.#$input.selectionEnd;
|
|
417
|
-
this
|
|
418
|
-
|
|
476
|
+
if (this.hasAttribute('value')) {
|
|
477
|
+
this.#$input.value = prevValue;
|
|
478
|
+
this.#setSelectionRange(this.#selectionStart, this.#selectionEnd);
|
|
479
|
+
}
|
|
419
480
|
this.#selectionStart = nextSelectionStart;
|
|
420
481
|
this.#selectionEnd = nextSelectionEnd;
|
|
421
482
|
this.#dispatchChangeEvent(nextValue);
|
|
@@ -595,10 +656,12 @@ defineCustomElement('sinch-input', class extends NectaryElement {
|
|
|
595
656
|
if (this.#maskSymbols === null) {
|
|
596
657
|
const value = this.placeholder;
|
|
597
658
|
this.#$input.placeholder = value ?? '';
|
|
659
|
+
this.#internals.ariaPlaceholder = value ?? '';
|
|
598
660
|
updateAttribute(this, 'aria-placeholder', value);
|
|
599
661
|
} else {
|
|
600
662
|
updateAttribute(this, 'aria-placeholder', null);
|
|
601
663
|
this.#$input.placeholder = '';
|
|
664
|
+
this.#internals.ariaPlaceholder = '';
|
|
602
665
|
}
|
|
603
666
|
}
|
|
604
667
|
#onIconSlotChange = () => {
|
package/input/types.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export type TSinchInputClipboardEvent = CustomEvent<{
|
|
|
6
6
|
replaceWith: (value: string) => void;
|
|
7
7
|
}>;
|
|
8
8
|
export type TSinchInputProps = {
|
|
9
|
+
/** Identification for uncontrolled form submissions */
|
|
10
|
+
name?: string;
|
|
9
11
|
/** Controlled value, doesn't change on its own and requres an onChange-value state loop */
|
|
10
|
-
value
|
|
12
|
+
value?: string;
|
|
11
13
|
/** Mask */
|
|
12
14
|
mask?: string | null;
|
|
13
15
|
/** Label that is used for a11y – might be different from `label` */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nectary/components",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.1",
|
|
4
4
|
"files": [
|
|
5
5
|
"**/*/*.css",
|
|
6
6
|
"**/*/*.json",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@babel/runtime": "^7.22.15",
|
|
23
|
-
"@nectary/assets": "2.
|
|
23
|
+
"@nectary/assets": "2.4.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@babel/cli": "^7.22.15",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"typescript": "^5.2.2"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@nectary/theme-base": "1.4.
|
|
37
|
+
"@nectary/theme-base": "1.4.3"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/radio/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { defineCustomElement, getAttribute, getBooleanAttribute, getReactEventHandler, getTargetByAttribute, isAttrTrue, NectaryElement, updateAttribute, updateBooleanAttribute } from '../utils';
|
|
2
|
+
import { setFormValue } from '../utils/form';
|
|
2
3
|
const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:var(--sinch-comp-radio-direction,column);gap:var(--sinch-comp-radio-gap,8px);box-sizing:border-box;width:100%}</style><div id="wrapper"><slot></slot></div>';
|
|
3
4
|
const template = document.createElement('template');
|
|
4
5
|
template.innerHTML = templateHTML;
|
|
5
6
|
defineCustomElement('sinch-radio', class extends NectaryElement {
|
|
6
7
|
#$slot;
|
|
7
8
|
#controller = null;
|
|
9
|
+
#internals;
|
|
10
|
+
static formAssociated = true;
|
|
8
11
|
constructor() {
|
|
9
12
|
super();
|
|
10
|
-
const shadowRoot = this.attachShadow(
|
|
13
|
+
const shadowRoot = this.attachShadow({
|
|
14
|
+
delegatesFocus: true
|
|
15
|
+
});
|
|
11
16
|
shadowRoot.appendChild(template.content.cloneNode(true));
|
|
17
|
+
this.#internals = this.attachInternals();
|
|
12
18
|
this.#$slot = shadowRoot.querySelector('slot');
|
|
13
19
|
}
|
|
14
20
|
connectedCallback() {
|
|
@@ -20,6 +26,7 @@ defineCustomElement('sinch-radio', class extends NectaryElement {
|
|
|
20
26
|
signal
|
|
21
27
|
};
|
|
22
28
|
this.setAttribute('role', 'radiogroup');
|
|
29
|
+
this.#internals.role = 'radiogroup';
|
|
23
30
|
this.#$slot.addEventListener('slotchange', this.#onSlotChange, options);
|
|
24
31
|
this.#$slot.addEventListener('keydown', this.#onOptionKeyDown, options);
|
|
25
32
|
this.#$slot.addEventListener('click', this.#onOptionClick, options);
|
|
@@ -29,9 +36,32 @@ defineCustomElement('sinch-radio', class extends NectaryElement {
|
|
|
29
36
|
this.#controller.abort();
|
|
30
37
|
this.#controller = null;
|
|
31
38
|
}
|
|
39
|
+
formAssociatedCallback() {
|
|
40
|
+
setFormValue(this.#internals, this.value);
|
|
41
|
+
}
|
|
42
|
+
formResetCallback() {
|
|
43
|
+
this.value = '';
|
|
44
|
+
setFormValue(this.#internals, '');
|
|
45
|
+
}
|
|
46
|
+
formStateRestoreCallback(state) {
|
|
47
|
+
if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (state !== null) {
|
|
51
|
+
const value = typeof state === 'string' ? state : state.get(this.name);
|
|
52
|
+
this.value = value?.toString() ?? '';
|
|
53
|
+
setFormValue(this.#internals, value?.toString() ?? '');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
32
56
|
static get observedAttributes() {
|
|
33
57
|
return ['value', 'invalid'];
|
|
34
58
|
}
|
|
59
|
+
set name(value) {
|
|
60
|
+
updateAttribute(this, 'name', value);
|
|
61
|
+
}
|
|
62
|
+
get name() {
|
|
63
|
+
return getAttribute(this, 'name', '');
|
|
64
|
+
}
|
|
35
65
|
set value(value) {
|
|
36
66
|
updateAttribute(this, 'value', value);
|
|
37
67
|
}
|
|
@@ -49,6 +79,7 @@ defineCustomElement('sinch-radio', class extends NectaryElement {
|
|
|
49
79
|
case 'value':
|
|
50
80
|
{
|
|
51
81
|
this.#onValueChange(newVal ?? '');
|
|
82
|
+
setFormValue(this.#internals, newVal ?? '');
|
|
52
83
|
break;
|
|
53
84
|
}
|
|
54
85
|
case 'invalid':
|
package/radio/types.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
|
|
2
2
|
export type TSinchRadioProps = {
|
|
3
|
-
|
|
3
|
+
/** Identification for uncontrolled form submissions */
|
|
4
|
+
name?: string;
|
|
5
|
+
/** Value */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Invalid state */
|
|
4
8
|
invalid?: boolean;
|
|
9
|
+
/** Label that is used for a11y – might be different from `label` */
|
|
5
10
|
'aria-label': string;
|
|
6
11
|
};
|
|
7
12
|
export type TSinchRadioEvents = {
|
package/select-menu/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import '../icon';
|
|
|
3
3
|
import '../text';
|
|
4
4
|
import { isSelectMenuOption } from '../select-menu-option/utils';
|
|
5
5
|
import { attrValueToPixels, defineCustomElement, getAttribute, getBooleanAttribute, unpackCsv, getFirstCsvValue, getIntegerAttribute, getReactEventHandler, isAttrTrue, NectaryElement, updateAttribute, updateBooleanAttribute, updateCsv, updateExplicitBooleanAttribute, updateIntegerAttribute, debounceTimeout, setClass, subscribeContext, hasClass, isTargetEqual } from '../utils';
|
|
6
|
+
import { CSVToFormData, setFormValue } from '../utils/form';
|
|
6
7
|
const templateHTML = '<style>:host{display:block;outline:0}#listbox{overflow-y:auto;max-height:var(--sinch-comp-select-menu-font-max-height)}#search{display:none;margin:10px}#search.active{display:block}#search-clear:not(.active){display:none}#not-found{display:flex;align-items:center;justify-content:center;width:100%;height:30px;margin-bottom:10px;pointer-events:none;user-select:none;--sinch-comp-text-font:var(--sinch-comp-select-menu-font-not-found-text);--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-not-found-text-initial)}#not-found:not(.active){display:none}::slotted(.hidden){display:none}::slotted(sinch-title){padding:8px 16px;--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-title-initial)}</style><sinch-input id="search" size="s" placeholder="Search"><sinch-icon icons-version="2" name="magnifying-glass" id="icon-search" slot="icon"></sinch-icon><sinch-button id="search-clear" slot="right"><sinch-icon icons-version="2" name="fa-xmark" slot="icon"></sinch-icon></sinch-button></sinch-input><div id="not-found"><sinch-text type="m">No results</sinch-text></div><div id="listbox" role="presentation"><slot></slot></div>';
|
|
7
8
|
const ITEM_HEIGHT = 40;
|
|
8
9
|
const NUM_ITEMS_SEARCH = 7;
|
|
@@ -16,11 +17,14 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
|
|
|
16
17
|
#$notFound;
|
|
17
18
|
#controller = null;
|
|
18
19
|
#searchDebounce;
|
|
20
|
+
#internals;
|
|
19
21
|
#userManagedSearch = false;
|
|
22
|
+
static formAssociated = true;
|
|
20
23
|
constructor() {
|
|
21
24
|
super();
|
|
22
25
|
const shadowRoot = this.attachShadow();
|
|
23
26
|
shadowRoot.appendChild(template.content.cloneNode(true));
|
|
27
|
+
this.#internals = this.attachInternals();
|
|
24
28
|
this.#$optionSlot = shadowRoot.querySelector('slot');
|
|
25
29
|
this.#$listbox = shadowRoot.querySelector('#listbox');
|
|
26
30
|
this.#$search = shadowRoot.querySelector('#search');
|
|
@@ -34,6 +38,7 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
|
|
|
34
38
|
signal: this.#controller.signal
|
|
35
39
|
};
|
|
36
40
|
this.setAttribute('role', 'listbox');
|
|
41
|
+
this.#internals.role = 'listbox';
|
|
37
42
|
this.tabIndex = 0;
|
|
38
43
|
this.addEventListener('keydown', this.#onListboxKeyDown, options);
|
|
39
44
|
this.addEventListener('focus', this.#onFocus, options);
|
|
@@ -53,6 +58,23 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
|
|
|
53
58
|
this.#controller.abort();
|
|
54
59
|
this.#controller = null;
|
|
55
60
|
}
|
|
61
|
+
formAssociatedCallback() {
|
|
62
|
+
setFormValue(this.#internals, CSVToFormData(this.name, this.value));
|
|
63
|
+
}
|
|
64
|
+
formResetCallback() {
|
|
65
|
+
this.value = '';
|
|
66
|
+
setFormValue(this.#internals, '');
|
|
67
|
+
}
|
|
68
|
+
formStateRestoreCallback(state) {
|
|
69
|
+
if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (state !== null) {
|
|
73
|
+
const value = typeof state === 'string' ? state : state.get(this.name);
|
|
74
|
+
this.value = value?.toString() ?? '';
|
|
75
|
+
setFormValue(this.#internals, CSVToFormData(this.name, value?.toString() ?? ''));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
56
78
|
static get observedAttributes() {
|
|
57
79
|
return ['value', 'rows', 'multiple', 'search-value', 'search-placeholder', 'search-autocomplete'];
|
|
58
80
|
}
|
|
@@ -62,6 +84,7 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
|
|
|
62
84
|
{
|
|
63
85
|
this.#onValueChange(this.value);
|
|
64
86
|
updateExplicitBooleanAttribute(this, 'aria-multiselectable', isAttrTrue(newVal));
|
|
87
|
+
this.#internals.ariaMultiSelectable = isAttrTrue(newVal).toString();
|
|
65
88
|
break;
|
|
66
89
|
}
|
|
67
90
|
case 'search-autocomplete':
|
|
@@ -99,6 +122,12 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
|
|
|
99
122
|
}
|
|
100
123
|
}
|
|
101
124
|
}
|
|
125
|
+
set name(value) {
|
|
126
|
+
updateAttribute(this, 'name', value);
|
|
127
|
+
}
|
|
128
|
+
get name() {
|
|
129
|
+
return getAttribute(this, 'name', '');
|
|
130
|
+
}
|
|
102
131
|
set value(value) {
|
|
103
132
|
updateAttribute(this, 'value', value);
|
|
104
133
|
}
|
|
@@ -265,12 +294,18 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
|
|
|
265
294
|
const isChecked = !getBooleanAttribute($option, 'disabled') && values.includes(getAttribute($option, 'value', ''));
|
|
266
295
|
updateBooleanAttribute($option, 'data-checked', isChecked);
|
|
267
296
|
}
|
|
297
|
+
const formData = new FormData();
|
|
298
|
+
values.forEach(value => {
|
|
299
|
+
formData.append(this.name, value);
|
|
300
|
+
});
|
|
301
|
+
setFormValue(this.#internals, formData);
|
|
268
302
|
} else {
|
|
269
303
|
const value = getFirstCsvValue(csv);
|
|
270
304
|
for (const $option of this.#getOptionElements()) {
|
|
271
305
|
const isChecked = !getBooleanAttribute($option, 'disabled') && value === getAttribute($option, 'value', '');
|
|
272
306
|
updateBooleanAttribute($option, 'data-checked', isChecked);
|
|
273
307
|
}
|
|
308
|
+
setFormValue(this.#internals, value ?? '');
|
|
274
309
|
}
|
|
275
310
|
}
|
|
276
311
|
#getFirstOption() {
|
package/select-menu/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
|
|
2
2
|
export type TSinchSelectMenuProps = {
|
|
3
|
+
/** Identification for uncontrolled form submissions */
|
|
4
|
+
name?: string;
|
|
3
5
|
/** Selected value, CSV when multiple */
|
|
4
6
|
value: string;
|
|
5
7
|
/** How many rows to show and scroll the rest */
|
package/textarea/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context, defineCustomElement, getAttribute, getBooleanAttribute, getIntegerAttribute, getReactEventHandler, getRect, hasClass, isAttrEqual, isAttrTrue, NectaryElement, setClass, updateAttribute, updateBooleanAttribute } from '../utils';
|
|
2
|
+
import { setFormValue } from '../utils/form';
|
|
2
3
|
import { DEFAULT_SIZE } from '../utils/size';
|
|
3
4
|
const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;position:relative;width:100%;box-sizing:border-box;background-color:var(--sinch-comp-textarea-color-default-background-initial);border-radius:var(--sinch-local-shape-radius);padding-right:2px;overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input{all:initial;display:block;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);resize:none;white-space:pre-wrap;overflow-wrap:break-word;padding:8px 10px 8px 12px;border:none;box-sizing:border-box}#input::placeholder{color:var(--sinch-comp-textarea-color-default-text-placeholder);opacity:1}#input:disabled{color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}#border{position:absolute;border:1px solid var(--sinch-comp-textarea-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([invalid]) #border{border-color:var(--sinch-comp-textarea-color-invalid-border-initial)}#input:focus+#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}#input:disabled+#border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}#bottom{display:flex;flex-direction:row;align-items:center;gap:8px;padding:12px 4px 4px}#bottom.empty{display:none}:host([resizable]) #bottom{padding-right:calc(var(--sinch-comp-textarea-size-resize-handle) + 4px)}#resize-handle{display:none;position:absolute;width:var(--sinch-comp-textarea-size-resize-handle);height:var(--sinch-comp-textarea-size-resize-handle);bottom:0;right:0;cursor:ns-resize}:host([resizable]) #resize-handle{display:block}#resize-icon{display:block;pointer-events:none;fill:var(--sinch-comp-textarea-color-default-border-initial)}</style><div id="wrapper"><textarea id="input"></textarea><div id="border"></div><div id="bottom"><slot name="bottom"></slot><div id="resize-handle"><svg id="resize-icon" width="16" height="16"><path d="m14.833 4.724-9.61 9.61-.942-.944 9.61-9.609.942.943ZM15.443 10 10.5 14.943 9.557 14 14.5 9.057l.943.943Z"/></svg></div></div></div>';
|
|
4
5
|
const template = document.createElement('template');
|
|
@@ -15,12 +16,15 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
15
16
|
#prevContentHeight = 0;
|
|
16
17
|
#dragStartY = 0;
|
|
17
18
|
#intersectionObserver = null;
|
|
19
|
+
#internals;
|
|
20
|
+
static formAssociated = true;
|
|
18
21
|
constructor() {
|
|
19
22
|
super();
|
|
20
23
|
const shadowRoot = this.attachShadow({
|
|
21
24
|
delegatesFocus: true
|
|
22
25
|
});
|
|
23
26
|
shadowRoot.appendChild(template.content.cloneNode(true));
|
|
27
|
+
this.#internals = this.attachInternals();
|
|
24
28
|
this.#$input = shadowRoot.querySelector('#input');
|
|
25
29
|
this.#$bottomSlot = shadowRoot.querySelector('slot[name="bottom"]');
|
|
26
30
|
this.#$bottomWrapper = shadowRoot.querySelector('#bottom');
|
|
@@ -34,7 +38,9 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
34
38
|
signal: this.#controller.signal
|
|
35
39
|
};
|
|
36
40
|
this.setAttribute('role', 'textbox');
|
|
41
|
+
this.#internals.role = 'textbox';
|
|
37
42
|
this.ariaMultiLine = 'true';
|
|
43
|
+
this.#internals.ariaMultiLine = 'true';
|
|
38
44
|
this.#$input.addEventListener('input', this.#onInput, options);
|
|
39
45
|
this.#$input.addEventListener('compositionstart', this.#onCompositionStart, options);
|
|
40
46
|
this.#$input.addEventListener('mousedown', this.#onSelectionChange, options);
|
|
@@ -60,6 +66,23 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
60
66
|
this.#intersectionObserver = null;
|
|
61
67
|
}
|
|
62
68
|
}
|
|
69
|
+
formAssociatedCallback() {
|
|
70
|
+
setFormValue(this.#internals, this.#$input.value);
|
|
71
|
+
}
|
|
72
|
+
formResetCallback() {
|
|
73
|
+
this.#$input.value = '';
|
|
74
|
+
setFormValue(this.#internals, '');
|
|
75
|
+
}
|
|
76
|
+
formStateRestoreCallback(state) {
|
|
77
|
+
if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (state !== null) {
|
|
81
|
+
const value = typeof state === 'string' ? state : state.get(this.name);
|
|
82
|
+
this.#$input.value = value?.toString() ?? '';
|
|
83
|
+
setFormValue(this.#internals, value?.toString() ?? '');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
63
86
|
static get observedAttributes() {
|
|
64
87
|
return ['value', 'placeholder', 'invalid', 'disabled', 'rows', 'minrows', 'resizable'];
|
|
65
88
|
}
|
|
@@ -74,6 +97,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
74
97
|
const isPrevCursorEnd = prevCursorPos === prevVal.length;
|
|
75
98
|
const isShrinkingContent = nextVal.length < prevVal.length;
|
|
76
99
|
this.#$input.value = nextVal;
|
|
100
|
+
setFormValue(this.#internals, nextVal);
|
|
77
101
|
if (!this.resizable) {
|
|
78
102
|
if (isShrinkingContent) {
|
|
79
103
|
this.#$input.style.removeProperty('height');
|
|
@@ -94,6 +118,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
94
118
|
{
|
|
95
119
|
this.#$input.placeholder = newVal ?? '';
|
|
96
120
|
updateAttribute(this, 'aria-placeholder', newVal);
|
|
121
|
+
this.#internals.ariaPlaceholder = newVal ?? '';
|
|
97
122
|
break;
|
|
98
123
|
}
|
|
99
124
|
case 'invalid':
|
|
@@ -103,6 +128,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
103
128
|
}
|
|
104
129
|
const isInvalid = isAttrTrue(newVal);
|
|
105
130
|
this.ariaInvalid = isInvalid.toString();
|
|
131
|
+
this.#internals.ariaInvalid = isInvalid.toString();
|
|
106
132
|
updateBooleanAttribute(this, 'invalid', isInvalid);
|
|
107
133
|
break;
|
|
108
134
|
}
|
|
@@ -140,6 +166,12 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
140
166
|
}
|
|
141
167
|
}
|
|
142
168
|
}
|
|
169
|
+
set name(value) {
|
|
170
|
+
updateAttribute(this, 'name', value);
|
|
171
|
+
}
|
|
172
|
+
get name() {
|
|
173
|
+
return getAttribute(this, 'name', '');
|
|
174
|
+
}
|
|
143
175
|
set value(value) {
|
|
144
176
|
updateAttribute(this, 'value', value);
|
|
145
177
|
}
|
|
@@ -250,6 +282,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
|
|
|
250
282
|
e.stopPropagation();
|
|
251
283
|
const nextValue = this.#$input.value;
|
|
252
284
|
const prevValue = this.value;
|
|
285
|
+
setFormValue(this.#internals, nextValue);
|
|
253
286
|
if (prevValue !== nextValue) {
|
|
254
287
|
const nextCursorPos = this.#$input.selectionEnd;
|
|
255
288
|
if (!this.#isPendingDk) {
|
package/textarea/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
|
|
2
2
|
export type TSinchTextareaProps = {
|
|
3
|
+
/** Identification for uncontrolled form submissions */
|
|
4
|
+
name?: string;
|
|
3
5
|
/** Value */
|
|
4
6
|
value: string;
|
|
5
7
|
/** Text that appears in the text field when it has no value set */
|
package/utils/form.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NectaryComponentVanilla } from '../types';
|
|
2
|
+
export declare const setFormValue: (internals: ElementInternals, value: File | string | FormData | null) => void;
|
|
3
|
+
/**
|
|
4
|
+
* The ElementInternals API currently does not support web components as form submitters,
|
|
5
|
+
* so we need to create a native button and copy form-related options to it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const requestSubmitForm: (form: HTMLFormElement, submitter: NectaryComponentVanilla<"sinch-button">) => void;
|
|
8
|
+
export declare const CSVToFormData: (name: string, csv: string) => "" | FormData;
|
package/utils/form.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const setFormValue = (internals, value) => {
|
|
2
|
+
let formValue = value ?? '';
|
|
3
|
+
if (formValue instanceof FormData && [...formValue.keys()].length === 0) {
|
|
4
|
+
formValue = '';
|
|
5
|
+
}
|
|
6
|
+
if (formValue instanceof File && formValue.size === 0) {
|
|
7
|
+
formValue = '';
|
|
8
|
+
}
|
|
9
|
+
if (internals.form !== null) {
|
|
10
|
+
internals.setFormValue(formValue);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export const requestSubmitForm = (form, submitter) => {
|
|
14
|
+
const submitterProxy = document.createElement('button');
|
|
15
|
+
submitterProxy.style.display = 'none';
|
|
16
|
+
submitterProxy.type = submitter.formType;
|
|
17
|
+
Array.from(submitter.attributes).filter(attr => attr.name.startsWith('aria-')).forEach(attr => {
|
|
18
|
+
submitterProxy.setAttribute(attr.name, attr.value);
|
|
19
|
+
});
|
|
20
|
+
form.appendChild(submitterProxy);
|
|
21
|
+
try {
|
|
22
|
+
form.requestSubmit(submitterProxy);
|
|
23
|
+
} finally {
|
|
24
|
+
form.removeChild(submitterProxy);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const CSVToFormData = (name, csv) => {
|
|
28
|
+
if (csv.length === 0) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
const formData = new FormData();
|
|
32
|
+
csv.split(',').forEach(value => {
|
|
33
|
+
formData.append(name, value);
|
|
34
|
+
});
|
|
35
|
+
return formData;
|
|
36
|
+
};
|