Skip to content

Commit 0548fe8

Browse files
authoredOct 17, 2022
fix(datetime): values are adjusted to be in bounds (#26125)
resolves #25894, resolves #25708
1 parent 479d56b commit 0548fe8

File tree

6 files changed

+188
-242
lines changed

6 files changed

+188
-242
lines changed
 

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ export class Datetime implements ComponentInterface {
585585
};
586586

587587
private setActiveParts = (parts: DatetimeParts, removeDate = false) => {
588-
const { multiple, activePartsClone } = this;
588+
const { multiple, minParts, maxParts, activePartsClone } = this;
589589

590590
/**
591591
* When setting the active parts, it is possible
@@ -597,7 +597,7 @@ export class Datetime implements ComponentInterface {
597597
* Additionally, we need to update the working parts
598598
* too in the event that the validated parts are different.
599599
*/
600-
const validatedParts = validateParts(parts);
600+
const validatedParts = validateParts(parts, minParts, maxParts);
601601
this.setWorkingParts(validatedParts);
602602

603603
if (multiple) {

‎core/src/components/datetime/test/manipulation.spec.ts

+70
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
calculateHourFromAMPM,
1515
subtractDays,
1616
addDays,
17+
validateParts,
1718
} from '../utils/manipulation';
1819

1920
describe('addDays()', () => {
@@ -487,3 +488,72 @@ describe('getPreviousYear()', () => {
487488
});
488489
});
489490
});
491+
492+
describe('validateParts()', () => {
493+
it('should move day in bounds', () => {
494+
expect(validateParts({ month: 2, day: 31, year: 2022, hour: 8, minute: 0 })).toEqual({
495+
month: 2,
496+
day: 28,
497+
year: 2022,
498+
hour: 8,
499+
minute: 0,
500+
});
501+
});
502+
it('should move the hour back in bounds according to the min', () => {
503+
expect(
504+
validateParts(
505+
{ month: 1, day: 1, year: 2022, hour: 8, minute: 0 },
506+
{ month: 1, day: 1, year: 2022, hour: 9, minute: 0 }
507+
)
508+
).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 0 });
509+
});
510+
it('should move the minute back in bounds according to the min', () => {
511+
expect(
512+
validateParts(
513+
{ month: 1, day: 1, year: 2022, hour: 9, minute: 20 },
514+
{ month: 1, day: 1, year: 2022, hour: 9, minute: 30 }
515+
)
516+
).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 });
517+
});
518+
it('should move the hour and minute back in bounds according to the min', () => {
519+
expect(
520+
validateParts(
521+
{ month: 1, day: 1, year: 2022, hour: 8, minute: 30 },
522+
{ month: 1, day: 1, year: 2022, hour: 9, minute: 0 }
523+
)
524+
).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 0 });
525+
});
526+
it('should move the hour back in bounds according to the max', () => {
527+
expect(
528+
validateParts({ month: 1, day: 1, year: 2022, hour: 10, minute: 0 }, undefined, {
529+
month: 1,
530+
day: 1,
531+
year: 2022,
532+
hour: 9,
533+
minute: 0,
534+
})
535+
).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 0 });
536+
});
537+
it('should move the minute back in bounds according to the max', () => {
538+
expect(
539+
validateParts({ month: 1, day: 1, year: 2022, hour: 9, minute: 40 }, undefined, {
540+
month: 1,
541+
day: 1,
542+
year: 2022,
543+
hour: 9,
544+
minute: 30,
545+
})
546+
).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 });
547+
});
548+
it('should move the hour and minute back in bounds according to the max', () => {
549+
expect(
550+
validateParts({ month: 1, day: 1, year: 2022, hour: 10, minute: 20 }, undefined, {
551+
month: 1,
552+
day: 1,
553+
year: 2022,
554+
hour: 9,
555+
minute: 30,
556+
})
557+
).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 });
558+
});
559+
});

‎core/src/components/datetime/test/minmax/datetime.e2e.ts

+49
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,53 @@ test.describe('datetime: minmax', () => {
234234
);
235235
await expect(hourPickerItems).toHaveText(['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']);
236236
});
237+
238+
test.describe('minmax value adjustment when out of bounds', () => {
239+
test.beforeEach(({ skip }) => {
240+
skip.rtl();
241+
skip.mode('ios', 'This implementation is the same across modes.');
242+
});
243+
test('should reset to min time if out of bounds', async ({ page }) => {
244+
await page.setContent(`
245+
<ion-datetime
246+
min="2022-10-10T08:00"
247+
value="2022-10-11T06:00"
248+
></ion-datetime>
249+
`);
250+
251+
await page.waitForSelector('.datetime-ready');
252+
253+
const datetime = page.locator('ion-datetime');
254+
const ionChange = await page.spyOnEvent('ionChange');
255+
const dayButton = page.locator('ion-datetime .calendar-day[data-day="10"][data-month="10"][data-year="2022"]');
256+
await dayButton.click();
257+
258+
await ionChange.next();
259+
260+
const value = await datetime.evaluate((el: HTMLIonDatetimeElement) => el.value);
261+
await expect(typeof value).toBe('string');
262+
await expect(value!.includes('2022-10-10T08:00')).toBe(true);
263+
});
264+
test('should reset to max time if out of bounds', async ({ page }) => {
265+
await page.setContent(`
266+
<ion-datetime
267+
max="2022-10-10T08:00"
268+
value="2022-10-11T09:00"
269+
></ion-datetime>
270+
`);
271+
272+
await page.waitForSelector('.datetime-ready');
273+
274+
const datetime = page.locator('ion-datetime');
275+
const ionChange = await page.spyOnEvent('ionChange');
276+
const dayButton = page.locator('ion-datetime .calendar-day[data-day="10"][data-month="10"][data-year="2022"]');
277+
await dayButton.click();
278+
279+
await ionChange.next();
280+
281+
const value = await datetime.evaluate((el: HTMLIonDatetimeElement) => el.value);
282+
await expect(typeof value).toBe('string');
283+
await expect(value!.includes('2022-10-10T08:00')).toBe(true);
284+
});
285+
});
237286
});

‎core/src/components/datetime/utils/manipulation.ts

+67-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { DatetimeParts } from '../datetime-interface';
22

3+
import { isSameDay } from './comparison';
34
import { getNumDaysInMonth } from './helpers';
45

56
const twoDigit = (val: number | undefined): string => {
@@ -345,7 +346,11 @@ export const calculateHourFromAMPM = (currentParts: DatetimeParts, newAMPM: 'am'
345346
* values are valid. For days that do not exist,
346347
* the closest valid day is used.
347348
*/
348-
export const validateParts = (parts: DatetimeParts): DatetimeParts => {
349+
export const validateParts = (
350+
parts: DatetimeParts,
351+
minParts?: DatetimeParts,
352+
maxParts?: DatetimeParts
353+
): DatetimeParts => {
349354
const { month, day, year } = parts;
350355
const partsCopy = { ...parts };
351356

@@ -361,5 +366,66 @@ export const validateParts = (parts: DatetimeParts): DatetimeParts => {
361366
partsCopy.day = numDays;
362367
}
363368

369+
/**
370+
* If value is same day as min day,
371+
* make sure the time value is in bounds.
372+
*/
373+
if (minParts !== undefined && isSameDay(partsCopy, minParts)) {
374+
/**
375+
* If the hour is out of bounds,
376+
* update both the hour and minute.
377+
* This is done so that the new time
378+
* is closest to what the user selected.
379+
*/
380+
if (partsCopy.hour !== undefined && minParts.hour !== undefined) {
381+
if (partsCopy.hour < minParts.hour) {
382+
partsCopy.hour = minParts.hour;
383+
partsCopy.minute = minParts.minute;
384+
385+
/**
386+
* If only the minute is out of bounds,
387+
* set it to the min minute.
388+
*/
389+
} else if (
390+
partsCopy.hour === minParts.hour &&
391+
partsCopy.minute !== undefined &&
392+
minParts.minute !== undefined &&
393+
partsCopy.minute < minParts.minute
394+
) {
395+
partsCopy.minute = minParts.minute;
396+
}
397+
}
398+
}
399+
400+
/**
401+
* If value is same day as max day,
402+
* make sure the time value is in bounds.
403+
*/
404+
if (maxParts !== undefined && isSameDay(parts, maxParts)) {
405+
/**
406+
* If the hour is out of bounds,
407+
* update both the hour and minute.
408+
* This is done so that the new time
409+
* is closest to what the user selected.
410+
*/
411+
if (partsCopy.hour !== undefined && maxParts.hour !== undefined) {
412+
if (partsCopy.hour > maxParts.hour) {
413+
partsCopy.hour = maxParts.hour;
414+
partsCopy.minute = maxParts.minute;
415+
/**
416+
* If only the minute is out of bounds,
417+
* set it to the max minute.
418+
*/
419+
} else if (
420+
partsCopy.hour === maxParts.hour &&
421+
partsCopy.minute !== undefined &&
422+
maxParts.minute !== undefined &&
423+
partsCopy.minute > maxParts.minute
424+
) {
425+
partsCopy.minute = maxParts.minute;
426+
}
427+
}
428+
}
429+
364430
return partsCopy;
365431
};

‎core/src/components/picker-column-internal/picker-column-internal.tsx

-64
Original file line numberDiff line numberDiff line change
@@ -36,70 +36,6 @@ export class PickerColumnInternal implements ComponentInterface {
3636
* A list of options to be displayed in the picker
3737
*/
3838
@Prop() items: PickerColumnItem[] = [];
39-
@Watch('items')
40-
itemsChange(currentItems: PickerColumnItem[], previousItems: PickerColumnItem[]) {
41-
const { value } = this;
42-
43-
/**
44-
* When the items change, it is possible for the item
45-
* that was selected to no longer exist. In that case, we need
46-
* to automatically select the nearest item. If we do not,
47-
* then the scroll position will be reset to zero and it will
48-
* look like the first item was automatically selected.
49-
*
50-
* If we cannot find a closest item then we do nothing, and
51-
* the browser will reset the scroll position to 0.
52-
*/
53-
const findCurrentItem = currentItems.find((item) => item.value === value);
54-
if (!findCurrentItem) {
55-
/**
56-
* The default behavior is to assume
57-
* that the new set of data is similar to the old
58-
* set of data, just with some items filtered out.
59-
* We walk backwards through the data to find the
60-
* closest enabled picker item and select it.
61-
*
62-
* Developers can also swap the items out for an entirely
63-
* new set of data. In that case, the value we select
64-
* here likely will not make much sense. For this use case,
65-
* developers should update the `value` prop themselves
66-
* when swapping out the data.
67-
*/
68-
const findPreviousItemIndex = previousItems.findIndex((item) => item.value === value);
69-
if (findPreviousItemIndex === -1) {
70-
return;
71-
}
72-
73-
/**
74-
* Step through the current items backwards
75-
* until we find a neighbor we can select.
76-
* We start at the last known location of the
77-
* current selected item in order to
78-
* account for data that has been added. This
79-
* search prioritizes stability in that it
80-
* tries to keep the scroll position as close
81-
* to where it was before the update.
82-
* Before Items: ['a', 'b', 'c'], Selected Value: 'b'
83-
* After Items: ['a', 'dog', 'c']
84-
* Even though 'dog' is a different item than 'b',
85-
* it is the closest item we can select while
86-
* preserving the scroll position.
87-
*/
88-
let nearestItem;
89-
for (let i = findPreviousItemIndex; i >= 0; i--) {
90-
const item = currentItems[i];
91-
if (item !== undefined && item.disabled !== true) {
92-
nearestItem = item;
93-
break;
94-
}
95-
}
96-
97-
if (nearestItem) {
98-
this.setValue(nearestItem.value);
99-
return;
100-
}
101-
}
102-
}
10339

10440
/**
10541
* The selected option in the picker.

‎core/src/components/picker-column-internal/test/update-items/picker-column-internal.e2e.ts

-175
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.