Skip to content

Commit

Permalink
feat(datetime): add shadow parts and CSS variables for styling wheel …
Browse files Browse the repository at this point in the history
…pickers (#27529)

Issue number: resolves #25945

---------

<!-- 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. -->

Datetime wheel pickers cannot be styled.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Adds styling APIs in accordance with the Wheel Pickers and Time Picker
sections of [this design
doc](https://github.com/ionic-team/ionic-framework-design-documents/blob/main/projects/ionic-framework/components/datetime/datetime-styling.md).

Shadow parts added:
- `wheel-item`
- `wheel-item active`
- `time-button`
- `time-button active`

CSS properties added:
- `--wheel-highlight-background`
- `--wheel-fade-background-rgb`

## 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. -->

Dev build: `7.0.7-dev.11685554390.10c2ca9b`
Docs PR: ionic-team/ionic-docs#2982
  • Loading branch information
averyjohnston authored Jun 6, 2023
1 parent ea817a5 commit f2c1845
Show file tree
Hide file tree
Showing 40 changed files with 206 additions and 14 deletions.
6 changes: 6 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,12 @@ ion-datetime,event,ionFocus,void,true
ion-datetime,css-prop,--background
ion-datetime,css-prop,--background-rgb
ion-datetime,css-prop,--title-color
ion-datetime,css-prop,--wheel-fade-background-rgb
ion-datetime,css-prop,--wheel-highlight-background
ion-datetime,part,time-button
ion-datetime,part,time-button active
ion-datetime,part,wheel-item
ion-datetime,part,wheel-item active

ion-datetime-button,shadow
ion-datetime-button,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,'primary',false,true
Expand Down
6 changes: 6 additions & 0 deletions core/src/components/datetime/datetime.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
* @prop --background: The primary background of the datetime component.
* @prop --background-rgb: The primary background of the datetime component in RGB format.
* @prop --title-color: The text color of the title.
*
* @prop --wheel-highlight-background: The background of the highlight under the selected
* item when using a wheel style layout, or in the month/year picker for grid style layouts.
* @prop --wheel-fade-background-rgb: The color of the gradient covering non-selected items
* when using a wheel style layout, or in the month/year picker for grid style layouts. Must
* be in RGB format, e.g. `255, 255, 255`.
*/

display: flex;
Expand Down
16 changes: 13 additions & 3 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ import {
* @slot title - The title of the datetime.
* @slot buttons - The buttons in the datetime.
* @slot time-label - The label for the time selector in the datetime.
*
* @part wheel-item - The individual items when using a wheel style layout, or in the
* month/year picker when using a grid style layout.
* @part wheel-item active - The currently selected wheel-item.
*
* @part time-button - The button that opens the time picker when using a grid style
* layout with `presentation="date-time"` or `"time-date"`.
* @part time-button active - The time picker button when the picker is open.
*/
@Component({
tag: 'ion-datetime',
Expand Down Expand Up @@ -2167,16 +2175,18 @@ export class Datetime implements ComponentInterface {
}

private renderTimeOverlay() {
const use24Hour = is24Hour(this.locale, this.hourCycle);
const { hourCycle, isTimePopoverOpen, locale } = this;
const use24Hour = is24Hour(locale, hourCycle);
const activePart = this.getActivePartsWithFallback();

return [
<div class="time-header">{this.renderTimeLabel()}</div>,
<button
class={{
'time-body': true,
'time-body-active': this.isTimePopoverOpen,
'time-body-active': isTimePopoverOpen,
}}
part={`time-button${isTimePopoverOpen ? ' active' : ''}`}
aria-expanded="false"
aria-haspopup="true"
onClick={async (ev) => {
Expand All @@ -2199,7 +2209,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
{getLocalizedTime(this.locale, activePart, use24Hour)}
{getLocalizedTime(locale, activePart, use24Hour)}
</button>,
<ion-popover
alignment="center"
Expand Down
40 changes: 40 additions & 0 deletions core/src/components/datetime/test/custom/datetime.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';

configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('datetime: custom'), () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/datetime/test/custom`, config);
});

test('should allow styling wheel style datetimes', async ({ page }) => {
const datetime = page.locator('#custom-wheel');

await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-wheel`));
});

test('should allow styling month/year picker in grid style datetimes', async ({ page }) => {
const datetime = page.locator('#custom-grid');
const monthYearToggle = datetime.locator('.calendar-month-year');

await monthYearToggle.click();
await page.waitForChanges();

await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-month-year`));
});

test('should allow styling time picker in grid style datetimes', async ({ page }) => {
const timeButton = page.locator('ion-datetime .time-body');
const popover = page.locator('.popover-viewport');
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');

await expect(timeButton).toHaveScreenshot(screenshot(`datetime-custom-time-button`));

await timeButton.click();
await ionPopoverDidPresent.next();

await expect(popover).toHaveScreenshot(screenshot(`datetime-custom-time-picker`));
await expect(timeButton).toHaveScreenshot(screenshot(`datetime-custom-time-button-active`));
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 98 additions & 0 deletions core/src/components/datetime/test/custom/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime - Custom</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;
}
}

/*
The second selectors that target ion-picker(-column)-internal
directly are for styling the time picker. This is currently
undocumented usage.
*/

ion-datetime,
ion-picker-internal {
--wheel-highlight-background: rgb(218, 216, 255);
--wheel-fade-background-rgb: 245, 235, 247;
}

ion-datetime {
--background: rgb(245, 235, 247);
--background-rgb: 245, 235, 247;
}

ion-picker-internal {
background-color: rgb(245, 235, 247);
}

ion-datetime::part(wheel-item),
ion-picker-column-internal::part(wheel-item) {
color: rgb(255, 134, 154);
}

ion-datetime::part(wheel-item active),
ion-picker-column-internal::part(wheel-item active) {
color: rgb(128, 30, 171);
}

ion-datetime::part(time-button) {
color: rgb(128, 30, 171);
}

ion-datetime::part(time-button active) {
background-color: rgb(248, 215, 255);
}
</style>
</head>

<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Custom</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Grid Style</h2>
<ion-datetime id="custom-grid" value="2020-03-14T14:23:00.000Z"></ion-datetime>
</div>
<div class="grid-item">
<h2>Wheel Style</h2>
<ion-datetime id="custom-wheel" prefer-wheel="true" value="2020-03-14T14:23:00.000Z"></ion-datetime>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,21 @@ export class PickerColumnInternal implements ComponentInterface {
const ev = entries[0];

if (ev.isIntersecting) {
const { activeItem, el } = this;

this.isColumnVisible = true;
/**
* Because this initial call to scrollActiveItemIntoView has to fire before
* the scroll listener is set up, we need to manage the active class manually.
*/
const oldActive = getElementRoot(this.el).querySelector(`.${PICKER_COL_ACTIVE}`);
oldActive?.classList.remove(PICKER_COL_ACTIVE);
const oldActive = getElementRoot(el).querySelector(`.${PICKER_ITEM_ACTIVE_CLASS}`);
if (oldActive) {
this.setPickerItemActiveState(oldActive, false);
}
this.scrollActiveItemIntoView();
this.activeItem?.classList.add(PICKER_COL_ACTIVE);
if (activeItem) {
this.setPickerItemActiveState(activeItem, true);
}

this.initializeScrollListener();
} else {
Expand Down Expand Up @@ -189,6 +195,16 @@ export class PickerColumnInternal implements ComponentInterface {
}
};

private setPickerItemActiveState = (item: Element, isActive: boolean) => {
if (isActive) {
item.classList.add(PICKER_ITEM_ACTIVE_CLASS);
item.part.add(PICKER_ITEM_ACTIVE_PART);
} else {
item.classList.remove(PICKER_ITEM_ACTIVE_CLASS);
item.part.remove(PICKER_ITEM_ACTIVE_PART);
}
};

/**
* When ionInputModeChange is emitted, each column
* needs to check if it is the one being made available
Expand Down Expand Up @@ -275,7 +291,7 @@ export class PickerColumnInternal implements ComponentInterface {
const activeElement = el.shadowRoot!.elementFromPoint(centerX, centerY) as HTMLButtonElement | null;

if (activeEl !== null) {
activeEl.classList.remove(PICKER_COL_ACTIVE);
this.setPickerItemActiveState(activeEl, false);
}

if (activeElement === null || activeElement.disabled) {
Expand Down Expand Up @@ -306,7 +322,7 @@ export class PickerColumnInternal implements ComponentInterface {
}

activeEl = activeElement;
activeElement.classList.add(PICKER_COL_ACTIVE);
this.setPickerItemActiveState(activeElement, true);

timeout = setTimeout(() => {
this.isScrolling = false;
Expand Down Expand Up @@ -401,8 +417,15 @@ export class PickerColumnInternal implements ComponentInterface {
const { items, color, isActive, numericInput } = this;
const mode = getIonMode(this);

/**
* exportparts is needed so ion-datetime can expose the parts
* from two layers of shadow nesting. If this causes problems,
* the attribute can be moved to datetime.tsx and set on every
* instance of ion-picker-column-internal there instead.
*/
return (
<Host
exportparts={`${PICKER_ITEM_PART}, ${PICKER_ITEM_ACTIVE_PART}`}
tabindex={0}
class={createColorClasses(color, {
[mode]: true,
Expand Down Expand Up @@ -443,6 +466,7 @@ export class PickerColumnInternal implements ComponentInterface {
this.centerPickerItemInView(ev.target as HTMLElement, true);
}}
disabled={item.disabled}
part={PICKER_ITEM_PART}
>
{item.text}
</button>
Expand All @@ -462,4 +486,6 @@ export class PickerColumnInternal implements ComponentInterface {
}
}

const PICKER_COL_ACTIVE = 'picker-item-active';
const PICKER_ITEM_ACTIVE_CLASS = 'picker-item-active';
const PICKER_ITEM_PART = 'wheel-item';
const PICKER_ITEM_ACTIVE_PART = 'active';
7 changes: 4 additions & 3 deletions core/src/components/picker-internal/picker-internal.ios.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
@import "./picker-internal.scss";
@import "./picker-internal.vars.scss";
@import "../../themes/ionic.globals.ios";

:host .picker-before {
background: linear-gradient(to bottom, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8) 100%);
background: linear-gradient(to bottom, rgba(#{$picker-fade-background}, 1) 20%, rgba(#{$picker-fade-background}, 0.8) 100%);
}

:host .picker-after {
background: linear-gradient(to top, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8) 100%);
background: linear-gradient(to top, rgba(#{$picker-fade-background}, 1) 20%, rgba(#{$picker-fade-background}, 0.8) 100%);
}

:host .picker-highlight {
background: var(--ion-color-step-150, #eeeeef);
background: var(--wheel-highlight-background, var(--ion-color-step-150, #eeeeef));
}
5 changes: 3 additions & 2 deletions core/src/components/picker-internal/picker-internal.md.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
@import "./picker-internal.scss";
@import "./picker-internal.vars.scss";
@import "../../themes/ionic.globals.md";

:host .picker-before {
background: linear-gradient(to bottom, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0) 90%);
background: linear-gradient(to bottom, rgba(#{$picker-fade-background}, 1) 20%, rgba(#{$picker-fade-background}, 0) 90%);
}

:host .picker-after {
background: linear-gradient(to top, var(--background, var(--ion-background-color, #fff)) 30%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0) 90%);
background: linear-gradient(to top, rgba(#{$picker-fade-background}, 1) 30%, rgba(#{$picker-fade-background}, 0) 90%);
}
2 changes: 2 additions & 0 deletions core/src/components/picker-internal/picker-internal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
height: 34px;

transform: translateY(-50%);

background: var(--wheel-highlight-background);

z-index: -1;
}
Expand Down
2 changes: 2 additions & 0 deletions core/src/components/picker-internal/picker-internal.vars.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$picker-fade-background-fallback: var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255));
$picker-fade-background: var(--wheel-fade-background-rgb, $picker-fade-background-fallback);

0 comments on commit f2c1845

Please sign in to comment.