@descope/web-components-ui 1.0.74 → 1.0.76

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/dist/index.esm.js +2550 -632
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/umd/447.js +1 -1
  4. package/dist/umd/744.js +1 -1
  5. package/dist/umd/878.js +1 -0
  6. package/dist/umd/descope-combo-box-index-js.js +1 -1
  7. package/dist/umd/descope-passcode-descope-passcode-internal-index-js.js +1 -1
  8. package/dist/umd/descope-passcode-index-js.js +1 -1
  9. package/dist/umd/descope-phone-field-descope-phone-field-internal-index-js.js +1 -0
  10. package/dist/umd/descope-phone-field-index-js.js +1 -0
  11. package/dist/umd/descope-text-area-index-js.js +1 -1
  12. package/dist/umd/descope-text-field-index-js.js +1 -1
  13. package/dist/umd/index.js +1 -1
  14. package/package.json +1 -1
  15. package/src/components/descope-combo-box/ComboBox.js +85 -50
  16. package/src/components/descope-passcode/Passcode.js +43 -12
  17. package/src/components/descope-passcode/descope-passcode-internal/PasscodeInternal.js +32 -14
  18. package/src/components/descope-phone-field/CountryCodes.js +1212 -0
  19. package/src/components/descope-phone-field/PhoneField.js +186 -0
  20. package/src/components/descope-phone-field/descope-phone-field-internal/PhoneFieldInternal.js +213 -0
  21. package/src/components/descope-phone-field/descope-phone-field-internal/index.js +6 -0
  22. package/src/components/descope-phone-field/helpers.js +23 -0
  23. package/src/components/descope-phone-field/index.js +9 -0
  24. package/src/components/descope-text-area/TextArea.js +3 -1
  25. package/src/components/descope-text-field/TextField.js +38 -3
  26. package/src/components/descope-text-field/textFieldMappings.js +22 -14
  27. package/src/index.js +1 -0
  28. package/src/mixins/inputValidationMixin.js +21 -2
  29. package/src/mixins/normalizeBooleanAttributesMixin.js +16 -7
  30. package/src/mixins/portalMixin.js +8 -3
  31. package/src/mixins/proxyInputMixin.js +1 -1
  32. package/src/theme/components/button.js +0 -3
  33. package/src/theme/components/comboBox.js +39 -9
  34. package/src/theme/components/index.js +3 -1
  35. package/src/theme/components/passcode.js +36 -3
  36. package/src/theme/components/phoneField.js +74 -0
  37. package/src/theme/components/textArea.js +5 -2
  38. package/src/theme/components/textField.js +6 -3
@@ -0,0 +1,186 @@
1
+ import { componentName as descopeInternalComponentName } from './descope-phone-field-internal/PhoneFieldInternal';
2
+ import { forwardAttrs, getComponentName } from '../../helpers/componentHelpers';
3
+ import { compose } from '../../helpers';
4
+ import {
5
+ createProxy,
6
+ createStyleMixin,
7
+ draggableMixin,
8
+ proxyInputMixin
9
+ } from '../../mixins';
10
+
11
+ import TextField from '../descope-text-field/TextField';
12
+ import ComboBox from '../descope-combo-box/ComboBox';
13
+
14
+ const textVars = TextField.cssVarList;
15
+ const comboVars = ComboBox.cssVarList;
16
+
17
+ export const componentName = getComponentName('phone-field');
18
+
19
+ const customMixin = (superclass) =>
20
+ class PhoneFieldClass extends superclass {
21
+ constructor() {
22
+ super();
23
+ }
24
+
25
+ init() {
26
+ super.init?.();
27
+
28
+ const template = document.createElement('template');
29
+
30
+ template.innerHTML = `
31
+ <${descopeInternalComponentName}
32
+ tabindex="-1"
33
+ slot="input"
34
+ ></${descopeInternalComponentName}>
35
+ `;
36
+
37
+ this.baseElement.appendChild(template.content.cloneNode(true));
38
+
39
+ this.inputElement = this.shadowRoot.querySelector(descopeInternalComponentName);
40
+
41
+ forwardAttrs(this.shadowRoot.host, this.inputElement, {
42
+ includeAttrs: [
43
+ 'size',
44
+ 'bordered',
45
+ 'invalid',
46
+ 'minlength',
47
+ 'maxlength',
48
+ 'default-code',
49
+ 'country-input-placeholder',
50
+ 'phone-input-placeholder',
51
+ ]
52
+ });
53
+ }
54
+ };
55
+
56
+ const {
57
+ inputWrapper,
58
+ countryCodeInput,
59
+ phoneInput,
60
+ label,
61
+ requiredIndicator,
62
+ separator
63
+ } = {
64
+ inputWrapper: { selector: '::part(input-field)' },
65
+ phoneInput: { selector: () => 'descope-text-field' },
66
+ countryCodeInput: { selector: () => 'descope-combo-box' },
67
+ label: { selector: '> label' },
68
+ requiredIndicator: { selector: '[required]::part(required-indicator)::after' },
69
+ separator: { selector: 'descope-phone-field-internal .separator' }
70
+ };
71
+
72
+ const PhoneField = compose(
73
+ createStyleMixin({
74
+ mappings: {
75
+ componentWidth: { selector: () => ':host', property: 'width' },
76
+
77
+ wrapperBorderStyle: [
78
+ { ...inputWrapper, property: 'border-style' },
79
+ { ...separator, property: 'border-left-style' }
80
+ ],
81
+ wrapperBorderWidth: [
82
+ { ...inputWrapper, property: 'border-width' },
83
+ { ...separator, property: 'border-left-width' }
84
+ ],
85
+ wrapperBorderColor: [
86
+ { ...inputWrapper, property: 'border-color' },
87
+ { ...separator, property: 'border-left-color' }
88
+ ],
89
+ wrapperBorderRadius: { ...inputWrapper, property: 'border-radius' },
90
+
91
+ inputHeight: { ...inputWrapper, property: 'height' },
92
+
93
+ countryCodeInputWidth: { ...countryCodeInput, property: comboVars.width },
94
+ countryCodeDropdownWidth: {
95
+ ...countryCodeInput,
96
+ property: '--vaadin-combo-box-overlay-width'
97
+ },
98
+
99
+ phoneInputWidth: { ...phoneInput, property: 'width' },
100
+
101
+ color: [label, requiredIndicator, {...phoneInput, property: textVars.color}, {...countryCodeInput, property: comboVars.color}],
102
+
103
+ placeholderColor: {
104
+ ...phoneInput,
105
+ property: textVars.placeholderColor
106
+ },
107
+
108
+ overlayItemBackgroundColor: {
109
+ selector: 'descope-combo-box',
110
+ property: comboVars.overlayItemBackgroundColor
111
+ },
112
+ },
113
+ }),
114
+ draggableMixin,
115
+ proxyInputMixin,
116
+ customMixin,
117
+ )(
118
+ createProxy({
119
+ slots: [],
120
+ wrappedEleName: 'vaadin-text-field',
121
+ style: () => `
122
+ :host {
123
+ --vaadin-field-default-width: auto;
124
+ display: inline-block;
125
+ }
126
+ div {
127
+ display: inline-flex;
128
+ }
129
+ vaadin-text-field {
130
+ padding: 0;
131
+ width: 100%;
132
+ height: 100%;
133
+ }
134
+ vaadin-text-field::part(input-field) {
135
+ padding: 0;
136
+ min-height: 0;
137
+ background: transparent;
138
+ overflow: hidden;
139
+ }
140
+ descope-phone-field-internal {
141
+ -webkit-mask-image: none;
142
+ padding: 0;
143
+ min-height: 0;
144
+ width: 100%;
145
+ height: 100%;
146
+ }
147
+ descope-phone-field-internal > div {
148
+ width: 100%;
149
+ height: 100%;
150
+ }
151
+ descope-phone-field-internal .separator {
152
+ flex: 0;
153
+ border: none;
154
+ }
155
+ descope-combo-box {
156
+ flex-shrink: 0;
157
+ height: 100%;
158
+ ${comboVars.borderWidth}: 0;
159
+ }
160
+ descope-text-field {
161
+ flex-grow: 1;
162
+ min-height: 0;
163
+ height: 100%;
164
+ ${textVars.borderWidth}: 0;
165
+ ${textVars.borderRadius}: 0;
166
+ }
167
+ vaadin-text-field[required]::part(required-indicator)::after {
168
+ content: "*";
169
+ }
170
+ vaadin-text-field[readonly] > input:placeholder-shown {
171
+ opacity: 1;
172
+ }
173
+ `,
174
+ excludeAttrsSync: ['tabindex'],
175
+ componentName
176
+ })
177
+ );
178
+
179
+ export default PhoneField;
180
+
181
+ /*
182
+ Bugs:
183
+ - default code value, open the dropdown and click outside, the value is gone
184
+ - make invalid by blur, enter a 6 digit number, component is valid but the divider is red
185
+ - missing handling of outline when focused, hiding the divider when focusing the phone
186
+ */
@@ -0,0 +1,213 @@
1
+ import { createBaseInputClass } from '../../../baseClasses/createBaseInputClass';
2
+ import { getComponentName } from '../../../helpers/componentHelpers';
3
+ import { createDispatchEvent } from '../../../helpers/mixinsHelpers';
4
+ import CountryCodes from '../CountryCodes';
5
+ import { comboBoxItem } from '../helpers';
6
+
7
+ export const componentName = getComponentName('phone-field-internal');
8
+
9
+ const commonAttrs = [
10
+ 'disabled',
11
+ 'size',
12
+ 'bordered',
13
+ 'invalid',
14
+ ];
15
+ const countryAttrs = ['country-input-placeholder', 'default-code'];
16
+ const phoneAttrs = ['phone-input-placeholder', 'maxlength'];
17
+ const mapAttrs = {
18
+ 'country-input-placeholder': 'placeholder',
19
+ 'phone-input-placeholder': 'placeholder',
20
+ }
21
+
22
+ const inputRelatedAttrs = [].concat(commonAttrs, countryAttrs, phoneAttrs);
23
+
24
+ const BaseInputClass = createBaseInputClass({ componentName, baseSelector: 'div' });
25
+
26
+ class PhoneFieldInternal extends BaseInputClass {
27
+ static get observedAttributes() {
28
+ return [].concat(
29
+ BaseInputClass.observedAttributes || [],
30
+ inputRelatedAttrs,
31
+ );
32
+ }
33
+
34
+ #dispatchBlur = createDispatchEvent.bind(this, 'blur');
35
+ #dispatchFocus = createDispatchEvent.bind(this, 'focus');
36
+
37
+ constructor() {
38
+ super();
39
+
40
+ this.innerHTML = `
41
+ <div>
42
+ <descope-combo-box
43
+ item-label-path="data-name"
44
+ item-value-path="data-id"
45
+ >
46
+ ${CountryCodes.map(item => comboBoxItem(item)).join('')}
47
+ </descope-combo-box>
48
+ <div class="separator"></div>
49
+ <descope-text-field type="tel"></descope-text-field>
50
+ </div>`;
51
+
52
+ this.countryCodeInput = this.querySelector('descope-combo-box');
53
+ this.phoneNumberInput = this.querySelector('descope-text-field');
54
+ this.inputs = [
55
+ this.countryCodeInput,
56
+ this.phoneNumberInput
57
+ ];
58
+ }
59
+
60
+ get value() {
61
+ return this.inputs.map(({ value }) => value).join('-');
62
+ }
63
+
64
+ set value(val) {
65
+ const [countryCode = '', phoneNumber = ''] = val.split('-');
66
+ this.countryCodeInput.value = countryCode;
67
+ this.phoneNumberInput.value = phoneNumber;
68
+ }
69
+
70
+ get phoneNumberValue() {
71
+ return this.phoneNumberInput.value;
72
+ }
73
+
74
+ get countryCodeValue() {
75
+ return this.countryCodeInput.shadowRoot.querySelector('input').value;
76
+ }
77
+
78
+ get minLength() {
79
+ return parseInt(this.getAttribute('minlength'), 10) || 0;
80
+ }
81
+
82
+ getValidity() {
83
+ const hasCode = this.countryCodeInput.value;
84
+ const hasPhone = this.phoneNumberInput.value;
85
+ const emptyValue = !hasCode || !hasPhone;
86
+ const hasMinPhoneLength = this.phoneNumberInput.value.length && this.phoneNumberInput.value.length < this.minLength;
87
+
88
+ if (this.isRequired && emptyValue) {
89
+ return { valueMissing: true };
90
+ }
91
+ if (hasMinPhoneLength) {
92
+ return { tooShort: true };
93
+ }
94
+ if ((hasCode && !hasPhone) || (!hasCode && hasPhone)) {
95
+ return { valueMissing: true };
96
+ }
97
+ return {}
98
+ };
99
+
100
+ init() {
101
+ super.init();
102
+ this.initInputs();
103
+ this.setComboBoxDescriptor();
104
+ }
105
+
106
+ handleDefaultCountryCode(countryCode) {
107
+ if (!this.countryCodeInput.value) {
108
+ const countryData = this.countryCodeInput.items.find(c => c['data-id'] === countryCode)?.['data-name'];
109
+
110
+ // When replacing the input component (inserting internal component into text-field) -
111
+ // Vaadin resets the input's value. We use setTimeout in order to make sure this happens
112
+ // after the reset.
113
+ if (countryData) {
114
+ setTimeout(() => this.countryCodeInput.value = countryData);
115
+ }
116
+ }
117
+ }
118
+
119
+ // We want to override Vaadin's Combo Box value setter. This is needed since Vaadin couples between the
120
+ // field that it searches the value, and the finaly display value of the input. We want to search ALL
121
+ // the values (so we made a field with all the values), but display ONLY the dial code, so we added
122
+ // this setter, which does that.
123
+ setComboBoxDescriptor() {
124
+ const comboBox = this.countryCodeInput;
125
+ const input = comboBox.shadowRoot.querySelector('input');
126
+ const valueDescriptor = Object.getOwnPropertyDescriptor(
127
+ input.constructor.prototype,
128
+ 'value'
129
+ );
130
+ Object.defineProperties(input, {
131
+ value: {
132
+ ...valueDescriptor,
133
+ set(val) {
134
+ if (!comboBox.items?.length) {
135
+ return;
136
+ }
137
+
138
+ const [_code, dialCode] = val.split(' ');
139
+
140
+ if (val === this.value) {
141
+ return;
142
+ }
143
+
144
+ valueDescriptor.set.call(this, dialCode || '');
145
+ }
146
+ }
147
+ });
148
+ }
149
+
150
+ initInputs() {
151
+ let prevVal = this.value
152
+ let blurTimerId
153
+
154
+ // Sanitize phone input value to filter everything but digits
155
+ this.phoneNumberInput.addEventListener('input', (e) => {
156
+ const telDigitsRegExp = /^\d$/;
157
+ const sanitizedInput = e.target.value
158
+ .split('')
159
+ .filter(char => telDigitsRegExp.test(char))
160
+ .join('');
161
+ e.target.value = sanitizedInput;
162
+ });
163
+
164
+ this.inputs.forEach(input => {
165
+ input.addEventListener('blur', (e) => {
166
+ e.stopImmediatePropagation();
167
+ blurTimerId = setTimeout(() => {
168
+ blurTimerId = null
169
+ this.#dispatchBlur()
170
+ });
171
+ })
172
+
173
+ input.addEventListener('focus', (e) => {
174
+ e.stopImmediatePropagation();
175
+ clearTimeout(blurTimerId)
176
+ if (!blurTimerId) {
177
+ this.#dispatchFocus()
178
+ }
179
+ })
180
+
181
+ input.addEventListener('input', (e) => {
182
+ if (prevVal === this.value) {
183
+ e.stopImmediatePropagation();
184
+ }
185
+ });
186
+ });
187
+ }
188
+
189
+ attributeChangedCallback(attrName, oldValue, newValue) {
190
+ super.attributeChangedCallback(attrName, oldValue, newValue);
191
+
192
+ if (oldValue !== newValue) {
193
+ if (attrName === 'default-code' && newValue) {
194
+ this.handleDefaultCountryCode(newValue);
195
+ }
196
+ else if (inputRelatedAttrs.includes(attrName)) {
197
+ const attr = mapAttrs[attrName] || attrName;
198
+
199
+ if (commonAttrs.includes(attrName)) {
200
+ this.inputs.forEach(input => input.setAttribute(attr, newValue));
201
+ }
202
+ else if (countryAttrs.includes(attrName)) {
203
+ this.countryCodeInput.setAttribute(attr, newValue);
204
+ }
205
+ else if (phoneAttrs.includes(attrName)) {
206
+ this.phoneNumberInput.setAttribute(attr, newValue);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ export default PhoneFieldInternal;
@@ -0,0 +1,6 @@
1
+ import '../../descope-combo-box';
2
+ import '../../descope-text-field';
3
+
4
+ import PhoneFieldInternal, { componentName } from './PhoneFieldInternal';
5
+
6
+ customElements.define(componentName, PhoneFieldInternal);
@@ -0,0 +1,23 @@
1
+ // We use JSDelivr in order to fetch the images as image file from this library (svg-country-flags)
2
+ // This reduces our bundle size, and we use it as a static remote resource.
3
+ export const getCountryFlag = (code) =>
4
+ `https://cdn.jsdelivr.net/npm/svg-country-flags@1.2.10/svg/${code.toLowerCase()}.svg`;
5
+
6
+ export const comboBoxItem = ({ code, dialCode, name: country }) => (`
7
+ <div
8
+ style="display:flex; flex-direction: column;"
9
+ data-id="${code}"
10
+ data-name="${code} ${dialCode} ${country}"
11
+ >
12
+ <div>
13
+ <span>
14
+ <img src="${getCountryFlag(code)}" width="20"/>
15
+ </span>
16
+ <span>${country}</span>
17
+ </div>
18
+ <div>
19
+ <span>${code}</span>
20
+ <span>${dialCode}</span>
21
+ </div>
22
+ </div>
23
+ `);
@@ -0,0 +1,9 @@
1
+ import './descope-phone-field-internal';
2
+ import '../descope-combo-box';
3
+ import '../descope-text-field';
4
+
5
+ import PhoneField, { componentName } from './PhoneField';
6
+
7
+ customElements.define(componentName, PhoneField);
8
+
9
+ export { PhoneField };
@@ -31,7 +31,9 @@ const TextArea = compose(
31
31
  borderStyle: { selector: selectors.input },
32
32
  borderColor: { selector: selectors.input },
33
33
  borderRadius: { selector: selectors.input },
34
- outline: { selector: selectors.input },
34
+ outlineWidth: { selector: selectors.input },
35
+ outlineStyle: { selector: selectors.input },
36
+ outlineColor: { selector: selectors.input },
35
37
  outlineOffset: { selector: selectors.input }
36
38
  }
37
39
  }),
@@ -13,13 +13,36 @@ export const componentName = getComponentName('text-field');
13
13
 
14
14
  let overrides = ``;
15
15
 
16
+ const observedAttrs = ['type'];
17
+
18
+ const customMixin = (superclass) =>
19
+ class TextFieldClass extends superclass {
20
+ static get observedAttributes() {
21
+ return observedAttrs.concat(superclass.observedAttributes || []);
22
+ }
23
+
24
+ attributeChangedCallback(attrName, oldVal, newVal) {
25
+ super.attributeChangeCallback?.(attrName, oldVal, newVal);
26
+
27
+ // Vaadin doesn't allow to change the input type attribute.
28
+ // We need the ability to do that, so we're overriding their
29
+ // behavior with their private API.
30
+ // When receiving a `type` attribute, we use their private API
31
+ // to set it on the input.
32
+ if (attrName === 'type') {
33
+ this.baseElement._setType(newVal);
34
+ }
35
+ }
36
+ }
37
+
16
38
  const TextField = compose(
17
39
  createStyleMixin({
18
40
  mappings: textFieldMappings
19
41
  }),
20
42
  draggableMixin,
21
43
  proxyInputMixin,
22
- componentNameValidationMixin
44
+ componentNameValidationMixin,
45
+ customMixin
23
46
  )(
24
47
  createProxy({
25
48
  slots: ['prefix', 'suffix'],
@@ -33,14 +56,18 @@ const TextField = compose(
33
56
  overrides = `
34
57
  :host {
35
58
  display: inline-block;
59
+ --vaadin-field-default-width: auto;
36
60
  }
37
-
38
61
  vaadin-text-field {
39
62
  margin: 0;
40
63
  padding: 0;
64
+ width: 100%;
65
+ height: 100%;
41
66
  }
67
+
42
68
  vaadin-text-field::part(input-field) {
43
69
  overflow: hidden;
70
+ padding: 0;
44
71
  }
45
72
  vaadin-text-field[readonly] > input:placeholder-shown {
46
73
  opacity: 1;
@@ -53,9 +80,13 @@ overrides = `
53
80
  -webkit-text-fill-color: var(${TextField.cssVarList.color});
54
81
  box-shadow: 0 0 0 var(${TextField.cssVarList.height}) var(${TextField.cssVarList.backgroundColor}) inset;
55
82
  }
83
+
84
+ vaadin-text-field input {
85
+ -webkit-mask-image: none;
86
+ }
87
+
56
88
  vaadin-text-field > label,
57
89
  vaadin-text-field::part(input-field) {
58
- cursor: pointer;
59
90
  color: var(${TextField.cssVarList.color});
60
91
  }
61
92
  vaadin-text-field::part(input-field):focus {
@@ -69,6 +100,10 @@ overrides = `
69
100
  vaadin-text-field[readonly]::part(input-field)::after {
70
101
  border: 0 solid;
71
102
  }
103
+
104
+ vaadin-text-field::before {
105
+ height: unset;
106
+ }
72
107
  `;
73
108
 
74
109
  export default TextField;
@@ -1,34 +1,42 @@
1
1
  const selectors = {
2
2
  label: '::part(label)',
3
- input: '::part(input-field)',
3
+ inputWrapper: '::part(input-field)',
4
4
  readOnlyInput: '[readonly]::part(input-field)::after',
5
5
  placeholder: '> input:placeholder-shown',
6
- host: () => ':host'
6
+ host: () => ':host',
7
+ input: 'input'
7
8
  };
8
9
 
9
10
  export default {
10
- backgroundColor: { selector: selectors.input },
11
- color: { selector: selectors.input },
11
+ backgroundColor: { selector: selectors.inputWrapper },
12
+ color: { selector: selectors.inputWrapper },
12
13
  width: { selector: selectors.host },
13
14
  borderColor: [
14
- { selector: selectors.input },
15
+ { selector: selectors.inputWrapper },
15
16
  { selector: selectors.readOnlyInput }
16
17
  ],
17
18
  borderWidth: [
18
- { selector: selectors.input },
19
+ { selector: selectors.inputWrapper },
19
20
  { selector: selectors.readOnlyInput }
20
21
  ],
21
22
  borderStyle: [
22
- { selector: selectors.input },
23
+ { selector: selectors.inputWrapper },
23
24
  { selector: selectors.readOnlyInput }
24
25
  ],
25
- borderRadius: { selector: selectors.input },
26
- boxShadow: { selector: selectors.input },
27
- fontSize: {},
28
- height: { selector: selectors.input },
29
- padding: { selector: selectors.input },
30
- outline: { selector: selectors.input },
31
- outlineOffset: { selector: selectors.input },
26
+ borderRadius: { selector: selectors.inputWrapper },
27
+ boxShadow: { selector: selectors.inputWrapper },
32
28
 
29
+ // we apply font-size also on the host so we can set its width with em
30
+ fontSize: [{}, { selector: selectors.host }],
31
+
32
+ height: { selector: selectors.inputWrapper },
33
+ padding: { selector: selectors.inputWrapper },
34
+ margin: { selector: selectors.inputWrapper },
35
+ caretColor: { selector: selectors.input },
36
+ outlineColor: { selector: selectors.inputWrapper },
37
+ outlineStyle: { selector: selectors.inputWrapper },
38
+ outlineWidth: { selector: selectors.inputWrapper },
39
+ outlineOffset: { selector: selectors.inputWrapper },
40
+ textAlign: {selector: selectors.input},
33
41
  placeholderColor: { selector: selectors.placeholder, property: 'color' }
34
42
  };
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ import './components/descope-text';
16
16
  import './components/descope-text-area';
17
17
  import './components/descope-text-field';
18
18
  import './components/descope-image';
19
+ import './components/descope-phone-field';
19
20
 
20
21
  export {
21
22
  globalsThemeToStyle,
@@ -5,7 +5,9 @@ const observedAttributes = [
5
5
 
6
6
  const errorAttributes = {
7
7
  valueMissing: 'data-errormessage-value-missing',
8
- patternMismatch: 'data-errormessage-pattern-mismatch'
8
+ patternMismatch: 'data-errormessage-pattern-mismatch',
9
+ tooShort: 'data-errormessage-pattern-mismatch-too-short',
10
+ tooLong: 'data-errormessage-pattern-mismatch-too-long',
9
11
  }
10
12
  export const inputValidationMixin = (superclass) => class InputValidationMixinClass extends superclass {
11
13
  static get observedAttributes() {
@@ -35,6 +37,14 @@ export const inputValidationMixin = (superclass) => class InputValidationMixinCl
35
37
  return 'Please match the requested format.'
36
38
  }
37
39
 
40
+ get defaultErrorMsgTooShort() {
41
+ return `Minimum length is ${this.getAttribute('minlength')}.`
42
+ }
43
+
44
+ get defaultErrorMsgTooLong() {
45
+ return `Maximum length is ${this.getAttribute('maxlength')}. `
46
+ }
47
+
38
48
  getErrorMessage(flags) {
39
49
  switch (true) {
40
50
  case flags.valueMissing:
@@ -43,6 +53,12 @@ export const inputValidationMixin = (superclass) => class InputValidationMixinCl
43
53
  case flags.patternMismatch || flags.typeMismatch:
44
54
  return this.getAttribute(errorAttributes.patternMismatch) ||
45
55
  this.defaultErrorMsgPatternMismatch
56
+ case flags.tooShort:
57
+ return this.getAttribute(errorAttributes.tooShort) ||
58
+ this.defaultErrorMsgTooShort
59
+ case flags.tooLong:
60
+ return this.getAttribute(errorAttributes.tooLong) ||
61
+ this.defaultErrorMsgTooLong
46
62
  case flags.customError:
47
63
  return this.validationMessage
48
64
  default:
@@ -110,6 +126,9 @@ export const inputValidationMixin = (superclass) => class InputValidationMixinCl
110
126
  this.addEventListener('invalid', (e) => e.stopPropagation())
111
127
  this.addEventListener('input', this.#setValidity)
112
128
 
113
- this.#setValidity();
129
+ // The attribute sync takes time, so getValidity returns valid,
130
+ // even when it shouldn't. This way allows all attributes to sync
131
+ // after render, before checking the validity.
132
+ setTimeout(() => this.#setValidity());
114
133
  }
115
134
  }
@@ -1,11 +1,20 @@
1
- // we want all the valueless attributes to have "true" value
1
+ import { observeAttributes } from "../helpers/componentHelpers";
2
+
3
+ // we want all the valueless attributes to have "true" value
4
+ // and all the falsy attribute to be removed
2
5
  export const normalizeBooleanAttributesMixin = (superclass) => class NormalizeBooleanAttributesMixinClass extends superclass {
3
- attributeChangedCallback(attrName, oldValue, newValue) {
4
- if (newValue === '') {
5
- this.setAttribute(attrName, 'true')
6
- newValue = 'true'
7
- }
6
+ init() {
7
+ super.init?.();
8
+
9
+ observeAttributes(this, (attrs) =>
10
+ attrs.forEach(attr => {
11
+ const attrVal = this.getAttribute(attr)
8
12
 
9
- super.attributeChangedCallback?.(attrName, oldValue, newValue)
13
+ if (attrVal === '') {
14
+ this.setAttribute(attr, 'true')
15
+ } else if (attrVal === 'false') {
16
+ this.removeAttribute(attr)
17
+ }
18
+ }), {})
10
19
  }
11
20
  }