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',