Skip to content

feat(datetime): add shadow parts and CSS variables for styling wheel pickers #27529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0951a44
add picker-item shadow parts
May 22, 2023
79c2246
add more testing for wheel styling
May 22, 2023
3ada35f
add highlight/gradient CSS props
May 22, 2023
b17dd95
add screenshot tests for wheel styling
May 23, 2023
eba89d0
add test for styling time picker
May 23, 2023
e35a424
add shadow parts for time button
May 23, 2023
d81929d
add test for time button shadow parts
May 23, 2023
d1f44c9
lint
May 23, 2023
f440d10
chore(): add updated snapshots
Ionitron May 23, 2023
d3fc186
destructure some variables ahead of time
May 24, 2023
ba74ee0
remove unneeded default values for CSS vars
May 24, 2023
cf39b1a
Merge remote-tracking branch 'origin/feature-7.1' into FW-2254
May 24, 2023
1d81bb5
lint
May 24, 2023
cd8efda
extract picker-item part to const and rename existing consts to be le…
May 24, 2023
32492b2
Merge branch 'feature-7.1' into FW-2254
averyjohnston May 24, 2023
ae8174f
change fade-bg to rgb format and apply it to both ends of the gradients
May 25, 2023
ffb7299
lint
May 25, 2023
a6384be
delete screenshots so they can be regenerated
May 25, 2023
0ed73aa
chore(): add updated snapshots
Ionitron May 25, 2023
ad19028
pull picker fade bg variables into separate file
May 25, 2023
47802fb
remove unnecessary variable escape
May 26, 2023
18aa61c
Merge branch 'feature-7.1' into FW-2254
averyjohnston May 26, 2023
8888915
fix phrasing in variable description
May 30, 2023
c2607f5
rename API to use "wheel" instead of "picker"
May 30, 2023
9eb7809
Merge branch 'feature-7.1' into FW-2254
averyjohnston May 30, 2023
0c2ca9b
chore(): add updated snapshots
Ionitron May 30, 2023
9114943
Merge branch 'feature-7.1' into FW-2254
liamdebeasi Jun 6, 2023
cf4c082
chore(): add updated snapshots
Ionitron Jun 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);