mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 16:16:41 +08:00
fix(datetime): allow calendar navigation in readonly mode; disallow keyboard navigation when disabled (#28336)
Issue number: #28121 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> It is not possible to navigate between months when ion-datetime is in readonly mode. This means that if there are multiple dates selected, the user cannot browse to view them all. Also, keyboard navigation is not prevented in `readonly` or `disabled` mode where it should be. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> When `readonly`: - Clicking the month-year button changes the month & year in readonly mode - Clicking the next & prev buttons changes the month in readonly mode - Left and right arrow keys change the month in readonly mode - Swiping/scrolling changes the month in readonly mode - The selected date does not change when doing any of the above - You cannot clear the value using keyboard navigation of the clear button in readonly mode When `disabled`: - You cannot navigate months via keyboard navigation of the month-year button in disabled mode - You cannot navigate months using keyboard navigation of the previous & next buttons in disabled mode - You cannot navigate months via the left and right arrow keys in disabled mode - The selected date does not change when doing any of the above - You cannot clear the value using keyboard navigation of the clear button in disabled mode Known bug: - It is still possible to navigate through dates in `prefers-wheel` when `disabled`. This bug existed prior to this PR. I created FW-5408 to track this. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> --------- Co-authored-by: ionitron <hi@ionicframework.com> Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com>
This commit is contained in:
4
core/src/components.d.ts
vendored
4
core/src/components.d.ts
vendored
@ -915,7 +915,7 @@ export namespace Components {
|
||||
*/
|
||||
"presentation": DatetimePresentation;
|
||||
/**
|
||||
* If `true`, the datetime appears normal but is not interactive.
|
||||
* If `true`, the datetime appears normal but the selected date cannot be changed.
|
||||
*/
|
||||
"readonly": boolean;
|
||||
/**
|
||||
@ -5595,7 +5595,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"presentation"?: DatetimePresentation;
|
||||
/**
|
||||
* If `true`, the datetime appears normal but is not interactive.
|
||||
* If `true`, the datetime appears normal but the selected date cannot be changed.
|
||||
*/
|
||||
"readonly"?: boolean;
|
||||
/**
|
||||
|
||||
@ -185,13 +185,37 @@ ion-picker-column-internal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host(.datetime-readonly),
|
||||
:host(.datetime-disabled) {
|
||||
pointer-events: none;
|
||||
|
||||
.calendar-days-of-week,
|
||||
.datetime-time {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
:host(.datetime-disabled) {
|
||||
opacity: 0.4;
|
||||
:host(.datetime-readonly) {
|
||||
pointer-events: none;
|
||||
|
||||
/**
|
||||
* Allow user to navigate months
|
||||
* while in readonly mode
|
||||
*/
|
||||
.calendar-action-buttons,
|
||||
.calendar-body,
|
||||
.datetime-year {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled buttons should have full opacity
|
||||
* in readonly mode
|
||||
*/
|
||||
|
||||
.calendar-day[disabled]:not(.calendar-day-constrained),
|
||||
.datetime-action-buttons ion-button[disabled] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -172,7 +172,7 @@ export class Datetime implements ComponentInterface {
|
||||
@Prop() disabled = false;
|
||||
|
||||
/**
|
||||
* If `true`, the datetime appears normal but is not interactive.
|
||||
* If `true`, the datetime appears normal but the selected date cannot be changed.
|
||||
*/
|
||||
@Prop() readonly = false;
|
||||
|
||||
@ -599,6 +599,14 @@ export class Datetime implements ComponentInterface {
|
||||
};
|
||||
|
||||
private setActiveParts = (parts: DatetimeParts, removeDate = false) => {
|
||||
/** if the datetime component is in readonly mode,
|
||||
* allow browsing of the calendar without changing
|
||||
* the set value
|
||||
*/
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { multiple, minParts, maxParts, activeParts } = this;
|
||||
|
||||
/**
|
||||
@ -1414,7 +1422,13 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
|
||||
private renderFooter() {
|
||||
const { showDefaultButtons, showClearButton } = this;
|
||||
const { disabled, readonly, showDefaultButtons, showClearButton } = this;
|
||||
/**
|
||||
* The cancel, clear, and confirm buttons
|
||||
* should not be interactive if the datetime
|
||||
* is disabled or readonly.
|
||||
*/
|
||||
const isButtonDisabled = disabled || readonly;
|
||||
const hasSlottedButtons = this.el.querySelector('[slot="buttons"]') !== null;
|
||||
if (!hasSlottedButtons && !showDefaultButtons && !showClearButton) {
|
||||
return;
|
||||
@ -1444,18 +1458,33 @@ export class Datetime implements ComponentInterface {
|
||||
<slot name="buttons">
|
||||
<ion-buttons>
|
||||
{showDefaultButtons && (
|
||||
<ion-button id="cancel-button" color={this.color} onClick={() => this.cancel(true)}>
|
||||
<ion-button
|
||||
id="cancel-button"
|
||||
color={this.color}
|
||||
onClick={() => this.cancel(true)}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
{this.cancelText}
|
||||
</ion-button>
|
||||
)}
|
||||
<div class="datetime-action-buttons-container">
|
||||
{showClearButton && (
|
||||
<ion-button id="clear-button" color={this.color} onClick={() => clearButtonClick()}>
|
||||
<ion-button
|
||||
id="clear-button"
|
||||
color={this.color}
|
||||
onClick={() => clearButtonClick()}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
{this.clearText}
|
||||
</ion-button>
|
||||
)}
|
||||
{showDefaultButtons && (
|
||||
<ion-button id="confirm-button" color={this.color} onClick={() => this.confirm(true)}>
|
||||
<ion-button
|
||||
id="confirm-button"
|
||||
color={this.color}
|
||||
onClick={() => this.confirm(true)}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
{this.doneText}
|
||||
</ion-button>
|
||||
)}
|
||||
@ -1957,11 +1986,12 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
|
||||
private renderCalendarHeader(mode: Mode) {
|
||||
const { disabled } = this;
|
||||
const expandedIcon = mode === 'ios' ? chevronDown : caretUpSharp;
|
||||
const collapsedIcon = mode === 'ios' ? chevronForward : caretDownSharp;
|
||||
|
||||
const prevMonthDisabled = isPrevMonthDisabled(this.workingParts, this.minParts, this.maxParts);
|
||||
const nextMonthDisabled = isNextMonthDisabled(this.workingParts, this.maxParts);
|
||||
const prevMonthDisabled = disabled || isPrevMonthDisabled(this.workingParts, this.minParts, this.maxParts);
|
||||
const nextMonthDisabled = disabled || isNextMonthDisabled(this.workingParts, this.maxParts);
|
||||
|
||||
// don't use the inheritAttributes util because it removes dir from the host, and we still need that
|
||||
const hostDir = this.el.getAttribute('dir') || undefined;
|
||||
@ -1977,6 +2007,7 @@ export class Datetime implements ComponentInterface {
|
||||
aria-label="Show year picker"
|
||||
detail={false}
|
||||
lines="none"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
this.toggleMonthAndYearView();
|
||||
/**
|
||||
@ -2043,10 +2074,15 @@ export class Datetime implements ComponentInterface {
|
||||
);
|
||||
}
|
||||
private renderMonth(month: number, year: number) {
|
||||
const { disabled, readonly } = this;
|
||||
|
||||
const yearAllowed = this.parsedYearValues === undefined || this.parsedYearValues.includes(year);
|
||||
const monthAllowed = this.parsedMonthValues === undefined || this.parsedMonthValues.includes(month);
|
||||
const isCalMonthDisabled = !yearAllowed || !monthAllowed;
|
||||
const swipeDisabled = isMonthDisabled(
|
||||
const isDatetimeDisabled = disabled || readonly;
|
||||
const swipeDisabled =
|
||||
disabled ||
|
||||
isMonthDisabled(
|
||||
{
|
||||
month,
|
||||
year,
|
||||
@ -2083,7 +2119,14 @@ export class Datetime implements ComponentInterface {
|
||||
const { el, highlightedDates, isDateEnabled, multiple } = this;
|
||||
const referenceParts = { month, day, year };
|
||||
const isCalendarPadding = day === null;
|
||||
const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(
|
||||
const {
|
||||
isActive,
|
||||
isToday,
|
||||
ariaLabel,
|
||||
ariaSelected,
|
||||
disabled: isDayDisabled,
|
||||
text,
|
||||
} = getCalendarDayState(
|
||||
this.locale,
|
||||
referenceParts,
|
||||
this.activeParts,
|
||||
@ -2094,7 +2137,8 @@ export class Datetime implements ComponentInterface {
|
||||
);
|
||||
|
||||
const dateIsoString = convertDataToISO(referenceParts);
|
||||
let isCalDayDisabled = isCalMonthDisabled || disabled;
|
||||
|
||||
let isCalDayDisabled = isCalMonthDisabled || isDayDisabled;
|
||||
|
||||
if (!isCalDayDisabled && isDateEnabled !== undefined) {
|
||||
try {
|
||||
@ -2113,6 +2157,15 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some days are constrained through max & min or allowed dates
|
||||
* and also disabled because the component is readonly or disabled.
|
||||
* These need to be displayed differently.
|
||||
*/
|
||||
const isCalDayConstrained = isCalDayDisabled && isDatetimeDisabled;
|
||||
|
||||
const isButtonDisabled = isCalDayDisabled || isDatetimeDisabled;
|
||||
|
||||
let dateStyle: DatetimeHighlightStyle | undefined = undefined;
|
||||
|
||||
/**
|
||||
@ -2158,11 +2211,12 @@ export class Datetime implements ComponentInterface {
|
||||
data-year={year}
|
||||
data-index={index}
|
||||
data-day-of-week={dayOfWeek}
|
||||
disabled={isCalDayDisabled}
|
||||
disabled={isButtonDisabled}
|
||||
class={{
|
||||
'calendar-day-padding': isCalendarPadding,
|
||||
'calendar-day': true,
|
||||
'calendar-day-active': isActive,
|
||||
'calendar-day-constrained': isCalDayConstrained,
|
||||
'calendar-day-today': isToday,
|
||||
}}
|
||||
part={dateParts}
|
||||
@ -2237,7 +2291,7 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
private renderTimeOverlay() {
|
||||
const { hourCycle, isTimePopoverOpen, locale } = this;
|
||||
const { disabled, hourCycle, isTimePopoverOpen, locale } = this;
|
||||
const computedHourCycle = getHourCycle(locale, hourCycle);
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
@ -2251,6 +2305,7 @@ export class Datetime implements ComponentInterface {
|
||||
part={`time-button${isTimePopoverOpen ? ' active' : ''}`}
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
disabled={disabled}
|
||||
onClick={async (ev) => {
|
||||
const { popoverRef } = this;
|
||||
|
||||
|
||||
@ -30,3 +30,102 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not differ across
|
||||
* modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('datetime: a11y'), () => {
|
||||
test('datetime should be keyboard navigable', async ({ page, browserName }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-22T16:30:00"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
||||
const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
|
||||
const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(monthYearButton).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(prevButton).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(nextButton).toBeFocused();
|
||||
|
||||
// check value before & after selecting via keyboard
|
||||
const initialValue = await datetime.evaluate((el: HTMLIonDatetimeElement) => el.value);
|
||||
expect(initialValue).toBe('2022-02-22T16:30:00');
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const newValue = await datetime.evaluate((el: HTMLIonDatetimeElement) => el.value);
|
||||
expect(newValue).not.toBe('2022-02-22T16:30:00');
|
||||
});
|
||||
|
||||
test('buttons should be keyboard navigable', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
|
||||
<ion-datetime value="2022-02-22T16:30:00" show-default-buttons="true" show-clear-button="true"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const clearButton = page.locator('#clear-button button');
|
||||
const selectedDay = page.locator('.calendar-day-active');
|
||||
|
||||
await expect(selectedDay).toHaveText('22');
|
||||
|
||||
await clearButton.focus();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(clearButton).toBeFocused();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(selectedDay).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should navigate through months via right arrow key', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
|
||||
<ion-datetime value="2022-02-28"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
const calendarBody = page.locator('.calendar-body');
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
|
||||
await calendarBody.focus();
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(calendarMonthYear).toHaveText('March 2022');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
103
core/src/components/datetime/test/disabled/datetime.e2e.ts
Normal file
103
core/src/components/datetime/test/disabled/datetime.e2e.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not differ across
|
||||
* modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('datetime: disabled'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-05T00:00:00" min="2022-01-01T00:00:00" max="2022-02-20T23:59:59" day-values="5,6,10,11,15,16,20" show-default-buttons disabled></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-disabled`));
|
||||
});
|
||||
|
||||
test('date should be disabled', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-28" disabled></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const febFirstButton = page.locator(`.calendar-day[data-day='1'][data-month='2']`);
|
||||
|
||||
await expect(febFirstButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('month-year button should be disabled', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-28" disabled></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
await expect(calendarMonthYear.locator('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('next and prev buttons should be disabled', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-28" disabled></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const prevMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button:first-of-type button');
|
||||
const nextMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button:last-of-type button');
|
||||
|
||||
await expect(prevMonthButton).toBeDisabled();
|
||||
await expect(nextMonthButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('clear button should be disabled', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
|
||||
<ion-datetime value="2022-02-22T16:30:00" show-default-buttons="true" show-clear-button="true" disabled></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const clearButton = page.locator('#clear-button button');
|
||||
|
||||
await expect(clearButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should not navigate through months via right arrow key', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-28" disabled></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
const calendarBody = page.locator('.calendar-body');
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
|
||||
await calendarBody.focus();
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
77
core/src/components/datetime/test/disabled/index.html
Normal file
77
core/src/components/datetime/test/disabled/index.html
Normal file
@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Datetime - 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(3, 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;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-datetime {
|
||||
width: 350px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header translucent="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>Datetime - Disabled</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Inline - Default Value</h2>
|
||||
<ion-datetime id="inline-datetime-value" disabled value="2022-04-21T00:00:00"></ion-datetime>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Inline</h2>
|
||||
<ion-datetime
|
||||
id="inline-datetime"
|
||||
presentation="date"
|
||||
disabled
|
||||
show-default-buttons="true"
|
||||
show-clear-button="true"
|
||||
multiple="true"
|
||||
></ion-datetime>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Inline - No Default Value</h2>
|
||||
<ion-datetime id="inline-datetime-no-value" disabled></ion-datetime>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
<script>
|
||||
const firstDatetime = document.querySelector('#inline-datetime');
|
||||
firstDatetime.value = ['2023-08-03', '2023-08-13', '2023-08-29'];
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
167
core/src/components/datetime/test/readonly/datetime.e2e.ts
Normal file
167
core/src/components/datetime/test/readonly/datetime.e2e.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not differ across
|
||||
* modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('datetime: readonly'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-05T00:00:00" min="2022-01-01T00:00:00" max="2022-02-20T23:59:59" day-values="5,6,10,11,15,16,20" show-default-buttons readonly></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-readonly`));
|
||||
});
|
||||
|
||||
test('date should be disabled', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-28" readonly></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const febFirstButton = page.locator(`.calendar-day[data-day='1'][data-month='2']`);
|
||||
|
||||
await expect(febFirstButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should navigate months via month-year button', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
|
||||
await calendarMonthYear.click();
|
||||
await page.waitForChanges();
|
||||
await page.locator('.month-column .picker-item[data-value="3"]').click();
|
||||
await page.waitForChanges();
|
||||
await expect(calendarMonthYear).toHaveText('March 2022');
|
||||
|
||||
await expect(ionChange).not.toHaveReceivedEvent();
|
||||
});
|
||||
|
||||
test('should open picker using keyboard navigation', async ({ page, browserName }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(monthYearButton).toBeFocused();
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForChanges();
|
||||
|
||||
const marchPickerItem = page.locator('.month-column .picker-item[data-value="3"]');
|
||||
await expect(marchPickerItem).toBeVisible();
|
||||
});
|
||||
|
||||
test('should view next month via next button', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
|
||||
const nextMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button + ion-button');
|
||||
await nextMonthButton.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(calendarMonthYear).toHaveText('March 2022');
|
||||
await expect(ionChange).not.toHaveReceivedEvent();
|
||||
});
|
||||
|
||||
test('should not change value when the month is changed via keyboard navigation', async ({ page, browserName }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
||||
const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
|
||||
const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(monthYearButton).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(prevButton).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(nextButton).toBeFocused();
|
||||
|
||||
// check value before & after selecting via keyboard
|
||||
const initialValue = await datetime.evaluate((el: HTMLIonDatetimeElement) => el.value);
|
||||
expect(initialValue).toBe('2022-02-22T16:30:00');
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(calendarMonthYear).toHaveText('January 2022');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForChanges();
|
||||
|
||||
const newValue = await datetime.evaluate((el: HTMLIonDatetimeElement) => el.value);
|
||||
// should not have changed
|
||||
expect(newValue).toBe('2022-02-22T16:30:00');
|
||||
});
|
||||
|
||||
test('clear button should be disabled', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
|
||||
<ion-datetime value="2022-02-22T16:30:00" show-default-buttons="true" show-clear-button="true" readonly></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const clearButton = page.locator('#clear-button button');
|
||||
|
||||
await expect(clearButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
83
core/src/components/datetime/test/readonly/index.html
Normal file
83
core/src/components/datetime/test/readonly/index.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Datetime - Readonly</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(3, 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;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-datetime {
|
||||
width: 350px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header translucent="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>Datetime - Readonly</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Inline</h2>
|
||||
<ion-datetime
|
||||
id="inline-datetime"
|
||||
presentation="date"
|
||||
readonly
|
||||
show-default-buttons="true"
|
||||
show-clear-button="true"
|
||||
multiple="true"
|
||||
></ion-datetime>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Inline - No Default Value</h2>
|
||||
<ion-datetime id="inline-datetime-no-value" readonly></ion-datetime>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
<script>
|
||||
const firstDatetime = document.querySelector('#inline-datetime');
|
||||
firstDatetime.value = ['2023-08-03', '2023-08-13', '2023-08-29'];
|
||||
|
||||
firstDatetime.isDateEnabled = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const utcDay = date.getUTCDay();
|
||||
|
||||
/**
|
||||
* Date will be enabled if it is not
|
||||
* Sunday or Saturday
|
||||
*/
|
||||
return utcDay !== 0 && utcDay !== 6;
|
||||
};
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user