@descope/web-components-ui 1.104.0 → 1.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descope/web-components-ui",
3
- "version": "1.104.0",
3
+ "version": "1.106.0",
4
4
  "description": "",
5
5
  "main": "dist/cjs/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -82,14 +82,14 @@
82
82
  "@descope-ui/descope-icon": "0.0.14",
83
83
  "@descope-ui/descope-text": "0.0.17",
84
84
  "@descope-ui/descope-avatar": "0.0.17",
85
- "@descope-ui/descope-combo-box": "0.1.6",
86
- "@descope-ui/descope-autocomplete-field": "0.0.19",
87
- "@descope-ui/descope-address-field": "0.0.18",
88
- "@descope-ui/descope-timer": "0.0.13",
89
- "@descope-ui/descope-timer-button": "0.0.15",
90
- "@descope-ui/descope-password-strength": "0.0.10",
91
- "@descope-ui/descope-collapsible-container": "0.0.14",
92
- "@descope-ui/descope-recovery-codes": "0.0.3"
85
+ "@descope-ui/descope-combo-box": "0.1.7",
86
+ "@descope-ui/descope-autocomplete-field": "0.0.20",
87
+ "@descope-ui/descope-address-field": "0.0.19",
88
+ "@descope-ui/descope-timer": "0.0.14",
89
+ "@descope-ui/descope-timer-button": "0.0.16",
90
+ "@descope-ui/descope-password-strength": "0.0.11",
91
+ "@descope-ui/descope-collapsible-container": "0.0.15",
92
+ "@descope-ui/descope-recovery-codes": "0.0.4"
93
93
  },
94
94
  "overrides": {
95
95
  "@vaadin/avatar": "24.3.4",
@@ -17,10 +17,18 @@ import {
17
17
  } from './helpers';
18
18
  import { formats } from './formats';
19
19
  import { calendarIcon } from './icons';
20
- import { counterConfig, DEFAULT_FORMAT, DIVIDER, NATIVE_FORMAT, valRange } from './consts';
20
+ import {
21
+ counterConfig,
22
+ DEFAULT_FORMAT,
23
+ DIVIDER,
24
+ MOBILE_DEVICE_INTERACTION_TIMEOUT_MS,
25
+ NATIVE_FORMAT,
26
+ valRange,
27
+ } from './consts';
21
28
  import { DateCounter } from './DateCounterClass';
22
29
  import { TextFieldClass } from '../descope-text-field/TextFieldClass';
23
30
  import { injectStyle } from '@descope-ui/common/components-helpers';
31
+ import { parseDateString } from './helpers';
24
32
 
25
33
  export const componentName = getComponentName('date-field');
26
34
 
@@ -40,8 +48,6 @@ class RawDateFieldClass extends BaseInputClass {
40
48
 
41
49
  selectedCounterIdx = 0;
42
50
 
43
- #focused = false;
44
-
45
51
  updateCountersDisplay() {
46
52
  this.inputElement.value = this.countersValue;
47
53
  }
@@ -215,6 +221,11 @@ class RawDateFieldClass extends BaseInputClass {
215
221
  return this.getAttribute('disable-calendar') === 'true';
216
222
  }
217
223
 
224
+ get isSelectAll() {
225
+ const inputEle = this.inputElement.baseElement.inputElement;
226
+ return inputEle.value.length === inputEle.selectionStart + inputEle.selectionEnd;
227
+ }
228
+
218
229
  reportValidity() {
219
230
  this.inputElement.reportValidity();
220
231
  }
@@ -249,24 +260,10 @@ class RawDateFieldClass extends BaseInputClass {
249
260
  this.inputElement.addEventListener('focus', this.onFocus.bind(this));
250
261
  this.inputElement.addEventListener('blur', this.onBlur.bind(this));
251
262
  this.inputElement.addEventListener('click', this.handleMouseCaretPositionChange.bind(this));
252
- this.inputElement.addEventListener('keydown', this.handleArrowKeys.bind(this));
263
+ this.inputElement.addEventListener('keydown', this.handleKeyboard.bind(this));
253
264
  this.inputElement.addEventListener('beforeinput', this.handleInput.bind(this));
254
-
255
- // We want to handle touch events the same way we handle `click` events.
256
- // Since we can't seem to block touch events (`touch-action: none` or preventing default on `touchstart`
257
- // or `touchend`, we listen to `pointerdown` and in case it's of type `touch` we execute
258
- // the component's logic for range selection.
259
- this.inputElement.addEventListener('pointerdown', (e) => {
260
- if (e.pointerType === 'touch') {
261
- e.preventDefault();
262
- if (!this.#focused) {
263
- this.inputElement.focus();
264
- }
265
- setTimeout(() => {
266
- this.handleMouseCaretPositionChange(e);
267
- }, 250);
268
- }
269
- });
265
+ this.inputElement.addEventListener('pointerdown', this.onPointerDown.bind(this));
266
+ this.inputElement.addEventListener('paste', this.onPaste.bind(this));
270
267
 
271
268
  forwardAttrs(this, this.inputElement, {
272
269
  includeAttrs: [
@@ -295,6 +292,8 @@ class RawDateFieldClass extends BaseInputClass {
295
292
  handleInput(e) {
296
293
  e.preventDefault();
297
294
 
295
+ this.handleSelectAll();
296
+
298
297
  if (e.data && isNumber(e.data)) {
299
298
  this.parseDigits(e.data);
300
299
  this.updateCountersDisplay();
@@ -315,6 +314,12 @@ class RawDateFieldClass extends BaseInputClass {
315
314
  });
316
315
  }
317
316
 
317
+ handleSelectAll() {
318
+ if (this.isSelectAll) {
319
+ this.selectFirstCounter();
320
+ }
321
+ }
322
+
318
323
  #popoverPosStylesheet;
319
324
 
320
325
  #popoverRenderer(root) {
@@ -464,16 +469,19 @@ class RawDateFieldClass extends BaseInputClass {
464
469
  });
465
470
  }
466
471
 
472
+ // In mobile devices, there are cases were `pointerdown` is triggered
473
+ // instead of `click`.
474
+ onPointerDown(e) {
475
+ setTimeout(() => this.handleMouseCaretPositionChange(e), MOBILE_DEVICE_INTERACTION_TIMEOUT_MS);
476
+ }
477
+
467
478
  onFocus() {
468
- if (this.isReadOnly || this.#focused) {
479
+ if (this.isReadOnly) {
469
480
  return;
470
481
  }
471
482
 
472
- // We use this flag to support mobile logic, which calls `focus` on touch event, we want to make sure
473
- // focus executes it logic only when needed.
474
- this.#focused = true;
475
-
476
- this.resetDisplay();
483
+ // We need to wait for focus to end before we set selection
484
+ setTimeout(() => this.resetDisplay());
477
485
  }
478
486
 
479
487
  resetDisplay() {
@@ -481,18 +489,13 @@ class RawDateFieldClass extends BaseInputClass {
481
489
  this.inputElement.value = this.format;
482
490
  }
483
491
 
484
- // On focus select the first part of the format placeholder
485
- this.selectedCounterIdx = 0;
486
-
487
- setTimeout(() => {
488
- // set selection on first counter
489
- this.inputElement.setSelectionRange(0, this.sortedCounters[0].length);
490
- });
492
+ // On focus select the first counter
493
+ this.selectFirstCounter();
494
+ // set selection on first counter
495
+ this.inputElement.setSelectionRange(0, this.sortedCounters[0].length);
491
496
  }
492
497
 
493
498
  onBlur() {
494
- this.#focused = false;
495
-
496
499
  if (this.opened) {
497
500
  return;
498
501
  }
@@ -522,6 +525,8 @@ class RawDateFieldClass extends BaseInputClass {
522
525
  this.selectNextCounter();
523
526
  }
524
527
 
528
+ // We wait for the digit to be parsed, and only then set the selection.
529
+ // Failing to do so results in unexpected "jump" of the screen in mobile devices.
525
530
  this.setInputSelectionRange();
526
531
  }
527
532
 
@@ -537,10 +542,18 @@ class RawDateFieldClass extends BaseInputClass {
537
542
 
538
543
  setSelectedCounterByCaretPosition(e) {
539
544
  this.selectedCounterIdx = this.getCounterIdx(
540
- e.target.selectionStart || this.inputElement.selectionStart
545
+ // if triggered by touch event, target might not include `selectionStart`
546
+ // in that case we fall back to the inputElement's `selectionStart` value.
547
+ // Therefore, it is recommended to run this function with setTimeout,
548
+ // at least for mobile events.
549
+ e.target?.selectionStart || this.inputElement.selectionStart
541
550
  );
542
551
  }
543
552
 
553
+ selectFirstCounter() {
554
+ this.selectedCounterIdx = 0;
555
+ }
556
+
544
557
  selectNextCounter() {
545
558
  if (this.selectedCounterIdx < this.dateCounters.length) {
546
559
  this.selectedCounterIdx = Math.min(this.selectedCounterIdx + 1, 2);
@@ -563,14 +576,18 @@ class RawDateFieldClass extends BaseInputClass {
563
576
  return;
564
577
  }
565
578
 
566
- const caretStart = this.sortedCounters
567
- .slice(0, this.selectedCounterIdx)
568
- .reduce((acc, counter) => acc + counter.length, this.selectedCounterIdx);
579
+ // We wait for before setting the selection, otherwise there's an
580
+ // unexpected "jump" of the screen in mobile devices.
581
+ setTimeout(() => {
582
+ const caretStart = this.sortedCounters
583
+ .slice(0, this.selectedCounterIdx)
584
+ .reduce((acc, counter) => acc + counter.length, this.selectedCounterIdx);
569
585
 
570
- this.inputElement.setSelectionRange(
571
- caretStart,
572
- caretStart + this.sortedCounters[this.selectedCounterIdx].length
573
- );
586
+ this.inputElement.setSelectionRange(
587
+ caretStart,
588
+ caretStart + this.sortedCounters[this.selectedCounterIdx].length
589
+ );
590
+ });
574
591
  }
575
592
 
576
593
  resetDateCounters() {
@@ -596,7 +613,17 @@ class RawDateFieldClass extends BaseInputClass {
596
613
  });
597
614
  }
598
615
 
599
- handleArrowKeys(e) {
616
+ handleKeyboard(e) {
617
+ if (e.metaKey || e.ctrlKey) {
618
+ if (e.key.toLowerCase() === 'x') {
619
+ this.onCut(e);
620
+ }
621
+
622
+ return;
623
+ }
624
+
625
+ this.handleSelectAll();
626
+
600
627
  if (e.key === 'ArrowUp') {
601
628
  this.activeCounter.inc();
602
629
  } else if (e.key === 'ArrowDown') {
@@ -607,9 +634,7 @@ class RawDateFieldClass extends BaseInputClass {
607
634
  this.selectPrevCounter();
608
635
  }
609
636
 
610
- setTimeout(() => {
611
- this.setInputSelectionRange();
612
- });
637
+ this.setInputSelectionRange();
613
638
  }
614
639
 
615
640
  handleNavKeys(e) {
@@ -631,27 +656,37 @@ class RawDateFieldClass extends BaseInputClass {
631
656
  }
632
657
 
633
658
  handleBackspace() {
634
- if (this.activeCounter.isEmpty) {
635
- this.activeCounter.clear();
659
+ if (this.isSelectAll) {
660
+ this.resetToInitialState();
661
+ return;
662
+ }
663
+
664
+ const counter = this.activeCounter;
665
+
666
+ if (counter.isEmpty) {
636
667
  this.selectPrevCounter();
637
668
  this.setInputSelectionRange();
638
669
  } else {
639
- this.activeCounter.del();
670
+ counter.set('');
640
671
  }
672
+
673
+ // To support keyboards like SwiftKey, we need to re-render the counters display and selection,
674
+ // otherwise we get an unexpected behavior, where the format is deleted.
675
+ setTimeout(() => {
676
+ this.updateCountersDisplay();
677
+ this.setInputSelectionRange();
678
+ });
641
679
  }
642
680
 
643
681
  handleMouseCaretPositionChange(e) {
644
682
  if (this.opened) {
645
683
  return;
646
684
  }
685
+
647
686
  e.preventDefault();
648
- this.setSelectedCounterByCaretPosition(e);
649
687
 
650
- // On keydown - in desktop mode - selection is sometimes not set, and instead there is a cursor.
651
- // We need to wait until we can set selection range.
652
- setTimeout(() => {
653
- this.setInputSelectionRange();
654
- });
688
+ this.setSelectedCounterByCaretPosition(e);
689
+ this.setInputSelectionRange();
655
690
  }
656
691
 
657
692
  onInitialValueChange(val) {
@@ -743,6 +778,62 @@ class RawDateFieldClass extends BaseInputClass {
743
778
 
744
779
  return ret;
745
780
  }
781
+
782
+ resetToInitialState() {
783
+ this.resetDateCounters();
784
+ this.selectFirstCounter();
785
+ this.resetDisplay();
786
+ }
787
+
788
+ onCut(e) {
789
+ e.preventDefault();
790
+
791
+ if (this.isSelectAll) {
792
+ this.#copyToClipboard(this.countersValue);
793
+ this.resetToInitialState();
794
+ } else {
795
+ this.#copyToClipboard(this.activeCounter.stringValue);
796
+ this.activeCounter.set('');
797
+ }
798
+
799
+ this.setInputSelectionRange();
800
+ }
801
+
802
+ #copyToClipboard(value) {
803
+ try {
804
+ navigator.clipboard.writeText(value);
805
+ } catch (err) {
806
+ console.error('Failed to copy date value:', err);
807
+ }
808
+ }
809
+
810
+ onPaste(e) {
811
+ e.preventDefault();
812
+
813
+ const clipboardData = e.clipboardData || window.clipboardData;
814
+ const pastedData = clipboardData.getData('Text');
815
+
816
+ // try paste entire date if valid
817
+ const validDate = parseDateString(pastedData, this.format);
818
+
819
+ if (validDate) {
820
+ this.value = validDate.getTime();
821
+ this.onDateCounterChange();
822
+
823
+ // select all
824
+ setTimeout(() => this.inputElement.setSelectionRange(0, this.inputElement.value.length));
825
+ } else {
826
+ const value = Number(pastedData);
827
+
828
+ // try paste in counter if possible
829
+ if (value && this.activeCounter.min <= value && this.activeCounter.max >= value) {
830
+ // use String to get rid of any zero padding
831
+ this.activeCounter.set(String(value));
832
+
833
+ setTimeout(() => this.setInputSelectionRange());
834
+ }
835
+ }
836
+ }
746
837
  }
747
838
 
748
839
  const textVars = TextFieldClass.cssVarList;
@@ -46,3 +46,5 @@ export const valRange = {
46
46
  export const BUTTON_LABEL_DONE = 'Done';
47
47
  export const BUTTON_LABEL_CANCEL = 'Cancel';
48
48
  export const CALENDAR_LABEL_TODAY = 'Today';
49
+
50
+ export const MOBILE_DEVICE_INTERACTION_TIMEOUT_MS = 150;
@@ -1,3 +1,5 @@
1
+ import { formats } from './formats';
2
+
1
3
  export const isValidTimestamp = (val) => !Number.isNaN(Number(val));
2
4
 
3
5
  export const isNumber = (val) => !!String(val || '').trim() && !Number.isNaN(Number(val));
@@ -47,3 +49,9 @@ export const overrideConstructedStylesheet = (ele) => {
47
49
  cs.insertRule(':host{display:block!important;}');
48
50
  ele?.shadowRoot?.adoptedStyleSheets?.push(cs);
49
51
  };
52
+
53
+ export const parseDateString = (val, format) => {
54
+ const trimmed = val.trim?.();
55
+ if (!trimmed) return null;
56
+ return formats[format].getDate(trimmed);
57
+ };