@ionic/core 8.6.5-dev.11752242329.17d249a3 → 8.6.5-dev.11752243397.18475074

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.
@@ -21,6 +21,7 @@ export class InputOTP {
21
21
  this.isKeyboardNavigation = false;
22
22
  this.inputValues = [];
23
23
  this.hasFocus = false;
24
+ this.previousInputValues = [];
24
25
  /**
25
26
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
26
27
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -121,19 +122,12 @@ export class InputOTP {
121
122
  }
122
123
  };
123
124
  /**
124
- * Handles keyboard navigation and input for the OTP component.
125
+ * Handles keyboard navigation for the OTP component.
125
126
  *
126
127
  * Navigation:
127
128
  * - Backspace: Clears current input and moves to previous box if empty
128
129
  * - Arrow Left/Right: Moves focus between input boxes
129
130
  * - Tab: Allows normal tab navigation between components
130
- *
131
- * Input Behavior:
132
- * - Validates input against the allowed pattern
133
- * - When entering a key in a filled box:
134
- * - Shifts existing values right if there is room
135
- * - Updates the value of the input group
136
- * - Prevents default behavior to avoid automatic focus shift
137
131
  */
138
132
  this.onKeyDown = (index) => (event) => {
139
133
  const { length } = this;
@@ -188,69 +182,132 @@ export class InputOTP {
188
182
  // Let all tab events proceed normally
189
183
  return;
190
184
  }
191
- // If the input box contains a value and the key being
192
- // entered is a valid key for the input box update the value
193
- // and shift the values to the right if there is room.
194
- if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
195
- if (!this.inputValues[length - 1]) {
196
- for (let i = length - 1; i > index; i--) {
197
- this.inputValues[i] = this.inputValues[i - 1];
198
- this.inputRefs[i].value = this.inputValues[i] || '';
199
- }
200
- }
201
- this.inputValues[index] = event.key;
202
- this.inputRefs[index].value = event.key;
203
- this.updateValue(event);
204
- // Prevent default to avoid the browser from
205
- // automatically moving the focus to the next input
206
- event.preventDefault();
207
- }
208
185
  };
186
+ /**
187
+ * Processes all input scenarios for each input box.
188
+ *
189
+ * This function manages:
190
+ * 1. Autofill handling
191
+ * 2. Input validation
192
+ * 3. Full selection replacement or typing in an empty box
193
+ * 4. Inserting in the middle with available space (shifting)
194
+ * 5. Single character replacement
195
+ */
209
196
  this.onInput = (index) => (event) => {
197
+ var _a, _b;
210
198
  const { length, validKeyPattern } = this;
211
- const value = event.target.value;
212
- // If the value is longer than 1 character (autofill), split it into
213
- // characters and filter out invalid ones
214
- if (value.length > 1) {
199
+ const input = event.target;
200
+ const value = input.value;
201
+ const previousValue = this.previousInputValues[index] || '';
202
+ // 1. Autofill handling
203
+ // If the length of the value increases by more than 1 from the previous
204
+ // value, treat this as autofill. This is to prevent the case where the
205
+ // user is typing a single character into an input box containing a value
206
+ // as that will trigger this function with a value length of 2 characters.
207
+ const isAutofill = value.length - previousValue.length > 1;
208
+ if (isAutofill) {
209
+ // Distribute valid characters across input boxes
215
210
  const validChars = value
216
211
  .split('')
217
212
  .filter((char) => validKeyPattern.test(char))
218
213
  .slice(0, length);
219
- // If there are no valid characters coming from the
220
- // autofill, all input refs have to be cleared after the
221
- // browser has finished the autofill behavior
222
- if (validChars.length === 0) {
223
- requestAnimationFrame(() => {
224
- this.inputRefs.forEach((input) => {
225
- input.value = '';
226
- });
227
- });
214
+ for (let i = 0; i < length; i++) {
215
+ this.inputValues[i] = validChars[i] || '';
216
+ if (this.inputRefs[i] != null) {
217
+ this.inputRefs[i].value = validChars[i] || '';
218
+ }
228
219
  }
229
- // Update the value of the input group and emit the input change event
230
- this.value = validChars.join('');
231
220
  this.updateValue(event);
232
- // Focus the first empty input box or the last input box if all boxes
233
- // are filled after a small delay to ensure the input boxes have been
234
- // updated before moving the focus
221
+ // Focus the next empty input or the last one
235
222
  setTimeout(() => {
236
- var _a;
237
223
  const nextIndex = validChars.length < length ? validChars.length : length - 1;
238
- (_a = this.inputRefs[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
224
+ if (this.inputRefs[nextIndex] != null) {
225
+ this.inputRefs[nextIndex].focus();
226
+ }
239
227
  }, 20);
228
+ this.previousInputValues = [...this.inputValues];
240
229
  return;
241
230
  }
242
- // Only allow input if it matches the pattern
243
- if (value.length > 0 && !validKeyPattern.test(value)) {
244
- this.inputRefs[index].value = '';
245
- this.inputValues[index] = '';
231
+ // 2. Input validation
232
+ // If the character entered is invalid (does not match the pattern),
233
+ // restore the previous value and exit
234
+ if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
235
+ input.value = this.inputValues[index] || '';
236
+ this.previousInputValues[index] = this.inputValues[index] || '';
237
+ this.previousInputValues = [...this.inputValues];
246
238
  return;
247
239
  }
248
- // For single character input, fill the current box
249
- this.inputValues[index] = value;
250
- this.updateValue(event);
251
- if (value.length > 0) {
240
+ // 3. Full selection replacement or typing in an empty box
241
+ // If the user selects all text in the input box and types, or if the
242
+ // input box is empty, replace only this input box. If the box is empty,
243
+ // move to the next box, otherwise stay focused on this box.
244
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
245
+ const isEmpty = !this.inputValues[index];
246
+ if (isAllSelected || isEmpty) {
247
+ this.inputValues[index] = value;
248
+ input.value = value;
249
+ this.previousInputValues[index] = value;
250
+ this.updateValue(event);
252
251
  this.focusNext(index);
252
+ this.previousInputValues = [...this.inputValues];
253
+ return;
253
254
  }
255
+ // 4. Inserting in the middle with available space (shifting)
256
+ // If typing in a filled input box and there are empty boxes at the end,
257
+ // shift all values starting at the current box to the right, and insert
258
+ // the new character at the current box.
259
+ const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
260
+ if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
261
+ // Get the inserted character (from event or by diffing value/previousValue)
262
+ let newChar = event.data;
263
+ if (!newChar) {
264
+ newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
265
+ }
266
+ // Validate the new character before shifting
267
+ if (!validKeyPattern.test(newChar)) {
268
+ input.value = this.inputValues[index] || '';
269
+ this.previousInputValues[index] = this.inputValues[index] || '';
270
+ this.previousInputValues = [...this.inputValues];
271
+ return;
272
+ }
273
+ // Shift values right from the end to the insertion point
274
+ for (let i = this.inputValues.length - 1; i > index; i--) {
275
+ this.inputValues[i] = this.inputValues[i - 1];
276
+ if (this.inputRefs[i] != null) {
277
+ this.inputRefs[i].value = this.inputValues[i] || '';
278
+ }
279
+ }
280
+ this.inputValues[index] = newChar;
281
+ if (this.inputRefs[index] != null) {
282
+ this.inputRefs[index].value = newChar;
283
+ }
284
+ this.previousInputValues[index] = newChar;
285
+ this.updateValue(event);
286
+ this.previousInputValues = [...this.inputValues];
287
+ return;
288
+ }
289
+ // 5. Single character replacement
290
+ // Handles replacing a single character in a box containing a value based
291
+ // on the cursor position. We need the cursor position to determine which
292
+ // character was the last character typed. For example, if the user types "2"
293
+ // in an input box with the cursor at the beginning of the value of "6",
294
+ // the value will be "26", but we want to grab the "2" as the last character
295
+ // typed.
296
+ const cursorPos = (_a = input.selectionStart) !== null && _a !== void 0 ? _a : value.length;
297
+ const newCharIndex = cursorPos - 1;
298
+ const newChar = (_b = value[newCharIndex]) !== null && _b !== void 0 ? _b : value[0];
299
+ // Check if the new character is valid before updating the value
300
+ if (!validKeyPattern.test(newChar)) {
301
+ input.value = this.inputValues[index] || '';
302
+ this.previousInputValues[index] = this.inputValues[index] || '';
303
+ this.previousInputValues = [...this.inputValues];
304
+ return;
305
+ }
306
+ input.value = newChar;
307
+ this.inputValues[index] = newChar;
308
+ this.previousInputValues[index] = newChar;
309
+ this.updateValue(event);
310
+ this.previousInputValues = [...this.inputValues];
254
311
  };
255
312
  /**
256
313
  * Handles pasting text into the input OTP component.
@@ -260,7 +317,7 @@ export class InputOTP {
260
317
  * the next empty input after pasting.
261
318
  */
262
319
  this.onPaste = (event) => {
263
- var _a, _b, _c;
320
+ var _a, _b;
264
321
  const { inputRefs, length, validKeyPattern } = this;
265
322
  event.preventDefault();
266
323
  const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
@@ -287,13 +344,8 @@ export class InputOTP {
287
344
  this.updateValue(event);
288
345
  // Focus the next empty input after pasting
289
346
  // If all boxes are filled, focus the last input
290
- const nextEmptyIndex = validChars.length;
291
- if (nextEmptyIndex < length) {
292
- (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
293
- }
294
- else {
295
- (_c = inputRefs[length - 1]) === null || _c === void 0 ? void 0 : _c.focus();
296
- }
347
+ const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
348
+ (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
297
349
  };
298
350
  }
299
351
  /**
@@ -432,6 +484,7 @@ export class InputOTP {
432
484
  });
433
485
  // Update the value without emitting events
434
486
  this.value = this.inputValues.join('');
487
+ this.previousInputValues = [...this.inputValues];
435
488
  }
436
489
  /**
437
490
  * Updates the value of the input group.
@@ -553,7 +606,7 @@ export class InputOTP {
553
606
  const tabbableIndex = this.getTabbableIndex();
554
607
  const pattern = this.getPattern();
555
608
  const hasDescription = ((_b = (_a = el.querySelector('.input-otp-description')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim()) !== '';
556
- return (h(Host, { key: 'df8fca036cedea0812185a02e3b655d7d76285e0', class: createColorClasses(color, {
609
+ return (h(Host, { key: '084b4f7d148a55aef6b4b51c11483ee51d70d3bd', class: createColorClasses(color, {
557
610
  [mode]: true,
558
611
  'has-focus': hasFocus,
559
612
  [`input-otp-size-${size}`]: true,
@@ -561,10 +614,10 @@ export class InputOTP {
561
614
  [`input-otp-fill-${fill}`]: true,
562
615
  'input-otp-disabled': disabled,
563
616
  'input-otp-readonly': readonly,
564
- }) }, h("div", Object.assign({ key: '831be3f939cf037f0eb8d7e37e0afd4ef9a3c2c5', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index) => (h(Fragment, null, h("div", { class: "native-wrapper" }, h("input", { class: "native-input", id: `${inputId}-${index}`, "aria-label": `Input ${index + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index === tabbableIndex ? 0 : -1, value: inputValues[index] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index] = el), onInput: this.onInput(index), onBlur: this.onBlur, onFocus: this.onFocus(index), onKeyDown: this.onKeyDown(index), onPaste: this.onPaste })), this.showSeparator(index) && h("div", { class: "input-otp-separator" }))))), h("div", { key: '5311fedc34f7af3efd5f69e5a3d768055119c4f1', class: {
617
+ }) }, h("div", Object.assign({ key: '9d797deb7170bf6e4cc1acf70cca0b5d4ef51610', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index) => (h(Fragment, null, h("div", { class: "native-wrapper" }, h("input", { class: "native-input", id: `${inputId}-${index}`, "aria-label": `Input ${index + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index === tabbableIndex ? 0 : -1, value: inputValues[index] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index] = el), onInput: this.onInput(index), onBlur: this.onBlur, onFocus: this.onFocus(index), onKeyDown: this.onKeyDown(index), onPaste: this.onPaste })), this.showSeparator(index) && h("div", { class: "input-otp-separator" }))))), h("div", { key: 'a0463205729699430560032a68ade2e2ffa49b61', class: {
565
618
  'input-otp-description': true,
566
619
  'input-otp-description-hidden': !hasDescription,
567
- } }, h("slot", { key: '9e8afa2f7fa76c3092582dc27770fdf565a1b9ba' }))));
620
+ } }, h("slot", { key: '287fdaf0375cda3dcfafa2762d7daebf6f2bfe68' }))));
568
621
  }
569
622
  static get is() { return "ion-input-otp"; }
570
623
  static get encapsulation() { return "scoped"; }
@@ -849,7 +902,8 @@ export class InputOTP {
849
902
  static get states() {
850
903
  return {
851
904
  "inputValues": {},
852
- "hasFocus": {}
905
+ "hasFocus": {},
906
+ "previousInputValues": {}
853
907
  };
854
908
  }
855
909
  static get events() {
@@ -196,26 +196,6 @@ export class Modal {
196
196
  dragHandleEl.focus();
197
197
  }
198
198
  };
199
- /**
200
- * When the slot changes, we need to find all the modals in the slot
201
- * and set the data-parent-ion-modal attribute on them so we can find them
202
- * and dismiss them when we get dismissed.
203
- * We need to do it this way because when a modal is opened, it's moved to
204
- * the end of the body and is no longer an actual child of the modal.
205
- */
206
- this.onSlotChange = ({ target }) => {
207
- const slot = target;
208
- slot.assignedElements().forEach((el) => {
209
- el.querySelectorAll('ion-modal').forEach((childModal) => {
210
- // We don't need to write to the DOM if the modal is already tagged
211
- // If this is a deeply nested modal, this effect should cascade so we don't
212
- // need to worry about another modal claiming the same child.
213
- if (childModal.getAttribute('data-parent-ion-modal') === null) {
214
- childModal.setAttribute('data-parent-ion-modal', this.el.id);
215
- }
216
- });
217
- });
218
- };
219
199
  }
220
200
  onIsOpenChange(newValue, oldValue) {
221
201
  if (newValue === true && oldValue === false) {
@@ -579,12 +559,6 @@ export class Modal {
579
559
  * in case the dismiss transition does run.
580
560
  */
581
561
  const unlock = await this.lockController.lock();
582
- /**
583
- * Dismiss all child modals. This is especially important in
584
- * Angular and React because it's possible to lose control of a child
585
- * modal when the parent modal is dismissed.
586
- */
587
- await this.dismissNestedModals();
588
562
  /**
589
563
  * If a canDismiss handler is responsible
590
564
  * for calling the dismiss method, we should
@@ -811,12 +785,6 @@ export class Modal {
811
785
  }
812
786
  }
813
787
  }
814
- async dismissNestedModals() {
815
- const nestedModals = document.querySelectorAll(`ion-modal[data-parent-ion-modal="${this.el.id}"]`);
816
- nestedModals === null || nestedModals === void 0 ? void 0 : nestedModals.forEach(async (modal) => {
817
- await modal.dismiss(undefined, 'parent-dismissed');
818
- });
819
- }
820
788
  render() {
821
789
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
822
790
  const showHandle = handle !== false && isSheetModal;
@@ -824,20 +792,20 @@ export class Modal {
824
792
  const isCardModal = presentingElement !== undefined && mode === 'ios';
825
793
  const isHandleCycle = handleBehavior === 'cycle';
826
794
  const isSheetModalWithHandle = isSheetModal && showHandle;
827
- return (h(Host, Object.assign({ key: '4cb27cda1c331191ccc623e15a999e524d053397', "no-router": true,
795
+ return (h(Host, Object.assign({ key: '1980fa23331381c568a2be8091d888e09754fc52', "no-router": true,
828
796
  // Allow the modal to be navigable when the handle is focusable
829
797
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
830
798
  zIndex: `${20000 + this.overlayIndex}`,
831
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), h("ion-backdrop", { key: '5de598f6aaad693da61a8cdfd8ea73e3d9cc762e', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '2e74d2ed93c7884cbb1e3561d7296bba46e10426', class: "modal-shadow" }), h("div", Object.assign({ key: '314a7f8f669639b2d3f3a61a86e3b9de31a3ec78',
799
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), h("ion-backdrop", { key: 'ba94b055c064e2907eabbe6d7a43cb52adff1048', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '991f47859250d2143275ebb9b0b01a6ea8c491c0', class: "modal-shadow" }), h("div", Object.assign({ key: '02ecf8ac6a5bdb309ff993cc74a3911e99502a89',
832
800
  /*
833
801
  role and aria-modal must be used on the
834
802
  same element. They must also be set inside the
835
803
  shadow DOM otherwise ion-button will not be highlighted
836
804
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
837
805
  */
838
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'fc16ab670165f5ecc6b27b026637094da16d7af4', class: "modal-handle",
806
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '0180a4d6952e41bfd736272d1a49d47d86ca7fef', class: "modal-handle",
839
807
  // Prevents the handle from receiving keyboard focus when it does not cycle
840
- tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: 'e3634c590f877dc34d39d651c059fa3f701cfb4d', onSlotchange: this.onSlotChange }))));
808
+ tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: 'd062f330675f730ad70c23267baed200ca9b43b0' }))));
841
809
  }
842
810
  static get is() { return "ion-modal"; }
843
811
  static get encapsulation() { return "shadow"; }
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2025-07-11T14:00:38",
2
+ "timestamp": "2025-07-11T14:18:30",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.33.1",
@@ -30,6 +30,7 @@ const InputOTP = class {
30
30
  this.isKeyboardNavigation = false;
31
31
  this.inputValues = [];
32
32
  this.hasFocus = false;
33
+ this.previousInputValues = [];
33
34
  /**
34
35
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
35
36
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -130,19 +131,12 @@ const InputOTP = class {
130
131
  }
131
132
  };
132
133
  /**
133
- * Handles keyboard navigation and input for the OTP component.
134
+ * Handles keyboard navigation for the OTP component.
134
135
  *
135
136
  * Navigation:
136
137
  * - Backspace: Clears current input and moves to previous box if empty
137
138
  * - Arrow Left/Right: Moves focus between input boxes
138
139
  * - Tab: Allows normal tab navigation between components
139
- *
140
- * Input Behavior:
141
- * - Validates input against the allowed pattern
142
- * - When entering a key in a filled box:
143
- * - Shifts existing values right if there is room
144
- * - Updates the value of the input group
145
- * - Prevents default behavior to avoid automatic focus shift
146
140
  */
147
141
  this.onKeyDown = (index) => (event) => {
148
142
  const { length } = this;
@@ -197,69 +191,132 @@ const InputOTP = class {
197
191
  // Let all tab events proceed normally
198
192
  return;
199
193
  }
200
- // If the input box contains a value and the key being
201
- // entered is a valid key for the input box update the value
202
- // and shift the values to the right if there is room.
203
- if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
204
- if (!this.inputValues[length - 1]) {
205
- for (let i = length - 1; i > index; i--) {
206
- this.inputValues[i] = this.inputValues[i - 1];
207
- this.inputRefs[i].value = this.inputValues[i] || '';
208
- }
209
- }
210
- this.inputValues[index] = event.key;
211
- this.inputRefs[index].value = event.key;
212
- this.updateValue(event);
213
- // Prevent default to avoid the browser from
214
- // automatically moving the focus to the next input
215
- event.preventDefault();
216
- }
217
194
  };
195
+ /**
196
+ * Processes all input scenarios for each input box.
197
+ *
198
+ * This function manages:
199
+ * 1. Autofill handling
200
+ * 2. Input validation
201
+ * 3. Full selection replacement or typing in an empty box
202
+ * 4. Inserting in the middle with available space (shifting)
203
+ * 5. Single character replacement
204
+ */
218
205
  this.onInput = (index) => (event) => {
206
+ var _a, _b;
219
207
  const { length, validKeyPattern } = this;
220
- const value = event.target.value;
221
- // If the value is longer than 1 character (autofill), split it into
222
- // characters and filter out invalid ones
223
- if (value.length > 1) {
208
+ const input = event.target;
209
+ const value = input.value;
210
+ const previousValue = this.previousInputValues[index] || '';
211
+ // 1. Autofill handling
212
+ // If the length of the value increases by more than 1 from the previous
213
+ // value, treat this as autofill. This is to prevent the case where the
214
+ // user is typing a single character into an input box containing a value
215
+ // as that will trigger this function with a value length of 2 characters.
216
+ const isAutofill = value.length - previousValue.length > 1;
217
+ if (isAutofill) {
218
+ // Distribute valid characters across input boxes
224
219
  const validChars = value
225
220
  .split('')
226
221
  .filter((char) => validKeyPattern.test(char))
227
222
  .slice(0, length);
228
- // If there are no valid characters coming from the
229
- // autofill, all input refs have to be cleared after the
230
- // browser has finished the autofill behavior
231
- if (validChars.length === 0) {
232
- requestAnimationFrame(() => {
233
- this.inputRefs.forEach((input) => {
234
- input.value = '';
235
- });
236
- });
223
+ for (let i = 0; i < length; i++) {
224
+ this.inputValues[i] = validChars[i] || '';
225
+ if (this.inputRefs[i] != null) {
226
+ this.inputRefs[i].value = validChars[i] || '';
227
+ }
237
228
  }
238
- // Update the value of the input group and emit the input change event
239
- this.value = validChars.join('');
240
229
  this.updateValue(event);
241
- // Focus the first empty input box or the last input box if all boxes
242
- // are filled after a small delay to ensure the input boxes have been
243
- // updated before moving the focus
230
+ // Focus the next empty input or the last one
244
231
  setTimeout(() => {
245
- var _a;
246
232
  const nextIndex = validChars.length < length ? validChars.length : length - 1;
247
- (_a = this.inputRefs[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
233
+ if (this.inputRefs[nextIndex] != null) {
234
+ this.inputRefs[nextIndex].focus();
235
+ }
248
236
  }, 20);
237
+ this.previousInputValues = [...this.inputValues];
249
238
  return;
250
239
  }
251
- // Only allow input if it matches the pattern
252
- if (value.length > 0 && !validKeyPattern.test(value)) {
253
- this.inputRefs[index].value = '';
254
- this.inputValues[index] = '';
240
+ // 2. Input validation
241
+ // If the character entered is invalid (does not match the pattern),
242
+ // restore the previous value and exit
243
+ if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
244
+ input.value = this.inputValues[index] || '';
245
+ this.previousInputValues[index] = this.inputValues[index] || '';
246
+ this.previousInputValues = [...this.inputValues];
255
247
  return;
256
248
  }
257
- // For single character input, fill the current box
258
- this.inputValues[index] = value;
259
- this.updateValue(event);
260
- if (value.length > 0) {
249
+ // 3. Full selection replacement or typing in an empty box
250
+ // If the user selects all text in the input box and types, or if the
251
+ // input box is empty, replace only this input box. If the box is empty,
252
+ // move to the next box, otherwise stay focused on this box.
253
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
254
+ const isEmpty = !this.inputValues[index];
255
+ if (isAllSelected || isEmpty) {
256
+ this.inputValues[index] = value;
257
+ input.value = value;
258
+ this.previousInputValues[index] = value;
259
+ this.updateValue(event);
261
260
  this.focusNext(index);
261
+ this.previousInputValues = [...this.inputValues];
262
+ return;
262
263
  }
264
+ // 4. Inserting in the middle with available space (shifting)
265
+ // If typing in a filled input box and there are empty boxes at the end,
266
+ // shift all values starting at the current box to the right, and insert
267
+ // the new character at the current box.
268
+ const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
269
+ if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
270
+ // Get the inserted character (from event or by diffing value/previousValue)
271
+ let newChar = event.data;
272
+ if (!newChar) {
273
+ newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
274
+ }
275
+ // Validate the new character before shifting
276
+ if (!validKeyPattern.test(newChar)) {
277
+ input.value = this.inputValues[index] || '';
278
+ this.previousInputValues[index] = this.inputValues[index] || '';
279
+ this.previousInputValues = [...this.inputValues];
280
+ return;
281
+ }
282
+ // Shift values right from the end to the insertion point
283
+ for (let i = this.inputValues.length - 1; i > index; i--) {
284
+ this.inputValues[i] = this.inputValues[i - 1];
285
+ if (this.inputRefs[i] != null) {
286
+ this.inputRefs[i].value = this.inputValues[i] || '';
287
+ }
288
+ }
289
+ this.inputValues[index] = newChar;
290
+ if (this.inputRefs[index] != null) {
291
+ this.inputRefs[index].value = newChar;
292
+ }
293
+ this.previousInputValues[index] = newChar;
294
+ this.updateValue(event);
295
+ this.previousInputValues = [...this.inputValues];
296
+ return;
297
+ }
298
+ // 5. Single character replacement
299
+ // Handles replacing a single character in a box containing a value based
300
+ // on the cursor position. We need the cursor position to determine which
301
+ // character was the last character typed. For example, if the user types "2"
302
+ // in an input box with the cursor at the beginning of the value of "6",
303
+ // the value will be "26", but we want to grab the "2" as the last character
304
+ // typed.
305
+ const cursorPos = (_a = input.selectionStart) !== null && _a !== void 0 ? _a : value.length;
306
+ const newCharIndex = cursorPos - 1;
307
+ const newChar = (_b = value[newCharIndex]) !== null && _b !== void 0 ? _b : value[0];
308
+ // Check if the new character is valid before updating the value
309
+ if (!validKeyPattern.test(newChar)) {
310
+ input.value = this.inputValues[index] || '';
311
+ this.previousInputValues[index] = this.inputValues[index] || '';
312
+ this.previousInputValues = [...this.inputValues];
313
+ return;
314
+ }
315
+ input.value = newChar;
316
+ this.inputValues[index] = newChar;
317
+ this.previousInputValues[index] = newChar;
318
+ this.updateValue(event);
319
+ this.previousInputValues = [...this.inputValues];
263
320
  };
264
321
  /**
265
322
  * Handles pasting text into the input OTP component.
@@ -269,7 +326,7 @@ const InputOTP = class {
269
326
  * the next empty input after pasting.
270
327
  */
271
328
  this.onPaste = (event) => {
272
- var _a, _b, _c;
329
+ var _a, _b;
273
330
  const { inputRefs, length, validKeyPattern } = this;
274
331
  event.preventDefault();
275
332
  const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
@@ -296,13 +353,8 @@ const InputOTP = class {
296
353
  this.updateValue(event);
297
354
  // Focus the next empty input after pasting
298
355
  // If all boxes are filled, focus the last input
299
- const nextEmptyIndex = validChars.length;
300
- if (nextEmptyIndex < length) {
301
- (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
302
- }
303
- else {
304
- (_c = inputRefs[length - 1]) === null || _c === void 0 ? void 0 : _c.focus();
305
- }
356
+ const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
357
+ (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
306
358
  };
307
359
  }
308
360
  /**
@@ -441,6 +493,7 @@ const InputOTP = class {
441
493
  });
442
494
  // Update the value without emitting events
443
495
  this.value = this.inputValues.join('');
496
+ this.previousInputValues = [...this.inputValues];
444
497
  }
445
498
  /**
446
499
  * Updates the value of the input group.
@@ -562,7 +615,7 @@ const InputOTP = class {
562
615
  const tabbableIndex = this.getTabbableIndex();
563
616
  const pattern = this.getPattern();
564
617
  const hasDescription = ((_b = (_a = el.querySelector('.input-otp-description')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim()) !== '';
565
- return (h(Host, { key: 'df8fca036cedea0812185a02e3b655d7d76285e0', class: createColorClasses(color, {
618
+ return (h(Host, { key: '084b4f7d148a55aef6b4b51c11483ee51d70d3bd', class: createColorClasses(color, {
566
619
  [mode]: true,
567
620
  'has-focus': hasFocus,
568
621
  [`input-otp-size-${size}`]: true,
@@ -570,10 +623,10 @@ const InputOTP = class {
570
623
  [`input-otp-fill-${fill}`]: true,
571
624
  'input-otp-disabled': disabled,
572
625
  'input-otp-readonly': readonly,
573
- }) }, h("div", Object.assign({ key: '831be3f939cf037f0eb8d7e37e0afd4ef9a3c2c5', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index) => (h(Fragment, null, h("div", { class: "native-wrapper" }, h("input", { class: "native-input", id: `${inputId}-${index}`, "aria-label": `Input ${index + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index === tabbableIndex ? 0 : -1, value: inputValues[index] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index] = el), onInput: this.onInput(index), onBlur: this.onBlur, onFocus: this.onFocus(index), onKeyDown: this.onKeyDown(index), onPaste: this.onPaste })), this.showSeparator(index) && h("div", { class: "input-otp-separator" }))))), h("div", { key: '5311fedc34f7af3efd5f69e5a3d768055119c4f1', class: {
626
+ }) }, h("div", Object.assign({ key: '9d797deb7170bf6e4cc1acf70cca0b5d4ef51610', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index) => (h(Fragment, null, h("div", { class: "native-wrapper" }, h("input", { class: "native-input", id: `${inputId}-${index}`, "aria-label": `Input ${index + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index === tabbableIndex ? 0 : -1, value: inputValues[index] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index] = el), onInput: this.onInput(index), onBlur: this.onBlur, onFocus: this.onFocus(index), onKeyDown: this.onKeyDown(index), onPaste: this.onPaste })), this.showSeparator(index) && h("div", { class: "input-otp-separator" }))))), h("div", { key: 'a0463205729699430560032a68ade2e2ffa49b61', class: {
574
627
  'input-otp-description': true,
575
628
  'input-otp-description-hidden': !hasDescription,
576
- } }, h("slot", { key: '9e8afa2f7fa76c3092582dc27770fdf565a1b9ba' }))));
629
+ } }, h("slot", { key: '287fdaf0375cda3dcfafa2762d7daebf6f2bfe68' }))));
577
630
  }
578
631
  get el() { return getElement(this); }
579
632
  static get watchers() { return {