1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-18 16:07:40 +01:00

Migrate date picker to cally calendar-date (#29994)

* Switch date picker to cally calendar-date

* Remove app-datepicker styles from date picker dialog

Clean up unused CSS overrides in ha-dialog-date-picker.

Remove custom properties, focus/body rules, and responsive media
queries for app-datepicker and calendar-date so the component uses
upstream defaults and avoids duplicate styling

* Review

* Apply suggestion from @MindFreeze

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Fix date parsing in HaDialogDatePicker to handle ISO string format

* Update src/components/ha-dialog-date-picker.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Wendelin
2026-03-17 09:18:18 +01:00
committed by GitHub
parent 2d1e211034
commit 8541494b3d
4 changed files with 238 additions and 191 deletions

View File

@@ -90,8 +90,8 @@
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.1.1",
"cally": "0.9.2",
"color-name": "2.1.0",
"comlink": "4.4.2",
"core-js": "3.48.0",
@@ -221,7 +221,6 @@
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
},
"resolutions": {
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "3.3.2",
"lit-html": "3.3.2",
"clean-css": "5.3.3",

View File

@@ -1,117 +1,189 @@
import "app-datepicker";
import { format } from "date-fns";
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume, type ContextType } from "@lit/context";
import { mdiBackspace, mdiCalendarToday } from "@mdi/js";
import "cally";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { haStyleDialog } from "../resources/styles";
import type { HomeAssistant } from "../types";
import type { DatePickerDialogParams } from "./ha-date-input";
import { customElement, state } from "lit/decorators";
import {
formatDateMonth,
formatDateShort,
formatDateYear,
} from "../common/datetime/format_date";
import { configContext, localeContext, localizeContext } from "../data/context";
import { DialogMixin } from "../dialogs/dialog-mixin";
import "./ha-button";
import "./ha-dialog-footer";
import type { DatePickerDialogParams } from "./ha-date-input";
import "./ha-dialog";
import "./ha-dialog-footer";
import "./ha-icon-button";
import "./ha-icon-button-next";
import "./ha-icon-button-prev";
type CalendarDate = HTMLElementTagNameMap["calendar-date"];
/**
* A date picker dialog component that displays a calendar for selecting dates.
* Uses the `cally` library for calendar rendering and supports localization,
* min/max date constraints, and optional clearing of the selected date.
*
* @element ha-dialog-date-picker
* Uses {@link DialogMixin} with {@link DatePickerDialogParams} to manage dialog state and parameters.
*/
@customElement("ha-dialog-date-picker")
export class HaDialogDatePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
LitElement
) {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@property() public value?: string;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@property({ type: Boolean }) public disabled = false;
@state()
@consume({ context: configContext, subscribe: true })
private hassConfig!: ContextType<typeof configContext>;
@property() public label?: string;
@state() private _value?: {
year: string;
title: string;
dateString: string;
};
@state() private _params?: DatePickerDialogParams;
/** used to show month in calendar-date header */
@state() private _pickerMonth?: string;
@state() private _open = false;
/** used to show year in calendar-date header */
@state() private _pickerYear?: string;
@state() private _value?: string;
/** used for today to navigate focus in cally-calendar-date */
@state() private _focusDate?: string;
public async showDialog(params: DatePickerDialogParams): Promise<void> {
// app-datepicker has a bug, that it removes its handlers when disconnected, but doesn't add them back when reconnected.
// So we need to wait for the next render to make sure the element is removed and re-created so the handlers are added.
await nextRender();
this._params = params;
this._value = params.value;
this._open = true;
}
public connectedCallback() {
super.connectedCallback();
public closeDialog() {
this._open = false;
}
if (this.params) {
const date = this.params.value
? new Date(`${this.params.value.split("T")[0]}T00:00:00`)
: new Date();
private _dialogClosed() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._value = this.params.value
? {
year: this._pickerYear,
title: formatDateShort(date, this.locale, this.hassConfig),
dateString: this.params.value.substring(0, 10),
}
: undefined;
}
}
render() {
if (!this._params) {
if (!this.params) {
return nothing;
}
return html`<ha-dialog
.hass=${this.hass}
.open=${this._open}
open
width="small"
without-header
@closed=${this._dialogClosed}
.headerTitle=${this._value?.title ||
this.localize("ui.dialogs.date-picker.title")}
.headerSubtitle=${this._value?.year}
header-subtitle-position="above"
>
<app-datepicker
.value=${this._value}
.min=${this._params.min}
.max=${this._params.max}
.locale=${this._params.locale}
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker>
<div class="bottom-actions">
${this._params.canClear
? html`<ha-button
slot="secondaryAction"
${this.params.canClear
? html`
<ha-icon-button
.path=${mdiBackspace}
.label=${this.localize("ui.dialogs.date-picker.clear")}
slot="headerActionItems"
@click=${this._clear}
variant="danger"
appearance="plain"
>
${this.hass.localize("ui.dialogs.date-picker.clear")}
</ha-button>`
: nothing}
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._setToday}
>
${this.hass.localize("ui.dialogs.date-picker.today")}
</ha-button>
</div>
></ha-icon-button>
`
: nothing}
<wa-divider></wa-divider>
<calendar-date
.value=${this._value?.dateString}
.min=${this.params.min}
.max=${this.params.max}
.locale=${this.params.locale}
.firstDayOfWeek=${this.params.firstWeekday}
.focusedDate=${this._focusDate}
@change=${this._valueChanged}
@focusday=${this._focusChanged}
>
<ha-icon-button-prev
tabindex="-1"
slot="previous"
></ha-icon-button-prev>
<div class="heading" slot="heading">
<span class="month-year"
>${this._pickerMonth} ${this._pickerYear}</span
>
<ha-icon-button
@click=${this._setToday}
.path=${mdiCalendarToday}
.label=${this.localize("ui.dialogs.date-picker.today")}
></ha-icon-button>
</div>
<ha-icon-button-next tabindex="-1" slot="next"></ha-icon-button-next>
<calendar-month></calendar-month>
</calendar-date>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
${this.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._setValue}>
${this.hass.localize("ui.common.ok")}
${this.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>`;
}
private _valueChanged(ev: CustomEvent) {
this._value = ev.detail.value;
private _valueChanged(ev: Event) {
const dateElement = ev.target as CalendarDate;
if (dateElement.value) {
this._updateValue(dateElement.value);
}
}
private _updateValue(value?: string, setFocusDay = false) {
const date = value
? new Date(`${value.split("T")[0]}T00:00:00`)
: new Date();
this._value = {
year: formatDateYear(date, this.locale, this.hassConfig),
title: formatDateShort(date, this.locale, this.hassConfig),
dateString: value || date.toISOString().substring(0, 10),
};
if (setFocusDay) {
this._focusDate = this._value.dateString;
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
}
}
private _focusChanged(ev: CustomEvent<Date>) {
const date = ev.detail;
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._focusDate = undefined;
}
private _clear() {
this._params?.onChange(undefined);
this.params?.onChange(undefined);
this.closeDialog();
}
private _setToday() {
const today = new Date();
this._value = format(today, "yyyy-MM-dd");
this._updateValue(undefined, true);
}
private _setValue() {
@@ -120,50 +192,85 @@ export class HaDialogDatePicker extends LitElement {
// without changing the date, should return todays date, not undefined.
this._setToday();
}
this._params?.onChange(this._value!);
this.params?.onChange(this._value?.dateString);
this.closeDialog();
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
}
.bottom-actions {
display: flex;
gap: var(--ha-space-4);
justify-content: center;
align-items: center;
width: 100%;
margin-bottom: var(--ha-space-1);
}
app-datepicker {
display: block;
margin-inline: auto;
--app-datepicker-accent-color: var(--primary-color);
--app-datepicker-bg-color: transparent;
--app-datepicker-color: var(--primary-text-color);
--app-datepicker-disabled-day-color: var(--disabled-text-color);
--app-datepicker-focused-day-color: var(--text-primary-color);
--app-datepicker-focused-year-bg-color: var(--primary-color);
--app-datepicker-selector-color: var(--secondary-text-color);
--app-datepicker-separator-color: var(--divider-color);
--app-datepicker-weekday-color: var(--secondary-text-color);
}
app-datepicker::part(calendar-day):focus {
outline: none;
}
app-datepicker::part(body) {
direction: ltr;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
app-datepicker {
width: 100%;
}
}
`,
];
static styles = css`
ha-dialog {
--dialog-content-padding: 0;
}
calendar-date {
width: 100%;
}
calendar-date::part(button) {
border: none;
background-color: unset;
border-radius: var(--ha-border-radius-circle);
outline-offset: -2px;
outline-color: var(--ha-color-neutral-60);
}
calendar-month {
width: 100%;
margin: 0 auto;
min-height: calc(42px * 7);
}
calendar-month::part(heading) {
display: none;
}
calendar-month::part(day) {
color: var(--disabled-text-color);
font-size: var(--ha-font-size-m);
font-family: var(--ha-font-body);
}
calendar-month::part(button),
calendar-month::part(selected):focus-visible {
color: var(--primary-text-color);
height: 32px;
width: 32px;
margin: var(--ha-space-1);
border-radius: var(--ha-border-radius-circle);
}
calendar-month::part(button):focus-visible {
background-color: inherit;
outline: 1px solid var(--ha-color-neutral-60);
outline-offset: 2px;
}
calendar-month::part(button):hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
calendar-month::part(today) {
color: var(--primary-color);
}
calendar-month::part(selected),
calendar-month::part(selected):hover {
color: var(--text-primary-color);
background-color: var(--primary-color);
height: 40px;
width: 40px;
margin: 0;
}
calendar-month::part(selected):focus-visible {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.heading {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
}
.month-year {
flex: 1;
text-align: center;
margin-left: 48px;
}
`;
}
declare global {

View File

@@ -1517,6 +1517,7 @@
"use_original": "Use original"
},
"date-picker": {
"title": "Select date",
"today": "Today",
"clear": "Clear"
},

View File

@@ -3458,13 +3458,6 @@ __metadata:
languageName: node
linkType: hard
"@reallyland/esm@npm:^0.0.1":
version: 0.0.1
resolution: "@reallyland/esm@npm:0.0.1"
checksum: 10/c06c4d38663e128466485d65dc006ee5bcacde46ae6acf3a846ca8047f9ebef107a3f78b82cb2f6ba65aaea9d4b1dad52642626b6fff2e024ee855e3b01af726
languageName: node
linkType: hard
"@replit/codemirror-indentation-markers@npm:6.5.3":
version: 6.5.3
resolution: "@replit/codemirror-indentation-markers@npm:6.5.3"
@@ -4548,15 +4541,6 @@ __metadata:
languageName: node
linkType: hard
"@types/lodash-es@npm:^4.17.4":
version: 4.17.12
resolution: "@types/lodash-es@npm:4.17.12"
dependencies:
"@types/lodash": "npm:*"
checksum: 10/56b9a433348b11c31051c6fa9028540a033a08fb80b400c589d740446c19444d73b217cf1471d4036448ef686a83e8cf2a35d1fadcb3f2105f26701f94aebb07
languageName: node
linkType: hard
"@types/lodash.merge@npm:4.6.9":
version: 4.6.9
resolution: "@types/lodash.merge@npm:4.6.9"
@@ -4644,20 +4628,6 @@ __metadata:
languageName: node
linkType: hard
"@types/parse5@npm:^6.0.0":
version: 6.0.3
resolution: "@types/parse5@npm:6.0.3"
checksum: 10/834d40c9b1a8a99a9574b0b3f6629cf48adcff2eda01a35d701f1de5dcf46ce24223684647890aba9f985d6c801b233f878168683de0ae425940403c383fba8f
languageName: node
linkType: hard
"@types/prismjs@npm:^1.16.5":
version: 1.26.5
resolution: "@types/prismjs@npm:1.26.5"
checksum: 10/617099479db9550119d0f84272dc79d64b2cf3e0d7a17167fe740d55fdf0f155697d935409464392d164e62080c2c88d649cf4bc4fdd30a87127337536657277
languageName: node
linkType: hard
"@types/qrcode@npm:1.5.6":
version: 1.5.6
resolution: "@types/qrcode@npm:1.5.6"
@@ -5454,18 +5424,6 @@ __metadata:
languageName: node
linkType: hard
"app-datepicker@npm:5.1.1":
version: 5.1.1
resolution: "app-datepicker@npm:5.1.1"
dependencies:
"@material/mwc-button": "npm:^0.25.3"
lit: "npm:^2.2.1"
nodemod: "npm:2.8.4"
tslib: "npm:^2.3.1"
checksum: 10/0c16a597e18ce0f3ce20b4cd0aab9f29f09324ac79642e5e62e12f2fb7993e649352f9544d74a640029619a29b135bf0b105ed7336e097b5ad6e6b6c78b23e6a
languageName: node
linkType: hard
"arch@npm:^2.2.0":
version: 2.2.0
resolution: "arch@npm:2.2.0"
@@ -5689,6 +5647,13 @@ __metadata:
languageName: node
linkType: hard
"atomico@npm:^1.79.2":
version: 1.79.2
resolution: "atomico@npm:1.79.2"
checksum: 10/89cdb46cccab4e156d05464b1b1d4ba755868b75d69973c94c251798ffced6a2ed1cba80dfeb128693bd36ab836c0105d99801ef8ecdc162d131b0be029ba9a2
languageName: node
linkType: hard
"available-typed-arrays@npm:^1.0.7":
version: 1.0.7
resolution: "available-typed-arrays@npm:1.0.7"
@@ -6172,6 +6137,15 @@ __metadata:
languageName: node
linkType: hard
"cally@npm:0.9.2":
version: 0.9.2
resolution: "cally@npm:0.9.2"
dependencies:
atomico: "npm:^1.79.2"
checksum: 10/eeaff4d6f86a41247b51e432b1117ffbf967bd51c2e849d64ccfb7c477dde72ede88187c4497acd5e2cf1837f069832323411bc9505f7b6e3cd5f5b75cf7988e
languageName: node
linkType: hard
"camel-case@npm:^4.1.1, camel-case@npm:^4.1.2":
version: 4.1.2
resolution: "camel-case@npm:4.1.2"
@@ -8913,11 +8887,11 @@ __metadata:
"@vue/web-component-wrapper": "npm:1.3.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
app-datepicker: "npm:5.1.1"
babel-loader: "npm:10.1.1"
babel-plugin-template-html-minifier: "npm:4.1.0"
barcode-detector: "npm:3.1.1"
browserslist-useragent-regexp: "npm:4.1.3"
cally: "npm:0.9.2"
color-name: "npm:2.1.0"
comlink: "npm:4.4.2"
core-js: "npm:3.48.0"
@@ -10467,17 +10441,6 @@ __metadata:
languageName: node
linkType: hard
"lit-ntml@npm:^2.18.2":
version: 2.20.0
resolution: "lit-ntml@npm:2.20.0"
dependencies:
"@reallyland/esm": "npm:^0.0.1"
parse5: "npm:^6.0.1"
tslib: "npm:^2.0.2"
checksum: 10/db24a9a3914896d24bc7a51502445cbf508a53968729faf3dfb86909b6bc32ebf7348fac873ec3e3fc6fbf158a12d655684c9ed78599b425d8a1349d42fa2bef
languageName: node
linkType: hard
"lit@npm:3.3.2":
version: 3.3.2
resolution: "lit@npm:3.3.2"
@@ -11119,20 +11082,6 @@ __metadata:
languageName: node
linkType: hard
"nodemod@npm:2.8.4":
version: 2.8.4
resolution: "nodemod@npm:2.8.4"
dependencies:
"@types/lodash-es": "npm:^4.17.4"
"@types/parse5": "npm:^6.0.0"
"@types/prismjs": "npm:^1.16.5"
lit-ntml: "npm:^2.18.2"
normalize-diacritics: "npm:^2.13.2"
tslib: "npm:^2.1.0"
checksum: 10/3cccce96dea824791f30648a7d5064a4d4791448ce20b1ffdcae394b1009bca7c939efebe3ee00d3b2ba6311760318a364d4cc01f0f6663ada3eeb4a747d0777
languageName: node
linkType: hard
"nopt@npm:^9.0.0":
version: 9.0.0
resolution: "nopt@npm:9.0.0"
@@ -11144,15 +11093,6 @@ __metadata:
languageName: node
linkType: hard
"normalize-diacritics@npm:^2.13.2":
version: 2.14.0
resolution: "normalize-diacritics@npm:2.14.0"
dependencies:
tslib: "npm:^2.1.0"
checksum: 10/1c2297eb1821e728793e935f744ce12a5969b05db90300e84f06a418e998ce06184d172c608c532d290ae6af2ea2f71ad561e4340ea7473f6329b417deca90bd
languageName: node
linkType: hard
"normalize-path@npm:3.0.0, normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0":
version: 3.0.0
resolution: "normalize-path@npm:3.0.0"