feat(datetime-button): support multiple date selection (#25971)

This commit is contained in:
Liam DeBeasi
2022-09-21 10:29:56 -05:00
committed by GitHub
parent f752ac6163
commit a56a4a9c05
3 changed files with 266 additions and 22 deletions

View File

@ -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;
} }
}; };

View File

@ -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');
});
});

View 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>