diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx
index 9154c24e60b..fcdb8bcabe0 100644
--- a/core/src/components/datetime/datetime.tsx
+++ b/core/src/components/datetime/datetime.tsx
@@ -1984,7 +1984,7 @@ export class Datetime implements ComponentInterface {
});
this.setActiveParts({
- ...activePart,
+ ...this.getActivePartsWithFallback(),
hour: ev.detail.value,
});
@@ -2024,7 +2024,7 @@ export class Datetime implements ComponentInterface {
});
this.setActiveParts({
- ...activePart,
+ ...this.getActivePartsWithFallback(),
minute: ev.detail.value,
});
@@ -2070,7 +2070,7 @@ export class Datetime implements ComponentInterface {
});
this.setActiveParts({
- ...activePart,
+ ...this.getActivePartsWithFallback(),
ampm: ev.detail.value,
hour,
});
diff --git a/core/src/components/picker/picker.tsx b/core/src/components/picker/picker.tsx
index 1d98e049d4b..71db6a3cac7 100644
--- a/core/src/components/picker/picker.tsx
+++ b/core/src/components/picker/picker.tsx
@@ -410,8 +410,13 @@ export class Picker implements ComponentInterface {
colEl: HTMLIonPickerColumnElement,
value: string,
zeroBehavior: 'start' | 'end' = 'start'
- ) => {
+ ): boolean => {
+ if (!value) {
+ return false;
+ }
+
const behavior = zeroBehavior === 'start' ? /^0+/ : /0$/;
+ value = value.replace(behavior, '');
const option = Array.from(colEl.querySelectorAll('ion-picker-column-option')).find((el) => {
return el.disabled !== true && el.textContent!.replace(behavior, '') === value;
});
@@ -419,6 +424,58 @@ export class Picker implements ComponentInterface {
if (option) {
colEl.setValue(option.value);
}
+
+ return !!option;
+ };
+
+ /**
+ * Attempts to intelligently search the first and second
+ * column as if they're number columns for the provided numbers
+ * where the first two numbers are the first column
+ * and the last 2 are the last column. Tries to allow for the first
+ * number to be ignored for situations where typos occurred.
+ */
+ private multiColumnSearch = (
+ firstColumn: HTMLIonPickerColumnElement,
+ secondColumn: HTMLIonPickerColumnElement,
+ input: string
+ ) => {
+ if (input.length === 0) {
+ return;
+ }
+
+ const inputArray = input.split('');
+ const hourValue = inputArray.slice(0, 2).join('');
+ // Try to find a match for the first two digits in the first column
+ const foundHour = this.searchColumn(firstColumn, hourValue);
+
+ // If we have more than 2 digits and found a match for hours,
+ // use the remaining digits for the second column (minutes)
+ if (inputArray.length > 2 && foundHour) {
+ const minuteValue = inputArray.slice(2, 4).join('');
+ this.searchColumn(secondColumn, minuteValue);
+ }
+ // If we couldn't find a match for the two-digit hour, try single digit approaches
+ else if (!foundHour && inputArray.length >= 1) {
+ // First try the first digit as a single-digit hour
+ let singleDigitHour = inputArray[0];
+ let singleDigitFound = this.searchColumn(firstColumn, singleDigitHour);
+
+ // If that didn't work, try the second digit as a single-digit hour
+ // (handles case where user made a typo in the first digit, or they typed over themselves)
+ if (!singleDigitFound) {
+ inputArray.shift();
+ singleDigitHour = inputArray[0];
+ singleDigitFound = this.searchColumn(firstColumn, singleDigitHour);
+ }
+
+ // If we found a single-digit hour and have remaining digits,
+ // use up to 2 of the remaining digits for the second column
+ if (singleDigitFound && inputArray.length > 1) {
+ const remainingDigits = inputArray.slice(1, 3).join('');
+ this.searchColumn(secondColumn, remainingDigits);
+ }
+ }
};
private selectMultiColumn = () => {
@@ -433,91 +490,15 @@ export class Picker implements ComponentInterface {
const lastColumn = numericPickers[1];
let value = inputEl.value;
- let minuteValue;
- switch (value.length) {
- case 1:
- this.searchColumn(firstColumn, value);
- break;
- case 2:
- /**
- * If the first character is `0` or `1` it is
- * possible that users are trying to type `09`
- * or `11` into the hour field, so we should look
- * at that first.
- */
- const firstCharacter = inputEl.value.substring(0, 1);
- value = firstCharacter === '0' || firstCharacter === '1' ? inputEl.value : firstCharacter;
-
- this.searchColumn(firstColumn, value);
-
- /**
- * If only checked the first value,
- * we can check the second value
- * for a match in the minutes column
- */
- if (value.length === 1) {
- minuteValue = inputEl.value.substring(inputEl.value.length - 1);
- this.searchColumn(lastColumn, minuteValue, 'end');
- }
- break;
- case 3:
- /**
- * If the first character is `0` or `1` it is
- * possible that users are trying to type `09`
- * or `11` into the hour field, so we should look
- * at that first.
- */
- const firstCharacterAgain = inputEl.value.substring(0, 1);
- value =
- firstCharacterAgain === '0' || firstCharacterAgain === '1'
- ? inputEl.value.substring(0, 2)
- : firstCharacterAgain;
-
- this.searchColumn(firstColumn, value);
-
- /**
- * If only checked the first value,
- * we can check the second value
- * for a match in the minutes column
- */
- minuteValue = value.length === 1 ? inputEl.value.substring(1) : inputEl.value.substring(2);
-
- this.searchColumn(lastColumn, minuteValue, 'end');
- break;
- case 4:
- /**
- * If the first character is `0` or `1` it is
- * possible that users are trying to type `09`
- * or `11` into the hour field, so we should look
- * at that first.
- */
- const firstCharacterAgainAgain = inputEl.value.substring(0, 1);
- value =
- firstCharacterAgainAgain === '0' || firstCharacterAgainAgain === '1'
- ? inputEl.value.substring(0, 2)
- : firstCharacterAgainAgain;
- this.searchColumn(firstColumn, value);
+ if (value.length > 4) {
+ const startIndex = inputEl.value.length - 4;
+ const newString = inputEl.value.substring(startIndex);
- /**
- * If only checked the first value,
- * we can check the second value
- * for a match in the minutes column
- */
- const minuteValueAgain =
- value.length === 1
- ? inputEl.value.substring(1, inputEl.value.length)
- : inputEl.value.substring(2, inputEl.value.length);
- this.searchColumn(lastColumn, minuteValueAgain, 'end');
-
- break;
- default:
- const startIndex = inputEl.value.length - 4;
- const newString = inputEl.value.substring(startIndex);
-
- inputEl.value = newString;
- this.selectMultiColumn();
- break;
+ inputEl.value = newString;
+ value = newString;
}
+
+ this.multiColumnSearch(firstColumn, lastColumn, value);
};
/**
diff --git a/core/src/components/picker/test/keyboard-entry/picker.e2e.ts b/core/src/components/picker/test/keyboard-entry/picker.e2e.ts
index 64d4a1cce13..b54cd03585a 100644
--- a/core/src/components/picker/test/keyboard-entry/picker.e2e.ts
+++ b/core/src/components/picker/test/keyboard-entry/picker.e2e.ts
@@ -163,6 +163,172 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(ionChange).toHaveReceivedEventDetail({ value: 12 });
await expect(column).toHaveJSProperty('value', 12);
});
+
+ test('should allow typing 22 in a column where the max value is 23 and not just set it to 2', async ({
+ page,
+ }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/28877',
+ });
+ await page.setContent(
+ `
+
+
+
+
+
+
+ `,
+ config
+ );
+
+ const hoursColumn = page.locator('ion-picker-column#hours');
+ const minutesColumn = page.locator('ion-picker-column#minutes');
+ const hoursIonChange = await (hoursColumn as E2ELocator).spyOnEvent('ionChange');
+ const minutesIonChange = await (minutesColumn as E2ELocator).spyOnEvent('ionChange');
+ const highlight = page.locator('ion-picker .picker-highlight');
+
+ const box = await highlight.boundingBox();
+ if (box !== null) {
+ await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
+ }
+
+ // Simulate typing '2230' (22 hours, 30 minutes)
+ await page.keyboard.press('Digit2');
+ await page.keyboard.press('Digit2');
+ await page.keyboard.press('Digit3');
+ await page.keyboard.press('Digit0');
+
+ // Ensure the hours column is set to 22
+ await expect(hoursIonChange).toHaveReceivedEventDetail({ value: 22 });
+ await expect(hoursColumn).toHaveJSProperty('value', 22);
+
+ // Ensure the minutes column is set to 30
+ await expect(minutesIonChange).toHaveReceivedEventDetail({ value: 30 });
+ await expect(minutesColumn).toHaveJSProperty('value', 30);
+ });
+
+ test('should set value to 2 and not wait for another digit when max value is 12', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/28877',
+ });
+ await page.setContent(
+ `
+
+
+
+
+
+
+ `,
+ config
+ );
+
+ const hoursColumn = page.locator('ion-picker-column#hours');
+ const minutesColumn = page.locator('ion-picker-column#minutes');
+ const hoursIonChange = await (hoursColumn as E2ELocator).spyOnEvent('ionChange');
+ const minutesIonChange = await (minutesColumn as E2ELocator).spyOnEvent('ionChange');
+ const highlight = page.locator('ion-picker .picker-highlight');
+
+ const box = await highlight.boundingBox();
+ if (box !== null) {
+ await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
+ }
+
+ // Simulate typing '245' (2 hours, 45 minutes)
+ await page.keyboard.press('Digit2');
+ await page.keyboard.press('Digit4');
+ await page.keyboard.press('Digit5');
+
+ // Ensure the hours column is set to 2
+ await expect(hoursIonChange).toHaveReceivedEventDetail({ value: 2 });
+ await expect(hoursColumn).toHaveJSProperty('value', 2);
+
+ // Ensure the minutes column is set to 45
+ await expect(minutesIonChange).toHaveReceivedEventDetail({ value: 45 });
+ await expect(minutesColumn).toHaveJSProperty('value', 45);
+ });
+
test('pressing Enter should dismiss the keyboard', async ({ page }) => {
test.info().annotations.push({
type: 'issue',