refactor(datetime): render button for month/year toggle (#28443)
Issue number: Internal --------- <!-- 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. --> `ion-datetime` uses an `ion-item` to render the month/year toggle button inside of the header. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - `ion-datetime` uses a `button` element for the month/year toggle button ## Does this introduce a breaking change? - [x] Yes - [ ] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> Impact and migration path is noted in the `BREAKING.md`. ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> - `translucent` is not a valid CSS value for `background`. This was always intended to be `transparent`. --------- Co-authored-by: ionitron <hi@ionicframework.com> Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com> Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com> BREAKING CHANGE: The CSS shadow part for `month-year-button` has been changed to target a `button` element instead of `ion-item`. Developers should verify their UI renders as expected for the month/year toggle button inside of `ion-datetime`.
13
BREAKING.md
@ -16,6 +16,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
|||||||
- [Browser and Platform Support](#version-8x-browser-platform-support)
|
- [Browser and Platform Support](#version-8x-browser-platform-support)
|
||||||
- [Components](#version-8x-components)
|
- [Components](#version-8x-components)
|
||||||
- [Content](#version-8x-content)
|
- [Content](#version-8x-content)
|
||||||
|
- [Datetime](#version-8x-datetime)
|
||||||
|
|
||||||
<h2 id="version-8x-browser-platform-support">Browser and Platform Support</h2>
|
<h2 id="version-8x-browser-platform-support">Browser and Platform Support</h2>
|
||||||
|
|
||||||
@ -47,3 +48,15 @@ This section details the desktop browser, JavaScript framework, and mobile platf
|
|||||||
<h4 id="version-8x-content">Content</h4>
|
<h4 id="version-8x-content">Content</h4>
|
||||||
|
|
||||||
- Content no longer sets the `--background` custom property when the `.outer-content` class is set on the host.
|
- Content no longer sets the `--background` custom property when the `.outer-content` class is set on the host.
|
||||||
|
|
||||||
|
<h4 id="version-8x-datetime">Datetime</h4>
|
||||||
|
|
||||||
|
- The CSS shadow part for `month-year-button` has been changed to target a `button` element instead of `ion-item`. Developers should verify their UI renders as expected for the month/year toggle button inside of `ion-datetime`.
|
||||||
|
- Developers using the CSS variables available on `ion-item` will need to migrate their CSS to use CSS properties. For example:
|
||||||
|
```diff
|
||||||
|
ion-datetime::part(month-year-button) {
|
||||||
|
- --background: red;
|
||||||
|
|
||||||
|
+ background: red;
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -34,16 +34,24 @@
|
|||||||
|
|
||||||
// Calendar / Header / Action Buttons
|
// Calendar / Header / Action Buttons
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
:host .calendar-action-buttons ion-item {
|
.calendar-month-year-toggle {
|
||||||
--padding-start: #{$datetime-ios-padding};
|
@include padding(0px, 16px, 0px, #{$datetime-ios-padding});
|
||||||
--background-hover: transparent;
|
|
||||||
--background-activated: transparent;
|
min-height: 44px;
|
||||||
|
|
||||||
font-size: dynamic-font-max(16px, 1.6);
|
font-size: dynamic-font-max(16px, 1.6);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.ion-focused::after {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month-year-toggle #toggle-wrapper {
|
||||||
|
@include margin(10px, 8px, 10px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host .calendar-action-buttons ion-item ion-icon,
|
:host .calendar-action-buttons .calendar-month-year-toggle ion-icon,
|
||||||
:host .calendar-action-buttons ion-buttons ion-button {
|
:host .calendar-action-buttons ion-buttons ion-button {
|
||||||
color: current-color(base);
|
color: current-color(base);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,15 +30,41 @@
|
|||||||
|
|
||||||
// Calendar / Header / Action Buttons
|
// Calendar / Header / Action Buttons
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
:host .datetime-calendar .calendar-action-buttons ion-item {
|
|
||||||
--padding-start: #{$datetime-md-header-padding};
|
|
||||||
}
|
|
||||||
|
|
||||||
:host .calendar-action-buttons ion-item,
|
|
||||||
:host .calendar-action-buttons ion-button {
|
:host .calendar-action-buttons ion-button {
|
||||||
--color: #{$text-color-step-350};
|
--color: #{$text-color-step-350};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-month-year-toggle {
|
||||||
|
@include padding(12px, 16px, 12px, #{$datetime-md-header-padding});
|
||||||
|
|
||||||
|
min-height: 48px;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
color: #{$text-color-step-350};
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&.ion-focused::after {
|
||||||
|
opacity: 0.04;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month-year-toggle ion-ripple-effect {
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (any-hover: hover) {
|
||||||
|
.calendar-month-year-toggle.ion-activatable:not(.ion-focused):hover {
|
||||||
|
&::after {
|
||||||
|
background: currentColor;
|
||||||
|
|
||||||
|
opacity: 0.04;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calendar / Header / Days of Week
|
// Calendar / Header / Days of Week
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
:host .calendar-days-of-week {
|
:host .calendar-days-of-week {
|
||||||
@ -64,7 +90,6 @@
|
|||||||
* if necessary.
|
* if necessary.
|
||||||
*/
|
*/
|
||||||
grid-template-rows: repeat(6, 1fr);
|
grid-template-rows: repeat(6, 1fr);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Individual day button in month
|
// Individual day button in month
|
||||||
|
|||||||
@ -48,7 +48,7 @@ ion-picker-column-internal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This ensures that the picker is apppropriately
|
* This ensures that the picker is appropriately
|
||||||
* sized and never truncates the text.
|
* sized and never truncates the text.
|
||||||
*/
|
*/
|
||||||
:host(.datetime-size-fixed.datetime-prefer-wheel) {
|
:host(.datetime-size-fixed.datetime-prefer-wheel) {
|
||||||
@ -267,19 +267,8 @@ ion-picker-column-internal {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host .calendar-action-buttons ion-item,
|
|
||||||
:host .calendar-action-buttons ion-button {
|
:host .calendar-action-buttons ion-button {
|
||||||
--background: translucent;
|
--background: transparent;
|
||||||
}
|
|
||||||
|
|
||||||
:host .calendar-action-buttons ion-item ion-label {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host .calendar-action-buttons ion-item ion-icon {
|
|
||||||
@include padding(0, 0, 0, 4px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calendar / Header / Days of Week
|
// Calendar / Header / Days of Week
|
||||||
@ -488,6 +477,55 @@ ion-picker-column-internal {
|
|||||||
|
|
||||||
// Year Picker
|
// Year Picker
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
:host(.show-month-and-year) .calendar-action-buttons ion-item {
|
:host(.show-month-and-year) .calendar-action-buttons .calendar-month-year-toggle {
|
||||||
--color: #{current-color(base)};
|
color: #{current-color(base)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month-year {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month-year-toggle {
|
||||||
|
@include text-inherit();
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@include button-state();
|
||||||
|
|
||||||
|
transition: opacity 15ms linear, background-color 15ms linear;
|
||||||
|
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ion-focused::after {
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month-year-toggle ion-icon {
|
||||||
|
@include padding(0, 0, 0, 4px);
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month-year-toggle #toggle-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,7 +104,6 @@ import {
|
|||||||
export class Datetime implements ComponentInterface {
|
export class Datetime implements ComponentInterface {
|
||||||
private inputId = `ion-dt-${datetimeIds++}`;
|
private inputId = `ion-dt-${datetimeIds++}`;
|
||||||
private calendarBodyRef?: HTMLElement;
|
private calendarBodyRef?: HTMLElement;
|
||||||
private monthYearToggleItemRef?: HTMLIonItemElement;
|
|
||||||
private popoverRef?: HTMLIonPopoverElement;
|
private popoverRef?: HTMLIonPopoverElement;
|
||||||
private clearFocusVisible?: () => void;
|
private clearFocusVisible?: () => void;
|
||||||
private parsedMinuteValues?: number[];
|
private parsedMinuteValues?: number[];
|
||||||
@ -2000,35 +1999,18 @@ export class Datetime implements ComponentInterface {
|
|||||||
<div class="calendar-header">
|
<div class="calendar-header">
|
||||||
<div class="calendar-action-buttons">
|
<div class="calendar-action-buttons">
|
||||||
<div class="calendar-month-year">
|
<div class="calendar-month-year">
|
||||||
<ion-item
|
<button
|
||||||
|
class={{
|
||||||
|
'calendar-month-year-toggle': true,
|
||||||
|
'ion-activatable': true,
|
||||||
|
'ion-focusable': true,
|
||||||
|
}}
|
||||||
part="month-year-button"
|
part="month-year-button"
|
||||||
ref={(el) => (this.monthYearToggleItemRef = el)}
|
|
||||||
button
|
|
||||||
aria-label="Show year picker"
|
|
||||||
detail={false}
|
|
||||||
lines="none"
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => {
|
aria-label={this.showMonthAndYear ? 'Hide year picker' : 'Show year picker'}
|
||||||
this.toggleMonthAndYearView();
|
onClick={() => this.toggleMonthAndYearView()}
|
||||||
/**
|
|
||||||
* TODO: FW-3547
|
|
||||||
*
|
|
||||||
* Currently there is not a way to set the aria-label on the inner button
|
|
||||||
* on the `ion-item` and have it be reactive to changes. This is a workaround
|
|
||||||
* until we either refactor `ion-item` to a button or Stencil adds a way to
|
|
||||||
* have reactive props for built-in properties, such as `aria-label`.
|
|
||||||
*/
|
|
||||||
const { monthYearToggleItemRef } = this;
|
|
||||||
if (monthYearToggleItemRef) {
|
|
||||||
const btn = monthYearToggleItemRef.shadowRoot?.querySelector('.item-native');
|
|
||||||
if (btn) {
|
|
||||||
const monthYearAriaLabel = this.showMonthAndYear ? 'Hide year picker' : 'Show year picker';
|
|
||||||
btn.setAttribute('aria-label', monthYearAriaLabel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ion-label>
|
<span id="toggle-wrapper">
|
||||||
{getMonthAndYear(this.locale, this.workingParts)}
|
{getMonthAndYear(this.locale, this.workingParts)}
|
||||||
<ion-icon
|
<ion-icon
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -2036,8 +2018,9 @@ export class Datetime implements ComponentInterface {
|
|||||||
lazy={false}
|
lazy={false}
|
||||||
flipRtl={true}
|
flipRtl={true}
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
</ion-label>
|
</span>
|
||||||
</ion-item>
|
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendar-next-prev">
|
<div class="calendar-next-prev">
|
||||||
|
|||||||
@ -47,7 +47,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||||
|
|
||||||
const datetime = page.locator('ion-datetime');
|
const datetime = page.locator('ion-datetime');
|
||||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
const monthYearButton = page.locator('.calendar-month-year-toggle');
|
||||||
const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
|
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 nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@ -1,7 +1,6 @@
|
|||||||
import type { SpecPage } from '@stencil/core/testing';
|
import type { SpecPage } from '@stencil/core/testing';
|
||||||
import { newSpecPage } from '@stencil/core/testing';
|
import { newSpecPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
import { Item } from '../../../item/item';
|
|
||||||
import { Datetime } from '../../datetime';
|
import { Datetime } from '../../datetime';
|
||||||
|
|
||||||
describe('datetime', () => {
|
describe('datetime', () => {
|
||||||
@ -20,15 +19,14 @@ describe('datetime', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
page = await newSpecPage({
|
page = await newSpecPage({
|
||||||
components: [Datetime, Item],
|
components: [Datetime],
|
||||||
html: `<ion-datetime></ion-datetime>`,
|
html: `<ion-datetime></ion-datetime>`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have aria-label "Show year picker" when collapsed', async () => {
|
it('should have aria-label "Show year picker" when collapsed', async () => {
|
||||||
const datetime = page.body.querySelector('ion-datetime')!;
|
const datetime = page.body.querySelector('ion-datetime')!;
|
||||||
const item = datetime.shadowRoot!.querySelector('.calendar-month-year ion-item');
|
const monthYearToggleBtn = datetime.shadowRoot!.querySelector('.calendar-month-year .calendar-month-year-toggle');
|
||||||
const monthYearToggleBtn = item!.shadowRoot!.querySelector('button');
|
|
||||||
const ariaLabel = monthYearToggleBtn!.getAttribute('aria-label');
|
const ariaLabel = monthYearToggleBtn!.getAttribute('aria-label');
|
||||||
|
|
||||||
expect(ariaLabel).toContain('Show year picker');
|
expect(ariaLabel).toContain('Show year picker');
|
||||||
@ -36,15 +34,18 @@ describe('datetime', () => {
|
|||||||
|
|
||||||
it('should have aria-label "Hide year picker" when expanded', async () => {
|
it('should have aria-label "Hide year picker" when expanded', async () => {
|
||||||
const datetime = page.body.querySelector('ion-datetime')!;
|
const datetime = page.body.querySelector('ion-datetime')!;
|
||||||
const item = datetime.shadowRoot!.querySelector<HTMLIonItemElement>('.calendar-month-year ion-item');
|
const monthYearToggleBtn = datetime.shadowRoot!.querySelector<HTMLButtonElement>(
|
||||||
|
'.calendar-month-year .calendar-month-year-toggle'
|
||||||
|
);
|
||||||
|
|
||||||
item!.click();
|
monthYearToggleBtn!.click();
|
||||||
|
|
||||||
await page.waitForChanges();
|
await page.waitForChanges();
|
||||||
|
|
||||||
const itemAfter = datetime.shadowRoot!.querySelector<HTMLIonItemElement>('.calendar-month-year ion-item');
|
const monthYearToggleBtnAfter = datetime.shadowRoot!.querySelector<HTMLButtonElement>(
|
||||||
const monthYearToggleBtn = itemAfter!.shadowRoot!.querySelector<HTMLElement>('button');
|
'.calendar-month-year .calendar-month-year-toggle'
|
||||||
const ariaLabel = monthYearToggleBtn!.getAttribute('aria-label');
|
);
|
||||||
|
const ariaLabel = monthYearToggleBtnAfter!.getAttribute('aria-label');
|
||||||
|
|
||||||
expect(ariaLabel).toContain('Hide year picker');
|
expect(ariaLabel).toContain('Hide year picker');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -526,4 +526,20 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
|||||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-focus-calendar-day`));
|
await expect(datetime).toHaveScreenshot(screenshot(`datetime-focus-calendar-day`));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe(title('datetime: calendar month toggle'), () => {
|
||||||
|
test('should have focus styles', async ({ page }) => {
|
||||||
|
await page.setContent('<ion-datetime value="2021-01-01"></ion-datetime>', config);
|
||||||
|
|
||||||
|
const datetime = page.locator('ion-datetime');
|
||||||
|
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
const monthYearToggle = datetime.locator('.calendar-month-year-toggle');
|
||||||
|
|
||||||
|
monthYearToggle.evaluate((el: HTMLElement) => el.classList.add('ion-focused'));
|
||||||
|
|
||||||
|
await expect(datetime).toHaveScreenshot(screenshot(`date-month-toggle-focused`));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
@ -15,7 +15,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
const datetimeFooter = page.locator('#date-time .datetime-footer');
|
const datetimeFooter = page.locator('#date-time .datetime-footer');
|
||||||
await expect(datetimeFooter).toBeVisible();
|
await expect(datetimeFooter).toBeVisible();
|
||||||
|
|
||||||
const pickerButton = page.locator('#date-time .calendar-month-year > ion-item');
|
const pickerButton = page.locator('#date-time .calendar-month-year > .calendar-month-year-toggle');
|
||||||
await pickerButton.click();
|
await pickerButton.click();
|
||||||
await page.waitForChanges();
|
await page.waitForChanges();
|
||||||
await expect(datetimeFooter).not.toBeVisible();
|
await expect(datetimeFooter).not.toBeVisible();
|
||||||
|
|||||||
@ -68,7 +68,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, scree
|
|||||||
|
|
||||||
await page.waitForSelector('.datetime-ready');
|
await page.waitForSelector('.datetime-ready');
|
||||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
const monthYearButton = page.locator('.calendar-month-year-toggle');
|
||||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||||
|
|
||||||
await page.keyboard.press(tabKey);
|
await page.keyboard.press(tabKey);
|
||||||
@ -114,7 +114,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, scree
|
|||||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||||
|
|
||||||
const datetime = page.locator('ion-datetime');
|
const datetime = page.locator('ion-datetime');
|
||||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
const monthYearButton = page.locator('.calendar-month-year-toggle');
|
||||||
const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
|
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 nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');
|
||||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||||
|
|||||||