Skip to content

Commit ac6e6a0

Browse files
vunizhonabrandyscarneyShaneK
authored
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>
1 parent 1cfa915 commit ac6e6a0

File tree

3 files changed

+234
-87
lines changed

3 files changed

+234
-87
lines changed

‎core/src/components/datetime/datetime.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1984,7 +1984,7 @@ export class Datetime implements ComponentInterface {
19841984
});
19851985

19861986
this.setActiveParts({
1987-
...activePart,
1987+
...this.getActivePartsWithFallback(),
19881988
hour: ev.detail.value,
19891989
});
19901990

@@ -2024,7 +2024,7 @@ export class Datetime implements ComponentInterface {
20242024
});
20252025

20262026
this.setActiveParts({
2027-
...activePart,
2027+
...this.getActivePartsWithFallback(),
20282028
minute: ev.detail.value,
20292029
});
20302030

@@ -2070,7 +2070,7 @@ export class Datetime implements ComponentInterface {
20702070
});
20712071

20722072
this.setActiveParts({
2073-
...activePart,
2073+
...this.getActivePartsWithFallback(),
20742074
ampm: ev.detail.value,
20752075
hour,
20762076
});

‎core/src/components/picker/picker.tsx

+65-84
Original file line numberDiff line numberDiff line change
@@ -410,15 +410,72 @@ export class Picker implements ComponentInterface {
410410
colEl: HTMLIonPickerColumnElement,
411411
value: string,
412412
zeroBehavior: 'start' | 'end' = 'start'
413-
) => {
413+
): boolean => {
414+
if (!value) {
415+
return false;
416+
}
417+
414418
const behavior = zeroBehavior === 'start' ? /^0+/ : /0$/;
419+
value = value.replace(behavior, '');
415420
const option = Array.from(colEl.querySelectorAll('ion-picker-column-option')).find((el) => {
416421
return el.disabled !== true && el.textContent!.replace(behavior, '') === value;
417422
});
418423

419424
if (option) {
420425
colEl.setValue(option.value);
421426
}
427+
428+
return !!option;
429+
};
430+
431+
/**
432+
* Attempts to intelligently search the first and second
433+
* column as if they're number columns for the provided numbers
434+
* where the first two numbers are the first column
435+
* and the last 2 are the last column. Tries to allow for the first
436+
* number to be ignored for situations where typos occurred.
437+
*/
438+
private multiColumnSearch = (
439+
firstColumn: HTMLIonPickerColumnElement,
440+
secondColumn: HTMLIonPickerColumnElement,
441+
input: string
442+
) => {
443+
if (input.length === 0) {
444+
return;
445+
}
446+
447+
const inputArray = input.split('');
448+
const hourValue = inputArray.slice(0, 2).join('');
449+
// Try to find a match for the first two digits in the first column
450+
const foundHour = this.searchColumn(firstColumn, hourValue);
451+
452+
// If we have more than 2 digits and found a match for hours,
453+
// use the remaining digits for the second column (minutes)
454+
if (inputArray.length > 2 && foundHour) {
455+
const minuteValue = inputArray.slice(2, 4).join('');
456+
this.searchColumn(secondColumn, minuteValue);
457+
}
458+
// If we couldn't find a match for the two-digit hour, try single digit approaches
459+
else if (!foundHour && inputArray.length >= 1) {
460+
// First try the first digit as a single-digit hour
461+
let singleDigitHour = inputArray[0];
462+
let singleDigitFound = this.searchColumn(firstColumn, singleDigitHour);
463+
464+
// If that didn't work, try the second digit as a single-digit hour
465+
// (handles case where user made a typo in the first digit, or they typed over themselves)
466+
if (!singleDigitFound) {
467+
inputArray.shift();
468+
singleDigitHour = inputArray[0];
469+
singleDigitFound = this.searchColumn(firstColumn, singleDigitHour);
470+
}
471+
472+
// If we found a single-digit hour and have remaining digits,
473+
// use up to 2 of the remaining digits for the second column
474+
if (singleDigitFound && inputArray.length > 1) {
475+
const remainingDigits = inputArray.slice(1, 3).join('');
476+
this.searchColumn(secondColumn, remainingDigits);
477+
}
478+
}
422479
};
423480

424481
private selectMultiColumn = () => {
@@ -433,91 +490,15 @@ export class Picker implements ComponentInterface {
433490
const lastColumn = numericPickers[1];
434491

435492
let value = inputEl.value;
436-
let minuteValue;
437-
switch (value.length) {
438-
case 1:
439-
this.searchColumn(firstColumn, value);
440-
break;
441-
case 2:
442-
/**
443-
* If the first character is `0` or `1` it is
444-
* possible that users are trying to type `09`
445-
* or `11` into the hour field, so we should look
446-
* at that first.
447-
*/
448-
const firstCharacter = inputEl.value.substring(0, 1);
449-
value = firstCharacter === '0' || firstCharacter === '1' ? inputEl.value : firstCharacter;
450-
451-
this.searchColumn(firstColumn, value);
452-
453-
/**
454-
* If only checked the first value,
455-
* we can check the second value
456-
* for a match in the minutes column
457-
*/
458-
if (value.length === 1) {
459-
minuteValue = inputEl.value.substring(inputEl.value.length - 1);
460-
this.searchColumn(lastColumn, minuteValue, 'end');
461-
}
462-
break;
463-
case 3:
464-
/**
465-
* If the first character is `0` or `1` it is
466-
* possible that users are trying to type `09`
467-
* or `11` into the hour field, so we should look
468-
* at that first.
469-
*/
470-
const firstCharacterAgain = inputEl.value.substring(0, 1);
471-
value =
472-
firstCharacterAgain === '0' || firstCharacterAgain === '1'
473-
? inputEl.value.substring(0, 2)
474-
: firstCharacterAgain;
475-
476-
this.searchColumn(firstColumn, value);
477-
478-
/**
479-
* If only checked the first value,
480-
* we can check the second value
481-
* for a match in the minutes column
482-
*/
483-
minuteValue = value.length === 1 ? inputEl.value.substring(1) : inputEl.value.substring(2);
484-
485-
this.searchColumn(lastColumn, minuteValue, 'end');
486-
break;
487-
case 4:
488-
/**
489-
* If the first character is `0` or `1` it is
490-
* possible that users are trying to type `09`
491-
* or `11` into the hour field, so we should look
492-
* at that first.
493-
*/
494-
const firstCharacterAgainAgain = inputEl.value.substring(0, 1);
495-
value =
496-
firstCharacterAgainAgain === '0' || firstCharacterAgainAgain === '1'
497-
? inputEl.value.substring(0, 2)
498-
: firstCharacterAgainAgain;
499-
this.searchColumn(firstColumn, value);
493+
if (value.length > 4) {
494+
const startIndex = inputEl.value.length - 4;
495+
const newString = inputEl.value.substring(startIndex);
500496

501-
/**
502-
* If only checked the first value,
503-
* we can check the second value
504-
* for a match in the minutes column
505-
*/
506-
const minuteValueAgain =
507-
value.length === 1
508-
? inputEl.value.substring(1, inputEl.value.length)
509-
: inputEl.value.substring(2, inputEl.value.length);
510-
this.searchColumn(lastColumn, minuteValueAgain, 'end');
511-
512-
break;
513-
default:
514-
const startIndex = inputEl.value.length - 4;
515-
const newString = inputEl.value.substring(startIndex);
516-
517-
inputEl.value = newString;
518-
this.selectMultiColumn();
519-
break;
497+
inputEl.value = newString;
498+
value = newString;
520499
}
500+
501+
this.multiColumnSearch(firstColumn, lastColumn, value);
521502
};
522503

523504
/**

‎core/src/components/picker/test/keyboard-entry/picker.e2e.ts

+166
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,172 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
163163
await expect(ionChange).toHaveReceivedEventDetail({ value: 12 });
164164
await expect(column).toHaveJSProperty('value', 12);
165165
});
166+
167+
test('should allow typing 22 in a column where the max value is 23 and not just set it to 2', async ({
168+
page,
169+
}, testInfo) => {
170+
testInfo.annotations.push({
171+
type: 'issue',
172+
description: 'https://github.com/ionic-team/ionic-framework/issues/28877',
173+
});
174+
await page.setContent(
175+
`
176+
<ion-picker>
177+
<ion-picker-column id="hours"></ion-picker-column>
178+
<ion-picker-column id="minutes"></ion-picker-column>
179+
</ion-picker>
180+
181+
<script>
182+
const hoursColumn = document.querySelector('ion-picker-column#hours');
183+
hoursColumn.numericInput = true;
184+
const hourItems = [
185+
{ text: '01', value: 1 },
186+
{ text: '02', value: 2},
187+
{ text: '20', value: 20 },
188+
{ text: '21', value: 21 },
189+
{ text: '22', value: 22 },
190+
{ text: '23', value: 23 }
191+
];
192+
193+
hourItems.forEach((item) => {
194+
const option = document.createElement('ion-picker-column-option');
195+
option.value = item.value;
196+
option.textContent = item.text;
197+
198+
hoursColumn.appendChild(option);
199+
});
200+
201+
const minutesColumn = document.querySelector('ion-picker-column#minutes');
202+
minutesColumn.numericInput = true;
203+
const minuteItems = [
204+
{ text: '00', value: 0 },
205+
{ text: '15', value: 15 },
206+
{ text: '30', value: 30 },
207+
{ text: '45', value: 45 }
208+
];
209+
210+
minuteItems.forEach((item) => {
211+
const option = document.createElement('ion-picker-column-option');
212+
option.value = item.value;
213+
option.textContent = item.text;
214+
215+
minutesColumn.appendChild(option);
216+
});
217+
</script>
218+
`,
219+
config
220+
);
221+
222+
const hoursColumn = page.locator('ion-picker-column#hours');
223+
const minutesColumn = page.locator('ion-picker-column#minutes');
224+
const hoursIonChange = await (hoursColumn as E2ELocator).spyOnEvent('ionChange');
225+
const minutesIonChange = await (minutesColumn as E2ELocator).spyOnEvent('ionChange');
226+
const highlight = page.locator('ion-picker .picker-highlight');
227+
228+
const box = await highlight.boundingBox();
229+
if (box !== null) {
230+
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
231+
}
232+
233+
// Simulate typing '2230' (22 hours, 30 minutes)
234+
await page.keyboard.press('Digit2');
235+
await page.keyboard.press('Digit2');
236+
await page.keyboard.press('Digit3');
237+
await page.keyboard.press('Digit0');
238+
239+
// Ensure the hours column is set to 22
240+
await expect(hoursIonChange).toHaveReceivedEventDetail({ value: 22 });
241+
await expect(hoursColumn).toHaveJSProperty('value', 22);
242+
243+
// Ensure the minutes column is set to 30
244+
await expect(minutesIonChange).toHaveReceivedEventDetail({ value: 30 });
245+
await expect(minutesColumn).toHaveJSProperty('value', 30);
246+
});
247+
248+
test('should set value to 2 and not wait for another digit when max value is 12', async ({ page }, testInfo) => {
249+
testInfo.annotations.push({
250+
type: 'issue',
251+
description: 'https://github.com/ionic-team/ionic-framework/issues/28877',
252+
});
253+
await page.setContent(
254+
`
255+
<ion-picker>
256+
<ion-picker-column id="hours"></ion-picker-column>
257+
<ion-picker-column id="minutes"></ion-picker-column>
258+
</ion-picker>
259+
260+
<script>
261+
const hoursColumn = document.querySelector('ion-picker-column#hours');
262+
hoursColumn.numericInput = true;
263+
const hourItems = [
264+
{ text: '01', value: 1 },
265+
{ text: '02', value: 2 },
266+
{ text: '03', value: 3 },
267+
{ text: '04', value: 4 },
268+
{ text: '05', value: 5 },
269+
{ text: '06', value: 6 },
270+
{ text: '07', value: 7 },
271+
{ text: '08', value: 8 },
272+
{ text: '09', value: 9 },
273+
{ text: '10', value: 10 },
274+
{ text: '11', value: 11 },
275+
{ text: '12', value: 12 }
276+
];
277+
278+
hourItems.forEach((item) => {
279+
const option = document.createElement('ion-picker-column-option');
280+
option.value = item.value;
281+
option.textContent = item.text;
282+
283+
hoursColumn.appendChild(option);
284+
});
285+
286+
const minutesColumn = document.querySelector('ion-picker-column#minutes');
287+
minutesColumn.numericInput = true;
288+
const minuteItems = [
289+
{ text: '00', value: 0 },
290+
{ text: '15', value: 15 },
291+
{ text: '30', value: 30 },
292+
{ text: '45', value: 45 }
293+
];
294+
295+
minuteItems.forEach((item) => {
296+
const option = document.createElement('ion-picker-column-option');
297+
option.value = item.value;
298+
option.textContent = item.text;
299+
300+
minutesColumn.appendChild(option);
301+
});
302+
</script>
303+
`,
304+
config
305+
);
306+
307+
const hoursColumn = page.locator('ion-picker-column#hours');
308+
const minutesColumn = page.locator('ion-picker-column#minutes');
309+
const hoursIonChange = await (hoursColumn as E2ELocator).spyOnEvent('ionChange');
310+
const minutesIonChange = await (minutesColumn as E2ELocator).spyOnEvent('ionChange');
311+
const highlight = page.locator('ion-picker .picker-highlight');
312+
313+
const box = await highlight.boundingBox();
314+
if (box !== null) {
315+
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
316+
}
317+
318+
// Simulate typing '245' (2 hours, 45 minutes)
319+
await page.keyboard.press('Digit2');
320+
await page.keyboard.press('Digit4');
321+
await page.keyboard.press('Digit5');
322+
323+
// Ensure the hours column is set to 2
324+
await expect(hoursIonChange).toHaveReceivedEventDetail({ value: 2 });
325+
await expect(hoursColumn).toHaveJSProperty('value', 2);
326+
327+
// Ensure the minutes column is set to 45
328+
await expect(minutesIonChange).toHaveReceivedEventDetail({ value: 45 });
329+
await expect(minutesColumn).toHaveJSProperty('value', 45);
330+
});
331+
166332
test('pressing Enter should dismiss the keyboard', async ({ page }) => {
167333
test.info().annotations.push({
168334
type: 'issue',

0 commit comments

Comments
 (0)