@ionic/core 8.6.5-dev.11752242329.17d249a3 → 8.6.5-dev.11752245279.14b7cb03

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,118 @@ 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
+ this.inputRefs[i].value = validChars[i] || '';
228
217
  }
229
- // Update the value of the input group and emit the input change event
230
- this.value = validChars.join('');
231
218
  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
219
+ // Focus the next empty input or the last one
235
220
  setTimeout(() => {
236
- var _a;
237
221
  const nextIndex = validChars.length < length ? validChars.length : length - 1;
238
- (_a = this.inputRefs[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
222
+ this.inputRefs[nextIndex].focus();
239
223
  }, 20);
224
+ this.previousInputValues = [...this.inputValues];
240
225
  return;
241
226
  }
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] = '';
227
+ // 2. Input validation
228
+ // If the character entered is invalid (does not match the pattern),
229
+ // restore the previous value and exit
230
+ if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
231
+ input.value = this.inputValues[index] || '';
232
+ this.previousInputValues = [...this.inputValues];
246
233
  return;
247
234
  }
248
- // For single character input, fill the current box
249
- this.inputValues[index] = value;
250
- this.updateValue(event);
251
- if (value.length > 0) {
235
+ // 3. Full selection replacement or typing in an empty box
236
+ // If the user selects all text in the input box and types, or if the
237
+ // input box is empty, replace only this input box. If the box is empty,
238
+ // move to the next box, otherwise stay focused on this box.
239
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
240
+ const isEmpty = !this.inputValues[index];
241
+ if (isAllSelected || isEmpty) {
242
+ this.inputValues[index] = value;
243
+ input.value = value;
244
+ this.updateValue(event);
252
245
  this.focusNext(index);
246
+ this.previousInputValues = [...this.inputValues];
247
+ return;
248
+ }
249
+ // 4. Inserting in the middle with available space (shifting)
250
+ // If typing in a filled input box and there are empty boxes at the end,
251
+ // shift all values starting at the current box to the right, and insert
252
+ // the new character at the current box.
253
+ const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
254
+ if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
255
+ // Get the inserted character (from event or by diffing value/previousValue)
256
+ let newChar = event.data;
257
+ if (!newChar) {
258
+ newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
259
+ }
260
+ // Validate the new character before shifting
261
+ if (!validKeyPattern.test(newChar)) {
262
+ input.value = this.inputValues[index] || '';
263
+ this.previousInputValues = [...this.inputValues];
264
+ return;
265
+ }
266
+ // Shift values right from the end to the insertion point
267
+ for (let i = this.inputValues.length - 1; i > index; i--) {
268
+ this.inputValues[i] = this.inputValues[i - 1];
269
+ this.inputRefs[i].value = this.inputValues[i] || '';
270
+ }
271
+ this.inputValues[index] = newChar;
272
+ this.inputRefs[index].value = newChar;
273
+ this.updateValue(event);
274
+ this.previousInputValues = [...this.inputValues];
275
+ return;
276
+ }
277
+ // 5. Single character replacement
278
+ // Handles replacing a single character in a box containing a value based
279
+ // on the cursor position. We need the cursor position to determine which
280
+ // character was the last character typed. For example, if the user types "2"
281
+ // in an input box with the cursor at the beginning of the value of "6",
282
+ // the value will be "26", but we want to grab the "2" as the last character
283
+ // typed.
284
+ const cursorPos = (_a = input.selectionStart) !== null && _a !== void 0 ? _a : value.length;
285
+ const newCharIndex = cursorPos - 1;
286
+ const newChar = (_b = value[newCharIndex]) !== null && _b !== void 0 ? _b : value[0];
287
+ // Check if the new character is valid before updating the value
288
+ if (!validKeyPattern.test(newChar)) {
289
+ input.value = this.inputValues[index] || '';
290
+ this.previousInputValues = [...this.inputValues];
291
+ return;
253
292
  }
293
+ this.inputValues[index] = newChar;
294
+ input.value = newChar;
295
+ this.updateValue(event);
296
+ this.previousInputValues = [...this.inputValues];
254
297
  };
255
298
  /**
256
299
  * Handles pasting text into the input OTP component.
@@ -260,7 +303,7 @@ export class InputOTP {
260
303
  * the next empty input after pasting.
261
304
  */
262
305
  this.onPaste = (event) => {
263
- var _a, _b, _c;
306
+ var _a, _b;
264
307
  const { inputRefs, length, validKeyPattern } = this;
265
308
  event.preventDefault();
266
309
  const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
@@ -287,13 +330,8 @@ export class InputOTP {
287
330
  this.updateValue(event);
288
331
  // Focus the next empty input after pasting
289
332
  // 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
- }
333
+ const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
334
+ (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
297
335
  };
298
336
  }
299
337
  /**
@@ -432,6 +470,7 @@ export class InputOTP {
432
470
  });
433
471
  // Update the value without emitting events
434
472
  this.value = this.inputValues.join('');
473
+ this.previousInputValues = [...this.inputValues];
435
474
  }
436
475
  /**
437
476
  * Updates the value of the input group.
@@ -553,7 +592,7 @@ export class InputOTP {
553
592
  const tabbableIndex = this.getTabbableIndex();
554
593
  const pattern = this.getPattern();
555
594
  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, {
595
+ return (h(Host, { key: '37d7d3663a6d2450862ba2e14796460dbb89db20', class: createColorClasses(color, {
557
596
  [mode]: true,
558
597
  'has-focus': hasFocus,
559
598
  [`input-otp-size-${size}`]: true,
@@ -561,10 +600,10 @@ export class InputOTP {
561
600
  [`input-otp-fill-${fill}`]: true,
562
601
  'input-otp-disabled': disabled,
563
602
  '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: {
603
+ }) }, h("div", Object.assign({ key: '1cf5d9e680229adc0f918d84143dbcd557bb2ab2', 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: '646ad36d4d49339c502ebca8c7bd81763ff54119', class: {
565
604
  'input-otp-description': true,
566
605
  'input-otp-description-hidden': !hasDescription,
567
- } }, h("slot", { key: '9e8afa2f7fa76c3092582dc27770fdf565a1b9ba' }))));
606
+ } }, h("slot", { key: 'b3594ae5455ec09868da0a7b76835157e921949a' }))));
568
607
  }
569
608
  static get is() { return "ion-input-otp"; }
570
609
  static get encapsulation() { return "scoped"; }
@@ -849,7 +888,8 @@ export class InputOTP {
849
888
  static get states() {
850
889
  return {
851
890
  "inputValues": {},
852
- "hasFocus": {}
891
+ "hasFocus": {},
892
+ "previousInputValues": {}
853
893
  };
854
894
  }
855
895
  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:49:42",
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,118 @@ 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
+ this.inputRefs[i].value = validChars[i] || '';
237
226
  }
238
- // Update the value of the input group and emit the input change event
239
- this.value = validChars.join('');
240
227
  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
228
+ // Focus the next empty input or the last one
244
229
  setTimeout(() => {
245
- var _a;
246
230
  const nextIndex = validChars.length < length ? validChars.length : length - 1;
247
- (_a = this.inputRefs[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
231
+ this.inputRefs[nextIndex].focus();
248
232
  }, 20);
233
+ this.previousInputValues = [...this.inputValues];
249
234
  return;
250
235
  }
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] = '';
236
+ // 2. Input validation
237
+ // If the character entered is invalid (does not match the pattern),
238
+ // restore the previous value and exit
239
+ if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
240
+ input.value = this.inputValues[index] || '';
241
+ this.previousInputValues = [...this.inputValues];
255
242
  return;
256
243
  }
257
- // For single character input, fill the current box
258
- this.inputValues[index] = value;
259
- this.updateValue(event);
260
- if (value.length > 0) {
244
+ // 3. Full selection replacement or typing in an empty box
245
+ // If the user selects all text in the input box and types, or if the
246
+ // input box is empty, replace only this input box. If the box is empty,
247
+ // move to the next box, otherwise stay focused on this box.
248
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
249
+ const isEmpty = !this.inputValues[index];
250
+ if (isAllSelected || isEmpty) {
251
+ this.inputValues[index] = value;
252
+ input.value = value;
253
+ this.updateValue(event);
261
254
  this.focusNext(index);
255
+ this.previousInputValues = [...this.inputValues];
256
+ return;
257
+ }
258
+ // 4. Inserting in the middle with available space (shifting)
259
+ // If typing in a filled input box and there are empty boxes at the end,
260
+ // shift all values starting at the current box to the right, and insert
261
+ // the new character at the current box.
262
+ const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
263
+ if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
264
+ // Get the inserted character (from event or by diffing value/previousValue)
265
+ let newChar = event.data;
266
+ if (!newChar) {
267
+ newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
268
+ }
269
+ // Validate the new character before shifting
270
+ if (!validKeyPattern.test(newChar)) {
271
+ input.value = this.inputValues[index] || '';
272
+ this.previousInputValues = [...this.inputValues];
273
+ return;
274
+ }
275
+ // Shift values right from the end to the insertion point
276
+ for (let i = this.inputValues.length - 1; i > index; i--) {
277
+ this.inputValues[i] = this.inputValues[i - 1];
278
+ this.inputRefs[i].value = this.inputValues[i] || '';
279
+ }
280
+ this.inputValues[index] = newChar;
281
+ this.inputRefs[index].value = newChar;
282
+ this.updateValue(event);
283
+ this.previousInputValues = [...this.inputValues];
284
+ return;
285
+ }
286
+ // 5. Single character replacement
287
+ // Handles replacing a single character in a box containing a value based
288
+ // on the cursor position. We need the cursor position to determine which
289
+ // character was the last character typed. For example, if the user types "2"
290
+ // in an input box with the cursor at the beginning of the value of "6",
291
+ // the value will be "26", but we want to grab the "2" as the last character
292
+ // typed.
293
+ const cursorPos = (_a = input.selectionStart) !== null && _a !== void 0 ? _a : value.length;
294
+ const newCharIndex = cursorPos - 1;
295
+ const newChar = (_b = value[newCharIndex]) !== null && _b !== void 0 ? _b : value[0];
296
+ // Check if the new character is valid before updating the value
297
+ if (!validKeyPattern.test(newChar)) {
298
+ input.value = this.inputValues[index] || '';
299
+ this.previousInputValues = [...this.inputValues];
300
+ return;
262
301
  }
302
+ this.inputValues[index] = newChar;
303
+ input.value = newChar;
304
+ this.updateValue(event);
305
+ this.previousInputValues = [...this.inputValues];
263
306
  };
264
307
  /**
265
308
  * Handles pasting text into the input OTP component.
@@ -269,7 +312,7 @@ const InputOTP = class {
269
312
  * the next empty input after pasting.
270
313
  */
271
314
  this.onPaste = (event) => {
272
- var _a, _b, _c;
315
+ var _a, _b;
273
316
  const { inputRefs, length, validKeyPattern } = this;
274
317
  event.preventDefault();
275
318
  const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
@@ -296,13 +339,8 @@ const InputOTP = class {
296
339
  this.updateValue(event);
297
340
  // Focus the next empty input after pasting
298
341
  // 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
- }
342
+ const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
343
+ (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
306
344
  };
307
345
  }
308
346
  /**
@@ -441,6 +479,7 @@ const InputOTP = class {
441
479
  });
442
480
  // Update the value without emitting events
443
481
  this.value = this.inputValues.join('');
482
+ this.previousInputValues = [...this.inputValues];
444
483
  }
445
484
  /**
446
485
  * Updates the value of the input group.
@@ -562,7 +601,7 @@ const InputOTP = class {
562
601
  const tabbableIndex = this.getTabbableIndex();
563
602
  const pattern = this.getPattern();
564
603
  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, {
604
+ return (h(Host, { key: '37d7d3663a6d2450862ba2e14796460dbb89db20', class: createColorClasses(color, {
566
605
  [mode]: true,
567
606
  'has-focus': hasFocus,
568
607
  [`input-otp-size-${size}`]: true,
@@ -570,10 +609,10 @@ const InputOTP = class {
570
609
  [`input-otp-fill-${fill}`]: true,
571
610
  'input-otp-disabled': disabled,
572
611
  '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: {
612
+ }) }, h("div", Object.assign({ key: '1cf5d9e680229adc0f918d84143dbcd557bb2ab2', 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: '646ad36d4d49339c502ebca8c7bd81763ff54119', class: {
574
613
  'input-otp-description': true,
575
614
  'input-otp-description-hidden': !hasDescription,
576
- } }, h("slot", { key: '9e8afa2f7fa76c3092582dc27770fdf565a1b9ba' }))));
615
+ } }, h("slot", { key: 'b3594ae5455ec09868da0a7b76835157e921949a' }))));
577
616
  }
578
617
  get el() { return getElement(this); }
579
618
  static get watchers() { return {