feat(datetime-button): add button for displaying datetime in overlays (#25655)

resolves #24316
This commit is contained in:
Liam DeBeasi
2022-07-27 09:47:02 -04:00
committed by GitHub
parent 52f0b22778
commit 499733105e
59 changed files with 1514 additions and 9 deletions

View File

@@ -0,0 +1,50 @@
@import "../../themes/ionic.globals";
// Datetime Button
// --------------------------------------------------
:host {
display: flex;
align-items: center;
justify-content: center;
}
:host button {
@include border-radius(8px);
@include padding(6px, 12px, 6px, 12px);
@include margin(0px, 2px, 0px, 2px);
position: relative;
transition: 150ms color ease-in-out;
border: none;
background: var(--ion-color-step-300, #edeef0);
color: $text-color;
font-family: inherit;
font-size: inherit;
cursor: pointer;
appearance: none;
overflow: hidden;
}
:host(.time-active) #time-button,
:host(.date-active) #date-button {
color: current-color(base);
}
:host(.datetime-button-disabled) {
pointer-events: none;
}
:host(.datetime-button-disabled) button {
opacity: 0.4;
}

View File

@@ -0,0 +1,414 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, State, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import type { Color, DatetimePresentation, DatetimeParts } from '../../interface';
import { componentOnReady, addEventListener } from '../../utils/helpers';
import { printIonError, printIonWarning } from '../../utils/logging';
import { createColorClasses } from '../../utils/theme';
import { getToday } from '../datetime/utils/data';
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
import { is24Hour } from '../datetime/utils/helpers';
import { parseDate } from '../datetime/utils/parse';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot date-target - Content displayed inside of the date button.
* @slot time-target - Content displayed inside of the time button.
*
* @part native - The native HTML button that wraps the slotted text.
*/
@Component({
tag: 'ion-datetime-button',
styleUrls: {
ios: 'datetime-button.scss',
md: 'datetime-button.scss',
},
shadow: true,
})
export class DatetimeButton implements ComponentInterface {
private datetimeEl: HTMLIonDatetimeElement | null = null;
private overlayEl: HTMLIonModalElement | HTMLIonPopoverElement | null = null;
private dateTargetEl: HTMLElement | undefined;
private timeTargetEl: HTMLElement | undefined;
@Element() el!: HTMLIonDatetimeButtonElement;
@State() datetimePresentation?: DatetimePresentation = 'date-time';
@State() dateText?: string;
@State() timeText?: string;
@State() datetimeActive = false;
@State() selectedButton?: 'date' | 'time';
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
* For more information on colors, see [theming](/docs/theming/basics).
*/
@Prop({ reflect: true }) color?: Color = 'primary';
/**
* If `true`, the user cannot interact with the button.
*/
@Prop({ reflect: true }) disabled = false;
/**
* The ID of the `ion-datetime` instance
* associated with the datetime button.
*/
@Prop() datetime?: string;
async componentWillLoad() {
const { datetime } = this;
if (!datetime) {
printIonError(
'An ID associated with an ion-datetime instance is required for ion-datetime-button to function properly.',
this.el
);
return;
}
const datetimeEl = (this.datetimeEl = document.getElementById(datetime) as HTMLIonDatetimeElement | null);
if (!datetimeEl) {
printIonError(`No ion-datetime instance found for ID '${datetime}'.`, this.el);
return;
}
/**
* Since the datetime can be used in any context (overlays, accordion, etc)
* we track when it is visible to determine when it is active.
* This informs which button is highlighted as well as the
* aria-expanded state.
*/
const io = new IntersectionObserver(
(entries: IntersectionObserverEntry[]) => {
const ev = entries[0];
this.datetimeActive = ev.isIntersecting;
},
{
threshold: 0.01,
}
);
io.observe(datetimeEl);
/**
* Get a reference to any modal/popover
* the datetime is being used in so we can
* correctly size it when it is presented.
*/
const overlayEl = (this.overlayEl = datetimeEl.closest('ion-modal, ion-popover'));
/**
* The .ion-datetime-button-overlay class contains
* styles that allow any modal/popover to be
* sized according to the dimensions of the datetime.
* If developers want a smaller/larger overlay all they need
* to do is change the width/height of the datetime.
* Additionally, this lets us avoid having to set
* explicit widths on each variant of datetime.
*/
if (overlayEl) {
overlayEl.classList.add('ion-datetime-button-overlay');
}
componentOnReady(datetimeEl, () => {
const datetimePresentation = (this.datetimePresentation = datetimeEl.presentation || 'date-time');
/**
* Set the initial display
* in the rendered buttons.
*
* From there, we need to listen
* for ionChange to be emitted
* from datetime so we know when
* to re-render the displayed
* text in the buttons.
*/
this.setDateTimeText();
addEventListener(datetimeEl, 'ionChange', this.setDateTimeText);
/**
* Configure the initial selected button
* in the event that the datetime is displayed
* without clicking one of the datetime buttons.
* For example, a datetime could be expanded
* in an accordion. In this case users only
* need to click the accordion header to show
* the datetime.
*/
switch (datetimePresentation) {
case 'date-time':
case 'date':
case 'month-year':
case 'month':
case 'year':
this.selectedButton = 'date';
break;
case 'time-date':
case 'time':
this.selectedButton = 'time';
break;
}
});
}
/**
* Check the value property on the linked
* ion-datetime and then format it according
* to the locale specified on ion-datetime.
*/
private setDateTimeText = () => {
const { datetimeEl, datetimePresentation } = this;
if (!datetimeEl) {
return;
}
const { value, locale, hourCycle, preferWheel, multiple } = datetimeEl;
if (multiple) {
printIonWarning(
`Multi-date selection cannot be used with ion-datetime-button.
Please upvote https://github.com/ionic-team/ionic-framework/issues/25668 if you are interested in seeing this functionality added.
`,
this.el
);
return;
}
/**
* Both ion-datetime and ion-datetime-button default
* to today's date and time if no value is set.
*/
const parsedDatetime = parseDate(value || getToday()) as DatetimeParts;
const use24Hour = is24Hour(locale, hourCycle);
// TODO(FW-1865) - Remove once FW-1831 is fixed.
parsedDatetime.tzOffset = undefined;
this.dateText = this.timeText = undefined;
switch (datetimePresentation) {
case 'date-time':
case 'time-date':
const dateText = getMonthDayAndYear(locale, parsedDatetime);
const timeText = getLocalizedTime(locale, parsedDatetime, use24Hour);
if (preferWheel) {
this.dateText = `${dateText} ${timeText}`;
} else {
this.dateText = dateText;
this.timeText = timeText;
}
break;
case 'date':
this.dateText = getMonthDayAndYear(locale, parsedDatetime);
break;
case 'time':
this.timeText = getLocalizedTime(locale, parsedDatetime, use24Hour);
break;
case 'month-year':
this.dateText = getMonthAndYear(locale, parsedDatetime);
break;
case 'month':
this.dateText = getLocalizedDateTime(locale, parsedDatetime, { month: 'long' });
break;
case 'year':
this.dateText = getLocalizedDateTime(locale, parsedDatetime, { year: 'numeric' });
break;
}
};
/**
* Waits for the ion-datetime to re-render.
* This is needed in order to correctly position
* a popover relative to the trigger element.
*/
private waitForDatetimeChanges = async () => {
const { datetimeEl } = this;
if (!datetimeEl) {
return Promise.resolve();
}
return new Promise((resolve) => {
addEventListener(datetimeEl, 'ionRender', resolve, { once: true });
});
};
private handleDateClick = async (ev: Event) => {
const { datetimeEl, datetimePresentation } = this;
if (!datetimeEl) {
return;
}
let needsPresentationChange = false;
/**
* When clicking the date button,
* we need to make sure that only a date
* picker is displayed. For presentation styles
* that display content other than a date picker,
* we need to update the presentation style.
*/
switch (datetimePresentation) {
case 'date-time':
case 'time-date':
const needsChange = datetimeEl.presentation !== 'date';
/**
* The date+time wheel picker
* shows date and time together,
* so do not adjust the presentation
* in that case.
*/
if (!datetimeEl.preferWheel && needsChange) {
datetimeEl.presentation = 'date';
needsPresentationChange = true;
}
break;
}
/**
* Track which button was clicked
* so that it can have the correct
* activated styles applied when
* the modal/popover containing
* the datetime is opened.
*/
this.selectedButton = 'date';
this.presentOverlay(ev, needsPresentationChange, this.dateTargetEl);
};
private handleTimeClick = (ev: Event) => {
const { datetimeEl, datetimePresentation } = this;
if (!datetimeEl) {
return;
}
let needsPresentationChange = false;
/**
* When clicking the time button,
* we need to make sure that only a time
* picker is displayed. For presentation styles
* that display content other than a time picker,
* we need to update the presentation style.
*/
switch (datetimePresentation) {
case 'date-time':
case 'time-date':
const needsChange = datetimeEl.presentation !== 'time';
if (needsChange) {
datetimeEl.presentation = 'time';
needsPresentationChange = true;
}
break;
}
/**
* Track which button was clicked
* so that it can have the correct
* activated styles applied when
* the modal/popover containing
* the datetime is opened.
*/
this.selectedButton = 'time';
this.presentOverlay(ev, needsPresentationChange, this.timeTargetEl);
};
/**
* If the datetime is presented in an
* overlay, the datetime and overlay
* should be appropriately sized.
* These classes provide default sizing values
* that developers can customize.
* The goal is to provide an overlay that is
* reasonably sized with a datetime that
* fills the entire container.
*/
private presentOverlay = async (ev: Event, needsPresentationChange: boolean, triggerEl?: HTMLElement) => {
const { overlayEl } = this;
if (!overlayEl) {
return;
}
if (overlayEl.tagName === 'ION-POPOVER') {
/**
* When the presentation on datetime changes,
* we need to wait for the component to re-render
* otherwise the computed width/height of the
* popover content will be wrong, causing
* the popover to not align with the trigger element.
*/
if (needsPresentationChange) {
await this.waitForDatetimeChanges();
}
/**
* We pass the trigger button element
* so that the popover aligns with the individual
* button that was clicked, not the component container.
*/
(overlayEl as HTMLIonPopoverElement).present({
...ev,
detail: {
ionShadowTarget: triggerEl,
},
} as CustomEvent);
} else {
overlayEl.present();
}
};
render() {
const { color, dateText, timeText, selectedButton, datetimeActive, disabled } = this;
const mode = getIonMode(this);
return (
<Host
class={createColorClasses(color, {
[mode]: true,
[`${selectedButton}-active`]: datetimeActive,
['datetime-button-disabled']: disabled,
})}
>
{dateText && (
<button
class="ion-activatable"
id="date-button"
aria-expanded={datetimeActive ? 'true' : 'false'}
onClick={this.handleDateClick}
disabled={disabled}
part="native"
ref={(el) => (this.dateTargetEl = el)}
>
<slot name="date-target">{dateText}</slot>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
)}
{timeText && (
<button
class="ion-activatable"
id="time-button"
aria-expanded={datetimeActive ? 'true' : 'false'}
onClick={this.handleTimeClick}
disabled={disabled}
part="native"
ref={(el) => (this.timeTargetEl = el)}
>
<slot name="time-target">{timeText}</slot>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
)}
</Host>
);
}
}

View File

@@ -0,0 +1,186 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('datetime-button: switching to correct view', () => {
test.beforeEach(async ({ page }, testInfo) => {
test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests');
test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic');
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
});
test('should switch to a date-only view when the date button is clicked', async ({ page }) => {
const datetime = page.locator('ion-datetime');
expect(datetime).toHaveJSProperty('presentation', 'date-time');
await page.locator('#date-button').click();
expect(datetime).toHaveJSProperty('presentation', 'date');
});
test('should switch to a time-only view when the time button is clicked', async ({ page }) => {
const datetime = page.locator('ion-datetime');
expect(datetime).toHaveJSProperty('presentation', 'date-time');
await page.locator('#time-button').click();
expect(datetime).toHaveJSProperty('presentation', 'time');
});
});
test.describe('datetime-button: labels', () => {
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests');
test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic');
});
test('should set date and time labels in separate buttons', async ({ page }) => {
await page.setContent(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="date-time"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#date-button')).toContainText('Jan 1, 2022');
await expect(page.locator('#time-button')).toContainText('6:30 AM');
});
test('should set only month and year', async ({ page }) => {
await page.setContent(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="month-year"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#date-button')).toContainText('January 2022');
await expect(page.locator('#time-button')).toBeHidden();
});
test('should set only year', async ({ page }) => {
await page.setContent(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="year"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#date-button')).toContainText('2022');
await expect(page.locator('#time-button')).toBeHidden();
});
test('should set only month', async ({ page }) => {
await page.setContent(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="month"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#date-button')).toContainText('January');
await expect(page.locator('#time-button')).toBeHidden();
});
test('should set only time', async ({ page }) => {
await page.setContent(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="time"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#time-button')).toContainText('6:30 AM');
await expect(page.locator('#date-button')).toBeHidden();
});
test('should update the label when the value of the datetime changes', async ({ page }) => {
await page.setContent(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="date"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const datetime = page.locator('ion-datetime');
const dateTarget = page.locator('#date-button');
await expect(dateTarget).toContainText('Jan 1, 2022');
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = '2023-05-10'));
await page.waitForChanges();
await expect(dateTarget).toContainText('May 10, 2023');
});
});
test.describe('datetime-button: locale', () => {
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests');
test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic');
});
test('should use the same locale as datetime', async ({ page }) => {
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="es-ES" id="datetime" value="2022-01-01T06:30:00" presentation="date-time"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
/**
* The entire text reads 1 ene 2022, but some browsers will add
* a period after "ene". Just checking ene allows us to verify the
* behavior while avoiding these cross browser differences.
*/
await expect(page.locator('#date-button')).toContainText(/ene/);
await expect(page.locator('#time-button')).toContainText('6:30');
});
test('should respect hour cycle even if different from locale default', async ({ page }) => {
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime hour-cycle="h23" locale="en-US" id="datetime" value="2022-01-01T16:30:00" presentation="date-time"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#time-button')).toContainText('16:30');
});
test('should ignore the timezone when selecting a date', async ({ page }) => {
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="en-US" id="datetime" value="2022-01-02T06:30:00" presentation="date-time"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const timeTarget = page.locator('#time-button');
await expect(timeTarget).toContainText('6:30');
const firstOfMonth = page.locator('ion-datetime .calendar-day[data-month="1"][data-day="1"]');
await firstOfMonth.click();
await page.waitForChanges();
await expect(timeTarget).toContainText('6:30');
});
});
test.describe('datetime-button: wheel', () => {
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests');
test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic');
});
test('should only show a single date button when presentation="date-time" and prefer-wheel="true"', async ({
page,
}) => {
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="en-US" id="datetime" value="2022-01-01T06:30:00" presentation="date-time" prefer-wheel="true"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#date-button')).toContainText('Jan 1, 2022 6:30 AM');
await expect(page.locator('#time-button')).not.toBeVisible();
});
test('should only show a single date button when presentation="time-date" and prefer-wheel="true"', async ({
page,
}) => {
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="en-US" id="datetime" value="2022-01-01T06:30:00" presentation="time-date" prefer-wheel="true"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#date-button')).toContainText('Jan 1, 2022 6:30 AM');
await expect(page.locator('#time-button')).not.toBeVisible();
});
});

View File

@@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Basic</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(325px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="grid">
<div class="grid-item">
<h2>Date/Time</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="default-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="date-time"
id="default-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Date Only</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="date-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="date"
id="date-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Time Only</h2>
<ion-item>
<ion-label>Start Time</ion-label>
<ion-datetime-button slot="end" datetime="time-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="time"
id="time-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Time Only (24 hour)</h2>
<ion-item>
<ion-label>Start Time</ion-label>
<ion-datetime-button slot="end" datetime="time-h23-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="time"
id="time-h23-datetime"
value="2022-03-15T00:43:00"
hour-cycle="h23"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Time/Date</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="time-date-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="time-date"
id="time-date-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Month</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="month-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="month"
id="month-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Month/Year</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="month-year-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="month-year"
id="month-year-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Year</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="year-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="year"
id="year-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>preferWheel / date</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="prefer-wheel-date"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
prefer-wheel="true"
presentation="date"
id="prefer-wheel-date"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>preferWheel / date-time</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button
slot="end"
id="prefer-wheel-date-time-button"
datetime="prefer-wheel-date-time"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
prefer-wheel="true"
presentation="date-time"
id="prefer-wheel-date-time"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Custom Buttons</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Custom Buttons</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-input value="March 15, 2022 at 12:43 AM"></ion-input>
<ion-datetime-button slot="end" id="default-button" datetime="default-datetime">
<ion-icon color="primary" id="custom-date-button" slot="date-target" name="calendar"></ion-icon>
<ion-icon color="primary" id="custom-time-button" slot="time-target" name="time"></ion-icon>
</ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="date-time"
id="default-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</ion-content>
<script>
const datetime = document.querySelector('ion-datetime');
const input = document.querySelector('ion-input');
datetime.addEventListener('ionChange', (ev) => {
input.value = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeStyle: 'short' }).format(
new Date(ev.detail.value)
);
});
</script>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,30 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('datetime-button: disabled buttons', () => {
test('buttons should not be enabled when component is disabled', async ({ page }, testInfo) => {
test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests');
test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic');
await page.setContent(`
<ion-datetime-button datetime="datetime" disabled="true"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
await expect(page.locator('#date-button')).toBeDisabled();
await expect(page.locator('#time-button')).toBeDisabled();
});
test('buttons should visually be disabled', async ({ page }) => {
await page.setContent(`
<ion-datetime-button datetime="datetime" disabled="true"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time" value="2022-01-01T16:30:00"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const datetimeButton = page.locator('ion-datetime-button');
expect(await datetimeButton.screenshot()).toMatchSnapshot(
`datetime-button-disabled-${page.getSnapshotSettings()}.png`
);
});
});

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Disabled</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(325px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Disabled</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="grid">
<div class="grid-item">
<h2>Datetime Button Disabled</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button
disabled="true"
slot="end"
id="year-button"
datetime="year-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime locale="en-US" id="year-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Custom Button Disabled</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button disabled="true" slot="end" id="custom-button" datetime="custom-datetime">
<ion-icon color="primary" id="custom-date-button" slot="date-target" name="calendar"></ion-icon>
<ion-icon color="primary" id="custom-time-button" slot="time-target" name="time"></ion-icon>
</ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime locale="en-US" id="custom-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-popover>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,162 @@
import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { test } from '@utils/test/playwright';
import type { EventSpy } from '@utils/test/playwright';
test.describe('datetime-button: rendering', () => {
test('should size the modal correctly', async ({ page }) => {
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-modal>
<ion-datetime id="datetime" show-default-title="true" show-default-buttons="true" value="2022-01-01T16:30:00"></ion-datetime>
</ion-modal>
`);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const dateButton = page.locator('ion-datetime-button #date-button');
await dateButton.click();
await ionModalDidPresent.next();
expect(await page.screenshot()).toMatchSnapshot(`datetime-overlay-modal-${page.getSnapshotSettings()}.png`);
});
test('should size the popover correctly', async ({ page }) => {
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-popover>
<ion-datetime id="datetime" show-default-title="true" show-default-buttons="true" value="2022-01-01T16:30:00"></ion-datetime>
</ion-popover>
`);
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
const dateButton = page.locator('ion-datetime-button #date-button');
await dateButton.click();
await ionPopoverDidPresent.next();
expect(await page.screenshot()).toMatchSnapshot(`datetime-overlay-popover-${page.getSnapshotSettings()}.png`);
});
});
test.describe('datetime-button: popover', () => {
let datetime: Locator;
let popover: Locator;
let ionPopoverDidPresent: EventSpy;
let ionPopoverDidDismiss: EventSpy;
test.beforeEach(async ({ page }, testInfo) => {
test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests');
test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic');
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-popover>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
</ion-popover>
`);
datetime = page.locator('ion-datetime');
popover = page.locator('ion-popover');
ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss');
});
test('should open the date popover', async ({ page }) => {
await page.locator('#date-button').click();
await ionPopoverDidPresent.next();
expect(datetime).toBeVisible();
});
test('should open the time popover', async ({ page }) => {
await page.locator('#time-button').click();
await ionPopoverDidPresent.next();
expect(datetime).toBeVisible();
});
test('should open the date popover then the time popover', async ({ page }) => {
await page.locator('#date-button').click();
await ionPopoverDidPresent.next();
expect(datetime).toBeVisible();
await popover.evaluate((el: HTMLIonPopoverElement) => el.dismiss());
await ionPopoverDidDismiss.next();
await page.locator('#time-button').click();
await ionPopoverDidPresent.next();
expect(datetime).toBeVisible();
});
test('should open the time popover then the date popover', async ({ page }) => {
await page.locator('#time-button').click();
await ionPopoverDidPresent.next();
expect(datetime).toBeVisible();
await popover.evaluate((el: HTMLIonPopoverElement) => el.dismiss());
await ionPopoverDidDismiss.next();
await page.locator('#date-button').click();
await ionPopoverDidPresent.next();
expect(datetime).toBeVisible();
});
});
test.describe('datetime-button: modal', () => {
let datetime: Locator;
let modal: Locator;
let ionModalDidPresent: EventSpy;
let ionModalDidDismiss: EventSpy;
test.beforeEach(async ({ page }, testInfo) => {
test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests');
test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic');
await page.setContent(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-modal>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
</ion-modal>
`);
datetime = page.locator('ion-datetime');
modal = page.locator('ion-modal');
ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
});
test('should open the date modal', async ({ page }) => {
await page.locator('#date-button').click();
await ionModalDidPresent.next();
expect(datetime).toBeVisible();
});
test('should open the time modal', async ({ page }) => {
await page.locator('#time-button').click();
await ionModalDidPresent.next();
expect(datetime).toBeVisible();
});
test('should open the date modal then the time modal', async ({ page }) => {
await page.locator('#date-button').click();
await ionModalDidPresent.next();
expect(datetime).toBeVisible();
await modal.evaluate((el: HTMLIonModalElement) => el.dismiss());
await ionModalDidDismiss.next();
await page.locator('#time-button').click();
await ionModalDidPresent.next();
expect(datetime).toBeVisible();
});
test('should open the time modal then the date modal', async ({ page }) => {
await page.locator('#time-button').click();
await ionModalDidPresent.next();
expect(datetime).toBeVisible();
await modal.evaluate((el: HTMLIonModalElement) => el.dismiss());
await ionModalDidDismiss.next();
await page.locator('#date-button').click();
await ionModalDidPresent.next();
expect(datetime).toBeVisible();
});
});

View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Overlays</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Overlays</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="grid">
<div class="grid-item">
<h2>Popover - Default</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="popover-default-button"
datetime="popover-default-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime locale="en-US" id="popover-default-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Modal - Default</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="modal-default-button"
datetime="modal-default-datetime"
></ion-datetime-button>
</ion-item>
<ion-modal>
<ion-datetime locale="en-US" id="modal-default-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-modal>
</div>
<div class="grid-item">
<h2>Popover - Custom</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="popover-custom-button"
datetime="popover-custom-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
id="popover-custom-datetime"
value="2022-03-15T00:43:00"
show-default-title="true"
show-default-buttons="true"
>
<div slot="title">Custom select a Date</div>
</ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Modal - Custom</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="modal-custom-button"
datetime="modal-custom-datetime"
></ion-datetime-button>
</ion-item>
<ion-modal>
<ion-datetime
locale="en-US"
id="modal-custom-datetime"
value="2022-03-15T00:43:00"
show-default-title="true"
show-default-buttons="true"
>
<div slot="title">Custom select a Date</div>
</ion-datetime>
</ion-modal>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Custom Style</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Custom Style</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-datetime-button
color="danger"
slot="end"
id="default-button"
datetime="default-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false" trigger="default-button">
<ion-datetime
locale="en-US"
presentation="date-time"
id="default-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</ion-content>
</ion-app>
</body>
</html>