feat(datetime): add ability to specify custom colors for specific dates (#26775)

This commit is contained in:
Amanda Johnston
2023-02-22 09:01:33 -06:00
committed by GitHub
parent fc5fcc064d
commit 2a761afd5a
30 changed files with 363 additions and 43 deletions

View File

@@ -25,3 +25,17 @@ export interface DatetimeParts {
export type DatetimePresentation = 'date-time' | 'time-date' | 'date' | 'time' | 'month' | 'year' | 'month-year';
export type TitleSelectedDatesFormatter = (selectedDates: string[]) => string;
export type DatetimeHighlightStyle =
| {
textColor: string;
backgroundColor?: string;
}
| {
textColor?: string;
backgroundColor: string;
};
export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;
export type DatetimeHighlightCallback = (dateIsoString: string) => DatetimeHighlightStyle | undefined;

View File

@@ -92,14 +92,22 @@
font-size: 20px;
}
:host .calendar-day:after {
.calendar-day:focus .calendar-day-highlight,
.calendar-day.calendar-day-active .calendar-day-highlight {
opacity: 0.2;
}
:host .calendar-day:focus:after {
.calendar-day.calendar-day-active .calendar-day-highlight {
background: current-color(base);
}
// !important is needed here to overwrite custom highlight background, which is inline.
// Does not apply to the active state because highlights aren't applied at all there.
.calendar-day:focus .calendar-day-highlight {
/* stylelint-disable-next-line declaration-no-important */
background: current-color(base) !important;
}
/**
* Day that today but not selected
* should have ion-color for text color.
@@ -119,10 +127,6 @@
font-weight: 600;
}
:host .calendar-day.calendar-day-active:after {
background: current-color(base);
}
/**
* Day that is selected and is today
* should have white color.
@@ -131,7 +135,7 @@
color: current-color(contrast);
}
:host .calendar-day.calendar-day-today.calendar-day-active:after {
.calendar-day.calendar-day-today.calendar-day-active .calendar-day-highlight {
background: current-color(base);
opacity: 1;

View File

@@ -74,7 +74,7 @@
font-size: $datetime-md-calendar-item-font-size;
}
:host .calendar-day:focus:after {
.calendar-day:focus .calendar-day-highlight {
background: current-color(base, 0.2);
box-shadow: 0px 0px 0px 4px current-color(base, 0.2);
@@ -88,7 +88,7 @@
color: current-color(base);
}
:host .calendar-day.calendar-day-today:after {
.calendar-day.calendar-day-today .calendar-day-highlight {
border: 1px solid current-color(base);
}
@@ -101,7 +101,7 @@
color: current-color(contrast);
}
:host .calendar-day.calendar-day-active:after {
.calendar-day.calendar-day-active .calendar-day-highlight {
border: 1px solid current-color(base);
background: current-color(base);

View File

@@ -356,31 +356,15 @@ ion-picker-column-internal {
opacity: 0.4;
}
:host .calendar-day:after {
.calendar-day-highlight {
@include border-radius(32px, 32px, 32px, 32px);
@include padding(4px, 4px, 4px, 4px);
position: absolute;
/**
* Explicit position values are required here
* as pseudo element positioning is incorrect
* in older implementations of css grid.
*
* TODO: FW-1720: Remove top/left styles when deprecating iOS 13 support
*/
/* stylelint-disable-next-line property-disallowed-list */
top: 50%;
/* stylelint-disable-next-line property-disallowed-list */
left: 50%;
width: 32px;
height: 32px;
transform: translate(-50%, -50%);
content: " ";
z-index: -1;
}

View File

@@ -3,15 +3,7 @@ import { Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTa
import { caretDownSharp, caretUpSharp, chevronBack, chevronDown, chevronForward } from 'ionicons/icons';
import { getIonMode } from '../../global/ionic-global';
import type {
Color,
DatetimePresentation,
DatetimeChangeEventDetail,
DatetimeParts,
Mode,
StyleEventDetail,
TitleSelectedDatesFormatter,
} from '../../interface';
import type { Color, Mode, StyleEventDetail } from '../../interface';
import { startFocusVisible } from '../../utils/focus-visible';
import { getElementRoot, raf, renderHiddenInput } from '../../utils/helpers';
import { printIonError, printIonWarning } from '../../utils/logging';
@@ -19,6 +11,15 @@ import { isRTL } from '../../utils/rtl';
import { createColorClasses } from '../../utils/theme';
import type { PickerColumnItem } from '../picker-column-internal/picker-column-internal-interfaces';
import type {
DatetimePresentation,
DatetimeChangeEventDetail,
DatetimeParts,
TitleSelectedDatesFormatter,
DatetimeHighlight,
DatetimeHighlightStyle,
DatetimeHighlightCallback,
} from './datetime-interface';
import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison';
import {
generateMonths,
@@ -60,6 +61,7 @@ import {
} from './utils/parse';
import {
getCalendarDayState,
getHighlightStyles,
isDayDisabled,
isMonthDisabled,
isNextMonthDisabled,
@@ -322,6 +324,17 @@ export class Datetime implements ComponentInterface {
*/
@Prop() multiple = false;
/**
* Used to apply custom text and background colors to specific dates.
*
* Can be either an array of objects containing ISO strings and colors,
* or a callback that receives an ISO string and returns the colors.
*
* Only applies to the `date`, `date-time`, and `time-date` presentations,
* with `preferWheel="false"`.
*/
@Prop() highlightedDates?: DatetimeHighlight[] | DatetimeHighlightCallback;
/**
* The value of the datetime as a valid ISO 8601 datetime string.
* Should be an array of strings if `multiple="true"`.
@@ -1235,7 +1248,7 @@ export class Datetime implements ComponentInterface {
};
componentWillLoad() {
const { el, multiple, presentation, preferWheel } = this;
const { el, highlightedDates, multiple, presentation, preferWheel } = this;
if (multiple) {
if (presentation !== 'date') {
@@ -1247,6 +1260,19 @@ export class Datetime implements ComponentInterface {
}
}
if (highlightedDates !== undefined) {
if (presentation !== 'date' && presentation !== 'date-time' && presentation !== 'time-date') {
printIonWarning(
'The highlightedDates property is only supported with the date, date-time, and time-date presentations.',
el
);
}
if (preferWheel) {
printIonWarning('The highlightedDates property is not supported with preferWheel="true".', el);
}
}
this.processMinParts();
this.processMaxParts();
const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
@@ -1971,7 +1997,7 @@ export class Datetime implements ComponentInterface {
<div class="calendar-month-grid">
{getDaysOfMonth(month, year, this.firstDayOfWeek % 7).map((dateObject, index) => {
const { day, dayOfWeek } = dateObject;
const { isDateEnabled, multiple } = this;
const { el, highlightedDates, isDateEnabled, multiple } = this;
const referenceParts = { month, day, year };
const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(
this.locale,
@@ -1983,6 +2009,7 @@ export class Datetime implements ComponentInterface {
this.parsedDayValues
);
const dateIsoString = convertDataToISO(referenceParts);
let isCalDayDisabled = isCalMonthDisabled || disabled;
if (!isCalDayDisabled && isDateEnabled !== undefined) {
@@ -1992,15 +2019,26 @@ export class Datetime implements ComponentInterface {
* to prevent exceptions in the user's function from
* interrupting the calendar rendering.
*/
isCalDayDisabled = !isDateEnabled(convertDataToISO(referenceParts));
isCalDayDisabled = !isDateEnabled(dateIsoString);
} catch (e) {
printIonError(
'Exception thrown from provided `isDateEnabled` function. Please check your function and try again.',
el,
e
);
}
}
let dateStyle: DatetimeHighlightStyle | undefined = undefined;
/**
* Custom highlight styles should not override the style for selected dates,
* nor apply to "filler days" at the start of the grid.
*/
if (highlightedDates !== undefined && !isActive && day !== null) {
dateStyle = getHighlightStyles(highlightedDates, dateIsoString, el);
}
return (
<button
tabindex="-1"
@@ -2016,6 +2054,11 @@ export class Datetime implements ComponentInterface {
'calendar-day-active': isActive,
'calendar-day-today': isToday,
}}
style={
dateStyle && {
color: dateStyle.textColor,
}
}
aria-selected={ariaSelected}
aria-label={ariaLabel}
onClick={() => {
@@ -2050,6 +2093,12 @@ export class Datetime implements ComponentInterface {
}
}}
>
<div
class="calendar-day-highlight"
style={{
backgroundColor: dateStyle?.backgroundColor,
}}
></div>
{text}
</button>
);

View File

@@ -0,0 +1,103 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('datetime: highlightedDates', () => {
test.beforeEach(async ({ page, skip }) => {
skip.rtl();
await page.setContent(`
<ion-datetime value="2023-01-01" locale="en-US"></ion-datetime>
`);
});
test('should render highlights correctly when using an array', async ({ page }) => {
const datetime = page.locator('ion-datetime');
await datetime.evaluate((el: HTMLIonDatetimeElement) => {
el.highlightedDates = [
{
date: '2023-01-01', // ensure selected date style overrides highlight
textColor: '#800080',
backgroundColor: '#ffc0cb',
},
{
date: '2023-01-02',
textColor: '#b22222',
backgroundColor: '#fa8072',
},
{
date: '2023-01-03',
textColor: '#0000ff',
backgroundColor: '#add8e6',
},
];
});
await page.waitForChanges();
expect(await datetime.screenshot()).toMatchSnapshot(
`datetime-highlightedDates-array-${page.getSnapshotSettings()}.png`
);
});
test('should render highlights correctly when using a callback', async ({ page }) => {
const datetime = page.locator('ion-datetime');
await datetime.evaluate((el: HTMLIonDatetimeElement) => {
el.highlightedDates = (isoString) => {
const date = new Date(isoString);
const utcDay = date.getUTCDate();
// ensure selected date style overrides highlight
if (utcDay === 1) {
return {
textColor: '#b22222',
backgroundColor: '#fa8072',
};
}
if (utcDay % 5 === 0) {
return {
textColor: '#800080',
backgroundColor: '#ffc0cb',
};
}
if (utcDay % 3 === 0) {
return {
textColor: '#0000ff',
backgroundColor: '#add8e6',
};
}
return undefined;
};
});
await page.waitForChanges();
expect(await datetime.screenshot()).toMatchSnapshot(
`datetime-highlightedDates-callback-${page.getSnapshotSettings()}.png`
);
});
test('should render highlights correctly when only using one color or the other', async ({ page }) => {
const datetime = page.locator('ion-datetime');
await datetime.evaluate((el: HTMLIonDatetimeElement) => {
el.highlightedDates = [
{
date: '2023-01-02',
backgroundColor: '#fa8072',
},
{
date: '2023-01-03',
textColor: '#0000ff',
},
];
});
await page.waitForChanges();
expect(await datetime.screenshot()).toMatchSnapshot(
`datetime-highlightedDates-single-color-${page.getSnapshotSettings()}.png`
);
});
});

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime - highlightedDates</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: flex;
flex-wrap: wrap;
}
.grid-item {
margin-bottom: 20px;
margin-right: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
ion-datetime {
box-shadow: 0px 8px 16px 0 rgba(0, 0, 0, 0.25);
border-radius: 8px;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - highlightedDates</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>With Array</h2>
<ion-datetime locale="en-US" id="withArray" presentation="date" value="2023-01-01"></ion-datetime>
</div>
<div class="grid-item">
<h2>With Callback</h2>
<ion-datetime locale="en-US" id="withCallback" presentation="date" value="2023-01-01"></ion-datetime>
</div>
<div class="grid-item">
<h2>Highlight Today</h2>
<ion-datetime locale="en-US" id="highlightToday" presentation="date"></ion-datetime>
</div>
</div>
</ion-content>
</ion-app>
<script>
document.querySelector('#withArray').highlightedDates = [
{
date: '2023-01-02',
textColor: 'purple',
backgroundColor: 'pink',
},
{
date: '2023-01-04',
textColor: 'firebrick',
backgroundColor: 'salmon',
},
{
date: '2023-01-06',
textColor: 'blue',
backgroundColor: 'lightblue',
},
];
document.querySelector('#withCallback').highlightedDates = (isoString) => {
const date = new Date(isoString);
const utcDay = date.getUTCDate();
if (utcDay % 5 === 0) {
return {
textColor: 'purple',
backgroundColor: 'pink',
};
}
if (utcDay % 3 === 0) {
return {
textColor: 'blue',
backgroundColor: 'lightblue',
};
}
};
document.querySelector('#highlightToday').highlightedDates = [
{
date: new Date().toISOString().split('T')[0],
textColor: 'purple',
backgroundColor: 'pink',
},
];
</script>
</body>
</html>

View File

@@ -1,4 +1,11 @@
import type { DatetimeParts } from '../datetime-interface';
import { printIonError } from '@utils/logging';
import type {
DatetimeHighlight,
DatetimeHighlightCallback,
DatetimeHighlightStyle,
DatetimeParts,
} from '../datetime-interface';
import { isAfter, isBefore, isSameDay } from './comparison';
import { generateDayAriaLabel, getDay } from './format';
@@ -182,3 +189,41 @@ export const isNextMonthDisabled = (refParts: DatetimeParts, maxParts?: Datetime
maxParts,
});
};
/**
* Given the value of the highlightedDates property
* and an ISO string, return the styles to use for
* that date, or undefined if none are found.
*/
export const getHighlightStyles = (
highlightedDates: DatetimeHighlight[] | DatetimeHighlightCallback,
dateIsoString: string,
el: HTMLIonDatetimeElement
): DatetimeHighlightStyle | undefined => {
if (Array.isArray(highlightedDates)) {
const dateStringWithoutTime = dateIsoString.split('T')[0];
const matchingHighlight = highlightedDates.find((hd) => hd.date === dateStringWithoutTime);
if (matchingHighlight) {
return {
textColor: matchingHighlight.textColor,
backgroundColor: matchingHighlight.backgroundColor,
} as DatetimeHighlightStyle;
}
} else {
/**
* Wrap in a try-catch to prevent exceptions in the user's function
* from interrupting the calendar's rendering.
*/
try {
return highlightedDates(dateIsoString);
} catch (e) {
printIonError(
'Exception thrown from provided `highlightedDates` callback. Please check your function and try again.',
el,
e
);
}
}
return undefined;
};