chore: sync

This commit is contained in:
Liam DeBeasi
2023-11-15 12:06:22 -05:00
226 changed files with 28981 additions and 7846 deletions

View File

@ -180,6 +180,16 @@ export class AccordionGroup implements ComponentInterface {
if (this.readonly) {
this.readonlyChanged();
}
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.valueChanged();
}
/**

View File

@ -128,8 +128,8 @@
height: 100%;
/* Fallback for browsers that do not support dvh */
max-height: 100vh;
max-height: 100dvh;
max-height: calc(100vh - (var(--ion-safe-area-top, 0) + var(--ion-safe-area-bottom, 0)));
max-height: calc(100dvh - (var(--ion-safe-area-top, 0) + var(--ion-safe-area-bottom, 0)));
}
.action-sheet-group {

View File

@ -100,3 +100,69 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) =>
});
});
});
/**
* This behavior needs to be tested in both modes but does not vary
* across directions due to the component only applying safe area
* to the top and bottom
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('action-sheet: basic'), () => {
test.describe('safe area', () => {
test('should have padding added by the safe area', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/27777',
});
await page.setContent(
`
<style>
:root {
--ion-safe-area-top: 60px;
--ion-safe-area-bottom: 40px;
}
</style>
<ion-action-sheet></ion-action-sheet>
<script>
const actionSheet = document.querySelector('ion-action-sheet');
actionSheet.header = 'Header';
actionSheet.subHeader = 'Sub Header';
actionSheet.buttons = [
'Add Reaction',
'Copy Text',
'Share Text',
'Copy Link to Message',
'Remind Me',
'Pin File',
'Star File',
'Mark Unread',
'Mark Read',
'Edit Title',
'Erase Title',
'Save Image',
'Copy Image',
'Erase Image',
'Delete File',
{
text: 'Cancel',
role: 'cancel'
},
];
</script>
`,
config
);
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
const actionSheet = page.locator('ion-action-sheet');
await actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.present());
await ionActionSheetDidPresent.next();
await expect(actionSheet).toHaveScreenshot(screenshot(`action-sheet-safe-area`));
});
});
});
});

View File

@ -136,7 +136,3 @@
justify-content: flex-end;
}
:host .datetime-view-buttons ion-button {
color: $text-color-step-200;
}

View File

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

View File

@ -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,23 +2074,28 @@ 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(
{
month,
year,
day: null,
},
{
// The day is not used when checking if a month is disabled.
// Users should be able to access the min or max month, even if the
// min/max date is out of bounds (e.g. min is set to Feb 15, Feb should not be disabled).
minParts: { ...this.minParts, day: null },
maxParts: { ...this.maxParts, day: null },
}
);
const isDatetimeDisabled = disabled || readonly;
const swipeDisabled =
disabled ||
isMonthDisabled(
{
month,
year,
day: null,
},
{
// The day is not used when checking if a month is disabled.
// Users should be able to access the min or max month, even if the
// min/max date is out of bounds (e.g. min is set to Feb 15, Feb should not be disabled).
minParts: { ...this.minParts, day: null },
maxParts: { ...this.maxParts, day: null },
}
);
// The working month should never have swipe disabled.
// Otherwise the CSS scroll snap will not work and the user
// can free-scroll the calendar.
@ -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;

View File

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

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

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

View 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();
});
});
});

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

View File

@ -130,7 +130,10 @@
* 2. This will only apply when that content has a collapse header (ion-header[collapse="condense"])
*
* We use opacity: 0 to avoid a layout shift.
* We target both the attribute and the class in the event that the attribute
* is not reflected on the host in some frameworks.
*/
ion-header:not(.header-collapse-main):has(~ ion-content ion-header[collapse="condense"]) {
ion-header:not(.header-collapse-main):has(~ ion-content ion-header[collapse="condense"],
~ ion-content ion-header.header-collapse-condense) {
opacity: 0;
}

View File

@ -31,12 +31,19 @@
@include font-smoothing();
@include margin(0);
@include padding(
var(--padding-top),
var(--padding-end),
var(--padding-bottom),
calc(var(--padding-start) + var(--ion-safe-area-left, 0px))
);
@include padding(var(--padding-top), null, var(--padding-bottom), null);
/* stylelint-disable */
@include ltr() {
padding-right: var(--padding-end);
padding-left: calc(var(--padding-start) + var(--ion-safe-area-left, 0px));
}
@include rtl() {
padding-right: calc(var(--padding-start) + var(--ion-safe-area-right, 0px));
padding-left: var(--padding-end);
}
/* stylelint-enable */
display: flex;
@ -67,12 +74,19 @@
.item-divider-inner {
@include margin(0);
@include padding(
var(--inner-padding-top),
calc(var(--ion-safe-area-right, 0px) + var(--inner-padding-end)),
var(--inner-padding-bottom),
var(--inner-padding-start)
);
@include padding(var(--inner-padding-top), null, var(--inner-padding-bottom), null);
/* stylelint-disable */
@include ltr() {
padding-right: calc(var(--ion-safe-area-right, 0px) + var(--inner-padding-end));
padding-left: var(--inner-padding-start);
}
@include rtl() {
padding-right: var(--inner-padding-start);
padding-left: calc(var(--ion-safe-area-left, 0px) + var(--inner-padding-end));
}
/* stylelint-enable */
display: flex;

View File

@ -46,5 +46,33 @@ configs().forEach(({ title, screenshot, config }) => {
const divider = page.locator('ion-item-divider');
await expect(divider).toHaveScreenshot(screenshot(`item-divider-icon-start`));
});
/**
* This behavior needs to be tested for all modes & directions
* Safe padding should stay on the same side when the direction changes
*/
test('should have safe area padding', async ({ page }) => {
await page.setContent(
`
<style>
:root {
--ion-safe-area-left: 40px;
--ion-safe-area-right: 20px;
}
</style>
<ion-list>
<ion-item-divider>
<ion-label>Item Divider</ion-label>
<ion-button slot="end">Button</ion-button>
</ion-item-divider>
</ion-list>
`,
config
);
const list = page.locator('ion-list');
await expect(list).toHaveScreenshot(screenshot('item-divider-safe-area'));
});
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -203,14 +203,21 @@
.item-native {
@include border-radius(var(--border-radius));
@include margin(0);
@include padding(
var(--padding-top),
var(--padding-end),
var(--padding-bottom),
calc(var(--padding-start) + var(--ion-safe-area-left, 0px))
);
@include padding(var(--padding-top), null, var(--padding-bottom));
@include text-inherit();
/* stylelint-disable */
@include ltr() {
padding-right: var(--padding-end);
padding-left: calc(var(--padding-start) + var(--ion-safe-area-left, 0px));
}
@include rtl() {
padding-right: calc(var(--padding-start) + var(--ion-safe-area-right, 0px));
padding-left: var(--padding-end);
}
/* stylelint-enable */
display: flex;
position: relative;
@ -261,12 +268,19 @@ button, a {
.item-inner {
@include margin(0);
@include padding(
var(--inner-padding-top),
calc(var(--ion-safe-area-right, 0px) + var(--inner-padding-end)),
var(--inner-padding-bottom),
var(--inner-padding-start)
);
@include padding(var(--inner-padding-top), null, var(--inner-padding-bottom));
/* stylelint-disable */
@include ltr() {
padding-right: calc(var(--ion-safe-area-right, 0px) + var(--inner-padding-end));
padding-left: var(--inner-padding-start);
}
@include rtl() {
padding-right: var(--inner-padding-start);
padding-left: calc(var(--ion-safe-area-left, 0px) + var(--inner-padding-end));
}
/* stylelint-enable */
display: flex;
@ -295,12 +309,19 @@ button, a {
.item-bottom {
@include margin(0);
@include padding(
0,
var(--inner-padding-end),
0,
calc(var(--padding-start) + var(--ion-safe-area-left, 0px))
);
@include padding(0, null);
/* stylelint-disable */
@include ltr() {
padding-left: calc(var(--padding-start) + var(--ion-safe-area-left, 0px));
padding-right: calc(var(--inner-padding-end) + var(--ion-safe-area-right, 0px));
}
@include rtl() {
padding-left: calc(var(--inner-padding-end) + var(--ion-safe-area-left, 0px));
padding-right: calc(var(--padding-start) + var(--ion-safe-area-right, 0px));
}
/* stylelint-enable */
display: flex;

View File

@ -10,5 +10,37 @@ configs().forEach(({ title, screenshot, config }) => {
await expect(page).toHaveScreenshot(screenshot(`item-diff`));
});
/**
* This behavior needs to be tested for all modes & directions
* Safe padding should stay on the same side when the direction changes
*/
test('should have safe area padding', async ({ page }) => {
await page.setContent(
`
<style>
:root {
--ion-safe-area-left: 40px;
--ion-safe-area-right: 20px;
}
</style>
<ion-list>
<ion-item>
<ion-label>Item with helper</ion-label>
<div slot="helper">Helper</div>
</ion-item>
<ion-item>
<ion-label> Single line text that should have ellipses when it doesn't all fit in the item</ion-label>
</ion-item>
</ion-list>
`,
config
);
const list = page.locator('ion-list');
await expect(list).toHaveScreenshot(screenshot('item-safe-area'));
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -19,12 +19,25 @@
@include border-radius($list-inset-ios-border-radius);
}
.list-ios.list-inset ion-item:last-child {
/**
* These selectors ensure the last item in the list
* has the correct border.
* We need to consider the following scenarios:
1. The only item in a list.
2. The last item in a list as long as it is not the only item.
3. The item in the last item-sliding in a list.
* Note that we do not select ion-item:last-of-type
* because that will cause the borders to disappear on
* items in an item-sliding when the item is the last
* element in the item-sliding container.
*/
.list-ios.list-inset ion-item:only-child,
.list-ios.list-inset ion-item:not(:only-of-type):last-of-type,
.list-ios.list-inset ion-item-sliding:last-of-type ion-item {
--border-width: 0;
--inner-border-width: 0;
}
.list-ios.list-inset + ion-list.list-inset {
@include margin(0, null, null, null);
}

View File

@ -23,12 +23,50 @@
@include border-radius($list-inset-md-border-radius);
}
.list-md.list-inset ion-item:first-child {
/**
* These selectors ensure the first item in the list
* has the correct radius.
* We need to consider the following scenarios:
1. The first item in a list as long as it is not the only item.
2. The item in the first item-sliding in a list.
* Note that we do not select "ion-item-sliding ion-item:first-of-type"
* because that will cause the borders to disappear on
* items in an item-sliding when the item is the first
* element in the item-sliding container.
*/
.list-md.list-inset ion-item:not(:only-of-type):first-of-type,
.list-md.list-inset ion-item-sliding:first-of-type ion-item {
--border-radius: #{$list-inset-md-border-radius $list-inset-md-border-radius 0 0};
}
.list-md.list-inset ion-item:last-child {
--border-radius: #{0 0 $list-inset-md-border-radius, $list-inset-md-border-radius};
/**
* These selectors ensure the last item in the list
* has the correct radius and border.
* We need to consider the following scenarios:
1. The last item in a list as long as it is not the only item.
2. The item in the last item-sliding in a list.
* Note that we do not select "ion-item-sliding ion-item:last-of-type"
* because that will cause the borders to disappear on
* items in an item-sliding when the item is the last
* element in the item-sliding container.
*/
.list-md.list-inset ion-item:not(:only-of-type):last-of-type,
.list-md.list-inset ion-item-sliding:last-of-type ion-item {
--border-radius: #{0 0 $list-inset-md-border-radius $list-inset-md-border-radius};
--border-width: 0;
--inner-border-width: 0;
}
/**
* The only item in a list should have a border radius
* on all corners.
* We target :only-child instead of :only-of-type
* otherwise borders will disappear on items inside of
* ion-item-sliding because the item will be the only
* one of its type inside of the ion-item-sliding group.
*/
.list-md.list-inset ion-item:only-child {
--border-radius: #{$list-inset-md-border-radius};
--border-width: 0;
--inner-border-width: 0;
}

View File

@ -26,3 +26,162 @@ configs().forEach(({ title, screenshot, config }) => {
});
});
});
/**
* Padding and border color ensures the bottom border can be easily seen if it regresses.
* The background color ensures that any border radius values can be seen.
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('list: lines with children'), () => {
test('only item in inset list should not have line', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/28435',
});
await page.setContent(
`
<style>
#container {
padding: 10px;
background: #0088cc;
}
ion-item {
--border-color: red;
}
</style>
<div id="container">
<ion-list inset="true">
<ion-item>
<ion-label>Item 0</ion-label>
</ion-item>
</ion-list>
</div>
`,
config
);
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot('inset-list-only-item-no-lines'));
});
test('last item in inset list with end options should not have line', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/28435',
});
await page.setContent(
`
<style>
#container {
padding: 10px;
background: #0088cc;
}
ion-item {
--border-color: red;
}
</style>
<div id="container">
<ion-list inset="true">
<ion-item-sliding>
<ion-item>
<ion-label>Item 0</ion-label>
</ion-item>
<ion-item-options slot="end">
<ion-item-option color="warning">
<ion-icon slot="icon-only" name="pin"></ion-icon>
</ion-item-option>
</ion-item-options>
</ion-item-sliding>
<ion-item-sliding>
<ion-item>
<ion-label>Item 1</ion-label>
</ion-item>
<ion-item-options slot="end">
<ion-item-option color="warning">
<ion-icon slot="icon-only" name="pin"></ion-icon>
</ion-item-option>
</ion-item-options>
</ion-item-sliding>
<ion-item-sliding>
<ion-item>
<ion-label>Item 2</ion-label>
</ion-item>
<ion-item-options slot="end">
<ion-item-option color="warning">
<ion-icon slot="icon-only" name="pin"></ion-icon>
</ion-item-option>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</div>
`,
config
);
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot('inset-list-end-options-no-lines'));
});
test('last item in inset list with start options should not have line', async ({ page }) => {
await page.setContent(
`
<style>
#container {
padding: 10px;
background: #0088cc;
}
ion-item {
--border-color: red;
}
</style>
<div id="container">
<ion-list inset="true">
<ion-item-sliding>
<ion-item-options slot="start">
<ion-item-option color="warning">
<ion-icon slot="icon-only" name="pin"></ion-icon>
</ion-item-option>
</ion-item-options>
<ion-item>
<ion-label>Item 0</ion-label>
</ion-item>
</ion-item-sliding>
<ion-item-sliding>
<ion-item-options slot="start">
<ion-item-option color="warning">
<ion-icon slot="icon-only" name="pin"></ion-icon>
</ion-item-option>
</ion-item-options>
<ion-item>
<ion-label>Item 1</ion-label>
</ion-item>
</ion-item-sliding>
<ion-item-sliding>
<ion-item-options slot="start">
<ion-item-option color="warning">
<ion-icon slot="icon-only" name="pin"></ion-icon>
</ion-item-option>
</ion-item-options>
<ion-item>
<ion-label>Item 2</ion-label>
</ion-item>
</ion-item-sliding>
</ion-list>
</div>
`,
config
);
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot('inset-list-start-options-no-lines'));
});
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -9,6 +9,14 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test('inline pickers should not have visual regression', async ({ page }) => {
await page.goto(`/src/components/picker-internal/test/basic`, config);
const fullStack = page.locator('#inline button[data-value="full-stack"]');
const onion = page.locator('#inline button[data-value="onion"]');
await expect(fullStack).toHaveClass(/picker-item-active/);
await expect(onion).toHaveClass(/picker-item-active/);
await page.waitForChanges();
await expect(page.locator('#inline')).toHaveScreenshot(screenshot(`picker-internal-inline-diff`));
});
});

View File

@ -52,7 +52,16 @@ export class RadioGroup implements ComponentInterface {
@Event() ionValueChange!: EventEmitter<RadioGroupChangeEventDetail>;
componentDidLoad() {
this.setRadioTabindex(this.value);
/**
* There's an issue when assigning a value to the radio group
* within the Angular primary content (rendering within the
* app component template). When the template is isolated to a route,
* the value is assigned correctly.
* To address this issue, we need to ensure that the watcher is
* called after the component has finished loading,
* allowing the emit to be dispatched correctly.
*/
this.valueChanged(this.value);
}
private setRadioTabindex = (value: any | undefined) => {

View File

@ -34,7 +34,13 @@
// -----------------------------------------
.searchbar-cancel-button {
@include position(0, null, null, 5px);
/**
* The left edge of the cancel button
* should align with the left edge
* of the back button if the searchbar
* is used in a toolbar.
*/
@include position(0, null, null, 9px);
background-color: $searchbar-md-cancel-button-background-color;

View File

@ -25,7 +25,7 @@ $searchbar-md-cancel-button-color: $text-color-step-100 !default;
$searchbar-md-cancel-button-background-color: transparent !default;
/// @prop - Font size of the searchbar cancel button
$searchbar-md-cancel-button-font-size: 1.6em !default;
$searchbar-md-cancel-button-font-size: 1.5em !default;
/// @prop - Color of the searchbar input search icon
$searchbar-md-input-search-icon-color: $text-color-step-400 !default;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Some files were not shown because too many files have changed in this diff Show More