@@ -117,11 +117,7 @@ export class Datetime implements ComponentInterface {
117
117
118
118
private prevPresentation : string | null = null ;
119
119
120
- /**
121
- * Duplicate reference to `activeParts` that does not trigger a re-render of the component.
122
- * Allows caching an instance of the `activeParts` in between render cycles.
123
- */
124
- private activePartsClone : DatetimeParts | DatetimeParts [ ] = [ ] ;
120
+ private resolveForceDateScrolling ?: ( ) => void ;
125
121
126
122
@State ( ) showMonthAndYear = false ;
127
123
@@ -140,6 +136,17 @@ export class Datetime implements ComponentInterface {
140
136
141
137
@State ( ) isTimePopoverOpen = false ;
142
138
139
+ /**
140
+ * When defined, will force the datetime to render the month
141
+ * containing the specified date. Currently, this should only
142
+ * be used to enable immediately auto-scrolling to the new month,
143
+ * and should then be reset to undefined once the transition is
144
+ * finished and the forced month is now in view.
145
+ *
146
+ * Applies to grid-style datetimes only.
147
+ */
148
+ @State ( ) forceRenderDate ?: DatetimeParts ;
149
+
143
150
/**
144
151
* The color to use from your application's color palette.
145
152
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -221,6 +228,12 @@ export class Datetime implements ComponentInterface {
221
228
*/
222
229
@Prop ( ) presentation : DatetimePresentation = 'date-time' ;
223
230
231
+ private get isGridStyle ( ) {
232
+ const { presentation, preferWheel } = this ;
233
+ const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date' ;
234
+ return hasDatePresentation && ! preferWheel ;
235
+ }
236
+
224
237
/**
225
238
* The text to display on the picker's cancel button.
226
239
*/
@@ -302,11 +315,6 @@ export class Datetime implements ComponentInterface {
302
315
this . parsedMinuteValues = convertToArrayOfNumbers ( this . minuteValues ) ;
303
316
}
304
317
305
- @Watch ( 'activeParts' )
306
- protected activePartsChanged ( ) {
307
- this . activePartsClone = this . activeParts ;
308
- }
309
-
310
318
/**
311
319
* The locale to use for `ion-datetime`. This
312
320
* impacts month and day name formatting.
@@ -356,54 +364,11 @@ export class Datetime implements ComponentInterface {
356
364
* Update the datetime value when the value changes
357
365
*/
358
366
@Watch ( 'value' )
359
- protected valueChanged ( ) {
360
- const { value, minParts , maxParts , workingParts } = this ;
367
+ protected async valueChanged ( ) {
368
+ const { value } = this ;
361
369
362
370
if ( this . hasValue ( ) ) {
363
- this . warnIfIncorrectValueUsage ( ) ;
364
-
365
- /**
366
- * Clones the value of the `activeParts` to the private clone, to update
367
- * the date display on the current render cycle without causing another render.
368
- *
369
- * This allows us to update the current value's date/time display without
370
- * refocusing or shifting the user's display (leaves the user in place).
371
- */
372
- const valueDateParts = parseDate ( value ) ;
373
- if ( valueDateParts ) {
374
- warnIfValueOutOfBounds ( valueDateParts , minParts , maxParts ) ;
375
-
376
- if ( Array . isArray ( valueDateParts ) ) {
377
- this . activePartsClone = [ ...valueDateParts ] ;
378
- } else {
379
- const { month, day, year, hour, minute } = valueDateParts ;
380
- const ampm = hour != null ? ( hour >= 12 ? 'pm' : 'am' ) : undefined ;
381
-
382
- this . activePartsClone = {
383
- ...this . activeParts ,
384
- month,
385
- day,
386
- year,
387
- hour,
388
- minute,
389
- ampm,
390
- } ;
391
-
392
- /**
393
- * The working parts am/pm value must be updated when the value changes, to
394
- * ensure the time picker hour column values are generated correctly.
395
- *
396
- * Note that we don't need to do this if valueDateParts is an array, since
397
- * multiple="true" does not apply to time pickers.
398
- */
399
- this . setWorkingParts ( {
400
- ...workingParts ,
401
- ampm,
402
- } ) ;
403
- }
404
- } else {
405
- printIonWarning ( `Unable to parse date string: ${ value } . Please provide a valid ISO 8601 datetime string.` ) ;
406
- }
371
+ this . processValue ( value ) ;
407
372
}
408
373
409
374
this . emitStyle ( ) ;
@@ -596,18 +561,18 @@ export class Datetime implements ComponentInterface {
596
561
* data. This should be used when rendering an
597
562
* interface in an environment where the `value`
598
563
* may not be set. This function works
599
- * by returning the first selected date in
600
- * "activePartsClone" and then falling back to
601
- * defaultParts if no active date is selected.
564
+ * by returning the first selected date and then
565
+ * falling back to defaultParts if no active date
566
+ * is selected.
602
567
*/
603
568
private getActivePartsWithFallback = ( ) => {
604
569
const { defaultParts } = this ;
605
570
return this . getActivePart ( ) ?? defaultParts ;
606
571
} ;
607
572
608
573
private getActivePart = ( ) => {
609
- const { activePartsClone } = this ;
610
- return Array . isArray ( activePartsClone ) ? activePartsClone [ 0 ] : activePartsClone ;
574
+ const { activeParts } = this ;
575
+ return Array . isArray ( activeParts ) ? activeParts [ 0 ] : activeParts ;
611
576
} ;
612
577
613
578
private closeParentOverlay = ( ) => {
@@ -627,7 +592,7 @@ export class Datetime implements ComponentInterface {
627
592
} ;
628
593
629
594
private setActiveParts = ( parts : DatetimeParts , removeDate = false ) => {
630
- const { multiple, minParts, maxParts, activePartsClone } = this ;
595
+ const { multiple, minParts, maxParts, activeParts } = this ;
631
596
632
597
/**
633
598
* When setting the active parts, it is possible
@@ -643,16 +608,7 @@ export class Datetime implements ComponentInterface {
643
608
this . setWorkingParts ( validatedParts ) ;
644
609
645
610
if ( multiple ) {
646
- /**
647
- * We read from activePartsClone here because valueChanged() only updates that,
648
- * so it's the more reliable source of truth. If we read from activeParts, then
649
- * if you click July 1, manually set the value to July 2, and then click July 3,
650
- * the new value would be [July 1, July 3], ignoring the value set.
651
- *
652
- * We can then pass the new value to activeParts (rather than activePartsClone)
653
- * since the clone will be updated automatically by activePartsChanged().
654
- */
655
- const activePartsArray = Array . isArray ( activePartsClone ) ? activePartsClone : [ activePartsClone ] ;
611
+ const activePartsArray = Array . isArray ( activeParts ) ? activeParts : [ activeParts ] ;
656
612
if ( removeDate ) {
657
613
this . activeParts = activePartsArray . filter ( ( p ) => ! isSameDay ( p , validatedParts ) ) ;
658
614
} else {
@@ -908,6 +864,20 @@ export class Datetime implements ComponentInterface {
908
864
const monthBox = month . getBoundingClientRect ( ) ;
909
865
if ( Math . abs ( monthBox . x - box . x ) > 2 ) return ;
910
866
867
+ /**
868
+ * If we're force-rendering a month, assume we've
869
+ * scrolled to that and return it.
870
+ *
871
+ * If forceRenderDate is ever used in a context where the
872
+ * forced month is not immediately auto-scrolled to, this
873
+ * should be updated to also check whether `month` has the
874
+ * same month and year as the forced date.
875
+ */
876
+ const { forceRenderDate } = this ;
877
+ if ( forceRenderDate !== undefined ) {
878
+ return { month : forceRenderDate . month , year : forceRenderDate . year , day : forceRenderDate . day } ;
879
+ }
880
+
911
881
/**
912
882
* From here, we can determine if the start
913
883
* month or the end month was scrolled into view.
@@ -976,6 +946,10 @@ export class Datetime implements ComponentInterface {
976
946
977
947
calendarBodyRef . scrollLeft = workingMonth . clientWidth * ( isRTL ( this . el ) ? - 1 : 1 ) ;
978
948
calendarBodyRef . style . removeProperty ( 'overflow' ) ;
949
+
950
+ if ( this . resolveForceDateScrolling ) {
951
+ this . resolveForceDateScrolling ( ) ;
952
+ }
979
953
} ) ;
980
954
} ;
981
955
@@ -1193,13 +1167,21 @@ export class Datetime implements ComponentInterface {
1193
1167
}
1194
1168
1195
1169
private processValue = ( value ?: string | string [ ] | null ) => {
1196
- const hasValue = value !== null && value !== undefined ;
1170
+ const hasValue = value !== null && value !== undefined && ( ! Array . isArray ( value ) || value . length > 0 ) ;
1197
1171
const valueToProcess = hasValue ? parseDate ( value ) : this . defaultParts ;
1198
1172
1199
- const { minParts, maxParts } = this ;
1173
+ const { minParts, maxParts, workingParts , el } = this ;
1200
1174
1201
1175
this . warnIfIncorrectValueUsage ( ) ;
1202
1176
1177
+ /**
1178
+ * Return early if the value wasn't parsed correctly, such as
1179
+ * if an improperly formatted date string was provided.
1180
+ */
1181
+ if ( ! valueToProcess ) {
1182
+ return ;
1183
+ }
1184
+
1203
1185
/**
1204
1186
* Datetime should only warn of out of bounds values
1205
1187
* if set by the user. If the `value` is undefined,
@@ -1218,19 +1200,11 @@ export class Datetime implements ComponentInterface {
1218
1200
* that the values don't necessarily have to be in order.
1219
1201
*/
1220
1202
const singleValue = Array . isArray ( valueToProcess ) ? valueToProcess [ 0 ] : valueToProcess ;
1203
+ const targetValue = clampDate ( singleValue , minParts , maxParts ) ;
1221
1204
1222
- const { month, day, year, hour, minute } = clampDate ( singleValue , minParts , maxParts ) ;
1205
+ const { month, day, year, hour, minute } = targetValue ;
1223
1206
const ampm = parseAmPm ( hour ! ) ;
1224
1207
1225
- this . setWorkingParts ( {
1226
- month,
1227
- day,
1228
- year,
1229
- hour,
1230
- minute,
1231
- ampm,
1232
- } ) ;
1233
-
1234
1208
/**
1235
1209
* Since `activeParts` indicates a value that
1236
1210
* been explicitly selected either by the
@@ -1258,6 +1232,67 @@ export class Datetime implements ComponentInterface {
1258
1232
*/
1259
1233
this . activeParts = [ ] ;
1260
1234
}
1235
+
1236
+ /**
1237
+ * Only animate if:
1238
+ * 1. We're using grid style (wheel style pickers should just jump to new value)
1239
+ * 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to)
1240
+ * 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example)
1241
+ * 4. The month/year picker is not open (since you wouldn't see the animation anyway)
1242
+ */
1243
+ const didChangeMonth =
1244
+ ( month !== undefined && month !== workingParts . month ) || ( year !== undefined && year !== workingParts . year ) ;
1245
+ const bodyIsVisible = el . classList . contains ( 'datetime-ready' ) ;
1246
+ const { isGridStyle, showMonthAndYear } = this ;
1247
+ if ( isGridStyle && didChangeMonth && bodyIsVisible && ! showMonthAndYear ) {
1248
+ this . animateToDate ( targetValue ) ;
1249
+ } else {
1250
+ /**
1251
+ * We only need to do this if we didn't just animate to a new month,
1252
+ * since that calls prevMonth/nextMonth which calls setWorkingParts for us.
1253
+ */
1254
+ this . setWorkingParts ( {
1255
+ month,
1256
+ day,
1257
+ year,
1258
+ hour,
1259
+ minute,
1260
+ ampm,
1261
+ } ) ;
1262
+ }
1263
+ } ;
1264
+
1265
+ private animateToDate = async ( targetValue : DatetimeParts ) => {
1266
+ const { workingParts } = this ;
1267
+
1268
+ /**
1269
+ * Tell other render functions that we need to force the
1270
+ * target month to appear in place of the actual next/prev month.
1271
+ * Because this is a State variable, a rerender will be triggered
1272
+ * automatically, updating the rendered months.
1273
+ */
1274
+ this . forceRenderDate = targetValue ;
1275
+
1276
+ /**
1277
+ * Flag that we've started scrolling to the forced date.
1278
+ * The resolve function will be called by the datetime's
1279
+ * scroll listener when it's done updating everything.
1280
+ * This is a replacement for making prev/nextMonth async,
1281
+ * since the logic we're waiting on is in a listener.
1282
+ */
1283
+ const forceDateScrollingPromise = new Promise < void > ( ( resolve ) => {
1284
+ this . resolveForceDateScrolling = resolve ;
1285
+ } ) ;
1286
+
1287
+ /**
1288
+ * Animate smoothly to the forced month. This will also update
1289
+ * workingParts and correct the surrounding months for us.
1290
+ */
1291
+ const targetMonthIsBefore = isBefore ( targetValue , workingParts ) ;
1292
+ targetMonthIsBefore ? this . prevMonth ( ) : this . nextMonth ( ) ;
1293
+ await forceDateScrollingPromise ;
1294
+ this . resolveForceDateScrolling = undefined ;
1295
+ this . forceRenderDate = undefined ;
1261
1296
} ;
1262
1297
1263
1298
componentWillLoad ( ) {
@@ -1286,16 +1321,18 @@ export class Datetime implements ComponentInterface {
1286
1321
}
1287
1322
}
1288
1323
1289
- this . processMinParts ( ) ;
1290
- this . processMaxParts ( ) ;
1291
1324
const hourValues = ( this . parsedHourValues = convertToArrayOfNumbers ( this . hourValues ) ) ;
1292
1325
const minuteValues = ( this . parsedMinuteValues = convertToArrayOfNumbers ( this . minuteValues ) ) ;
1293
1326
const monthValues = ( this . parsedMonthValues = convertToArrayOfNumbers ( this . monthValues ) ) ;
1294
1327
const yearValues = ( this . parsedYearValues = convertToArrayOfNumbers ( this . yearValues ) ) ;
1295
1328
const dayValues = ( this . parsedDayValues = convertToArrayOfNumbers ( this . dayValues ) ) ;
1296
1329
1297
- const todayParts = ( this . todayParts = parseDate ( getToday ( ) ) ) ;
1330
+ const todayParts = ( this . todayParts = parseDate ( getToday ( ) ) ! ) ;
1298
1331
this . defaultParts = getClosestValidDate ( todayParts , monthValues , dayValues , yearValues , hourValues , minuteValues ) ;
1332
+
1333
+ this . processMinParts ( ) ;
1334
+ this . processMaxParts ( ) ;
1335
+
1299
1336
this . processValue ( this . value ) ;
1300
1337
1301
1338
this . emitStyle ( ) ;
@@ -2042,7 +2079,7 @@ export class Datetime implements ComponentInterface {
2042
2079
const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState (
2043
2080
this . locale ,
2044
2081
referenceParts ,
2045
- this . activePartsClone ,
2082
+ this . activeParts ,
2046
2083
this . todayParts ,
2047
2084
this . minParts ,
2048
2085
this . maxParts ,
@@ -2151,7 +2188,7 @@ export class Datetime implements ComponentInterface {
2151
2188
private renderCalendarBody ( ) {
2152
2189
return (
2153
2190
< div class = "calendar-body ion-focusable" ref = { ( el ) => ( this . calendarBodyRef = el ) } tabindex = "0" >
2154
- { generateMonths ( this . workingParts ) . map ( ( { month, year } ) => {
2191
+ { generateMonths ( this . workingParts , this . forceRenderDate ) . map ( ( { month, year } ) => {
2155
2192
return this . renderMonth ( month , year ) ;
2156
2193
} ) }
2157
2194
</ div >
@@ -2360,15 +2397,26 @@ export class Datetime implements ComponentInterface {
2360
2397
}
2361
2398
2362
2399
render ( ) {
2363
- const { name, value, disabled, el, color, readonly, showMonthAndYear, preferWheel, presentation, size } = this ;
2400
+ const {
2401
+ name,
2402
+ value,
2403
+ disabled,
2404
+ el,
2405
+ color,
2406
+ readonly,
2407
+ showMonthAndYear,
2408
+ preferWheel,
2409
+ presentation,
2410
+ size,
2411
+ isGridStyle,
2412
+ } = this ;
2364
2413
const mode = getIonMode ( this ) ;
2365
2414
const isMonthAndYearPresentation =
2366
2415
presentation === 'year' || presentation === 'month' || presentation === 'month-year' ;
2367
2416
const shouldShowMonthAndYear = showMonthAndYear || isMonthAndYearPresentation ;
2368
2417
const monthYearPickerOpen = showMonthAndYear && ! isMonthAndYearPresentation ;
2369
2418
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date' ;
2370
2419
const hasWheelVariant = hasDatePresentation && preferWheel ;
2371
- const hasGrid = hasDatePresentation && ! preferWheel ;
2372
2420
2373
2421
renderHiddenInput ( true , el , name , formatValue ( value ) , disabled ) ;
2374
2422
@@ -2387,7 +2435,7 @@ export class Datetime implements ComponentInterface {
2387
2435
[ `datetime-presentation-${ presentation } ` ] : true ,
2388
2436
[ `datetime-size-${ size } ` ] : true ,
2389
2437
[ `datetime-prefer-wheel` ] : hasWheelVariant ,
2390
- [ `datetime-grid` ] : hasGrid ,
2438
+ [ `datetime-grid` ] : isGridStyle ,
2391
2439
} ) ,
2392
2440
} }
2393
2441
>
0 commit comments