@descope/web-components-ui 1.0.78 → 1.0.80

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. package/dist/index.esm.js +954 -607
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/umd/387.js +1 -0
  4. package/dist/umd/878.js +1 -1
  5. package/dist/umd/988.js +1 -0
  6. package/dist/umd/descope-button-index-js.js +1 -1
  7. package/dist/umd/descope-checkbox-index-js.js +1 -1
  8. package/dist/umd/descope-combo-box-index-js.js +1 -1
  9. package/dist/umd/descope-container-index-js.js +1 -1
  10. package/dist/umd/descope-date-picker-index-js.js +1 -1
  11. package/dist/umd/descope-divider-index-js.js +1 -1
  12. package/dist/umd/descope-email-field-index-js.js +1 -1
  13. package/dist/umd/descope-image-index-js.js +1 -1
  14. package/dist/umd/descope-link-index-js.js +1 -1
  15. package/dist/umd/descope-loader-linear-index-js.js +1 -1
  16. package/dist/umd/descope-loader-radial-index-js.js +1 -1
  17. package/dist/umd/descope-logo-index-js.js +1 -1
  18. package/dist/umd/descope-new-password-descope-new-password-internal-index-js.js +1 -0
  19. package/dist/umd/descope-new-password-index-js.js +1 -0
  20. package/dist/umd/descope-number-field-index-js.js +1 -1
  21. package/dist/umd/descope-passcode-descope-passcode-internal-index-js.js +1 -1
  22. package/dist/umd/descope-passcode-index-js.js +1 -1
  23. package/dist/umd/descope-password-field-index-js.js +1 -1
  24. package/dist/umd/descope-phone-field-descope-phone-field-internal-index-js.js +1 -1
  25. package/dist/umd/descope-phone-field-index-js.js +1 -1
  26. package/dist/umd/descope-switch-toggle-index-js.js +1 -1
  27. package/dist/umd/descope-text-area-index-js.js +1 -1
  28. package/dist/umd/descope-text-field-index-js.js +1 -1
  29. package/dist/umd/descope-text-index-js.js +1 -1
  30. package/dist/umd/index.js +1 -1
  31. package/package.json +1 -1
  32. package/src/baseClasses/createBaseInputClass.js +1 -2
  33. package/src/components/descope-new-password/NewPassword.js +129 -0
  34. package/src/components/descope-new-password/descope-new-password-internal/NewPasswordInternal.js +189 -0
  35. package/src/components/descope-new-password/descope-new-password-internal/componentName.js +3 -0
  36. package/src/components/descope-new-password/descope-new-password-internal/index.js +4 -0
  37. package/src/components/descope-new-password/index.js +6 -0
  38. package/src/components/descope-passcode/descope-passcode-internal/PasscodeInternal.js +2 -2
  39. package/src/components/descope-password-field/PasswordField.js +35 -36
  40. package/src/components/descope-phone-field/PhoneField.js +3 -2
  41. package/src/components/descope-phone-field/descope-phone-field-internal/PhoneFieldInternal.js +1 -1
  42. package/src/index.js +1 -0
  43. package/src/mixins/createProxy.js +1 -23
  44. package/src/mixins/index.js +0 -1
  45. package/src/mixins/inputEventsDispatchingMixin.js +48 -31
  46. package/src/mixins/inputValidationMixin.js +6 -2
  47. package/src/mixins/proxyInputMixin.js +6 -36
  48. package/src/theme/components/index.js +3 -1
  49. package/src/theme/components/newPassword.js +48 -0
  50. package/src/theme/components/passwordField.js +55 -3
  51. package/dist/umd/933.js +0 -1
  52. package/src/mixins/focusMixin.js +0 -23
@@ -0,0 +1,129 @@
1
+ import { forwardAttrs, getComponentName } from '../../helpers/componentHelpers';
2
+ import { compose } from '../../helpers';
3
+ import {
4
+ createStyleMixin,
5
+ proxyInputMixin,
6
+ draggableMixin,
7
+ createProxy,
8
+ } from '../../mixins';
9
+ import { componentName as descopeInternalComponentName } from './descope-new-password-internal/componentName';
10
+ import PasswordField from "../descope-password-field/PasswordField";
11
+
12
+ export const componentName = getComponentName('new-password');
13
+
14
+ const customMixin = (superclass) =>
15
+ class NewPasswordMixinClass extends superclass {
16
+ constructor() {
17
+ super();
18
+ }
19
+
20
+ init() {
21
+ super.init?.();
22
+
23
+ const template = document.createElement('template');
24
+
25
+ template.innerHTML = `
26
+ <${descopeInternalComponentName}
27
+ name="new-password"
28
+ tabindex="-1"
29
+ slot="input"
30
+ ></${descopeInternalComponentName}>
31
+ `;
32
+
33
+ this.baseElement.appendChild(template.content.cloneNode(true));
34
+
35
+ this.inputElement = this.shadowRoot.querySelector(descopeInternalComponentName);
36
+
37
+ forwardAttrs(this, this.inputElement, {
38
+ includeAttrs: [
39
+ 'password-label',
40
+ 'password-placeholder',
41
+ 'confirm-label',
42
+ 'confirm-placeholder',
43
+ 'full-width',
44
+ 'size',
45
+ 'bordered',
46
+ 'label',
47
+ 'has-confirm',
48
+ 'invalid',
49
+ ]
50
+ });
51
+ }
52
+ };
53
+
54
+ const { host, internalInputsWrapper } = {
55
+ host: { selector: () => ':host' },
56
+ internalInputsWrapper: { selector: 'descope-new-password-internal .wrapper' }
57
+ }
58
+ const NewPassword = compose(
59
+ createStyleMixin({
60
+ mappings: {
61
+ componentWidth: { ...host, property: 'width' },
62
+ requiredContent: { ...host, property: 'content' },
63
+ inputLabelTextColor: {
64
+ selector: PasswordField.componentName,
65
+ property: PasswordField.cssVarList.labelTextColor
66
+ },
67
+ inputTextColor: {
68
+ selector: PasswordField.componentName,
69
+ property: PasswordField.cssVarList.inputTextColor
70
+ },
71
+ inputsGap: {...internalInputsWrapper, property: 'gap'}
72
+ }
73
+ }),
74
+ draggableMixin,
75
+ proxyInputMixin,
76
+ customMixin,
77
+ )(
78
+ createProxy({
79
+ slots: [],
80
+ wrappedEleName: 'vaadin-text-field',
81
+ style: () => overrides,
82
+ excludeAttrsSync: ['tabindex'],
83
+ componentName
84
+ })
85
+ );
86
+
87
+ const overrides = `
88
+ :host {
89
+ --vaadin-field-default-width: auto;
90
+ display: inline-block;
91
+ }
92
+ vaadin-text-field {
93
+ padding: 0;
94
+ width: 100%;
95
+ height: 100%;
96
+ }
97
+ vaadin-text-field::part(input-field) {
98
+ min-height: 0;
99
+ background: transparent;
100
+ overflow: hidden;
101
+ box-shadow: none;
102
+ }
103
+ vaadin-text-field::part(input-field)::after {
104
+ background: transparent;
105
+ opacity: 0;
106
+ }
107
+ descope-new-password-internal {
108
+ -webkit-mask-image: none;
109
+ padding: 0;
110
+ min-height: 0;
111
+ width: 100%;
112
+ height: 100%;
113
+ }
114
+ descope-new-password-internal > .wrapper {
115
+ width: 100%;
116
+ height: 100%;
117
+ display: flex;
118
+ flex-direction: column;
119
+ }
120
+ descope-password-field {
121
+ display: block;
122
+ width: 100%;
123
+ }
124
+ descope-new-password-internal vaadin-password-field::before {
125
+ height: initial;
126
+ }
127
+ `;
128
+
129
+ export default NewPassword;
@@ -0,0 +1,189 @@
1
+ import { createBaseInputClass } from '../../../baseClasses/createBaseInputClass';
2
+ import { observeAttributes } from '../../../helpers/componentHelpers';
3
+ import NewPassword from '../NewPassword';
4
+ import { componentName } from './componentName';
5
+
6
+ const passwordAttrPrefixRegex = /^password-/
7
+ const confirmAttrPrefixRegex = /^confirm-/
8
+
9
+ const removeAttrPrefix = (attr, prefix) => attr.replace(prefix, '')
10
+
11
+ const passwordInputAttrs = ['password-label', 'password-placeholder'];
12
+ const confirmInputAttrs = ['confirm-label', 'confirm-placeholder'];
13
+ const commonAttrs = [
14
+ 'disabled',
15
+ 'bordered',
16
+ 'size',
17
+ 'full-width',
18
+ 'maxlength',
19
+ 'invalid',
20
+ ];
21
+
22
+ const inputRelatedAttrs = [].concat(commonAttrs, passwordInputAttrs, confirmInputAttrs);
23
+
24
+ const BaseInputClass = createBaseInputClass({ componentName, baseSelector: 'div' });
25
+
26
+ class NewPasswordInternal extends BaseInputClass {
27
+ static get observedAttributes() {
28
+ return ['has-confirm'].concat(
29
+ BaseInputClass.observedAttributes || [],
30
+ inputRelatedAttrs,
31
+ );
32
+ }
33
+
34
+ constructor() {
35
+ super();
36
+
37
+ this.innerHTML = `
38
+ <div class="wrapper"></div>
39
+ `;
40
+
41
+ this.wrapperEle = this.querySelector('.wrapper')
42
+ }
43
+
44
+ get value() {
45
+ return this.passwordInput?.value || '';
46
+ }
47
+
48
+ set value(val) {
49
+ if (val === this.value) return;
50
+ this.value = val;
51
+ }
52
+
53
+ get hasConfirm() {
54
+ return this.getAttribute('has-confirm') === 'true';
55
+ }
56
+
57
+ getValidity() {
58
+ if (this.isRequired && !this.value) {
59
+ return { valueMissing: true };
60
+ }
61
+ if (this.hasConfirm && this.confirmInput && this.value !== this.confirmInput.value) {
62
+ return { patternMismatch: true };
63
+ }
64
+
65
+ const min = this.getAttribute('minlength');
66
+ const minVal = parseInt(min, 10) || 0;
67
+ const minValid = this.value.length >= minVal;
68
+ if (!minValid) {
69
+ return { tooShort: true }
70
+ }
71
+
72
+ return {}
73
+ };
74
+
75
+ init() {
76
+ this.addEventListener('focus', (e) => {
77
+ // we want to ignore focus events we are dispatching
78
+ if (e.isTrusted) {
79
+ this.passwordInput.focus()
80
+ }
81
+ });
82
+
83
+ super.init();
84
+ this.renderInputs(this.hasConfirm);
85
+ }
86
+
87
+ renderInputs(shouldRenderConfirm) {
88
+ let template = `<descope-password-field data-id="password"></descope-password-field>`;
89
+
90
+ if (shouldRenderConfirm) {
91
+ template += `<descope-password-field data-id="confirm"></descope-password-field>`
92
+ }
93
+
94
+ this.wrapperEle.innerHTML = template;
95
+
96
+ this.passwordInput = this.querySelector('[data-id="password"]');
97
+ this.confirmInput = this.querySelector('[data-id="confirm"]');
98
+
99
+ this.inputs = [this.passwordInput, this.confirmInput];
100
+
101
+ this.initInputs();
102
+
103
+ // we are calling attributeChangedCallback with all the input related attributes
104
+ // in order to set it on the newly generated input
105
+ [...passwordInputAttrs, ...confirmInputAttrs, ...commonAttrs].forEach(attr => {
106
+ this.attributeChangedCallback(attr, null, this.getAttribute(attr))
107
+ })
108
+ }
109
+
110
+ // the inputs are not required but we still want it to have a required
111
+ // indicator in case the root component is required
112
+ handleIndicatorStyle() {
113
+ for (const input of this.inputs) {
114
+ const styleTag = document.createElement('style');
115
+ styleTag.innerHTML = `
116
+ :host::part(required-indicator)::after {
117
+ content: var(${NewPassword.cssVarList.requiredContent});
118
+ }
119
+ `;
120
+ input?.shadowRoot.appendChild(styleTag);
121
+ }
122
+ }
123
+
124
+ get isInvalid() {
125
+ return this.hasAttribute('invalid') && this.getAttribute('invalid') !== 'false'
126
+ }
127
+
128
+ // for some reason, Vaadin is removing the invalid attribute on several events,
129
+ // e.g. focus, input, etc..., so we need to make sure the inputs will stay invalid
130
+ // if the root component is invalid
131
+ handleInputsInvalidAttribute() {
132
+ this.inputs.forEach(input => {
133
+ input && observeAttributes(input, (changedAttributes) => {
134
+ if (changedAttributes.includes('invalid')) {
135
+ const inputInvalidValue = input.getAttribute('invalid')
136
+ const rootInvalidValue = this.getAttribute('invalid')
137
+
138
+ if (this.isInvalid && rootInvalidValue !== inputInvalidValue) {
139
+ input.setAttribute('invalid', 'true')
140
+ }
141
+ }
142
+ }, {});
143
+ })
144
+ }
145
+
146
+ initInputs() {
147
+ this.handleIndicatorStyle()
148
+ this.handleInputsInvalidAttribute();
149
+ this.handleFocusEventsDispatching(this.inputs)
150
+ }
151
+
152
+ toggleBooleanAttribute(ele, name, value) {
153
+ value === null ?
154
+ ele?.removeAttribute(name) :
155
+ ele?.setAttribute(name, value)
156
+ }
157
+
158
+ attributeChangedCallback(attrName, oldValue, newValue) {
159
+ super.attributeChangedCallback?.(attrName, oldValue, newValue);
160
+
161
+ if (oldValue !== newValue) {
162
+ if (attrName === 'has-confirm') {
163
+ this.renderInputs(newValue !== null && newValue !== 'false');
164
+ }
165
+ else if (commonAttrs.includes(attrName)) {
166
+ this.inputs.forEach(
167
+ (input) => this.toggleBooleanAttribute(input, attrName, newValue)
168
+ );
169
+ }
170
+ else if (passwordInputAttrs.includes(attrName)) {
171
+ this.toggleBooleanAttribute(
172
+ this.passwordInput,
173
+ removeAttrPrefix(attrName, passwordAttrPrefixRegex),
174
+ newValue
175
+ );
176
+ }
177
+ else if (confirmInputAttrs.includes(attrName)) {
178
+ this.toggleBooleanAttribute(
179
+ this.confirmInput,
180
+ removeAttrPrefix(attrName, confirmAttrPrefixRegex),
181
+ newValue
182
+ );
183
+
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ export default NewPasswordInternal;
@@ -0,0 +1,3 @@
1
+ import { getComponentName } from "../../../helpers/componentHelpers";
2
+
3
+ export const componentName = getComponentName('new-password-internal');
@@ -0,0 +1,4 @@
1
+ import NewPasswordInternal from "./NewPasswordInternal"
2
+ import { componentName } from "./componentName"
3
+
4
+ customElements.define(componentName, NewPasswordInternal)
@@ -0,0 +1,6 @@
1
+ import NewPassword, { componentName } from './NewPassword';
2
+ import '../descope-text-field'
3
+ import '../descope-password-field'
4
+ import './descope-new-password-internal'
5
+
6
+ customElements.define(componentName, NewPassword);
@@ -173,8 +173,8 @@ class PasscodeInternal extends BaseInputClass {
173
173
  forwardAttrs(this, input, { includeAttrs: forwardAttributes })
174
174
  })
175
175
 
176
- this.handleFocusEventsDispatching(this.inputs)
177
- this.handleInputEventDispatching(this.inputs)
176
+ this.handleFocusEventsDispatching(this.inputs);
177
+ this.handleInputEventDispatching();
178
178
  }
179
179
 
180
180
  attributeChangedCallback(attrName, oldValue, newValue) {
@@ -5,7 +5,6 @@ import {
5
5
  proxyInputMixin,
6
6
  componentNameValidationMixin
7
7
  } from '../../mixins';
8
- import textFieldMappings from '../descope-text-field/textFieldMappings';
9
8
  import { compose } from '../../helpers';
10
9
  import { getComponentName } from '../../helpers/componentHelpers';
11
10
 
@@ -13,16 +12,37 @@ export const componentName = getComponentName('password-field');
13
12
 
14
13
  let overrides = ``;
15
14
 
15
+ const { host, inputWrapper, inputElement, inputElementPlaceholder, revealButton, label, requiredIndicator } = {
16
+ host: () => ':host',
17
+ inputWrapper: { selector: '::part(input-field)' },
18
+ inputElement: { selector: '> input' },
19
+ inputElementPlaceholder: { selector: '> input:placeholder-shown' },
20
+ revealButton: { selector: 'vaadin-password-field-button' },
21
+ label: { selector: '> label' },
22
+ requiredIndicator: { selector: '::part(required-indicator)::after' },
23
+ }
24
+
16
25
  const PasswordField = compose(
17
26
  createStyleMixin({
18
27
  mappings: {
19
- ...textFieldMappings,
20
- revealCursor: [
21
- {
22
- selector: '::part(reveal-button)::before',
23
- property: 'cursor'
24
- }
25
- ]
28
+ wrapperBorderStyle: { ...inputWrapper, property: 'border-style' },
29
+ wrapperBorderWidth: { ...inputWrapper, property: 'border-width' },
30
+ wrapperBorderColor: { ...inputWrapper, property: 'border-color' },
31
+ wrapperBorderRadius: { ...inputWrapper, property: 'border-radius' },
32
+ labelTextColor: [
33
+ { ...label, property: 'color' },
34
+ { ...requiredIndicator, property: 'color' }
35
+ ],
36
+ inputTextColor: [{ ...inputElement, property: 'color' }],
37
+ placeholderTextColor: { ...inputElementPlaceholder, property: 'color' },
38
+ fontSize: [{}, host],
39
+ height: inputWrapper,
40
+ padding: inputWrapper,
41
+ pointerCursor: [
42
+ { ...revealButton, property: 'cursor' },
43
+ { ...label, property: 'cursor' },
44
+ { ...requiredIndicator, property: 'cursor' }
45
+ ],
26
46
  }
27
47
  }),
28
48
  draggableMixin,
@@ -43,38 +63,17 @@ overrides = `
43
63
  display: inline-block;
44
64
  }
45
65
  vaadin-password-field {
46
- margin: 0;
66
+ width: 100%;
47
67
  padding: 0;
48
68
  }
49
69
  vaadin-password-field::part(input-field) {
50
- overflow: hidden;
51
- }
52
- vaadin-password-field[readonly] > input:placeholder-shown {
53
- opacity: 1;
54
- }
55
- vaadin-password-field input:-webkit-autofill,
56
- vaadin-password-field input:-webkit-autofill::first-line,
57
- vaadin-password-field input:-webkit-autofill:hover,
58
- vaadin-password-field input:-webkit-autofill:active,
59
- vaadin-password-field input:-webkit-autofill:focus {
60
- -webkit-text-fill-color: var(${PasswordField.cssVarList.color});
61
- box-shadow: 0 0 0 var(${PasswordField.cssVarList.height}) var(${PasswordField.cssVarList.backgroundColor}) inset;
62
- }
63
- vaadin-password-field > label,
64
- vaadin-password-field::part(input-field) {
65
- cursor: pointer;
66
- color: var(${PasswordField.cssVarList.color});
67
- }
68
- vaadin-password-field::part(input-field):focus {
69
- cursor: text;
70
- }
71
- vaadin-password-field[required]::part(required-indicator)::after {
72
- font-size: "12px";
73
- content: "*";
74
- color: var(${PasswordField.cssVarList.color});
70
+ background: transparent;
75
71
  }
76
- vaadin-password-field[readonly]::part(input-field)::after {
77
- border: 0 solid;
72
+ vaadin-password-field::part(input-field)::after {
73
+ opacity: 0;
74
+ }
75
+ vaadin-password-field::before {
76
+ height: initial;
78
77
  }
79
78
  `;
80
79
 
@@ -7,7 +7,6 @@ import {
7
7
  draggableMixin,
8
8
  proxyInputMixin
9
9
  } from '../../mixins';
10
-
11
10
  import TextField from '../descope-text-field/TextField';
12
11
  import ComboBox from '../descope-combo-box/ComboBox';
13
12
 
@@ -54,6 +53,7 @@ const customMixin = (superclass) =>
54
53
  };
55
54
 
56
55
  const {
56
+ host,
57
57
  inputWrapper,
58
58
  countryCodeInput,
59
59
  phoneInput,
@@ -61,6 +61,7 @@ const {
61
61
  requiredIndicator,
62
62
  separator
63
63
  } = {
64
+ host: { selector: () => ':host' },
64
65
  inputWrapper: { selector: '::part(input-field)' },
65
66
  phoneInput: { selector: () => 'descope-text-field' },
66
67
  countryCodeInput: { selector: () => 'descope-combo-box' },
@@ -72,7 +73,7 @@ const {
72
73
  const PhoneField = compose(
73
74
  createStyleMixin({
74
75
  mappings: {
75
- componentWidth: { selector: () => ':host', property: 'width' },
76
+ componentWidth: { ...host, property: 'width' },
76
77
 
77
78
  wrapperBorderStyle: [
78
79
  { ...inputWrapper, property: 'border-style' },
@@ -162,7 +162,7 @@ class PhoneFieldInternal extends BaseInputClass {
162
162
  });
163
163
 
164
164
  this.handleFocusEventsDispatching(this.inputs)
165
- this.handleInputEventDispatching(this.inputs)
165
+ this.handleInputEventDispatching()
166
166
  }
167
167
 
168
168
  attributeChangedCallback(attrName, oldValue, newValue) {
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ import './components/descope-text-area';
17
17
  import './components/descope-text-field';
18
18
  import './components/descope-image';
19
19
  import './components/descope-phone-field';
20
+ import './components/descope-new-password';
20
21
 
21
22
  export {
22
23
  globalsThemeToStyle,
@@ -17,7 +17,7 @@ export const createProxy = ({
17
17
  #dispatchFocus = createDispatchEvent.bind(this, 'focus')
18
18
 
19
19
  constructor() {
20
- super().attachShadow({ mode: 'open' }).innerHTML = `
20
+ super().attachShadow({ mode: 'open', delegatesFocus: true }).innerHTML = `
21
21
  <style id="create-proxy">${isFunction(style) ? style() : style
22
22
  }</style>
23
23
  <${wrappedEleName}>
@@ -27,8 +27,6 @@ export const createProxy = ({
27
27
  `;
28
28
  }
29
29
 
30
- focus = () => this.baseElement.focus()
31
-
32
30
  init() {
33
31
  super.init?.();
34
32
 
@@ -40,30 +38,10 @@ export const createProxy = ({
40
38
  this.#dispatchFocus()
41
39
  })
42
40
 
43
- this.addEventListener('focus', (e) => {
44
- // if we got a focus event we want to focus the proxy element
45
- if (e.isTrusted) {
46
- this.focus();
47
- }
48
- })
49
41
 
50
42
  // this is needed for components that uses props, such as combo box
51
43
  forwardProps(this, this.baseElement, includeForwardProps)
52
44
 
53
- // `onkeydown` is set on `baseElement` support proper tab-index navigation
54
- // this support is needed since both proxy host and element catch `focus`/`blur` event
55
- // which causes faulty behavior.
56
- // we need this to happen only when the proxy component is in the light DOM,
57
- // otherwise it will focus the nested proxy element
58
- this.baseElement.onkeydown = (e) => {
59
- if (e.shiftKey && e.keyCode === 9 && this.getRootNode() === document) {
60
- this.removeAttribute('tabindex');
61
- // We want to defer the action of setting the tab index back
62
- // so it will happen after focusing the previous element
63
- setTimeout(() => this.setAttribute('tabindex', '0'), 0);
64
- }
65
- };
66
-
67
45
  syncAttrs(this.baseElement, this, {
68
46
  excludeAttrs: excludeAttrsSync,
69
47
  includeAttrs: includeAttrsSync
@@ -4,7 +4,6 @@ export { createProxy } from './createProxy';
4
4
  export { proxyInputMixin } from './proxyInputMixin';
5
5
  export { componentNameValidationMixin } from './componentNameValidationMixin';
6
6
  export { hoverableMixin } from './hoverableMixin';
7
- export { focusMixin } from './focusMixin'
8
7
  export { inputValidationMixin } from './inputValidationMixin'
9
8
  export { portalMixin } from './portalMixin'
10
9
  export { changeMixin } from './changeMixin'
@@ -1,54 +1,71 @@
1
1
  import { createDispatchEvent } from "../helpers/mixinsHelpers";
2
2
 
3
3
  export const inputEventsDispatchingMixin = (superclass) => class InputEventsDispatchingMixinClass extends superclass {
4
- handleFocusEventsDispatching(focusableElements = []) {
5
- let blurTimerId
6
-
7
- // in order to simulate blur on the input
8
- // we are checking if focus on one of the other elements happened immediately after blur
9
- // if not, the component is no longer focused and we should dispatch blur
10
- focusableElements.forEach(ele => {
11
- ele?.addEventListener('blur', (e) => {
4
+ init() {
5
+ this.#blockNativeEvents();
6
+
7
+ super.init?.();
8
+ }
9
+
10
+ // we want to block the native (trusted) events and control when these events are being dispatched
11
+ #blockNativeEvents() {
12
+ ['blur', 'focus', 'focusin', 'focusout'].forEach(event => {
13
+ this.addEventListener(event, (e) => {
14
+ e.isTrusted && e.target === this && e.stopImmediatePropagation()
15
+ })
16
+ })
17
+ }
18
+
19
+ handleFocusEventsDispatching(inputs) {
20
+ let timerId
21
+
22
+ // in order to simulate blur & focusout on root input element
23
+ // we are checking if focus on one of the inner elements happened immediately after blur
24
+ // if not, the component is no longer focused and we should dispatch blur & focusout
25
+ inputs?.forEach(input => {
26
+ input?.addEventListener('focusout', (e) => {
12
27
  e.stopImmediatePropagation();
13
- blurTimerId = setTimeout(() => {
14
- blurTimerId = null
28
+ timerId = setTimeout(() => {
29
+ timerId = null
30
+
15
31
  createDispatchEvent.call(this, 'blur');
16
32
  createDispatchEvent.call(this, 'focusout', { bubbles: true });
17
33
  });
18
34
  })
19
35
 
20
- // in order to simulate focus on the input
21
- // we are holding a blur timer id and clearing it on blur
22
- // if there is a timer id, it means that there was no blur on the input, so it's still focused
23
- // otherwise, it was not focused before, and we need to dispatch a focus event
24
- ele?.addEventListener('focus', (e) => {
36
+ // in order to simulate focus & focusin on the root input element
37
+ // we are holding a timer id and clearing it on focusin
38
+ // if there is a timer id, it means that the root input is still focused
39
+ // otherwise, it was not focused before, and we should dispatch focus & focusin
40
+ const onFocus = (e) => {
25
41
  e.stopImmediatePropagation();
26
- clearTimeout(blurTimerId)
27
- if (!blurTimerId) {
42
+ clearTimeout(timerId)
43
+ if (!timerId) {
28
44
  createDispatchEvent.call(this, 'focus');
29
45
  createDispatchEvent.call(this, 'focusin', { bubbles: true });
30
46
  }
31
- })
47
+ }
48
+
49
+ // some components are not dispatching focusin but only focus
50
+ input?.addEventListener('focusin', onFocus)
51
+ input?.addEventListener('focus', onFocus)
32
52
  })
33
53
  }
34
54
 
35
55
  // we want to block the input events from propagating in case the value of the root input wasn't change
36
56
  // this can happen if we are sanitizing characters on the internal inputs and do not want it to affect the root input element value
37
57
  // in this case, on each input event, we are comparing the root input value to the previous one, and only if it does not match, we are allowing the input event to propagate
38
- handleInputEventDispatching(inputElements) {
58
+ handleInputEventDispatching() {
39
59
  let previousRootComponentValue = this.value
40
60
 
41
- inputElements.forEach(input => {
42
- input.addEventListener('input', (e) => {
43
- // we are comparing the previous value to the new one,
44
- // and if they have the same value, we are blocking the input event
45
- if (previousRootComponentValue === this.value) {
46
- e.stopImmediatePropagation();
47
- } else {
48
- previousRootComponentValue = this.value
49
- }
50
- });
61
+ this.addEventListener('input', (e) => {
62
+ // we are comparing the previous value to the new one,
63
+ // and if they have the same value, we are blocking the input event
64
+ if (previousRootComponentValue === this.value) {
65
+ e.stopImmediatePropagation();
66
+ } else {
67
+ previousRootComponentValue = this.value
68
+ }
51
69
  });
52
-
53
70
  }
54
- }
71
+ }
@@ -68,7 +68,7 @@ export const inputValidationMixin = (superclass) => class InputValidationMixinCl
68
68
 
69
69
  #setValidity() {
70
70
  const validity = this.getValidity()
71
- this.#internals.setValidity(validity, this.getErrorMessage(validity))
71
+ this.#internals.setValidity(validity, this.getErrorMessage(validity), this.validationTarget)
72
72
  }
73
73
 
74
74
  get validationMessage() {
@@ -91,9 +91,13 @@ export const inputValidationMixin = (superclass) => class InputValidationMixinCl
91
91
  return this.#internals.validity
92
92
  }
93
93
 
94
+ get validationTarget () {
95
+ return this.inputElement
96
+ }
97
+
94
98
  setCustomValidity(errorMessage) {
95
99
  if (errorMessage) {
96
- this.#internals.setValidity({ customError: true }, errorMessage)
100
+ this.#internals.setValidity({ customError: true }, errorMessage, this.validationTarget)
97
101
  } else {
98
102
  this.#internals.setValidity({})
99
103
  this.#setValidity()