@@ -25,6 +25,9 @@ import { parseDate } from '../datetime/utils/parse';
25
25
} )
26
26
export class DatetimeButton implements ComponentInterface {
27
27
private datetimeEl : HTMLIonDatetimeElement | null = null ;
28
+ private overlayEl : HTMLIonModalElement | HTMLIonPopoverElement | null = null ;
29
+ private dateTargetEl : HTMLElement | undefined ;
30
+ private timeTargetEl : HTMLElement | undefined ;
28
31
29
32
@Element ( ) el ! : HTMLIonDatetimeButtonElement ;
30
33
@@ -86,6 +89,26 @@ export class DatetimeButton implements ComponentInterface {
86
89
87
90
io . observe ( datetimeEl ) ;
88
91
92
+ /**
93
+ * Get a reference to any modal/popover
94
+ * the datetime is being used in so we can
95
+ * correctly size it when it is presented.
96
+ */
97
+ const overlayEl = ( this . overlayEl = datetimeEl . closest ( 'ion-modal, ion-popover' ) ) ;
98
+
99
+ /**
100
+ * The .ion-datetime-button-overlay class contains
101
+ * styles that allow any modal/popover to be
102
+ * sized according to the dimensions of the datetime.
103
+ * If developers want a smaller/larger overlay all they need
104
+ * to do is change the width/height of the datetime.
105
+ * Additionally, this lets us avoid having to set
106
+ * explicit widths on each variant of datetime.
107
+ */
108
+ if ( overlayEl ) {
109
+ overlayEl . classList . add ( 'ion-datetime-button-overlay' ) ;
110
+ }
111
+
89
112
componentOnReady ( datetimeEl , ( ) => {
90
113
const datetimePresentation = ( this . datetimePresentation = datetimeEl . presentation || 'date-time' ) ;
91
114
@@ -183,13 +206,31 @@ export class DatetimeButton implements ComponentInterface {
183
206
}
184
207
} ;
185
208
186
- private handleDateClick = ( ) => {
209
+ /**
210
+ * Waits for the ion-datetime to re-render.
211
+ * This is needed in order to correctly position
212
+ * a popover relative to the trigger element.
213
+ */
214
+ private waitForDatetimeChanges = async ( ) => {
215
+ const { datetimeEl } = this ;
216
+ if ( ! datetimeEl ) {
217
+ return Promise . resolve ( ) ;
218
+ }
219
+
220
+ return new Promise ( ( resolve ) => {
221
+ datetimeEl . addEventListener ( 'ionRender' , resolve , { once : true } ) ;
222
+ } ) ;
223
+ } ;
224
+
225
+ private handleDateClick = async ( ev : Event ) => {
187
226
const { datetimeEl, datetimePresentation } = this ;
188
227
189
228
if ( ! datetimeEl ) {
190
229
return ;
191
230
}
192
231
232
+ let needsPresentationChange = false ;
233
+
193
234
/**
194
235
* When clicking the date button,
195
236
* we need to make sure that only a date
@@ -200,18 +241,18 @@ export class DatetimeButton implements ComponentInterface {
200
241
switch ( datetimePresentation ) {
201
242
case 'date-time' :
202
243
case 'time-date' :
244
+ const needsChange = datetimeEl . presentation !== 'date' ;
203
245
/**
204
246
* The date+time wheel picker
205
247
* shows date and time together,
206
248
* so do not adjust the presentation
207
249
* in that case.
208
250
*/
209
- if ( ! datetimeEl . preferWheel ) {
251
+ if ( ! datetimeEl . preferWheel && needsChange ) {
210
252
datetimeEl . presentation = 'date' ;
253
+ needsPresentationChange = true ;
211
254
}
212
255
break ;
213
- default :
214
- break ;
215
256
}
216
257
217
258
/**
@@ -222,15 +263,19 @@ export class DatetimeButton implements ComponentInterface {
222
263
* the datetime is opened.
223
264
*/
224
265
this . selectedButton = 'date' ;
266
+
267
+ this . presentOverlay ( ev , needsPresentationChange , this . dateTargetEl ) ;
225
268
} ;
226
269
227
- private handleTimeClick = ( ) => {
270
+ private handleTimeClick = ( ev : Event ) => {
228
271
const { datetimeEl, datetimePresentation } = this ;
229
272
230
273
if ( ! datetimeEl ) {
231
274
return ;
232
275
}
233
276
277
+ let needsPresentationChange = false ;
278
+
234
279
/**
235
280
* When clicking the time button,
236
281
* we need to make sure that only a time
@@ -241,7 +286,11 @@ export class DatetimeButton implements ComponentInterface {
241
286
switch ( datetimePresentation ) {
242
287
case 'date-time' :
243
288
case 'time-date' :
244
- datetimeEl . presentation = 'time' ;
289
+ const needsChange = datetimeEl . presentation !== 'time' ;
290
+ if ( needsChange ) {
291
+ datetimeEl . presentation = 'time' ;
292
+ needsPresentationChange = true ;
293
+ }
245
294
break ;
246
295
}
247
296
@@ -253,6 +302,54 @@ export class DatetimeButton implements ComponentInterface {
253
302
* the datetime is opened.
254
303
*/
255
304
this . selectedButton = 'time' ;
305
+
306
+ this . presentOverlay ( ev , needsPresentationChange , this . timeTargetEl ) ;
307
+ } ;
308
+
309
+ /**
310
+ * If the datetime is presented in an
311
+ * overlay, the datetime and overlay
312
+ * should be appropriately sized.
313
+ * These classes provide default sizing values
314
+ * that developers can customize.
315
+ * The goal is to provide an overlay that is
316
+ * reasonably sized with a datetime that
317
+ * fills the entire container.
318
+ */
319
+ private presentOverlay = async ( ev : Event , needsPresentationChange : boolean , triggerEl ?: HTMLElement ) => {
320
+ const { overlayEl } = this ;
321
+
322
+ if ( ! overlayEl ) {
323
+ return ;
324
+ }
325
+
326
+ if ( overlayEl . tagName === 'ION-POPOVER' ) {
327
+ /**
328
+ * When the presentation on datetime changes,
329
+ * we need to wait for the component to re-render
330
+ * otherwise the computed width/height of the
331
+ * popover content will be wrong, causing
332
+ * the popover to not align with the trigger element.
333
+ */
334
+
335
+ if ( needsPresentationChange ) {
336
+ await this . waitForDatetimeChanges ( ) ;
337
+ }
338
+
339
+ /**
340
+ * We pass the trigger button element
341
+ * so that the popover aligns with the individual
342
+ * button that was clicked, not the component container.
343
+ */
344
+ ( overlayEl as HTMLIonPopoverElement ) . present ( {
345
+ ...ev ,
346
+ detail : {
347
+ ionShadowTarget : triggerEl ,
348
+ } ,
349
+ } as CustomEvent ) ;
350
+ } else {
351
+ overlayEl . present ( ) ;
352
+ }
256
353
} ;
257
354
258
355
render ( ) {
@@ -273,9 +370,10 @@ export class DatetimeButton implements ComponentInterface {
273
370
class = "ion-activatable"
274
371
id = "date-button"
275
372
aria-expanded = { datetimeActive ? 'true' : 'false' }
276
- onClick = { ( ) => this . handleDateClick ( ) }
373
+ onClick = { this . handleDateClick }
277
374
disabled = { disabled }
278
375
part = "native"
376
+ ref = { ( el ) => ( this . dateTargetEl = el ) }
279
377
>
280
378
< slot name = "date-target" > { dateText } </ slot >
281
379
{ mode === 'md' && < ion-ripple-effect > </ ion-ripple-effect > }
@@ -287,9 +385,10 @@ export class DatetimeButton implements ComponentInterface {
287
385
class = "ion-activatable"
288
386
id = "time-button"
289
387
aria-expanded = { datetimeActive ? 'true' : 'false' }
290
- onClick = { ( ) => this . handleTimeClick ( ) }
388
+ onClick = { this . handleTimeClick }
291
389
disabled = { disabled }
292
390
part = "native"
391
+ ref = { ( el ) => ( this . timeTargetEl = el ) }
293
392
>
294
393
< slot name = "time-target" > { timeText } </ slot >
295
394
{ mode === 'md' && < ion-ripple-effect > </ ion-ripple-effect > }
0 commit comments