@descope/web-components-ui 1.0.69 → 1.0.71

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.
Files changed (41) hide show
  1. package/dist/index.esm.js +290 -385
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/umd/809.js +1 -0
  4. package/dist/umd/descope-button-index-js.js +1 -1
  5. package/dist/umd/descope-checkbox-index-js.js +1 -1
  6. package/dist/umd/descope-combo-box-index-js.js +1 -1
  7. package/dist/umd/descope-container-index-js.js +1 -1
  8. package/dist/umd/descope-date-picker-index-js.js +1 -1
  9. package/dist/umd/descope-divider-index-js.js +1 -1
  10. package/dist/umd/descope-email-field-index-js.js +1 -1
  11. package/dist/umd/descope-link-index-js.js +1 -1
  12. package/dist/umd/descope-loader-linear-index-js.js +1 -1
  13. package/dist/umd/descope-loader-radial-index-js.js +1 -1
  14. package/dist/umd/descope-logo-index-js.js +1 -1
  15. package/dist/umd/descope-number-field-index-js.js +1 -1
  16. package/dist/umd/descope-passcode-descope-passcode-internal-index-js.js +1 -1
  17. package/dist/umd/descope-passcode-index-js.js +1 -1
  18. package/dist/umd/descope-password-field-index-js.js +1 -1
  19. package/dist/umd/descope-switch-toggle-index-js.js +1 -1
  20. package/dist/umd/descope-text-area-index-js.js +1 -1
  21. package/dist/umd/descope-text-field-index-js.js +1 -1
  22. package/dist/umd/descope-text-index-js.js +1 -1
  23. package/dist/umd/index.js +1 -1
  24. package/package.json +1 -1
  25. package/src/baseClasses/createBaseClass.js +29 -2
  26. package/src/baseClasses/createBaseInputClass.js +9 -0
  27. package/src/components/descope-passcode/Passcode.js +1 -1
  28. package/src/components/descope-passcode/descope-passcode-internal/PasscodeInternal.js +67 -51
  29. package/src/helpers/mixinsHelpers.js +26 -9
  30. package/src/mixins/changeMixin.js +13 -39
  31. package/src/mixins/componentNameValidationMixin.js +2 -4
  32. package/src/mixins/createProxy.js +45 -53
  33. package/src/mixins/createStyleMixin/index.js +2 -0
  34. package/src/mixins/focusMixin.js +20 -125
  35. package/src/mixins/hoverableMixin.js +10 -16
  36. package/src/mixins/index.js +1 -0
  37. package/src/mixins/inputValidationMixin.js +10 -30
  38. package/src/mixins/normalizeBooleanAttributesMixin.js +11 -0
  39. package/src/mixins/proxyInputMixin.js +51 -58
  40. package/dist/umd/135.js +0 -1
  41. package/src/baseClasses/BaseInputClass.js +0 -4
package/dist/index.esm.js CHANGED
@@ -22,8 +22,6 @@ const compose = (...fns) =>
22
22
  (val) =>
23
23
  fns.reduceRight((res, fn) => fn(res), val);
24
24
 
25
- const upperFirst = (str) => str.charAt(0).toUpperCase() + str.slice(1);
26
-
27
25
  const isFunction = (maybeFunc) => typeof maybeFunc === 'function';
28
26
 
29
27
  const DESCOPE_PREFIX = 'descope';
@@ -384,6 +382,8 @@ const createStyleMixin =
384
382
  }
385
383
 
386
384
  disconnectedCallback() {
385
+ super.disconnectedCallback?.();
386
+
387
387
  this.#disconnectThemeManager?.();
388
388
  }
389
389
  };
@@ -420,6 +420,41 @@ const draggableMixin = (superclass) =>
420
420
  }
421
421
  };
422
422
 
423
+ // create a dispatch event function that also calls to the onevent function in case it's set
424
+ // usage example:
425
+ // #dispatchSomething = createDispatchEvent.bind(this, 'something' { ...options })
426
+ // this will dispatch a new event when called, but also call to "onsomething"
427
+ function createDispatchEvent(eventName, options = {}) {
428
+ const event = new Event(eventName, options);
429
+
430
+ this[`on${eventName}`]?.(event); // in case we got an event callback as property
431
+ this.dispatchEvent(event);
432
+ }
433
+
434
+ // add an event listener that is automatically removed on disconnect
435
+ // usage example:
436
+ // createEventListener.call(this,'change', this.onChange, { element? , ...options })
437
+ function createEventListener(event, callback, { element, ...options } = {}) {
438
+ const timerId = setTimeout(() => console.warn(this.localName, 'is not using "createBaseClass", events will not be removed automatically on disconnect'), 2000);
439
+
440
+ this.addEventListener('connected', () => {
441
+ clearTimeout(timerId);
442
+ }, { once: true });
443
+
444
+ const targetEle = element || this;
445
+ const boundCallback = callback.bind(this);
446
+
447
+ const onDisconnect = () => {
448
+ targetEle.removeEventListener(event, boundCallback);
449
+ };
450
+
451
+ this.addEventListener('disconnected', onDisconnect, { once: true });
452
+
453
+ targetEle.addEventListener(event, boundCallback, options);
454
+
455
+ return onDisconnect
456
+ }
457
+
423
458
  const createBaseClass = ({ componentName, baseSelector = '' }) => {
424
459
  class DescopeBaseClass extends HTMLElement {
425
460
  static get componentName() {
@@ -427,10 +462,17 @@ const createBaseClass = ({ componentName, baseSelector = '' }) => {
427
462
  }
428
463
 
429
464
  #baseElement;
465
+
466
+ #dispatchConnected = createDispatchEvent.bind(this, 'connected')
467
+ #dispatchDisconnected = createDispatchEvent.bind(this, 'disconnected')
468
+
469
+ // base selector is the selector for the component wrapper,
470
+ // it's the highest element that is relevant for the layout
430
471
  get baseSelector() {
431
472
  return baseSelector
432
473
  }
433
474
 
475
+ // this is the base element, which returned by querying the base selector
434
476
  get baseElement() {
435
477
  this.#baseElement ??= this.baseSelector ?
436
478
  this.rootElement.querySelector(this.baseSelector) :
@@ -439,12 +481,31 @@ const createBaseClass = ({ componentName, baseSelector = '' }) => {
439
481
  return this.#baseElement
440
482
  }
441
483
 
484
+ // this is the component root level element,
485
+ // it can be either a shadow root or the component's node from the light DOM
442
486
  get rootElement() {
443
487
  return this.shadowRoot || this
444
488
  }
489
+
490
+ connectedCallback() {
491
+ super.connectedCallback?.();
492
+
493
+ // we are waiting for all components to listen before dispatching
494
+ setTimeout(this.#dispatchConnected);
495
+ }
496
+
497
+ disconnectedCallback() {
498
+ super.disconnectedCallback?.();
499
+
500
+ this.#dispatchDisconnected();
501
+ }
445
502
  }
446
503
 
447
- return compose(componentNameValidationMixin, hoverableMixin)(DescopeBaseClass)
504
+ return compose(
505
+ componentNameValidationMixin,
506
+ hoverableMixin,
507
+ normalizeBooleanAttributesMixin
508
+ )(DescopeBaseClass)
448
509
  };
449
510
 
450
511
  const createProxy = ({
@@ -456,87 +517,69 @@ const createProxy = ({
456
517
  includeAttrsSync = [],
457
518
  includeForwardProps = []
458
519
  }) => {
459
- const template = `
460
- <style id="create-proxy"></style>
461
- <${wrappedEleName}>
462
- <slot></slot>
463
- ${slots.map((slot) => `<slot name="${slot}" slot="${slot}"></slot>`).join('')}
464
- </${wrappedEleName}>
465
- `;
466
-
467
520
  class ProxyClass extends createBaseClass({ componentName, baseSelector: wrappedEleName }) {
468
- constructor() {
469
- super().attachShadow({ mode: 'open' }).innerHTML = template;
470
- this.hostElement = this.shadowRoot.host;
471
- this.shadowRoot.getElementById('create-proxy').innerHTML =
472
- isFunction(style) ? style() : style;
473
- }
521
+ #dispatchBlur = createDispatchEvent.bind(this, 'blur')
522
+ #dispatchFocus = createDispatchEvent.bind(this, 'focus')
474
523
 
475
- #boundOnFocus = this.#onFocus.bind(this);
476
-
477
- // we want to focus on the proxy element when focusing our WCP
478
- #onFocus() {
479
- this.proxyElement.focus();
524
+ constructor() {
525
+ super().attachShadow({ mode: 'open' }).innerHTML = `
526
+ <style id="create-proxy">${isFunction(style) ? style() : style
527
+ }</style>
528
+ <${wrappedEleName}>
529
+ <slot></slot>
530
+ ${slots.map((slot) => `<slot name="${slot}" slot="${slot}"></slot>`).join('')}
531
+ </${wrappedEleName}>
532
+ `;
480
533
  }
481
534
 
482
- focus = this.#onFocus
535
+ focus = () => this.baseElement.focus()
483
536
 
484
537
  connectedCallback() {
485
- if (this.shadowRoot.isConnected) {
486
- this.proxyElement = this.shadowRoot.querySelector(wrappedEleName);
487
-
488
- this.addEventListener('focus', this.#boundOnFocus);
489
-
490
- // this is needed for components that uses props, such as combo box
491
- forwardProps(this.hostElement, this.proxyElement, includeForwardProps);
492
-
493
- // `onkeydown` is set on `proxyElement` support proper tab-index navigation
494
- // this support is needed since both proxy host and element catch `focus`/`blur` event
495
- // which causes faulty behavior.
496
- // we need this to happen only when the proxy component is in the light DOM,
497
- // otherwise it will focus the nested proxy element
498
- this.proxyElement.onkeydown = (e) => {
499
- if (e.shiftKey && e.keyCode === 9 && this.getRootNode() === document) {
500
- this.removeAttribute('tabindex');
501
- // We want to defer the action of setting the tab index back
502
- // so it will happen after focusing the previous element
503
- setTimeout(() => this.setAttribute('tabindex', '0'), 0);
504
- }
505
- };
538
+ super.connectedCallback?.();
506
539
 
507
- syncAttrs(this.proxyElement, this.hostElement, {
508
- excludeAttrs: excludeAttrsSync,
509
- includeAttrs: includeAttrsSync
510
- });
511
- }
512
- }
540
+ createEventListener.call(this, 'blur', (e) => {
541
+ this.#dispatchBlur();
542
+ }, { element: this.baseElement });
513
543
 
514
- attributeChangedCallback() {
515
- if (!this.proxyElement) {
516
- return;
517
- }
518
- }
544
+ createEventListener.call(this, 'focus', (e) => {
545
+ this.#dispatchFocus();
546
+ }, { element: this.baseElement });
519
547
 
520
- disconnectedCallback() {
521
- super.disconnectedCallback?.();
548
+ createEventListener.call(this, 'focus', (e) => {
549
+ // if we got a focus event we want to focus the proxy element
550
+ if (e.isTrusted) {
551
+ this.focus();
552
+ }
553
+ });
554
+
555
+ // this is needed for components that uses props, such as combo box
556
+ forwardProps(this, this.baseElement, includeForwardProps);
557
+
558
+ // `onkeydown` is set on `baseElement` support proper tab-index navigation
559
+ // this support is needed since both proxy host and element catch `focus`/`blur` event
560
+ // which causes faulty behavior.
561
+ // we need this to happen only when the proxy component is in the light DOM,
562
+ // otherwise it will focus the nested proxy element
563
+ this.baseElement.onkeydown = (e) => {
564
+ if (e.shiftKey && e.keyCode === 9 && this.getRootNode() === document) {
565
+ this.removeAttribute('tabindex');
566
+ // We want to defer the action of setting the tab index back
567
+ // so it will happen after focusing the previous element
568
+ setTimeout(() => this.setAttribute('tabindex', '0'), 0);
569
+ }
570
+ };
522
571
 
523
- this.removeEventListener('focus', this.#boundOnFocus);
572
+ syncAttrs(this.baseElement, this, {
573
+ excludeAttrs: excludeAttrsSync,
574
+ includeAttrs: includeAttrsSync
575
+ });
524
576
  }
525
577
  }
526
578
 
527
579
  return ProxyClass;
528
580
  };
529
581
 
530
- // move create event to here
531
-
532
- // usage example:
533
- // #dispatchSomething = createDispatchEvent.bind(this, 'something')
534
- function createDispatchEvent(eventName) {
535
- this[`on${eventName}`]?.(); // in case we got an event callback as property
536
- this.dispatchEvent(new Event(eventName));
537
- }
538
-
539
- const observedAttributes = [
582
+ const observedAttributes$1 = [
540
583
  'required',
541
584
  'pattern',
542
585
  ];
@@ -549,7 +592,7 @@ const inputValidationMixin = (superclass) => class InputValidationMixinClass ext
549
592
  static get observedAttributes() {
550
593
  return [
551
594
  ...superclass.observedAttributes || [],
552
- ...observedAttributes
595
+ ...observedAttributes$1
553
596
  ];
554
597
  }
555
598
 
@@ -557,19 +600,12 @@ const inputValidationMixin = (superclass) => class InputValidationMixinClass ext
557
600
  return true;
558
601
  }
559
602
 
560
- #dispatchValid = createDispatchEvent.bind(this, 'valid')
561
- #dispatchInvalid = createDispatchEvent.bind(this, 'invalid')
562
-
563
603
  #internals
564
604
 
565
- #boundedHandleInput
566
-
567
605
  constructor() {
568
606
  super();
569
607
 
570
608
  this.#internals = this.attachInternals();
571
-
572
- this.#boundedHandleInput = this.#handleInput.bind(this);
573
609
  }
574
610
 
575
611
  get defaultErrorMsgValueMissing() {
@@ -595,7 +631,7 @@ const inputValidationMixin = (superclass) => class InputValidationMixinClass ext
595
631
  }
596
632
  }
597
633
 
598
- setValidity() {
634
+ #setValidity() {
599
635
  const validity = this.getValidity();
600
636
  this.#internals.setValidity(validity, this.getErrorMessage(validity));
601
637
  }
@@ -625,7 +661,7 @@ const inputValidationMixin = (superclass) => class InputValidationMixinClass ext
625
661
  this.#internals.setValidity({ customError: true }, errorMessage);
626
662
  } else {
627
663
  this.#internals.setValidity({});
628
- this.setValidity();
664
+ this.#setValidity();
629
665
  }
630
666
  }
631
667
 
@@ -637,38 +673,25 @@ const inputValidationMixin = (superclass) => class InputValidationMixinClass ext
637
673
  return this.getAttribute('pattern')
638
674
  }
639
675
 
640
- #handleInput() {
641
- this.setValidity();
642
- this.handleDispatchValidationEvents();
676
+ get form() {
677
+ return this.#internals.form
643
678
  }
644
679
 
645
680
  attributeChangedCallback(attrName, oldValue, newValue) {
646
681
  super.attributeChangedCallback?.(attrName, oldValue, newValue);
647
682
 
648
- if (observedAttributes.includes(attrName)) {
649
- this.setValidity();
650
- }
651
- }
652
-
653
- handleDispatchValidationEvents() {
654
- if (this.checkValidity()) {
655
- this.#dispatchValid();
656
- } else {
657
- this.#dispatchInvalid();
683
+ if (observedAttributes$1.includes(attrName)) {
684
+ this.#setValidity();
658
685
  }
659
686
  }
660
687
 
661
688
  connectedCallback() {
662
689
  super.connectedCallback?.();
690
+ createEventListener.call(this, 'change', this.#setValidity);
691
+ createEventListener.call(this, 'invalid', (e) => e.stopPropagation());
692
+ createEventListener.call(this, 'input', this.#setValidity);
663
693
 
664
- this.addEventListener('input', this.#boundedHandleInput);
665
-
666
- this.setValidity();
667
- this.handleDispatchValidationEvents();
668
- }
669
-
670
- disconnectedCallback() {
671
- this.removeEventListener('input', this.#boundedHandleInput);
694
+ this.#setValidity();
672
695
  }
673
696
  };
674
697
 
@@ -708,26 +731,17 @@ const proxyInputMixin = (superclass) =>
708
731
 
709
732
  #inputElement
710
733
 
711
- #boundHandleFocus
712
- #boundHandleInvalid
713
- #boundHandleValid
734
+ #dispatchChange = createDispatchEvent.bind(this, 'change')
714
735
 
715
736
  constructor() {
716
737
  super();
717
738
 
718
739
  this.#inputElement = super.inputElement;
719
-
720
- this.#boundHandleFocus = this.#handleFocus.bind(this);
721
- this.#boundHandleInvalid = this.#handleInvalid.bind(this);
722
- this.#boundHandleValid = this.#handleValid.bind(this);
723
-
724
- this.baseEle = this.shadowRoot.querySelector(this.baseSelector);
725
-
726
740
  }
727
741
 
728
742
  get inputElement() {
729
- const inputSlot = this.baseEle.shadowRoot.querySelector('slot[name="input"]');
730
- const textAreaSlot = this.baseEle.shadowRoot.querySelector('slot[name="textarea"]');
743
+ const inputSlot = this.baseElement.shadowRoot.querySelector('slot[name="input"]');
744
+ const textAreaSlot = this.baseElement.shadowRoot.querySelector('slot[name="textarea"]');
731
745
 
732
746
  this.#inputElement ??= getNestedInput(inputSlot) || getNestedInput(textAreaSlot);
733
747
 
@@ -746,7 +760,6 @@ const proxyInputMixin = (superclass) =>
746
760
 
747
761
  reportValidityOnInternalInput() {
748
762
  setTimeout(() => {
749
- this.baseEle.focus(); //TODO: check if this is needed
750
763
  this.inputElement.reportValidity();
751
764
  });
752
765
  }
@@ -759,80 +772,82 @@ const proxyInputMixin = (superclass) =>
759
772
  }
760
773
  }
761
774
 
762
- setInternalInputErrorMessage() {
763
- if (!this.checkValidity()) {
775
+ handleInternalInputErrorMessage() {
776
+ if (!this.inputElement.checkValidity()) {
764
777
  this.inputElement.setCustomValidity(this.validationMessage);
765
778
  }
766
779
  }
767
780
 
768
- // when clicking on the form submit button and the input is invalid
769
- // we want it to appear as invalid
770
- #handleFocus(e) {
771
- if (e.relatedTarget?.form) {
772
- if (!this.checkValidity()) {
773
- this.setAttribute('invalid', 'true');
774
- }
775
-
776
- if (this.hasAttribute('invalid')) {
777
- this.reportValidityOnInternalInput();
778
- }
779
- }
780
- }
781
-
782
- #handleInvalid() {
783
- this.setInternalInputErrorMessage();
781
+ #handleErrorMessage() {
782
+ this.handleInternalInputErrorMessage();
784
783
  this.setAttribute('error-message', this.validationMessage);
785
784
  }
786
785
 
787
- #handleValid() {
788
- this.removeAttribute('invalid');
789
- }
790
-
791
786
  connectedCallback() {
792
787
  super.connectedCallback?.();
793
788
 
794
- // this is our way to identify that the form was submitted
795
- // in this case, we want the input to be in error state if it's not valid
796
- this.addEventListener('focus', this.#boundHandleFocus);
789
+ createEventListener.call(this, 'input', (e) => {
790
+ if (!this.inputElement.checkValidity()) {
791
+ this.inputElement.setCustomValidity('');
792
+ // after updating the input validity we want to trigger set validity on the wrapping element
793
+ // so we will get the updated validity
794
+ this.setCustomValidity('');
795
+
796
+ // Vaadin is getting the input event before us,
797
+ // so in order to make sure they use the updated validity
798
+ // we calling their fn after updating the input validity
799
+ this.baseElement.__onInput(e);
797
800
 
798
- this.addEventListener('invalid', this.#boundHandleInvalid);
799
- this.addEventListener('valid', this.#boundHandleValid);
801
+ this.#handleErrorMessage();
802
+ }
803
+ }, { element: this.inputElement });
804
+
805
+ createEventListener.call(this, 'change', () => {
806
+ this.#dispatchChange();
807
+ }, { element: this.baseElement });
800
808
 
801
- this.addEventListener('input', () => {
802
- this.inputElement.setCustomValidity('');
803
- if (!this.inputElement.checkValidity())
804
- this.setInternalInputErrorMessage();
809
+ createEventListener.call(this, 'blur', () => {
810
+ if (!this.checkValidity()) {
811
+ this.setAttribute('invalid', 'true');
812
+ this.#handleErrorMessage();
813
+ }
805
814
  });
806
815
 
816
+ createEventListener.call(this, 'focus', (e) => {
817
+ // when clicking on the form submit button and the input is invalid
818
+ // we want it to appear as invalid
819
+ if (e.relatedTarget?.form === this.form) {
820
+ if (!this.checkValidity()) {
821
+ this.setAttribute('invalid', 'true');
822
+ }
823
+
824
+ if (this.hasAttribute('invalid')) {
825
+ this.reportValidityOnInternalInput();
826
+ }
827
+ }
828
+ });
829
+
830
+ createEventListener.call(this, 'invalid', this.#handleErrorMessage);
831
+
832
+ this.handleInternalInputErrorMessage();
833
+
807
834
  // this is needed in order to make sure the form input validation is working
835
+ // we do not want it to happen when the component is nested
808
836
  if (!this.hasAttribute('tabindex') && this.getRootNode() === document) {
809
837
  this.setAttribute('tabindex', 0);
810
838
  }
811
839
 
812
840
  // sync properties
813
841
  propertyObserver(this, this.inputElement, 'value');
842
+ propertyObserver(this, this.inputElement, 'selectionStart');
814
843
  this.setSelectionRange = this.inputElement.setSelectionRange?.bind(this.inputElement);
815
844
  }
816
-
817
- disconnectedCallback() {
818
- this.removeEventListener('focus', this.#boundHandleFocus);
819
- this.removeEventListener('invalid', this.#boundHandleInvalid);
820
- this.removeEventListener('valid', this.#boundHandleValid);
821
- }
822
-
823
- attributeChangedCallback(attrName, oldValue, newValue) {
824
- super.attributeChangedCallback?.(attrName, oldValue, newValue);
825
-
826
- if (attrName === 'invalid' && newValue === '') {
827
- this.setAttribute('invalid', 'true');
828
- }
829
- }
830
845
  };
831
846
 
832
847
  const componentNameValidationMixin = (superclass) =>
833
848
  class ComponentNameValidationMixinClass extends superclass {
834
849
  #checkComponentName() {
835
- const currentComponentName = this.shadowRoot.host.tagName.toLowerCase();
850
+ const currentComponentName = this.localName;
836
851
 
837
852
  if (!superclass.componentName) {
838
853
  throw Error(
@@ -849,163 +864,48 @@ const componentNameValidationMixin = (superclass) =>
849
864
 
850
865
  connectedCallback() {
851
866
  super.connectedCallback?.();
852
- if (this.shadowRoot.isConnected) {
853
- this.#checkComponentName();
854
- }
867
+ this.#checkComponentName();
855
868
  }
856
869
  };
857
870
 
858
871
  const hoverableMixin =
859
872
  (superclass) =>
860
873
  class HoverableMixinClass extends superclass {
861
- #boundOnMouseOver = this.#onMouseOver.bind(this)
862
-
863
- #onMouseOver(e) {
864
- this.setAttribute('hover', 'true');
865
- e.target.addEventListener(
866
- 'mouseleave',
867
- () => this.shadowRoot.host.removeAttribute('hover'),
868
- { once: true }
869
- );
870
- }
871
-
872
874
  connectedCallback() {
873
875
  super.connectedCallback?.();
874
876
 
875
- const baseElement = this.shadowRoot.querySelector(
876
- this.baseSelector
877
- );
878
-
879
- baseElement.addEventListener('mouseover', this.#boundOnMouseOver);
877
+ createEventListener.call(this, 'mouseover', (e) => {
878
+ this.setAttribute('hover', 'true');
879
+ e.target.addEventListener(
880
+ 'mouseleave',
881
+ () => this.removeAttribute('hover'),
882
+ { once: true }
883
+ );
884
+ }, { element: this.baseElement });
880
885
  }
881
886
  };
882
887
 
883
- const events = [
884
- 'blur',
885
- 'focus',
886
- 'focusin',
887
- 'focusout',
888
- ];
889
-
890
888
  const focusMixin = (superclass) => class FocusMixinClass extends superclass {
891
- #isFocused = false
892
- #setFocusTimer
893
-
894
- #boundedHandleFocus
895
- #boundedHandleBlur
896
- #boundedHandleFocusIn
897
- #boundedHandleFocusOut
898
-
899
- constructor() {
900
- super();
901
-
902
- for (const event of events) {
903
- this[`dispatch${upperFirst(event)}`] = function () {
904
- this.dispatchInputEvent(event);
905
- };
906
- }
907
-
908
- this.#boundedHandleFocus = this.#handleFocus.bind(this);
909
- this.#boundedHandleBlur = this.#handleBlur.bind(this);
910
- this.#boundedHandleFocusIn = this.#handleFocusIn.bind(this);
911
- this.#boundedHandleFocusOut = this.#handleFocusOut.bind(this);
912
- }
913
-
914
- #handleFocus(e) {
915
- if (this.isReadOnly) {
916
- e.stopPropagation();
917
- return
918
- }
919
- // we want to listen only to browser events
920
- // and not to events we are dispatching
921
- if (e.isTrusted) { // TODO: check if this is needed, because Vaadin is also dispatching events
922
- // we want to control the focus events that dispatched by the component
923
- // so we are stopping propagation and handling it in setFocus
924
- e.stopPropagation();
925
- this.setFocus(true);
926
-
927
- // if the focus event is on the root component (and not on the inner components)
928
- // we want to notify the component and let it decide what to do with it
929
- if (e.target === this) {
930
- this.onFocus(e);
931
- }
932
- }
933
- }
934
-
935
- #handleFocusOut(e) {
936
- // we want to listen only to browser events
937
- // and not to events we are dispatching
938
- if (e.isTrusted) {
939
- // we want to control the focus events that dispatched by the component
940
- // so we are stopping propagation and handling it in setFocus
941
- e.stopPropagation();
942
- }
943
- }
944
-
945
- #handleFocusIn(e) {
946
- // we want to listen only to browser events
947
- // and not to events we are dispatching
948
- if (e.isTrusted) {
949
- // we want to control the focus events that dispatched by the component
950
- // so we are stopping propagation and handling it in setFocus
951
- e.stopPropagation();
952
- }
953
- }
954
-
955
- #handleBlur(e) {
956
- if (e.isTrusted) {
957
- e.stopPropagation();
958
- this.setFocus(false);
959
- }
960
- }
961
-
962
- get isReadOnly() {
963
- return this.hasAttribute('readonly') && this.getAttribute('readonly') !== 'false'
964
- }
965
-
966
- // we want to debounce the calls to this fn
967
- // so we can support input like components with multiple inputs inside
968
- setFocus(isFocused) {
969
- clearTimeout(this.#setFocusTimer);
970
-
971
- this.#setFocusTimer = setTimeout(() => {
972
- if (this.#isFocused !== isFocused) {
973
- this.#isFocused = isFocused;
974
- if (isFocused) {
975
- this.dispatchFocus();
976
- this.dispatchFocusin();
977
- }
978
- else {
979
- this.dispatchBlur();
980
- this.dispatchFocusout();
981
- }
982
- }
983
- });
984
- }
985
-
986
- onFocus() {
987
- console.warn('onFocus', 'is not implemented');
988
- }
989
-
990
- dispatchInputEvent(eventName) {
991
- this[`on${eventName}`]?.(); // in case we got an event callback as property
992
- this.dispatchEvent(new InputEvent(eventName));
993
- }
994
-
889
+ // we want to block all native events,
890
+ // so the input can control when to dispatch it based on its internal behavior
995
891
  connectedCallback() {
996
- super.connectedCallback?.();
892
+ super.connectedCallback?.();
893
+
894
+ createEventListener.call(this, 'blur', (e) => {
895
+ e.isTrusted && e.stopImmediatePropagation();
896
+ });
997
897
 
998
- this.addEventListener('focus', this.#boundedHandleFocus, true);
999
- this.addEventListener('blur', this.#boundedHandleBlur, true);
1000
- this.addEventListener('focusin', this.#boundedHandleFocusIn);
1001
- this.addEventListener('focusout', this.#boundedHandleFocusOut);
1002
- }
898
+ createEventListener.call(this, 'focus', (e) => {
899
+ e.isTrusted && e.stopImmediatePropagation();
900
+ });
1003
901
 
1004
- disconnectedCallback() {
1005
- this.removeEventListener('focus', this.#boundedHandleFocus);
1006
- this.removeEventListener('blur', this.#boundedHandleBlur);
1007
- this.removeEventListener('focusin', this.#boundedHandleFocusIn);
1008
- this.removeEventListener('focusout', this.#boundedHandleFocusOut);
902
+ createEventListener.call(this, 'focusout', (e) => {
903
+ e.isTrusted && e.stopImmediatePropagation();
904
+ });
905
+
906
+ createEventListener.call(this, 'focusin', (e) => {
907
+ e.isTrusted && e.stopImmediatePropagation();
908
+ });
1009
909
  }
1010
910
  };
1011
911
 
@@ -1071,48 +971,34 @@ const portalMixin = ({ name, selector, mappings = {} }) => (superclass) => {
1071
971
  };
1072
972
 
1073
973
  const changeMixin = (superclass) => class ChangeMixinClass extends superclass {
1074
-
1075
- #boundedHandleChange
1076
- #boundedHandleBlur
1077
-
1078
- #removeChangeListener
1079
- #removeBlurListener
1080
-
1081
974
  #dispatchChange = createDispatchEvent.bind(this, 'change')
1082
975
 
1083
- constructor() {
1084
- super();
1085
-
1086
- this.#boundedHandleChange = this.#handleChange.bind(this);
1087
- this.#boundedHandleBlur = this.#handleBlur.bind(this);
1088
- }
976
+ connectedCallback() {
977
+ super.connectedCallback?.();
978
+ this.prevValue = this.value;
1089
979
 
1090
- #handleChange(e) {
1091
- // we want to listen only to browser events
1092
- // and not to events we are dispatching
1093
- if (e.isTrusted) {
1094
- // we want to control the change events that dispatched by the component
1095
- // so we are stopping propagation and handling it in handleBlur
980
+ createEventListener.call(this, 'change', (e) => {
1096
981
  e.stopPropagation();
1097
- }
1098
- }
982
+ });
1099
983
 
1100
- #handleBlur() {
1101
- // on blur, we want to dispatch a change event
1102
- this.#dispatchChange();
984
+ createEventListener.call(this, 'blur', () => {
985
+ if (this.value !== this.prevValue) {
986
+ this.#dispatchChange();
987
+ this.prevValue = this.value;
988
+ }
989
+ });
1103
990
  }
991
+ };
1104
992
 
1105
- connectedCallback() {
1106
- super.connectedCallback?.();
1107
-
1108
- this.#removeChangeListener = addEventListener.bind();
1109
- this.addEventListener('change', this.#boundedHandleChange, true);
1110
- this.addEventListener('blur', this.#boundedHandleBlur);
1111
- }
993
+ // we want all the valueless attributes to have "true" value
994
+ const normalizeBooleanAttributesMixin = (superclass) => class NormalizeBooleanAttributesMixinClass extends superclass {
995
+ attributeChangedCallback(attrName, oldValue, newValue) {
996
+ if (newValue === '') {
997
+ this.setAttribute(attrName, 'true');
998
+ newValue = 'true';
999
+ }
1112
1000
 
1113
- disconnectedCallback() {
1114
- this.removeEventListener('change', this.#boundedHandleChange);
1115
- this.removeEventListener('blur', this.#boundedHandleBlur);
1001
+ super.attributeChangedCallback?.(attrName, oldValue, newValue);
1116
1002
  }
1117
1003
  };
1118
1004
 
@@ -1829,7 +1715,11 @@ overrides$4 = `
1829
1715
 
1830
1716
  customElements.define(componentName$7, NumberField);
1831
1717
 
1832
- var BaseInputClass = compose(focusMixin, inputValidationMixin, changeMixin)(HTMLElement); //todo: maybe we should use base class?
1718
+ const createBaseInputClass = (...args) => compose(
1719
+ focusMixin,
1720
+ inputValidationMixin,
1721
+ changeMixin,
1722
+ )(createBaseClass(...args));
1833
1723
 
1834
1724
  const focusElement = (ele) => {
1835
1725
  ele?.focus();
@@ -1847,23 +1737,26 @@ const getSanitizedCharacters = (str) => {
1847
1737
 
1848
1738
  const componentName$6 = getComponentName('passcode-internal');
1849
1739
 
1740
+ const observedAttributes = [
1741
+ 'disabled',
1742
+ 'bordered',
1743
+ 'size',
1744
+ 'invalid'
1745
+ ];
1746
+
1747
+ const BaseInputClass = createBaseInputClass({ componentName: componentName$6, baseSelector: ':host > div' });
1748
+
1850
1749
  class PasscodeInternal extends BaseInputClass {
1851
1750
  static get observedAttributes() {
1852
- return [
1853
- ...(BaseInputClass.observedAttributes || []),
1854
- 'disabled',
1855
- 'bordered',
1856
- 'size',
1857
- ];
1751
+ return observedAttributes.concat(BaseInputClass.observedAttributes || []);
1858
1752
  }
1859
1753
 
1860
1754
  static get componentName() {
1861
1755
  return componentName$6;
1862
1756
  }
1863
1757
 
1864
- #boundHandleInvalid = this.#handleInvalid.bind(this)
1865
- #boundHandleValid = this.#handleValid.bind(this)
1866
- #boundHandleBlur = this.#handleBlur.bind(this)
1758
+ #dispatchBlur = createDispatchEvent.bind(this, 'blur')
1759
+ #dispatchFocus = createDispatchEvent.bind(this, 'focus')
1867
1760
 
1868
1761
  constructor() {
1869
1762
  super();
@@ -1872,7 +1765,7 @@ class PasscodeInternal extends BaseInputClass {
1872
1765
  st-width="35px"
1873
1766
  data-id=${idx}
1874
1767
  type="tel"
1875
- autocomplete="none"
1768
+ autocomplete="off"
1876
1769
  ></descope-text-field>
1877
1770
  `);
1878
1771
 
@@ -1882,8 +1775,6 @@ class PasscodeInternal extends BaseInputClass {
1882
1775
  </div>
1883
1776
  `;
1884
1777
 
1885
- this.baseSelector = ':host > div';
1886
-
1887
1778
  this.inputs = Array.from(this.querySelectorAll('descope-text-field'));
1888
1779
  }
1889
1780
 
@@ -1909,16 +1800,6 @@ class PasscodeInternal extends BaseInputClass {
1909
1800
  return `^$|^\\d{${this.digits},}$`
1910
1801
  }
1911
1802
 
1912
- #handleInvalid() {
1913
- if (this.hasAttribute('invalid')) {
1914
- this.inputs.forEach(input => input.setAttribute('invalid', 'true'));
1915
- }
1916
- }
1917
-
1918
- #handleValid() {
1919
- this.inputs.forEach(input => input.removeAttribute('invalid'));
1920
- }
1921
-
1922
1803
  getValidity() {
1923
1804
  if (this.isRequired && !this.value) {
1924
1805
  return { valueMissing: true };
@@ -1931,25 +1812,17 @@ class PasscodeInternal extends BaseInputClass {
1931
1812
  }
1932
1813
  };
1933
1814
 
1934
- onFocus(){
1935
- this.inputs[0].focus();
1936
- }
1937
-
1938
1815
  connectedCallback() {
1816
+ // we are adding listeners before calling to super because it's stopping the events
1817
+ createEventListener.call(this, 'focus', (e) => {
1818
+ // we want to ignore focus events we are dispatching
1819
+ if (e.isTrusted)
1820
+ this.inputs[0].focus();
1821
+ });
1822
+
1939
1823
  super.connectedCallback?.();
1940
1824
 
1941
1825
  this.initInputs();
1942
-
1943
- this.addEventListener('invalid', this.#boundHandleInvalid);
1944
- this.addEventListener('valid', this.#boundHandleValid);
1945
- this.addEventListener('blur', this.#boundHandleBlur);
1946
- }
1947
-
1948
- disconnectedCallback() {
1949
- super.connectedCallback?.();
1950
- this.removeEventListener('invalid', this.#boundHandleInvalid);
1951
- this.removeEventListener('valid', this.#boundHandleValid);
1952
- this.removeEventListener('blur', this.#boundHandleBlur);
1953
1826
  }
1954
1827
 
1955
1828
  getInputIdx(inputEle) {
@@ -1981,33 +1854,60 @@ class PasscodeInternal extends BaseInputClass {
1981
1854
  focusElement(currentInput);
1982
1855
  };
1983
1856
 
1984
- #handleBlur() {
1985
- this.#handleInvalid();
1986
- }
1987
-
1988
1857
  initInputs() {
1858
+ let prevVal = this.value;
1859
+ let blurTimerId;
1860
+
1989
1861
  this.inputs.forEach((input) => {
1990
- input.oninput = (e) => {
1862
+ // in order to simulate blur on the input
1863
+ // we are checking if focus on one of the digits happened immediately after blur on another digit
1864
+ // if not, the component is no longer focused and we should simulate blur
1865
+ createEventListener.call(this, 'blur', (e) => {
1866
+ e.stopImmediatePropagation();
1867
+
1868
+ blurTimerId = setTimeout(() => {
1869
+ blurTimerId = null;
1870
+ this.#dispatchBlur();
1871
+ });
1872
+ }, { element: input });
1873
+
1874
+ createEventListener.call(this, 'focus', (e) => {
1875
+ e.stopImmediatePropagation();
1876
+
1877
+ clearTimeout(blurTimerId);
1878
+ if (!blurTimerId) {
1879
+ this.#dispatchFocus();
1880
+ }
1881
+ }, { element: input });
1882
+
1883
+ createEventListener.call(this, 'input', (e) => {
1991
1884
  const charArr = getSanitizedCharacters(input.value);
1992
1885
 
1993
1886
  if (!charArr.length) {
1994
1887
  // if we got an invalid value we want to clear the input
1995
1888
  input.value = '';
1996
- if (e.data === null) {
1997
- // if the user deleted the char, we want to focus the prev digit
1998
- focusElement(this.getPrevInput(input));
1999
- }
2000
1889
  }
2001
1890
  else this.fillDigits(charArr, input);
2002
- };
1891
+
1892
+ // we want to stop input events if the value wasn't change
1893
+ if (prevVal === this.value) {
1894
+ e.stopImmediatePropagation();
1895
+ }
1896
+ }, { element: input });
2003
1897
 
2004
1898
  input.onkeydown = ({ key }) => {
1899
+ prevVal = this.value;
1900
+
2005
1901
  // when user deletes a digit, we want to focus the previous digit
2006
1902
  if (key === 'Backspace') {
1903
+ // if the cursor is at 0, we want to move it to 1, so the value will be deleted
1904
+ if (!input.selectionStart) {
1905
+ input.setSelectionRange(1, 1);
1906
+ }
2007
1907
  setTimeout(() => {
2008
1908
  focusElement(this.getPrevInput(input));
2009
1909
  });
2010
- } else if (key.match(/^(\d)$/g)) { // if input is a digit
1910
+ } else if (key.length === 1) { // we want only characters and not command keys
2011
1911
  input.value = ''; // we are clearing the previous value so we can override it with the new value
2012
1912
  }
2013
1913
  };
@@ -2017,9 +1917,14 @@ class PasscodeInternal extends BaseInputClass {
2017
1917
  attributeChangedCallback(attrName, oldValue, newValue) {
2018
1918
  super.attributeChangedCallback?.(attrName, oldValue, newValue);
2019
1919
 
1920
+ // sync attributes to inputs
2020
1921
  if (oldValue !== newValue) {
2021
- if (PasscodeInternal.observedAttributes.includes(attrName) && !BaseInputClass.observedAttributes.includes(attrName)) {
2022
- this.inputs.forEach((input) => input.setAttribute(attrName, newValue));
1922
+ if (observedAttributes.includes(attrName)) {
1923
+ this.inputs.forEach(
1924
+ (input) => newValue === null ?
1925
+ input.removeAttribute(attrName) :
1926
+ input.setAttribute(attrName, newValue)
1927
+ );
2023
1928
  }
2024
1929
  }
2025
1930
  }
@@ -2112,7 +2017,7 @@ const customMixin = (superclass) =>
2112
2017
  ></${componentName$6}>
2113
2018
  `;
2114
2019
 
2115
- this.proxyElement.appendChild(template.content.cloneNode(true));
2020
+ this.baseElement.appendChild(template.content.cloneNode(true));
2116
2021
 
2117
2022
  this.inputElement = this.shadowRoot.querySelector(componentName$6);
2118
2023