Skip to content

Commit 9e3d028

Browse files
authored
feat(datetime-button): add support for opening datetimes in overlays with button (#25649)
1 parent b5403d1 commit 9e3d028

File tree

11 files changed

+315
-117
lines changed

11 files changed

+315
-117
lines changed

core/src/components.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4753,6 +4753,10 @@ declare namespace LocalJSX {
47534753
* Emitted when the datetime has focus.
47544754
*/
47554755
"onIonFocus"?: (event: IonDatetimeCustomEvent<void>) => void;
4756+
/**
4757+
* Emitted when componentDidRender is fired.
4758+
*/
4759+
"onIonRender"?: (event: IonDatetimeCustomEvent<void>) => void;
47564760
/**
47574761
* Emitted when the styles change.
47584762
*/

core/src/components/datetime-button/datetime-button.tsx

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import { parseDate } from '../datetime/utils/parse';
2525
})
2626
export class DatetimeButton implements ComponentInterface {
2727
private datetimeEl: HTMLIonDatetimeElement | null = null;
28+
private overlayEl: HTMLIonModalElement | HTMLIonPopoverElement | null = null;
29+
private dateTargetEl: HTMLElement | undefined;
30+
private timeTargetEl: HTMLElement | undefined;
2831

2932
@Element() el!: HTMLIonDatetimeButtonElement;
3033

@@ -86,6 +89,26 @@ export class DatetimeButton implements ComponentInterface {
8689

8790
io.observe(datetimeEl);
8891

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+
89112
componentOnReady(datetimeEl, () => {
90113
const datetimePresentation = (this.datetimePresentation = datetimeEl.presentation || 'date-time');
91114

@@ -183,13 +206,31 @@ export class DatetimeButton implements ComponentInterface {
183206
}
184207
};
185208

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) => {
187226
const { datetimeEl, datetimePresentation } = this;
188227

189228
if (!datetimeEl) {
190229
return;
191230
}
192231

232+
let needsPresentationChange = false;
233+
193234
/**
194235
* When clicking the date button,
195236
* we need to make sure that only a date
@@ -200,18 +241,18 @@ export class DatetimeButton implements ComponentInterface {
200241
switch (datetimePresentation) {
201242
case 'date-time':
202243
case 'time-date':
244+
const needsChange = datetimeEl.presentation !== 'date';
203245
/**
204246
* The date+time wheel picker
205247
* shows date and time together,
206248
* so do not adjust the presentation
207249
* in that case.
208250
*/
209-
if (!datetimeEl.preferWheel) {
251+
if (!datetimeEl.preferWheel && needsChange) {
210252
datetimeEl.presentation = 'date';
253+
needsPresentationChange = true;
211254
}
212255
break;
213-
default:
214-
break;
215256
}
216257

217258
/**
@@ -222,15 +263,19 @@ export class DatetimeButton implements ComponentInterface {
222263
* the datetime is opened.
223264
*/
224265
this.selectedButton = 'date';
266+
267+
this.presentOverlay(ev, needsPresentationChange, this.dateTargetEl);
225268
};
226269

227-
private handleTimeClick = () => {
270+
private handleTimeClick = (ev: Event) => {
228271
const { datetimeEl, datetimePresentation } = this;
229272

230273
if (!datetimeEl) {
231274
return;
232275
}
233276

277+
let needsPresentationChange = false;
278+
234279
/**
235280
* When clicking the time button,
236281
* we need to make sure that only a time
@@ -241,7 +286,11 @@ export class DatetimeButton implements ComponentInterface {
241286
switch (datetimePresentation) {
242287
case 'date-time':
243288
case 'time-date':
244-
datetimeEl.presentation = 'time';
289+
const needsChange = datetimeEl.presentation !== 'time';
290+
if (needsChange) {
291+
datetimeEl.presentation = 'time';
292+
needsPresentationChange = true;
293+
}
245294
break;
246295
}
247296

@@ -253,6 +302,54 @@ export class DatetimeButton implements ComponentInterface {
253302
* the datetime is opened.
254303
*/
255304
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+
}
256353
};
257354

258355
render() {
@@ -273,9 +370,10 @@ export class DatetimeButton implements ComponentInterface {
273370
class="ion-activatable"
274371
id="date-button"
275372
aria-expanded={datetimeActive ? 'true' : 'false'}
276-
onClick={() => this.handleDateClick()}
373+
onClick={this.handleDateClick}
277374
disabled={disabled}
278375
part="native"
376+
ref={(el) => (this.dateTargetEl = el)}
279377
>
280378
<slot name="date-target">{dateText}</slot>
281379
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
@@ -287,9 +385,10 @@ export class DatetimeButton implements ComponentInterface {
287385
class="ion-activatable"
288386
id="time-button"
289387
aria-expanded={datetimeActive ? 'true' : 'false'}
290-
onClick={() => this.handleTimeClick()}
388+
onClick={this.handleTimeClick}
291389
disabled={disabled}
292390
part="native"
391+
ref={(el) => (this.timeTargetEl = el)}
293392
>
294393
<slot name="time-target">{timeText}</slot>
295394
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}

core/src/components/datetime-button/test/accordion/index.html

Lines changed: 0 additions & 72 deletions
This file was deleted.

0 commit comments

Comments
 (0)