mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-26 16:21:55 +08:00
fix(datetime): support typing time values in a 24-hour format (#30147)
- Adjusted the `selectMultiColumn` logic to handle keyboard values like 20 and 22 dynamically. - Introduced checks for the maximum column value to enable flexible input behavior. - Added e2e tests to verify correct value selection for both 12-hour and 24-hour formats. Issue number: resolves #28877 --------- ## What is the current behavior? In the ion-datetime component, when typing 2000 in the keyboard the resulted time value is 02:00 (in 24-hour format) Examples: https://forum.ionicframework.com/t/ion-datetime-disable-opening-keyboard/224558/6?u=dennisskylegs ## What is the new behavior? In the ion-datetime component, when typing 2000 in the keyboard the resulted time value is 20:00 (in 24-hour format) ## Does this introduce a breaking change? - [ ] Yes - [x] No --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Co-authored-by: ShaneK <shane@shanessite.net>
This commit is contained in:

committed by
GitHub

parent
1cfa915e8f
commit
ac6e6a0317
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
if (value.length > 4) {
|
||||
const startIndex = inputEl.value.length - 4;
|
||||
const newString = inputEl.value.substring(startIndex);
|
||||
|
||||
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 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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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(
|
||||
`
|
||||
<ion-picker>
|
||||
<ion-picker-column id="hours"></ion-picker-column>
|
||||
<ion-picker-column id="minutes"></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const hoursColumn = document.querySelector('ion-picker-column#hours');
|
||||
hoursColumn.numericInput = true;
|
||||
const hourItems = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2},
|
||||
{ text: '20', value: 20 },
|
||||
{ text: '21', value: 21 },
|
||||
{ text: '22', value: 22 },
|
||||
{ text: '23', value: 23 }
|
||||
];
|
||||
|
||||
hourItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
hoursColumn.appendChild(option);
|
||||
});
|
||||
|
||||
const minutesColumn = document.querySelector('ion-picker-column#minutes');
|
||||
minutesColumn.numericInput = true;
|
||||
const minuteItems = [
|
||||
{ text: '00', value: 0 },
|
||||
{ text: '15', value: 15 },
|
||||
{ text: '30', value: 30 },
|
||||
{ text: '45', value: 45 }
|
||||
];
|
||||
|
||||
minuteItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
minutesColumn.appendChild(option);
|
||||
});
|
||||
</script>
|
||||
`,
|
||||
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(
|
||||
`
|
||||
<ion-picker>
|
||||
<ion-picker-column id="hours"></ion-picker-column>
|
||||
<ion-picker-column id="minutes"></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const hoursColumn = document.querySelector('ion-picker-column#hours');
|
||||
hoursColumn.numericInput = true;
|
||||
const hourItems = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 },
|
||||
{ text: '06', value: 6 },
|
||||
{ text: '07', value: 7 },
|
||||
{ text: '08', value: 8 },
|
||||
{ text: '09', value: 9 },
|
||||
{ text: '10', value: 10 },
|
||||
{ text: '11', value: 11 },
|
||||
{ text: '12', value: 12 }
|
||||
];
|
||||
|
||||
hourItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
hoursColumn.appendChild(option);
|
||||
});
|
||||
|
||||
const minutesColumn = document.querySelector('ion-picker-column#minutes');
|
||||
minutesColumn.numericInput = true;
|
||||
const minuteItems = [
|
||||
{ text: '00', value: 0 },
|
||||
{ text: '15', value: 15 },
|
||||
{ text: '30', value: 30 },
|
||||
{ text: '45', value: 45 }
|
||||
];
|
||||
|
||||
minuteItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
minutesColumn.appendChild(option);
|
||||
});
|
||||
</script>
|
||||
`,
|
||||
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',
|
||||
|
Reference in New Issue
Block a user