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`.
This commit is contained in:
Sean Perkins
2023-11-27 10:19:19 -05:00
committed by GitHub
parent 9883eac0f7
commit 4b5e62e60f
20 changed files with 152 additions and 68 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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">

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

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

View File

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

View File

@ -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();

View File

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