-
Notifications
You must be signed in to change notification settings - Fork 13.4k
fix(datetime): prevent navigating to disabled months #24421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5cd1ff2
2af54e7
58f24ff
1910f5b
b589542
75501d0
b4913c9
4733bf6
4ea28d0
4419ee0
4434299
16d46c8
06c2710
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,10 @@ import { | |
| } from './utils/parse'; | ||
| import { | ||
| getCalendarDayState, | ||
| isDayDisabled | ||
| isDayDisabled, | ||
| isMonthDisabled, | ||
| isNextMonthDisabled, | ||
| isPrevMonthDisabled | ||
| } from './utils/state'; | ||
|
|
||
| /** | ||
|
|
@@ -714,6 +717,15 @@ export class Datetime implements ComponentInterface { | |
| return; | ||
| } | ||
|
|
||
| const { month, year, day } = refMonthFn(this.workingParts); | ||
|
|
||
| if (isMonthDisabled({ month, year, day: null }, { | ||
| minParts: this.minParts, | ||
| maxParts: this.maxParts | ||
| })) { | ||
| return; | ||
| } | ||
|
|
||
| /** | ||
| * On iOS, we need to set pointer-events: none | ||
| * when the user is almost done with the gesture | ||
|
|
@@ -724,7 +736,8 @@ export class Datetime implements ComponentInterface { | |
| */ | ||
| if (mode === 'ios') { | ||
| const ratio = ev.intersectionRatio; | ||
| const shouldDisable = Math.abs(ratio - 0.7) <= 0.1; | ||
| // `maxTouchPoints` will be 1 in device preview, but > 1 on device | ||
| const shouldDisable = Math.abs(ratio - 0.7) <= 0.1 && navigator.maxTouchPoints > 1; | ||
|
|
||
| if (shouldDisable) { | ||
| calendarBodyRef.style.setProperty('pointer-events', 'none'); | ||
|
|
@@ -757,7 +770,6 @@ export class Datetime implements ComponentInterface { | |
| * if we did not do this. | ||
| */ | ||
| writeTask(() => { | ||
| const { month, year, day } = refMonthFn(this.workingParts); | ||
|
|
||
| this.setWorkingParts({ | ||
| ...this.workingParts, | ||
|
|
@@ -766,9 +778,11 @@ export class Datetime implements ComponentInterface { | |
| year | ||
| }); | ||
|
|
||
| calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1); | ||
| calendarBodyRef.style.removeProperty('overflow'); | ||
| calendarBodyRef.style.removeProperty('pointer-events'); | ||
| raf(() => { | ||
| calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1); | ||
| calendarBodyRef.style.removeProperty('overflow'); | ||
| calendarBodyRef.style.removeProperty('pointer-events'); | ||
| }); | ||
|
|
||
| /** | ||
| * Now that state has been updated | ||
|
|
@@ -781,6 +795,12 @@ export class Datetime implements ComponentInterface { | |
| }); | ||
| } | ||
|
|
||
| const threshold = mode === 'ios' && | ||
| // tslint:disable-next-line | ||
| typeof navigator !== 'undefined' && | ||
| navigator.maxTouchPoints > 1 ? | ||
| [0.7, 1] : 1; | ||
|
|
||
| /** | ||
| * Listen on the first month to | ||
| * prepend a new month and on the last | ||
|
|
@@ -800,13 +820,13 @@ export class Datetime implements ComponentInterface { | |
| * something WebKit does. | ||
| */ | ||
| endIO = new IntersectionObserver(ev => ioCallback('end', ev), { | ||
| threshold: mode === 'ios' ? [0.7, 1] : 1, | ||
| threshold, | ||
| root: calendarBodyRef | ||
| }); | ||
| endIO.observe(endMonth); | ||
|
|
||
| startIO = new IntersectionObserver(ev => ioCallback('start', ev), { | ||
| threshold: mode === 'ios' ? [0.7, 1] : 1, | ||
| threshold, | ||
| root: calendarBodyRef | ||
| }); | ||
| startIO.observe(startMonth); | ||
|
|
@@ -963,9 +983,9 @@ export class Datetime implements ComponentInterface { | |
| } | ||
|
|
||
| componentWillLoad() { | ||
| this.processValue(this.value); | ||
| this.processMinParts(); | ||
| this.processMaxParts(); | ||
| this.processValue(this.value); | ||
liamdebeasi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.parsedHourValues = convertToArrayOfNumbers(this.hourValues); | ||
| this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues); | ||
| this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues); | ||
|
|
@@ -1091,6 +1111,13 @@ export class Datetime implements ComponentInterface { | |
| items={months} | ||
| value={workingParts.month} | ||
| onIonChange={(ev: CustomEvent) => { | ||
| // Due to a Safari 14 issue we need to destroy | ||
| // the intersection observer before we update state | ||
| // and trigger a re-render. | ||
| if (this.destroyCalendarIO) { | ||
| this.destroyCalendarIO(); | ||
| } | ||
|
|
||
| this.setWorkingParts({ | ||
| ...this.workingParts, | ||
| month: ev.detail.value | ||
|
|
@@ -1103,6 +1130,10 @@ export class Datetime implements ComponentInterface { | |
| }); | ||
| } | ||
|
|
||
| // We can re-attach the intersection observer after | ||
| // the working parts have been updated. | ||
| this.initializeCalendarIOListeners(); | ||
|
|
||
| ev.stopPropagation(); | ||
| }} | ||
| ></ion-picker-column-internal> | ||
|
|
@@ -1114,6 +1145,13 @@ export class Datetime implements ComponentInterface { | |
| items={years} | ||
| value={workingParts.year} | ||
| onIonChange={(ev: CustomEvent) => { | ||
| // Due to a Safari 14 issue we need to destroy | ||
| // the intersection observer before we update state | ||
| // and trigger a re-render. | ||
| if (this.destroyCalendarIO) { | ||
| this.destroyCalendarIO(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if we remove this? I tried testing in iOS 14 and I did not notice anything break.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This specific code was to catch an issue when using the calendar month picker and changing months through that UI would re-render and cause the intersection observer to update the month back to the previous month. I noticed it prior by going from October -> September, then expanding the month picker and changing between months. Was from this comment: #24421 (comment) (only applicable in Safari < 15). Will prioritize time later tonight/early tomorrow to address the other quick scroll issue.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have identified this as a webkit bug, new issue being tracked here: #24683. PR is ready to merge in the interim. |
||
| } | ||
|
|
||
| this.setWorkingParts({ | ||
| ...this.workingParts, | ||
| year: ev.detail.value | ||
|
|
@@ -1126,6 +1164,10 @@ export class Datetime implements ComponentInterface { | |
| }); | ||
| } | ||
|
|
||
| // We can re-attach the intersection observer after | ||
| // the working parts have been updated. | ||
| this.initializeCalendarIOListeners(); | ||
|
|
||
| ev.stopPropagation(); | ||
| }} | ||
| ></ion-picker-column-internal> | ||
|
|
@@ -1139,6 +1181,10 @@ export class Datetime implements ComponentInterface { | |
| private renderCalendarHeader(mode: Mode) { | ||
| const expandedIcon = mode === 'ios' ? chevronDown : caretUpSharp; | ||
| const collapsedIcon = mode === 'ios' ? chevronForward : caretDownSharp; | ||
|
|
||
| const prevMonthDisabled = isPrevMonthDisabled(this.workingParts, this.minParts, this.maxParts); | ||
| const nextMonthDisabled = isNextMonthDisabled(this.workingParts, this.maxParts); | ||
|
|
||
| return ( | ||
| <div class="calendar-header"> | ||
| <div class="calendar-action-buttons"> | ||
|
|
@@ -1152,10 +1198,14 @@ export class Datetime implements ComponentInterface { | |
|
|
||
| <div class="calendar-next-prev"> | ||
| <ion-buttons> | ||
| <ion-button onClick={() => this.prevMonth()}> | ||
| <ion-button | ||
| disabled={prevMonthDisabled} | ||
| onClick={() => this.prevMonth()}> | ||
| <ion-icon slot="icon-only" icon={chevronBack} lazy={false} flipRtl></ion-icon> | ||
| </ion-button> | ||
| <ion-button onClick={() => this.nextMonth()}> | ||
| <ion-button | ||
| disabled={nextMonthDisabled} | ||
| onClick={() => this.nextMonth()}> | ||
| <ion-icon slot="icon-only" icon={chevronForward} lazy={false} flipRtl></ion-icon> | ||
| </ion-button> | ||
| </ion-buttons> | ||
|
|
@@ -1173,9 +1223,26 @@ export class Datetime implements ComponentInterface { | |
| private renderMonth(month: number, year: number) { | ||
| const yearAllowed = this.parsedYearValues === undefined || this.parsedYearValues.includes(year); | ||
| const monthAllowed = this.parsedMonthValues === undefined || this.parsedMonthValues.includes(month); | ||
| const isMonthDisabled = !yearAllowed || !monthAllowed; | ||
| const isCalMonthDisabled = !yearAllowed || !monthAllowed; | ||
| const swipeDisabled = isMonthDisabled({ | ||
| month, | ||
| year, | ||
| day: null | ||
| }, { | ||
| minParts: this.minParts, | ||
| maxParts: this.maxParts | ||
| }); | ||
| // The working month should never have swipe disabled. | ||
| // Otherwise the CSS scroll snap will not work and the user | ||
| // can free-scroll the calendar. | ||
| const isWorkingMonth = this.workingParts.month === month && this.workingParts.year === year; | ||
|
|
||
| return ( | ||
| <div class="calendar-month"> | ||
| <div class={{ | ||
| 'calendar-month': true, | ||
| // Prevents scroll snap swipe gestures for months outside of the min/max bounds | ||
| 'calendar-month-disabled': !isWorkingMonth && swipeDisabled | ||
| }}> | ||
| <div class="calendar-month-grid"> | ||
| {getDaysOfMonth(month, year, this.firstDayOfWeek % 7).map((dateObject, index) => { | ||
| const { day, dayOfWeek } = dateObject; | ||
|
|
@@ -1190,7 +1257,7 @@ export class Datetime implements ComponentInterface { | |
| data-year={year} | ||
| data-index={index} | ||
| data-day-of-week={dayOfWeek} | ||
| disabled={isMonthDisabled || disabled} | ||
| disabled={isCalMonthDisabled || disabled} | ||
| class={{ | ||
| 'calendar-day-padding': day === null, | ||
| 'calendar-day': true, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.