mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
feat(datetime-button): support multiple date selection (#25971)
This commit is contained in:
@ -2,9 +2,9 @@ import type { ComponentInterface } from '@stencil/core';
|
|||||||
import { Component, Element, Host, Prop, State, h } from '@stencil/core';
|
import { Component, Element, Host, Prop, State, h } from '@stencil/core';
|
||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import type { Color, DatetimePresentation, DatetimeParts } from '../../interface';
|
import type { Color, DatetimePresentation } from '../../interface';
|
||||||
import { componentOnReady, addEventListener } from '../../utils/helpers';
|
import { componentOnReady, addEventListener } from '../../utils/helpers';
|
||||||
import { printIonError, printIonWarning } from '../../utils/logging';
|
import { printIonError } from '../../utils/logging';
|
||||||
import { createColorClasses } from '../../utils/theme';
|
import { createColorClasses } from '../../utils/theme';
|
||||||
import { getToday } from '../datetime/utils/data';
|
import { getToday } from '../datetime/utils/data';
|
||||||
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
|
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
|
||||||
@ -153,6 +153,24 @@ export class DatetimeButton implements ComponentInterface {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts one or more string values and converts
|
||||||
|
* them to DatetimeParts. This is done so datetime-button
|
||||||
|
* can work with an array internally and not need
|
||||||
|
* to keep checking if the datetime value is `string` or `string[]`.
|
||||||
|
*/
|
||||||
|
private getParsedDateValues = (value?: string[] | string | null): string[] => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the value property on the linked
|
* Check the value property on the linked
|
||||||
* ion-datetime and then format it according
|
* ion-datetime and then format it according
|
||||||
@ -165,36 +183,38 @@ export class DatetimeButton implements ComponentInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value, locale, hourCycle, preferWheel, multiple } = datetimeEl;
|
const { value, locale, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl;
|
||||||
|
|
||||||
if (multiple) {
|
const parsedValues = this.getParsedDateValues(value);
|
||||||
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
|
* Both ion-datetime and ion-datetime-button default
|
||||||
* to today's date and time if no value is set.
|
* to today's date and time if no value is set.
|
||||||
*/
|
*/
|
||||||
const parsedDatetime = parseDate(value ?? getToday()) as DatetimeParts;
|
const parsedDatetimes = parseDate(parsedValues.length > 0 ? parsedValues : [getToday()]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If developers incorrectly use multiple="true"
|
||||||
|
* with non "date" datetimes, then just select
|
||||||
|
* the first value so the interface does
|
||||||
|
* not appear broken. Datetime will provide a
|
||||||
|
* warning in the console.
|
||||||
|
*/
|
||||||
|
const firstParsedDatetime = parsedDatetimes[0];
|
||||||
const use24Hour = is24Hour(locale, hourCycle);
|
const use24Hour = is24Hour(locale, hourCycle);
|
||||||
|
|
||||||
// TODO(FW-1865) - Remove once FW-1831 is fixed.
|
// TODO(FW-1865) - Remove once FW-1831 is fixed.
|
||||||
|
parsedDatetimes.forEach((parsedDatetime) => {
|
||||||
parsedDatetime.tzOffset = undefined;
|
parsedDatetime.tzOffset = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
this.dateText = this.timeText = undefined;
|
this.dateText = this.timeText = undefined;
|
||||||
|
|
||||||
switch (datetimePresentation) {
|
switch (datetimePresentation) {
|
||||||
case 'date-time':
|
case 'date-time':
|
||||||
case 'time-date':
|
case 'time-date':
|
||||||
const dateText = getMonthDayAndYear(locale, parsedDatetime);
|
const dateText = getMonthDayAndYear(locale, firstParsedDatetime);
|
||||||
const timeText = getLocalizedTime(locale, parsedDatetime, use24Hour);
|
const timeText = getLocalizedTime(locale, firstParsedDatetime, use24Hour);
|
||||||
if (preferWheel) {
|
if (preferWheel) {
|
||||||
this.dateText = `${dateText} ${timeText}`;
|
this.dateText = `${dateText} ${timeText}`;
|
||||||
} else {
|
} else {
|
||||||
@ -203,19 +223,31 @@ Please upvote https://github.com/ionic-team/ionic-framework/issues/25668 if you
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'date':
|
case 'date':
|
||||||
this.dateText = getMonthDayAndYear(locale, parsedDatetime);
|
if (multiple && parsedValues.length !== 1) {
|
||||||
|
let headerText = `${parsedValues.length} days`; // default/fallback for multiple selection
|
||||||
|
if (titleSelectedDatesFormatter !== undefined) {
|
||||||
|
try {
|
||||||
|
headerText = titleSelectedDatesFormatter(parsedValues);
|
||||||
|
} catch (e) {
|
||||||
|
printIonError('Exception in provided `titleSelectedDatesFormatter`: ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.dateText = headerText;
|
||||||
|
} else {
|
||||||
|
this.dateText = getMonthDayAndYear(locale, firstParsedDatetime);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'time':
|
case 'time':
|
||||||
this.timeText = getLocalizedTime(locale, parsedDatetime, use24Hour);
|
this.timeText = getLocalizedTime(locale, firstParsedDatetime, use24Hour);
|
||||||
break;
|
break;
|
||||||
case 'month-year':
|
case 'month-year':
|
||||||
this.dateText = getMonthAndYear(locale, parsedDatetime);
|
this.dateText = getMonthAndYear(locale, firstParsedDatetime);
|
||||||
break;
|
break;
|
||||||
case 'month':
|
case 'month':
|
||||||
this.dateText = getLocalizedDateTime(locale, parsedDatetime, { month: 'long' });
|
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { month: 'long' });
|
||||||
break;
|
break;
|
||||||
case 'year':
|
case 'year':
|
||||||
this.dateText = getLocalizedDateTime(locale, parsedDatetime, { year: 'numeric' });
|
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { year: 'numeric' });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
test.describe('datetime-button: multiple selection', () => {
|
||||||
|
test.beforeEach(async ({ skip }) => {
|
||||||
|
skip.rtl();
|
||||||
|
skip.mode('ios', 'No mode-specific logic');
|
||||||
|
});
|
||||||
|
test('should render number of dates when more than 1 date is selected', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-datetime-button datetime="datetime"></ion-datetime-button>
|
||||||
|
<ion-datetime locale="en-US" id="datetime" presentation="date" multiple="true"></ion-datetime>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const datetime = document.querySelector('ion-datetime');
|
||||||
|
datetime.value = ['2022-06-01', '2022-06-02', '2022-06-03'];
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
await expect(page.locator('#date-button')).toContainText('3 days');
|
||||||
|
});
|
||||||
|
test('should render number of dates when 0 dates are selected', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-datetime-button datetime="datetime"></ion-datetime-button>
|
||||||
|
<ion-datetime locale="en-US" id="datetime" presentation="date" multiple="true"></ion-datetime>
|
||||||
|
`);
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
await expect(page.locator('#date-button')).toHaveText('0 days');
|
||||||
|
});
|
||||||
|
test('should render date when only 1 day is selected', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-datetime-button datetime="datetime"></ion-datetime-button>
|
||||||
|
<ion-datetime locale="en-US" id="datetime" presentation="date" multiple="true"></ion-datetime>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const datetime = document.querySelector('ion-datetime');
|
||||||
|
datetime.value = ['2022-06-01'];
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
await expect(page.locator('#date-button')).toHaveText('Jun 1, 2022');
|
||||||
|
});
|
||||||
|
test('should use customFormatter', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-datetime-button datetime="datetime"></ion-datetime-button>
|
||||||
|
<ion-datetime locale="en-US" id="datetime" presentation="date" multiple="true"></ion-datetime>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const datetime = document.querySelector('ion-datetime');
|
||||||
|
datetime.titleSelectedDatesFormatter = (selectedDates) => {
|
||||||
|
return 'Selected: ' + selectedDates.length;
|
||||||
|
};
|
||||||
|
datetime.value = ['2022-06-01', '2022-06-02', '2022-06-03'];
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
await expect(page.locator('#date-button')).toHaveText('Selected: 3');
|
||||||
|
});
|
||||||
|
test('should re-render when value is programmatically changed', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-datetime-button datetime="datetime"></ion-datetime-button>
|
||||||
|
<ion-datetime locale="en-US" id="datetime" presentation="date" multiple="true"></ion-datetime>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const datetime = document.querySelector('ion-datetime');
|
||||||
|
datetime.value = ['2022-06-01', '2022-06-02'];
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
const datetime = page.locator('ion-datetime');
|
||||||
|
const ionChange = await page.spyOnEvent('ionChange');
|
||||||
|
const dateButton = page.locator('#date-button');
|
||||||
|
await expect(dateButton).toHaveText('2 days');
|
||||||
|
|
||||||
|
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = ['2022-06-01', '2022-06-02', '2022-06-03']));
|
||||||
|
await ionChange.next();
|
||||||
|
|
||||||
|
await expect(dateButton).toHaveText('3 days');
|
||||||
|
});
|
||||||
|
test('should render single date if datetime is used incorrectly', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-datetime-button datetime="datetime"></ion-datetime-button>
|
||||||
|
<ion-datetime locale="en-US" id="datetime" presentation="date-time" multiple="true"></ion-datetime>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const datetime = document.querySelector('ion-datetime');
|
||||||
|
datetime.value = ['2022-06-01T16:30', '2022-06-02'];
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
await expect(page.locator('#date-button')).toHaveText('Jun 1, 2022');
|
||||||
|
await expect(page.locator('#time-button')).toHaveText('4:30 PM');
|
||||||
|
});
|
||||||
|
});
|
112
core/src/components/datetime-button/test/multiple/index.html
Normal file
112
core/src/components/datetime-button/test/multiple/index.html
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Datetime Button - Multiple</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 - Multiple</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>One Date</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"
|
||||||
|
id="default-datetime"
|
||||||
|
multiple="true"
|
||||||
|
value="2022-03-15T00:43:00"
|
||||||
|
></ion-datetime>
|
||||||
|
</ion-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>No Dates</h2>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Start Date</ion-label>
|
||||||
|
<ion-datetime-button slot="end" datetime="no-dates-datetime"></ion-datetime-button>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-popover arrow="false">
|
||||||
|
<ion-datetime locale="en-US" presentation="date" id="no-dates-datetime" multiple="true"></ion-datetime>
|
||||||
|
</ion-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>Multiple Dates</h2>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Start Date</ion-label>
|
||||||
|
<ion-datetime-button slot="end" datetime="multiple-dates-datetime"></ion-datetime-button>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-popover arrow="false">
|
||||||
|
<ion-datetime
|
||||||
|
locale="en-US"
|
||||||
|
presentation="date"
|
||||||
|
id="multiple-dates-datetime"
|
||||||
|
multiple="true"
|
||||||
|
></ion-datetime>
|
||||||
|
</ion-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>Custom Formatter</h2>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Start Date</ion-label>
|
||||||
|
<ion-datetime-button slot="end" datetime="custom-datetime"></ion-datetime-button>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-popover arrow="false">
|
||||||
|
<ion-datetime locale="en-US" presentation="date" id="custom-datetime" multiple="true"></ion-datetime>
|
||||||
|
</ion-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const multipleDatesDatetime = document.querySelector('ion-datetime#multiple-dates-datetime');
|
||||||
|
const customDatetime = document.querySelector('ion-datetime#custom-datetime');
|
||||||
|
|
||||||
|
multipleDatesDatetime.value = ['2022-06-01', '2022-06-02', '2022-06-03'];
|
||||||
|
customDatetime.titleSelectedDatesFormatter = (selected) => `${selected.length} Selected`;
|
||||||
|
</script>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user