@ionic/core 8.6.5-nightly.20250711 → 8.6.5-nightly.20250715

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.
@@ -33,6 +33,7 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
33
33
  this.isKeyboardNavigation = false;
34
34
  this.inputValues = [];
35
35
  this.hasFocus = false;
36
+ this.previousInputValues = [];
36
37
  /**
37
38
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
38
39
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -133,19 +134,12 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
133
134
  }
134
135
  };
135
136
  /**
136
- * Handles keyboard navigation and input for the OTP component.
137
+ * Handles keyboard navigation for the OTP component.
137
138
  *
138
139
  * Navigation:
139
140
  * - Backspace: Clears current input and moves to previous box if empty
140
141
  * - Arrow Left/Right: Moves focus between input boxes
141
142
  * - Tab: Allows normal tab navigation between components
142
- *
143
- * Input Behavior:
144
- * - Validates input against the allowed pattern
145
- * - When entering a key in a filled box:
146
- * - Shifts existing values right if there is room
147
- * - Updates the value of the input group
148
- * - Prevents default behavior to avoid automatic focus shift
149
143
  */
150
144
  this.onKeyDown = (index) => (event) => {
151
145
  const { length } = this;
@@ -200,30 +194,31 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
200
194
  // Let all tab events proceed normally
201
195
  return;
202
196
  }
203
- // If the input box contains a value and the key being
204
- // entered is a valid key for the input box update the value
205
- // and shift the values to the right if there is room.
206
- if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
207
- if (!this.inputValues[length - 1]) {
208
- for (let i = length - 1; i > index; i--) {
209
- this.inputValues[i] = this.inputValues[i - 1];
210
- this.inputRefs[i].value = this.inputValues[i] || '';
211
- }
212
- }
213
- this.inputValues[index] = event.key;
214
- this.inputRefs[index].value = event.key;
215
- this.updateValue(event);
216
- // Prevent default to avoid the browser from
217
- // automatically moving the focus to the next input
218
- event.preventDefault();
219
- }
220
197
  };
198
+ /**
199
+ * Processes all input scenarios for each input box.
200
+ *
201
+ * This function manages:
202
+ * 1. Autofill handling
203
+ * 2. Input validation
204
+ * 3. Full selection replacement or typing in an empty box
205
+ * 4. Inserting in the middle with available space (shifting)
206
+ * 5. Single character replacement
207
+ */
221
208
  this.onInput = (index) => (event) => {
209
+ var _a, _b;
222
210
  const { length, validKeyPattern } = this;
223
- const value = event.target.value;
224
- // If the value is longer than 1 character (autofill), split it into
225
- // characters and filter out invalid ones
226
- if (value.length > 1) {
211
+ const input = event.target;
212
+ const value = input.value;
213
+ const previousValue = this.previousInputValues[index] || '';
214
+ // 1. Autofill handling
215
+ // If the length of the value increases by more than 1 from the previous
216
+ // value, treat this as autofill. This is to prevent the case where the
217
+ // user is typing a single character into an input box containing a value
218
+ // as that will trigger this function with a value length of 2 characters.
219
+ const isAutofill = value.length - previousValue.length > 1;
220
+ if (isAutofill) {
221
+ // Distribute valid characters across input boxes
227
222
  const validChars = value
228
223
  .split('')
229
224
  .filter((char) => validKeyPattern.test(char))
@@ -238,8 +233,10 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
238
233
  });
239
234
  });
240
235
  }
241
- // Update the value of the input group and emit the input change event
242
- this.value = validChars.join('');
236
+ for (let i = 0; i < length; i++) {
237
+ this.inputValues[i] = validChars[i] || '';
238
+ this.inputRefs[i].value = validChars[i] || '';
239
+ }
243
240
  this.updateValue(event);
244
241
  // Focus the first empty input box or the last input box if all boxes
245
242
  // are filled after a small delay to ensure the input boxes have been
@@ -249,20 +246,79 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
249
246
  const nextIndex = validChars.length < length ? validChars.length : length - 1;
250
247
  (_a = this.inputRefs[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
251
248
  }, 20);
249
+ this.previousInputValues = [...this.inputValues];
252
250
  return;
253
251
  }
254
- // Only allow input if it matches the pattern
255
- if (value.length > 0 && !validKeyPattern.test(value)) {
256
- this.inputRefs[index].value = '';
257
- this.inputValues[index] = '';
252
+ // 2. Input validation
253
+ // If the character entered is invalid (does not match the pattern),
254
+ // restore the previous value and exit
255
+ if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
256
+ input.value = this.inputValues[index] || '';
257
+ this.previousInputValues = [...this.inputValues];
258
258
  return;
259
259
  }
260
- // For single character input, fill the current box
261
- this.inputValues[index] = value;
262
- this.updateValue(event);
263
- if (value.length > 0) {
260
+ // 3. Full selection replacement or typing in an empty box
261
+ // If the user selects all text in the input box and types, or if the
262
+ // input box is empty, replace only this input box. If the box is empty,
263
+ // move to the next box, otherwise stay focused on this box.
264
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
265
+ const isEmpty = !this.inputValues[index];
266
+ if (isAllSelected || isEmpty) {
267
+ this.inputValues[index] = value;
268
+ input.value = value;
269
+ this.updateValue(event);
264
270
  this.focusNext(index);
271
+ this.previousInputValues = [...this.inputValues];
272
+ return;
265
273
  }
274
+ // 4. Inserting in the middle with available space (shifting)
275
+ // If typing in a filled input box and there are empty boxes at the end,
276
+ // shift all values starting at the current box to the right, and insert
277
+ // the new character at the current box.
278
+ const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
279
+ if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
280
+ // Get the inserted character (from event or by diffing value/previousValue)
281
+ let newChar = event.data;
282
+ if (!newChar) {
283
+ newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
284
+ }
285
+ // Validate the new character before shifting
286
+ if (!validKeyPattern.test(newChar)) {
287
+ input.value = this.inputValues[index] || '';
288
+ this.previousInputValues = [...this.inputValues];
289
+ return;
290
+ }
291
+ // Shift values right from the end to the insertion point
292
+ for (let i = this.inputValues.length - 1; i > index; i--) {
293
+ this.inputValues[i] = this.inputValues[i - 1];
294
+ this.inputRefs[i].value = this.inputValues[i] || '';
295
+ }
296
+ this.inputValues[index] = newChar;
297
+ this.inputRefs[index].value = newChar;
298
+ this.updateValue(event);
299
+ this.previousInputValues = [...this.inputValues];
300
+ return;
301
+ }
302
+ // 5. Single character replacement
303
+ // Handles replacing a single character in a box containing a value based
304
+ // on the cursor position. We need the cursor position to determine which
305
+ // character was the last character typed. For example, if the user types "2"
306
+ // in an input box with the cursor at the beginning of the value of "6",
307
+ // the value will be "26", but we want to grab the "2" as the last character
308
+ // typed.
309
+ const cursorPos = (_a = input.selectionStart) !== null && _a !== void 0 ? _a : value.length;
310
+ const newCharIndex = cursorPos - 1;
311
+ const newChar = (_b = value[newCharIndex]) !== null && _b !== void 0 ? _b : value[0];
312
+ // Check if the new character is valid before updating the value
313
+ if (!validKeyPattern.test(newChar)) {
314
+ input.value = this.inputValues[index] || '';
315
+ this.previousInputValues = [...this.inputValues];
316
+ return;
317
+ }
318
+ this.inputValues[index] = newChar;
319
+ input.value = newChar;
320
+ this.updateValue(event);
321
+ this.previousInputValues = [...this.inputValues];
266
322
  };
267
323
  /**
268
324
  * Handles pasting text into the input OTP component.
@@ -272,7 +328,7 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
272
328
  * the next empty input after pasting.
273
329
  */
274
330
  this.onPaste = (event) => {
275
- var _a, _b, _c;
331
+ var _a, _b;
276
332
  const { inputRefs, length, validKeyPattern } = this;
277
333
  event.preventDefault();
278
334
  const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
@@ -299,13 +355,8 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
299
355
  this.updateValue(event);
300
356
  // Focus the next empty input after pasting
301
357
  // If all boxes are filled, focus the last input
302
- const nextEmptyIndex = validChars.length;
303
- if (nextEmptyIndex < length) {
304
- (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
305
- }
306
- else {
307
- (_c = inputRefs[length - 1]) === null || _c === void 0 ? void 0 : _c.focus();
308
- }
358
+ const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
359
+ (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
309
360
  };
310
361
  }
311
362
  /**
@@ -444,6 +495,7 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
444
495
  });
445
496
  // Update the value without emitting events
446
497
  this.value = this.inputValues.join('');
498
+ this.previousInputValues = [...this.inputValues];
447
499
  }
448
500
  /**
449
501
  * Updates the value of the input group.
@@ -565,7 +617,7 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
565
617
  const tabbableIndex = this.getTabbableIndex();
566
618
  const pattern = this.getPattern();
567
619
  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()) !== '';
568
- return (h(Host, { key: 'df8fca036cedea0812185a02e3b655d7d76285e0', class: createColorClasses(color, {
620
+ return (h(Host, { key: 'f15a29fb17b681ef55885ca36d3d5f899cbaca83', class: createColorClasses(color, {
569
621
  [mode]: true,
570
622
  'has-focus': hasFocus,
571
623
  [`input-otp-size-${size}`]: true,
@@ -573,10 +625,10 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
573
625
  [`input-otp-fill-${fill}`]: true,
574
626
  'input-otp-disabled': disabled,
575
627
  'input-otp-readonly': readonly,
576
- }) }, 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: {
628
+ }) }, h("div", Object.assign({ key: 'd7e1d4edd8aafcf2ed4313301287282e90fc7e82', 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: '3724a3159d02860971879a906092f9965f5a7c47', class: {
577
629
  'input-otp-description': true,
578
630
  'input-otp-description-hidden': !hasDescription,
579
- } }, h("slot", { key: '9e8afa2f7fa76c3092582dc27770fdf565a1b9ba' }))));
631
+ } }, h("slot", { key: '11baa2624926a08274508afe0833d9237a8dc35c' }))));
580
632
  }
581
633
  get el() { return this; }
582
634
  static get watchers() { return {
@@ -604,6 +656,7 @@ const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLEle
604
656
  "value": [1032],
605
657
  "inputValues": [32],
606
658
  "hasFocus": [32],
659
+ "previousInputValues": [32],
607
660
  "setFocus": [64]
608
661
  }, undefined, {
609
662
  "value": ["valueChanged"],
@@ -1652,6 +1652,26 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1652
1652
  dragHandleEl.focus();
1653
1653
  }
1654
1654
  };
1655
+ /**
1656
+ * When the slot changes, we need to find all the modals in the slot
1657
+ * and set the data-parent-ion-modal attribute on them so we can find them
1658
+ * and dismiss them when we get dismissed.
1659
+ * We need to do it this way because when a modal is opened, it's moved to
1660
+ * the end of the body and is no longer an actual child of the modal.
1661
+ */
1662
+ this.onSlotChange = ({ target }) => {
1663
+ const slot = target;
1664
+ slot.assignedElements().forEach((el) => {
1665
+ el.querySelectorAll('ion-modal').forEach((childModal) => {
1666
+ // We don't need to write to the DOM if the modal is already tagged
1667
+ // If this is a deeply nested modal, this effect should cascade so we don't
1668
+ // need to worry about another modal claiming the same child.
1669
+ if (childModal.getAttribute('data-parent-ion-modal') === null) {
1670
+ childModal.setAttribute('data-parent-ion-modal', this.el.id);
1671
+ }
1672
+ });
1673
+ });
1674
+ };
1655
1675
  }
1656
1676
  onIsOpenChange(newValue, oldValue) {
1657
1677
  if (newValue === true && oldValue === false) {
@@ -1690,6 +1710,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1690
1710
  disconnectedCallback() {
1691
1711
  this.triggerController.removeClickListener();
1692
1712
  this.cleanupViewTransitionListener();
1713
+ this.cleanupParentRemovalObserver();
1693
1714
  }
1694
1715
  componentWillLoad() {
1695
1716
  var _a;
@@ -1697,6 +1718,10 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1697
1718
  const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
1698
1719
  const attributesToInherit = ['aria-label', 'role'];
1699
1720
  this.inheritedAttributes = inheritAttributes(el, attributesToInherit);
1721
+ // Cache original parent before modal gets moved to body during presentation
1722
+ if (el.parentNode) {
1723
+ this.cachedOriginalParent = el.parentNode;
1724
+ }
1700
1725
  /**
1701
1726
  * When using a controller modal you can set attributes
1702
1727
  * using the htmlAttributes property. Since the above attributes
@@ -1902,6 +1927,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1902
1927
  }
1903
1928
  // Initialize view transition listener for iOS card modals
1904
1929
  this.initViewTransitionListener();
1930
+ // Initialize parent removal observer
1931
+ this.initParentRemovalObserver();
1905
1932
  unlock();
1906
1933
  }
1907
1934
  initSwipeToClose() {
@@ -2015,6 +2042,12 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2015
2042
  * in case the dismiss transition does run.
2016
2043
  */
2017
2044
  const unlock = await this.lockController.lock();
2045
+ /**
2046
+ * Dismiss all child modals. This is especially important in
2047
+ * Angular and React because it's possible to lose control of a child
2048
+ * modal when the parent modal is dismissed.
2049
+ */
2050
+ await this.dismissNestedModals();
2018
2051
  /**
2019
2052
  * If a canDismiss handler is responsible
2020
2053
  * for calling the dismiss method, we should
@@ -2056,6 +2089,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2056
2089
  this.gesture.destroy();
2057
2090
  }
2058
2091
  this.cleanupViewTransitionListener();
2092
+ this.cleanupParentRemovalObserver();
2059
2093
  }
2060
2094
  this.currentBreakpoint = undefined;
2061
2095
  this.animation = undefined;
@@ -2233,6 +2267,59 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2233
2267
  }
2234
2268
  }
2235
2269
  }
2270
+ async dismissNestedModals() {
2271
+ const nestedModals = document.querySelectorAll(`ion-modal[data-parent-ion-modal="${this.el.id}"]`);
2272
+ nestedModals === null || nestedModals === void 0 ? void 0 : nestedModals.forEach(async (modal) => {
2273
+ await modal.dismiss(undefined, 'parent-dismissed');
2274
+ });
2275
+ }
2276
+ initParentRemovalObserver() {
2277
+ if (typeof MutationObserver === 'undefined') {
2278
+ return;
2279
+ }
2280
+ // Only observe if we have a cached parent and are in browser environment
2281
+ if (typeof window === 'undefined' || !this.cachedOriginalParent) {
2282
+ return;
2283
+ }
2284
+ // Don't observe document or fragment nodes as they can't be "removed"
2285
+ if (this.cachedOriginalParent.nodeType === Node.DOCUMENT_NODE ||
2286
+ this.cachedOriginalParent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
2287
+ return;
2288
+ }
2289
+ this.parentRemovalObserver = new MutationObserver((mutations) => {
2290
+ mutations.forEach((mutation) => {
2291
+ if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
2292
+ // Check if our cached original parent was removed
2293
+ const cachedParentWasRemoved = Array.from(mutation.removedNodes).some((node) => {
2294
+ var _a, _b;
2295
+ const isDirectMatch = node === this.cachedOriginalParent;
2296
+ const isContainedMatch = this.cachedOriginalParent
2297
+ ? (_b = (_a = node).contains) === null || _b === void 0 ? void 0 : _b.call(_a, this.cachedOriginalParent)
2298
+ : false;
2299
+ return isDirectMatch || isContainedMatch;
2300
+ });
2301
+ // Also check if parent is no longer connected to DOM
2302
+ const cachedParentDisconnected = this.cachedOriginalParent && !this.cachedOriginalParent.isConnected;
2303
+ if (cachedParentWasRemoved || cachedParentDisconnected) {
2304
+ this.dismiss(undefined, 'parent-removed');
2305
+ // Release the reference to the cached original parent
2306
+ // so we don't have a memory leak
2307
+ this.cachedOriginalParent = undefined;
2308
+ }
2309
+ }
2310
+ });
2311
+ });
2312
+ // Observe document body with subtree to catch removals at any level
2313
+ this.parentRemovalObserver.observe(document.body, {
2314
+ childList: true,
2315
+ subtree: true,
2316
+ });
2317
+ }
2318
+ cleanupParentRemovalObserver() {
2319
+ var _a;
2320
+ (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2321
+ this.parentRemovalObserver = undefined;
2322
+ }
2236
2323
  render() {
2237
2324
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
2238
2325
  const showHandle = handle !== false && isSheetModal;
@@ -2240,20 +2327,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2240
2327
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2241
2328
  const isHandleCycle = handleBehavior === 'cycle';
2242
2329
  const isSheetModalWithHandle = isSheetModal && showHandle;
2243
- return (h(Host, Object.assign({ key: '1980fa23331381c568a2be8091d888e09754fc52', "no-router": true,
2330
+ return (h(Host, Object.assign({ key: 'f274213df23995f7d3495648c828dbab6d040a53', "no-router": true,
2244
2331
  // Allow the modal to be navigable when the handle is focusable
2245
2332
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2246
2333
  zIndex: `${20000 + this.overlayIndex}`,
2247
- }, 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',
2334
+ }, 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: '8893496a8ce7364b3695a628eb4e6bd8c275f78e', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '66ceabd642566503f2c75e84ef7f643d73a97428', class: "modal-shadow" }), h("div", Object.assign({ key: '4cb89ce5b84219271d0d203babf066348a4c5433',
2248
2335
  /*
2249
2336
  role and aria-modal must be used on the
2250
2337
  same element. They must also be set inside the
2251
2338
  shadow DOM otherwise ion-button will not be highlighted
2252
2339
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2253
2340
  */
2254
- 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",
2341
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '9062f7bcd45203642ce7ca0521b2bb0bf1f77192', class: "modal-handle",
2255
2342
  // Prevents the handle from receiving keyboard focus when it does not cycle
2256
- 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' }))));
2343
+ 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: '34a29599e80254186c6d7d77b3161abcda76eab7', onSlotchange: this.onSlotChange }))));
2257
2344
  }
2258
2345
  get el() { return this; }
2259
2346
  static get watchers() { return {
@@ -32,6 +32,7 @@ const InputOTP = class {
32
32
  this.isKeyboardNavigation = false;
33
33
  this.inputValues = [];
34
34
  this.hasFocus = false;
35
+ this.previousInputValues = [];
35
36
  /**
36
37
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
37
38
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -132,19 +133,12 @@ const InputOTP = class {
132
133
  }
133
134
  };
134
135
  /**
135
- * Handles keyboard navigation and input for the OTP component.
136
+ * Handles keyboard navigation for the OTP component.
136
137
  *
137
138
  * Navigation:
138
139
  * - Backspace: Clears current input and moves to previous box if empty
139
140
  * - Arrow Left/Right: Moves focus between input boxes
140
141
  * - Tab: Allows normal tab navigation between components
141
- *
142
- * Input Behavior:
143
- * - Validates input against the allowed pattern
144
- * - When entering a key in a filled box:
145
- * - Shifts existing values right if there is room
146
- * - Updates the value of the input group
147
- * - Prevents default behavior to avoid automatic focus shift
148
142
  */
149
143
  this.onKeyDown = (index) => (event) => {
150
144
  const { length } = this;
@@ -199,30 +193,31 @@ const InputOTP = class {
199
193
  // Let all tab events proceed normally
200
194
  return;
201
195
  }
202
- // If the input box contains a value and the key being
203
- // entered is a valid key for the input box update the value
204
- // and shift the values to the right if there is room.
205
- if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
206
- if (!this.inputValues[length - 1]) {
207
- for (let i = length - 1; i > index; i--) {
208
- this.inputValues[i] = this.inputValues[i - 1];
209
- this.inputRefs[i].value = this.inputValues[i] || '';
210
- }
211
- }
212
- this.inputValues[index] = event.key;
213
- this.inputRefs[index].value = event.key;
214
- this.updateValue(event);
215
- // Prevent default to avoid the browser from
216
- // automatically moving the focus to the next input
217
- event.preventDefault();
218
- }
219
196
  };
197
+ /**
198
+ * Processes all input scenarios for each input box.
199
+ *
200
+ * This function manages:
201
+ * 1. Autofill handling
202
+ * 2. Input validation
203
+ * 3. Full selection replacement or typing in an empty box
204
+ * 4. Inserting in the middle with available space (shifting)
205
+ * 5. Single character replacement
206
+ */
220
207
  this.onInput = (index) => (event) => {
208
+ var _a, _b;
221
209
  const { length, validKeyPattern } = this;
222
- const value = event.target.value;
223
- // If the value is longer than 1 character (autofill), split it into
224
- // characters and filter out invalid ones
225
- if (value.length > 1) {
210
+ const input = event.target;
211
+ const value = input.value;
212
+ const previousValue = this.previousInputValues[index] || '';
213
+ // 1. Autofill handling
214
+ // If the length of the value increases by more than 1 from the previous
215
+ // value, treat this as autofill. This is to prevent the case where the
216
+ // user is typing a single character into an input box containing a value
217
+ // as that will trigger this function with a value length of 2 characters.
218
+ const isAutofill = value.length - previousValue.length > 1;
219
+ if (isAutofill) {
220
+ // Distribute valid characters across input boxes
226
221
  const validChars = value
227
222
  .split('')
228
223
  .filter((char) => validKeyPattern.test(char))
@@ -237,8 +232,10 @@ const InputOTP = class {
237
232
  });
238
233
  });
239
234
  }
240
- // Update the value of the input group and emit the input change event
241
- this.value = validChars.join('');
235
+ for (let i = 0; i < length; i++) {
236
+ this.inputValues[i] = validChars[i] || '';
237
+ this.inputRefs[i].value = validChars[i] || '';
238
+ }
242
239
  this.updateValue(event);
243
240
  // Focus the first empty input box or the last input box if all boxes
244
241
  // are filled after a small delay to ensure the input boxes have been
@@ -248,20 +245,79 @@ const InputOTP = class {
248
245
  const nextIndex = validChars.length < length ? validChars.length : length - 1;
249
246
  (_a = this.inputRefs[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
250
247
  }, 20);
248
+ this.previousInputValues = [...this.inputValues];
251
249
  return;
252
250
  }
253
- // Only allow input if it matches the pattern
254
- if (value.length > 0 && !validKeyPattern.test(value)) {
255
- this.inputRefs[index].value = '';
256
- this.inputValues[index] = '';
251
+ // 2. Input validation
252
+ // If the character entered is invalid (does not match the pattern),
253
+ // restore the previous value and exit
254
+ if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
255
+ input.value = this.inputValues[index] || '';
256
+ this.previousInputValues = [...this.inputValues];
257
257
  return;
258
258
  }
259
- // For single character input, fill the current box
260
- this.inputValues[index] = value;
261
- this.updateValue(event);
262
- if (value.length > 0) {
259
+ // 3. Full selection replacement or typing in an empty box
260
+ // If the user selects all text in the input box and types, or if the
261
+ // input box is empty, replace only this input box. If the box is empty,
262
+ // move to the next box, otherwise stay focused on this box.
263
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
264
+ const isEmpty = !this.inputValues[index];
265
+ if (isAllSelected || isEmpty) {
266
+ this.inputValues[index] = value;
267
+ input.value = value;
268
+ this.updateValue(event);
263
269
  this.focusNext(index);
270
+ this.previousInputValues = [...this.inputValues];
271
+ return;
264
272
  }
273
+ // 4. Inserting in the middle with available space (shifting)
274
+ // If typing in a filled input box and there are empty boxes at the end,
275
+ // shift all values starting at the current box to the right, and insert
276
+ // the new character at the current box.
277
+ const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
278
+ if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
279
+ // Get the inserted character (from event or by diffing value/previousValue)
280
+ let newChar = event.data;
281
+ if (!newChar) {
282
+ newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
283
+ }
284
+ // Validate the new character before shifting
285
+ if (!validKeyPattern.test(newChar)) {
286
+ input.value = this.inputValues[index] || '';
287
+ this.previousInputValues = [...this.inputValues];
288
+ return;
289
+ }
290
+ // Shift values right from the end to the insertion point
291
+ for (let i = this.inputValues.length - 1; i > index; i--) {
292
+ this.inputValues[i] = this.inputValues[i - 1];
293
+ this.inputRefs[i].value = this.inputValues[i] || '';
294
+ }
295
+ this.inputValues[index] = newChar;
296
+ this.inputRefs[index].value = newChar;
297
+ this.updateValue(event);
298
+ this.previousInputValues = [...this.inputValues];
299
+ return;
300
+ }
301
+ // 5. Single character replacement
302
+ // Handles replacing a single character in a box containing a value based
303
+ // on the cursor position. We need the cursor position to determine which
304
+ // character was the last character typed. For example, if the user types "2"
305
+ // in an input box with the cursor at the beginning of the value of "6",
306
+ // the value will be "26", but we want to grab the "2" as the last character
307
+ // typed.
308
+ const cursorPos = (_a = input.selectionStart) !== null && _a !== void 0 ? _a : value.length;
309
+ const newCharIndex = cursorPos - 1;
310
+ const newChar = (_b = value[newCharIndex]) !== null && _b !== void 0 ? _b : value[0];
311
+ // Check if the new character is valid before updating the value
312
+ if (!validKeyPattern.test(newChar)) {
313
+ input.value = this.inputValues[index] || '';
314
+ this.previousInputValues = [...this.inputValues];
315
+ return;
316
+ }
317
+ this.inputValues[index] = newChar;
318
+ input.value = newChar;
319
+ this.updateValue(event);
320
+ this.previousInputValues = [...this.inputValues];
265
321
  };
266
322
  /**
267
323
  * Handles pasting text into the input OTP component.
@@ -271,7 +327,7 @@ const InputOTP = class {
271
327
  * the next empty input after pasting.
272
328
  */
273
329
  this.onPaste = (event) => {
274
- var _a, _b, _c;
330
+ var _a, _b;
275
331
  const { inputRefs, length, validKeyPattern } = this;
276
332
  event.preventDefault();
277
333
  const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
@@ -298,13 +354,8 @@ const InputOTP = class {
298
354
  this.updateValue(event);
299
355
  // Focus the next empty input after pasting
300
356
  // If all boxes are filled, focus the last input
301
- const nextEmptyIndex = validChars.length;
302
- if (nextEmptyIndex < length) {
303
- (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
304
- }
305
- else {
306
- (_c = inputRefs[length - 1]) === null || _c === void 0 ? void 0 : _c.focus();
307
- }
357
+ const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
358
+ (_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
308
359
  };
309
360
  }
310
361
  /**
@@ -443,6 +494,7 @@ const InputOTP = class {
443
494
  });
444
495
  // Update the value without emitting events
445
496
  this.value = this.inputValues.join('');
497
+ this.previousInputValues = [...this.inputValues];
446
498
  }
447
499
  /**
448
500
  * Updates the value of the input group.
@@ -564,7 +616,7 @@ const InputOTP = class {
564
616
  const tabbableIndex = this.getTabbableIndex();
565
617
  const pattern = this.getPattern();
566
618
  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()) !== '';
567
- return (index.h(index.Host, { key: 'df8fca036cedea0812185a02e3b655d7d76285e0', class: theme.createColorClasses(color, {
619
+ return (index.h(index.Host, { key: 'f15a29fb17b681ef55885ca36d3d5f899cbaca83', class: theme.createColorClasses(color, {
568
620
  [mode]: true,
569
621
  'has-focus': hasFocus,
570
622
  [`input-otp-size-${size}`]: true,
@@ -572,10 +624,10 @@ const InputOTP = class {
572
624
  [`input-otp-fill-${fill}`]: true,
573
625
  'input-otp-disabled': disabled,
574
626
  'input-otp-readonly': readonly,
575
- }) }, index.h("div", Object.assign({ key: '831be3f939cf037f0eb8d7e37e0afd4ef9a3c2c5', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index$1) => (index.h(index.Fragment, null, index.h("div", { class: "native-wrapper" }, index.h("input", { class: "native-input", id: `${inputId}-${index$1}`, "aria-label": `Input ${index$1 + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index$1 === tabbableIndex ? 0 : -1, value: inputValues[index$1] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index$1] = el), onInput: this.onInput(index$1), onBlur: this.onBlur, onFocus: this.onFocus(index$1), onKeyDown: this.onKeyDown(index$1), onPaste: this.onPaste })), this.showSeparator(index$1) && index.h("div", { class: "input-otp-separator" }))))), index.h("div", { key: '5311fedc34f7af3efd5f69e5a3d768055119c4f1', class: {
627
+ }) }, index.h("div", Object.assign({ key: 'd7e1d4edd8aafcf2ed4313301287282e90fc7e82', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index$1) => (index.h(index.Fragment, null, index.h("div", { class: "native-wrapper" }, index.h("input", { class: "native-input", id: `${inputId}-${index$1}`, "aria-label": `Input ${index$1 + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index$1 === tabbableIndex ? 0 : -1, value: inputValues[index$1] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index$1] = el), onInput: this.onInput(index$1), onBlur: this.onBlur, onFocus: this.onFocus(index$1), onKeyDown: this.onKeyDown(index$1), onPaste: this.onPaste })), this.showSeparator(index$1) && index.h("div", { class: "input-otp-separator" }))))), index.h("div", { key: '3724a3159d02860971879a906092f9965f5a7c47', class: {
576
628
  'input-otp-description': true,
577
629
  'input-otp-description-hidden': !hasDescription,
578
- } }, index.h("slot", { key: '9e8afa2f7fa76c3092582dc27770fdf565a1b9ba' }))));
630
+ } }, index.h("slot", { key: '11baa2624926a08274508afe0833d9237a8dc35c' }))));
579
631
  }
580
632
  get el() { return index.getElement(this); }
581
633
  static get watchers() { return {