@descope/web-components-ui 1.0.69 → 1.0.71

Sign up to get free protection for your applications and to get access to all the features.
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