feat(datetime): add ability to specify custom colors for specific dates (#26775)
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 50 KiB |
110
core/src/components/datetime/test/highlightedDates/index.html
Normal 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>
|
||||
@@ -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;
|
||||
};
|
||||
|
||||