diff --git a/elements/package.json b/elements/package.json
index 9685158d59..0a21d1639c 100644
--- a/elements/package.json
+++ b/elements/package.json
@@ -33,6 +33,7 @@
"./pf-clipboard-copy/pf-clipboard-copy.js": "./pf-clipboard-copy/pf-clipboard-copy.js",
"./pf-code-block/BaseCodeBlock.js": "./pf-code-block/BaseCodeBlock.js",
"./pf-code-block/pf-code-block.js": "./pf-code-block/pf-code-block.js",
+ "./pf-date-picker/pf-date-picker.js": "./pf-date-picker/pf-date-picker.js",
"./pf-dropdown/pf-dropdown.js": "./pf-dropdown/pf-dropdown.js",
"./pf-dropdown/pf-dropdown-group.js": "./pf-dropdown/pf-dropdown-group.ts",
"./pf-dropdown/pf-dropdown-menu.js": "./pf-dropdown/pf-dropdown-menu.ts",
diff --git a/elements/pf-date-picker/README.md b/elements/pf-date-picker/README.md
new file mode 100644
index 0000000000..cb75687097
--- /dev/null
+++ b/elements/pf-date-picker/README.md
@@ -0,0 +1,80 @@
+# Date Picker
+
+The Date Picker component lets users choose dates within a set range defined by both a minimum and maximum date. This feature makes it easy for users to select dates that fall within the specified limits
+
+## Usage
+
+The date-picker component includes an input field for entering dates, a toggle for opening and closing the calendar, and a popup calendar interface for selecting dates.
+
+```html
+
+ Date Format
+ <pf-date-picker date-format-input="YYYY-DD-MM"></pf-date-picker>
+
+ The date picker supports the following 12 date formats:
+ 'DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY/MM/DD', 'YYYY/DD/MM', 'DD-MM-YYYY', 'MM-DD-YYYY', 'YYYY-MM-DD',
+ 'YYYY-DD-MM', 'DD.MM.YYYY', 'MM.DD.YYYY', 'YYYY.MM.DD', 'YYYY.DD.MM'
+ The date format is set globally, independent of the user's locale.
+ This means that all users will see the same date format, regardless of their language or region.
+
+
+
+ Set Input Date
+ <pf-date-picker input-date=${new Date(2023, 0, 1)}></pf-date-picker>
+
+
+ The default date value can be passed to the date picker to set the initial date that is displayed.
+ In the above given example, the date set to be displayed initially is January 1, 2023 .
+
+
+
+ Disabled
+ <pf-date-picker disabled="true"></pf-date-picker>
+
+ The disabled attribute can be used to disable the date picker.
+
+
+
+ Localization
+ <pf-date-picker localization-language-code="fi"></pf-date-picker>
+
+ The locale string can be passed to the date picker to set the date format.
+ In the above given example, the locale is set to Finnish: fi
+ The date format is set globally, independent of the user's locale.
+ This means that all users will see the same date format, regardless of their language or region.
+
+
+
+ Set minimum and maximum date range
+ <pf-date-picker min-date=${new Date(2023, 0, 2)} max-date=${new Date(2023, 0, 20)}
+ inputDate=${new Date(2023, 0, 3)}></pf-date-picker>
+
+
+
+ The minimum and maximum valid dates can be passed to the date picker to restrict the range of dates that can be selected
+ In the above given example, the minimum valid date
+ is January 2, 2023 , and the maximum valid date is January 20, 2023
+
+
+
+ Basic
+ <pf-date-picker></pf-date-picker>
+
+ The basic date picker will use the user's locale to determine the date format. The default minimum valid date
+ is January 1, 1900 , and the default maximum valid date is December 31, 9999 .
+
+
+
+ Translation
+ <pf-date-picker translation-language-code="fr"></pf-date-picker>
+
+ The translation language string can be passed to the date picker to localize the month names.
+ In the above given example, the language is set to French: fr
+
+```
+
diff --git a/elements/pf-date-picker/date-picker-helper.ts b/elements/pf-date-picker/date-picker-helper.ts
new file mode 100644
index 0000000000..d92eab3cdf
--- /dev/null
+++ b/elements/pf-date-picker/date-picker-helper.ts
@@ -0,0 +1,344 @@
+export interface DateFormatDetails {
+ dateParts: string[];
+ literal: string | undefined;
+}
+
+export interface InputDate {
+ day: number;
+ month: number;
+ year: number;
+ literal: string;
+}
+
+export const days: string[] = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
+export const defaultWeekdays: number[] = [0, 1, 2, 3, 4, 5, 6]; // S, M, T, W, T, F, S
+export const defaultWeeks: number[] = [0, 1, 2, 3, 4, 5]; // 1 previous month week, 4 current month weeks, 1 next month week
+
+// Function to return date object
+export const getFormattedDate = (date: Date) => {
+ const focusDate = {
+ day: date.getDate(),
+ month: date.getMonth(),
+ year: date.getFullYear()
+ };
+
+ return focusDate;
+};
+
+
+// Function to get the date format locale parts
+export const getLocaleParts = (language?: string) => {
+ // Get the browser user locale - Commented for reference
+ // const userLocale: string = navigator.languages && navigator.languages.length ?
+ // navigator.languages[0] : navigator.language;
+
+ const { timeZone } = Intl.DateTimeFormat().resolvedOptions();
+ let formatter: Intl.DateTimeFormatPart[] = [];
+
+ // Set date format options
+ const options: Intl.DateTimeFormatOptions = {
+ timeZone: timeZone,
+ dateStyle: 'short',
+ localeMatcher: 'lookup'
+ };
+
+ // If there is locale passed from parent, pass locale else use "default"
+ const locale: string = language ? language : 'default';
+
+ // Try - Catch block is used to catch error and format date using default locale if invalid locale is passed from parent
+ try {
+ // Get the date components breakdown array
+ formatter = new Intl.DateTimeFormat(locale, options).formatToParts(new Date());
+ } catch (error) {
+ if (error) { // Get the date components breakdown array with default locale
+ formatter = new Intl.DateTimeFormat('default', options).formatToParts(new Date());
+ }
+ }
+
+ return formatter;
+};
+
+
+// Function to return the date values from user input
+export const getDateValues = (dateString: string, languageCode?: string, dateFormatInput?: string) => {
+ let parseDay!: number;
+ let parseMonth!: number;
+ let parseYear!: number;
+ let splitWith!: string;
+
+ // If there is a dateFormat input from parent, the input format will be applied
+ // else date format will be generated and applied from languageCode input or default locale
+ if (dateFormatInput && isInputDateFormatValid(dateFormatInput)) {
+ const dateFormatDetails: DateFormatDetails = parseDateFormat(dateFormatInput);
+ let index = 0;
+
+ dateFormatDetails.dateParts.map((part: string) => { // Generate the date format
+ switch (part) {
+ case 'MM':
+ parseMonth = index;
+ index++;
+ break;
+ case 'DD':
+ parseDay = index;
+ index++;
+ break;
+ case 'YYYY':
+ parseYear = index;
+ index++;
+ break;
+ default:
+ break;
+ }
+ });
+ splitWith = dateFormatDetails.literal ? dateFormatDetails.literal : '/';
+ } else {
+ const formatter: Intl.DateTimeFormatPart[] = getLocaleParts(languageCode);
+ let index = 0;
+
+ formatter.map((part: Intl.DateTimeFormatPart) => { // Generate the date format
+ switch (part.type) {
+ case 'month':
+ parseMonth = index;
+ index++;
+ break;
+ case 'day':
+ parseDay = index;
+ index++;
+ break;
+ case 'year':
+ parseYear = index;
+ index++;
+ break;
+ default:
+ splitWith = part.value;
+ }
+ });
+ }
+
+ const dateStringArray: string[] = dateString.split(splitWith);
+ const selectedDayInput: number = parseInt(dateStringArray[parseDay], 10);
+ const selectedMonthInput: number = parseInt(dateStringArray[parseMonth], 10);
+ const selectedYearInput: number = parseInt(dateStringArray[parseYear], 10);
+ const inputDate: InputDate = {
+ day: selectedDayInput,
+ month: selectedMonthInput,
+ year: selectedYearInput,
+ literal: splitWith
+ };
+
+ return inputDate;
+};
+
+// Function to return the fomatted date string
+export const getDateFormat = (day: string, month: string, year: string, languageCode?: string, dateFormatInput?: string) => {
+ let formattedDate = ``;
+ const dd: string = day;
+ const mm: string = month;
+ const yyyy: string = year;
+
+ // If there is a dateFormat input from parent, the input format will be applied
+ // else date format will be generated and applied from languageCode input or default locale
+ if (dateFormatInput && isInputDateFormatValid(dateFormatInput)) {
+ const dateFormatDetails: DateFormatDetails = parseDateFormat(dateFormatInput);
+
+ dateFormatDetails.dateParts.map((part: string, index: number) => { // Generate the date format
+ switch (part) {
+ case 'MM':
+ formattedDate += `${mm}`;
+ break;
+ case 'DD':
+ formattedDate += `${dd}`;
+ break;
+ case 'YYYY':
+ formattedDate += `${yyyy}`;
+ break;
+ default:
+ break;
+ }
+ if (index < 2) {
+ formattedDate += dateFormatDetails.literal ? dateFormatDetails.literal : '/';
+ }
+ });
+ } else {
+ const formatter: Intl.DateTimeFormatPart[] = getLocaleParts(languageCode);
+
+ formatter.map((part: Intl.DateTimeFormatPart) => { // Generate the date format
+ switch (part.type) {
+ case 'month':
+ formattedDate += `${mm}`;
+ break;
+ case 'day':
+ formattedDate += `${dd}`;
+ break;
+ case 'year':
+ formattedDate += `${yyyy}`;
+ break;
+ default:
+ formattedDate += part.value;
+ }
+ });
+ }
+
+ return formattedDate;
+};
+
+// Function to return date format based on date format input or locale
+export const getDatePatternFromLocale = (languageCode?: string, dateFormatInput?: string) => {
+ let localeDateFormat = '';
+
+ // If there is a dateFormat input from parent, the input format will be applied
+ // else date format will be generated and applied from languageCode input or default locale
+ if (dateFormatInput && isInputDateFormatValid(dateFormatInput)) {
+ localeDateFormat = dateFormatInput;
+ } else {
+ const formatter: Intl.DateTimeFormatPart[] = getLocaleParts(languageCode);
+
+ formatter.map((part: Intl.DateTimeFormatPart) => { // Generate the date format
+ switch (part.type) {
+ case 'month':
+ localeDateFormat += 'MM';
+ break;
+ case 'day':
+ localeDateFormat += 'DD';
+ break;
+ case 'year':
+ localeDateFormat += 'YYYY';
+ break;
+ default:
+ localeDateFormat += part.value;
+ }
+ });
+ }
+
+ return localeDateFormat;
+};
+
+// Function to generate regex pattern based on date format input or locale
+export const getRegexPattern = (languageCode?: string, dateFormatInput?: string) => {
+ const regDay = '[0-9]{1,2}';
+ const regMonth = '[0-9]{1,2}';
+ const regYear = '[0-9]{2,4}';
+ let regex = '^';
+
+ // If there is a dateFormat input from parent, the input format will be applied
+ // else date format will be generated and applied from languageCode input or default locale
+ if (dateFormatInput && isInputDateFormatValid(dateFormatInput)) {
+ const dateFormatDetails: DateFormatDetails = parseDateFormat(dateFormatInput);
+
+ dateFormatDetails.dateParts.map((part: string, index: number) => { // Generate the date format
+ switch (part) {
+ case 'MM':
+ regex += regMonth;
+ break;
+ case 'DD':
+ regex += regDay;
+ break;
+ case 'YYYY':
+ regex += regYear;
+ break;
+ default:
+ break;
+ }
+ if (index < 2) {
+ regex += dateFormatDetails.literal ? dateFormatDetails.literal : '/';
+ }
+ });
+ } else {
+ const formatter: Intl.DateTimeFormatPart[] = getLocaleParts(languageCode);
+
+ formatter.map((part: Intl.DateTimeFormatPart) => { // Generate the regex for the date format according to locale
+ switch (part.type) {
+ case 'month':
+ regex += regMonth;
+ break;
+ case 'day':
+ regex += regDay;
+ break;
+ case 'year':
+ regex += regYear;
+ break;
+ default:
+ regex += part.value; // Will append the part literal '/' or '.' or '-' to the regex
+ break;
+ }
+ });
+ }
+ regex += '$';
+
+ return regex;
+};
+
+// Function to generate date parts array from date format input
+export const parseDateFormat = (dateFormatInput: string) => {
+ const literals: string[] = ['/', '-', '.']; // Supported date format literals
+ let datePartsArray: string[] = [];
+
+ const literal = literals.find((literal: string) => { // Find the literal in the format
+ if (dateFormatInput.includes(literal)) {
+ return literal;
+ }
+ });
+
+ if (literal) {
+ datePartsArray = dateFormatInput.split(literal); // Split the format to date parts
+ }
+
+ const dateFormatParts = {
+ dateParts: datePartsArray,
+ literal: literal
+ };
+
+ return dateFormatParts;
+};
+
+// Function to check validity of the date format input
+export const isInputDateFormatValid = (dateFormatInput: string) => {
+ let isDateFormatValid = false;
+ const supportedDateFormats: string[] = [ // Supported date formats from parent
+ 'DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY/MM/DD', 'YYYY/DD/MM', 'DD-MM-YYYY', 'MM-DD-YYYY',
+ 'YYYY-MM-DD', 'YYYY-DD-MM', 'DD.MM.YYYY', 'MM.DD.YYYY', 'YYYY.MM.DD', 'YYYY.DD.MM'
+ ];
+
+ if (supportedDateFormats.includes(dateFormatInput)) {
+ isDateFormatValid = true;
+ } else {
+ isDateFormatValid = false;
+ }
+
+ return isDateFormatValid;
+};
+
+export const getMonthNamesFromLocale = (languageCode?: string) => {
+ const monthNames: string[] = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ ];
+
+ if (languageCode) {
+ const date: Date = new Date();
+ try {
+ const translatedMonthNames: string[] = [];
+ for (let i = 0; i < 12; i++) {
+ date.setMonth(i);
+ const monthName: string = new Intl.DateTimeFormat(languageCode, { month: 'long' }).format(new Date(date.getFullYear(), i, date.getDate()));
+ translatedMonthNames.push(monthName);
+ }
+ return translatedMonthNames;
+ } catch (error) {
+ if (error) {
+ return monthNames;
+ }
+ }
+ }
+ return monthNames;
+};
diff --git a/elements/pf-date-picker/demo/date-format.html b/elements/pf-date-picker/demo/date-format.html
new file mode 100644
index 0000000000..82b45f5ef0
--- /dev/null
+++ b/elements/pf-date-picker/demo/date-format.html
@@ -0,0 +1,44 @@
+
+
+ Date Format
+ <pf-date-picker date-format-input="YYYY-DD-MM"></pf-date-picker>
+
+ The date picker supports the following 12 date formats:
+ 'DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY/MM/DD', 'YYYY/DD/MM', 'DD-MM-YYYY', 'MM-DD-YYYY', 'YYYY-MM-DD',
+ 'YYYY-DD-MM', 'DD.MM.YYYY', 'MM.DD.YYYY', 'YYYY.MM.DD', 'YYYY.DD.MM'
+ The date format is set globally, independent of the user's locale.
+ This means that all users will see the same date format, regardless of their language or region.
+
+
+
+
+
+
+
+
diff --git a/elements/pf-date-picker/demo/date-input.html b/elements/pf-date-picker/demo/date-input.html
new file mode 100644
index 0000000000..051165741e
--- /dev/null
+++ b/elements/pf-date-picker/demo/date-input.html
@@ -0,0 +1,43 @@
+
+
+ Set Input Date
+ <pf-date-picker input-date=${new Date(2023, 0, 1)}></pf-date-picker>
+
+
+ The default date value can be passed to the date picker to set the initial date that is displayed.
+ In the above given example, the date set to be displayed initially is January 1, 2023 .
+
+
+
+
+
+
+
+
diff --git a/elements/pf-date-picker/demo/disabled.html b/elements/pf-date-picker/demo/disabled.html
new file mode 100644
index 0000000000..cd17d77957
--- /dev/null
+++ b/elements/pf-date-picker/demo/disabled.html
@@ -0,0 +1,40 @@
+
+
+ Disabled
+ <pf-date-picker disabled="true"></pf-date-picker>
+
+ The disabled attribute can be used to disable the date picker.
+
+
+
+
+
+
+
+
diff --git a/elements/pf-date-picker/demo/localization.html b/elements/pf-date-picker/demo/localization.html
new file mode 100644
index 0000000000..23d8dad125
--- /dev/null
+++ b/elements/pf-date-picker/demo/localization.html
@@ -0,0 +1,43 @@
+
+
+ Localization
+ <pf-date-picker localization-language-code="fi"></pf-date-picker>
+
+ The locale string can be passed to the date picker to set the date format.
+ In the above given example, the locale is set to Finnish: fi
+ The date format is set globally, independent of the user's locale.
+ This means that all users will see the same date format, regardless of their language or region.
+
+
+
+
+
+
+
+
diff --git a/elements/pf-date-picker/demo/min-and-max-date.html b/elements/pf-date-picker/demo/min-and-max-date.html
new file mode 100644
index 0000000000..217b85aed7
--- /dev/null
+++ b/elements/pf-date-picker/demo/min-and-max-date.html
@@ -0,0 +1,49 @@
+
+
+ Set minimum and maximum date range
+ <pf-date-picker min-date=${new Date(2023, 0, 2)} max-date=${new Date(2023, 0, 20)}
+ inputDate=${new Date(2023, 0, 3)}></pf-date-picker>
+
+
+
+ The minimum and maximum valid dates can be passed to the date picker to restrict the range of dates that can be selected
+ In the above given example, the minimum valid date
+ is January 2, 2023 , and the maximum valid date is January 20, 2023
+
+
+
+
+
+
+
+
diff --git a/elements/pf-date-picker/demo/pf-date-picker.html b/elements/pf-date-picker/demo/pf-date-picker.html
new file mode 100644
index 0000000000..f764e6829b
--- /dev/null
+++ b/elements/pf-date-picker/demo/pf-date-picker.html
@@ -0,0 +1,41 @@
+
+
+ Basic
+ <pf-date-picker></pf-date-picker>
+
+ The basic date picker will use the user's locale to determine the date format. The default minimum valid date
+ is January 1, 1900 , and the default maximum valid date is December 31, 9999 .
+
+
+
+
+
+
+
+
diff --git a/elements/pf-date-picker/demo/translation.html b/elements/pf-date-picker/demo/translation.html
new file mode 100644
index 0000000000..841535beec
--- /dev/null
+++ b/elements/pf-date-picker/demo/translation.html
@@ -0,0 +1,41 @@
+
+
+ Translation
+ <pf-date-picker translation-language-code="fr"></pf-date-picker>
+
+ The translation language string can be passed to the date picker to localize the month names.
+ In the above given example, the language is set to French: fr
+
+
+
+
+
+
+
+
diff --git a/elements/pf-date-picker/docs/pf-date-picker.md b/elements/pf-date-picker/docs/pf-date-picker.md
new file mode 100644
index 0000000000..89025c51c7
--- /dev/null
+++ b/elements/pf-date-picker/docs/pf-date-picker.md
@@ -0,0 +1,17 @@
+{% renderOverview %}
+
+{% endrenderOverview %}
+
+{% band header="Usage" %}{% endband %}
+
+{% renderSlots %}{% endrenderSlots %}
+
+{% renderAttributes %}{% endrenderAttributes %}
+
+{% renderMethods %}{% endrenderMethods %}
+
+{% renderEvents %}{% endrenderEvents %}
+
+{% renderCssCustomProperties %}{% endrenderCssCustomProperties %}
+
+{% renderCssParts %}{% endrenderCssParts %}
diff --git a/elements/pf-date-picker/docs/screenshot.png b/elements/pf-date-picker/docs/screenshot.png
new file mode 100644
index 0000000000..ba57f54dce
Binary files /dev/null and b/elements/pf-date-picker/docs/screenshot.png differ
diff --git a/elements/pf-date-picker/pf-calendar.css b/elements/pf-date-picker/pf-calendar.css
new file mode 100644
index 0000000000..678833b86f
--- /dev/null
+++ b/elements/pf-date-picker/pf-calendar.css
@@ -0,0 +1,74 @@
+:host {
+ display: block;
+}
+
+.date-picker-table-row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 0.125rem;
+}
+
+.date-picker-table-col {
+ height: 39px;
+ width: 39px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+pf-button:focus, pf-button:focus-visible {
+ border: 2px solid var(--rh-color-accent-base-on-light, #0066cc);
+ outline: none;
+}
+
+pf-button.calendar-date-button {
+ --pf-c-button--BorderRadius:50%;
+ width: 37px;
+ height: 37px;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ box-sizing: border-box;
+}
+
+pf-button.calendar-date-button.isToday {
+ --pf-c-button--m-plain--BackgroundColor: #f0f0f0;
+ --pf-c-button--m-plain--hover--BackgroundColor: var(--rh-color-blue-50, #e7f1fa);
+ --pf-c-button--m-plain--focus--BackgroundColor: var(--rh-color-blue-50, #e7f1fa);
+}
+
+pf-button.calendar-date-button:focus {
+ --pf-c-button--m-plain--BackgroundColor: var(--rh-color-blue-50, #e7f1fa);
+}
+
+pf-button.calendar-date-button {
+ --pf-c-button--m-plain--Color: var(--rh-color-canvas-black, #151515);
+ --pf-c-button--m-plain--hover--BackgroundColor: var(--rh-color-blue-50, #e7f1fa);
+ --pf-c-button--m-plain--focus--BackgroundColor: var(--rh-color-blue-50, #e7f1fa);
+ --pf-c-button--FontSize: var(--rh-font-size-body-text-sm, 0.875rem);
+
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+}
+
+pf-button.calendar-date-button.previous-next-date {
+ --pf-c-button--m-plain--Color: #6A6E73;
+}
+
+pf-button.calendar-date-button.selected-date {
+ --pf-c-button--m-plain--BackgroundColor: var(--rh-color-accent-base-on-light, #0066cc);
+ --pf-c-button--m-plain--Color: var(--rh-color-white, #ffffff);
+ --pf-c-button--m-plain--hover--Color: var(--rh-color-white, #ffffff);
+ --pf-c-button--m-plain--hover--BackgroundColor: var(--rh-color-interactive-blue-darkest, #004080);
+ --pf-c-button--m-plain--focus--BackgroundColor: var(--rh-color-interactive-blue-darkest, #004080);
+}
+
+pf-button.calendar-date-button.selected-date:focus {
+ box-shadow: 0 0 0.3125rem var(--rh-color-accent-base-on-light, #0066cc);
+ background: var(--rh-color-interactive-blue-darkest, #004080);
+ border: none;
+ outline: none;
+}
diff --git a/elements/pf-date-picker/pf-calendar.ts b/elements/pf-date-picker/pf-calendar.ts
new file mode 100644
index 0000000000..170e9ea64d
--- /dev/null
+++ b/elements/pf-date-picker/pf-calendar.ts
@@ -0,0 +1,282 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { property } from 'lit/decorators/property.js';
+import { ref, createRef } from 'lit/directives/ref.js';
+import styles from './pf-calendar.css';
+import {
+ getFormattedDate,
+ getMonthNamesFromLocale,
+ defaultWeekdays,
+ defaultWeeks,
+} from './date-picker-helper.js';
+
+export interface FocusedDateValues {
+ day: number;
+ month: number;
+ year: number;
+}
+
+export interface CalendarDateValues {
+ day: number;
+ month: number;
+ year: number;
+ isCurrentMonth: boolean;
+}
+
+/**
+ * Date Calendar
+ * @slot - Place element content here
+ */
+
+export class DayChangeEvent extends Event {
+ constructor(public event: Event, public day: number, public month: number, public year: number) {
+ super('daySelected', { bubbles: true, cancelable: true });
+ }
+}
+
+export class FocusChangeEvent extends Event {
+ constructor(public dateToBeFocusedRef: HTMLButtonElement | undefined) {
+ super('setDateFocus', { bubbles: true, cancelable: true });
+ }
+}
+
+export class KeyboardNavigationFocusEvent extends Event {
+ constructor(public event: Event, public day: number, public month: number, public year: number) {
+ super('onCalendarKeydown', { bubbles: true, cancelable: true });
+ }
+}
+
+/**
+ * Calendar
+ * @slot - Place element content here
+ */
+@customElement('pf-calendar')
+export class PfCalendar extends LitElement {
+ static readonly styles = [styles];
+
+ #currentDate = new Date();
+ #weekdays = defaultWeekdays;
+ #weeks = defaultWeeks;
+
+ // Input properties from the parent
+ @property() currentYear: number = this.#currentDate.getFullYear();
+ @property() currentMonth: number = this.#currentDate.getMonth();
+ @property() currentWeek = 0;
+ @property() selectedDay: number = this.#currentDate.getDate();
+ @property() focusRef!: HTMLButtonElement;
+ @property() dayToBeFocused!: number;
+ @property() focusedDateValues!: FocusedDateValues;
+ @property() dateSelected!: Date;
+ @property() firstDayToBeFocused!: number;
+ @property() minDate!: Date;
+ @property() maxDate!: Date;
+ @property() translationLanguageCode!: string;
+ @property() monthNames: string[] = getMonthNamesFromLocale(this.translationLanguageCode);
+ focusDateRef: any = createRef(); // Reference to the button that needs to be focused
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.#init();
+ if (this.translationLanguageCode) {
+ this.monthNames = getMonthNamesFromLocale(this.translationLanguageCode);
+ }
+ }
+
+ render() {
+ this.focusedDateValues = getFormattedDate(
+ new Date(this.currentYear, this.currentMonth, this.dayToBeFocused)
+ );
+
+ // Get the total number of days of the current month
+ const totalDays: number = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
+
+ // Find the day of the week for the first day of the month
+ const firstDay: number = new Date(this.currentYear, this.currentMonth, 1).getDay();
+
+ // Get the last date of the previous month
+ const previousMonthLastDate: number =
+ new Date(this.currentYear, this.currentMonth, 0).getDate();
+
+ // Get the number of weeks in a month
+ this.#weeks = this.#getNoOfWeeks(this.currentYear, this.currentMonth);
+
+ let day = 1;
+ let nextMonthPointer = 0;
+
+ return html`
+ ${this.#weeks.map((week: number) => {
+ return html`
+
+ ${this.#weekdays.map((weekDay: number) => {
+ let calendarDayTemplate: unknown;
+ if (week === 0 && weekDay < firstDay) {
+ // Add the last dates of the previous month
+ const dateValue: CalendarDateValues = {
+ day: (previousMonthLastDate - (firstDay - 1) + weekDay),
+ month: this.currentMonth - 1,
+ year: this.currentYear,
+ isCurrentMonth: false,
+ };
+ calendarDayTemplate = this.#renderCalendarDates(dateValue);
+ } else if (day > totalDays) {
+ // Add the first dates of the next month
+ const dateValue: CalendarDateValues = {
+ day: nextMonthPointer + 1,
+ month: this.currentMonth + 1,
+ year: this.currentYear,
+ isCurrentMonth: false,
+ };
+ calendarDayTemplate = this.#renderCalendarDates(dateValue);
+ nextMonthPointer++;
+ } else {
+ // Add the dates of the current month
+ const dateValue: CalendarDateValues = {
+ day: day,
+ month: this.currentMonth,
+ year: this.currentYear,
+ isCurrentMonth: true,
+ };
+ calendarDayTemplate = this.#renderCalendarDates(dateValue);
+ day++;
+ }
+ return calendarDayTemplate;
+ })}
+
`;
+ })}
+
`;
+ }
+
+ // Function to render the days of the calendar
+ #renderCalendarDates(value: CalendarDateValues) {
+ const isDayToBeFocused: boolean = ((value.day === this.dayToBeFocused)
+ && (value.month === this.focusedDateValues?.month)
+ && (value.year === this.focusedDateValues?.year));
+
+ const selectedDate: Date = new Date(this.dateSelected);
+ const isToday: boolean = ((value.day === this.#currentDate.getDate())
+ && (value.month === this.#currentDate.getMonth())
+ && (value.year === this.#currentDate.getFullYear()));
+
+ const isSelectedDay: boolean = ((value.day === selectedDate?.getDate())
+ && (value.month === selectedDate?.getMonth())
+ && (value.year === selectedDate?.getFullYear()));
+
+ const dateRendering = new Date(value.year, value.month, value.day);
+ const isDateInvalid = dateRendering.toString() === 'Invalid Date';
+ const isDateDisabled: boolean = isDateInvalid
+ || dateRendering < this.minDate || dateRendering > this.maxDate;
+
+
+ const dateValue = html`
+
this.#selectDate(e, value.month, value.year)}
+ class='${this.selectedDay && isSelectedDay ? 'selected-date' : ''} ${!value.isCurrentMonth ? 'previous-next-date' : ''} ${isToday ? 'isToday' : ''} calendar-date-button'>
+ ${value.day}
+
+
`;
+
+ return dateValue;
+ }
+
+ // Function to dispatch the selected date
+ #selectDate(event: Event, month: number, year: number) {
+ const { value } = event.target as HTMLButtonElement;
+ const selectedDay: number = parseInt(value);
+ let currentMonth: number = month;
+ let currentYear: number = year;
+ if (month < 0) {
+ currentMonth = 11;
+ currentYear = currentYear - 1;
+ }
+ if (month > 11) {
+ currentMonth = 0;
+ currentYear = currentYear + 1;
+ }
+
+ this.dispatchEvent(new DayChangeEvent(event, selectedDay, currentMonth, currentYear));
+ }
+
+ async #init() {
+ await this.updateComplete;
+ this.#setDateFocus();
+ }
+
+ // Function to focus date button;
+ #setDateFocus() {
+ const date: HTMLButtonElement = this.focusDateRef.value!;
+ this.focusRef = date;
+ setTimeout(() => {
+ date?.focus();
+ }, 25);
+ }
+
+ // Function to handle focus on property update
+ updated() {
+ this.dispatchEvent(new FocusChangeEvent(this.focusDateRef.value!));
+ this.#setDateFocus();
+ }
+
+ // Funtion to handle focus based on arrow keys
+ #onCalendarKeydown(event: KeyboardEvent) {
+ const date = new Date(this.currentYear, this.currentMonth, this.dayToBeFocused);
+
+ switch (event.key) {
+ case 'ArrowUp':
+ date.setDate(date.getDate() - 7);
+ break;
+ case 'ArrowDown':
+ date.setDate(date.getDate() + 7);
+ break;
+ case 'ArrowLeft':
+ date.setDate(date.getDate() - 1);
+ break;
+ case 'ArrowRight':
+ date.setDate(date.getDate() + 1);
+ break;
+ }
+
+ if (
+ event.key === 'ArrowUp'
+ || event.key === 'ArrowDown'
+ || event.key === 'ArrowLeft'
+ || event.key === 'ArrowRight'
+ ) {
+ this.focusedDateValues = getFormattedDate(date);
+ this.dispatchEvent(new KeyboardNavigationFocusEvent(
+ event,
+ this.focusedDateValues.day,
+ this.focusedDateValues.month,
+ this.focusedDateValues.year
+ )
+ );
+ }
+ }
+
+ // Find total number of weeks of the month
+ #getNoOfWeeks(year: number, month: number) {
+ const firstDayOfMonth: Date = new Date(year, month, 1);
+ const lastDayOfMonth: Date = new Date(year, month + 1, 0);
+
+ const totalNoOfDays: number = firstDayOfMonth.getDay() + lastDayOfMonth.getDate();
+ const totalWeeks: number = Math.ceil( totalNoOfDays / 7);
+
+ const weeks: number[] = [];
+
+ for (let i = 0; i < totalWeeks; i++) {
+ weeks.push(i);
+ }
+ return weeks;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-calendar': PfCalendar;
+ }
+}
diff --git a/elements/pf-date-picker/pf-date-picker.css b/elements/pf-date-picker/pf-date-picker.css
new file mode 100644
index 0000000000..9159896be1
--- /dev/null
+++ b/elements/pf-date-picker/pf-date-picker.css
@@ -0,0 +1,151 @@
+:host {
+ display: block;
+}
+
+.datepicker {
+ position: relative;
+ display: inline-block;
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+}
+
+.calendar {
+ width: auto;
+ min-width: 330px;
+ max-width: 330px;
+ background-color: var(--rh-color-white, #ffffff);
+ padding: var(--rh-space-md, 8px);
+ padding-bottom: 0;
+ box-sizing: border-box;
+}
+
+.date-picker-table {
+ display: block;
+ width: 100%;
+}
+
+.date-picker-table-row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.date-picker-table-main-header {
+ justify-content: space-around;
+}
+
+.date-picker-table-col {
+ height: 39px;
+ width: 39px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--rh-font-size-body-text-sm, 0.875rem);
+ font-style: normal;
+ font-weight: var(--rh-font-weight-body-text-regular, 400);
+ line-height: 21px;
+}
+
+.date-picker-table-month-year {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: fit-content;
+}
+
+.date-picker-week-days {
+ border-bottom: 1px solid #D2D2D2;
+ margin-bottom: var(--rh-space-md, 8px);
+ margin-top: var(--rh-space-md, 8px);
+}
+
+#date-input {
+ padding: 9px 10px;
+ border-radius: 0;
+ background-color: transparent;
+ cursor:text;
+ position: relative;
+ border: 1px solid #f0f0f0;
+ border-bottom: 1px solid #8b8d90;
+ border-right: none;
+ box-sizing: border-box;
+ width: 150px;
+ z-index: 1;
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+ font-size: var(--rh-font-size-body-text-md, 1rem);
+ font-style: normal;
+ font-weight: var(--rh-font-weight-body-text-regular, 400);
+ line-height: 24px;
+ height: 36px;
+}
+
+#date-input:focus {
+ border-color: var(--rh-color-accent-base-on-light, #0066cc);
+ outline: var(--rh-color-accent-base-on-light, #0066cc) auto 2px;
+}
+
+.date-input-box {
+ width: auto;
+ background-color: transparent;
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+}
+
+.date-input-box-container {
+ position: relative;
+}
+
+.isDateInvalid {
+ position: absolute;
+ top: 50%;
+ right: var(--rh-space-md, 8px);
+ transform: translateY(-50%);
+ color: #c9190b;
+ display: block;
+}
+
+.invalidDateInput {
+ border-bottom: 2px solid #c9190b !important;
+}
+
+.showInvalidText {
+ display: block;
+ color: #a30000;
+ font-size: var(--rh-font-size-body-text-sm, 0.875rem);
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+ position: absolute;
+ bottom: -24px;
+ left: 0;
+ white-space: nowrap;
+}
+
+.hidden {
+ display: none;
+}
+
+.date-picker-container pf-popover::part(content) {
+ min-width: 330px;
+}
+
+.disable-popover {
+ pointer-events: none;
+}
+
+pf-button:focus, pf-button:focus-visible {
+ outline: var(--rh-color-accent-base-on-light, #0066cc) auto 2px;
+}
+
+:host .date-picker-toggle-container pf-button.date-picker-calendar-icon {
+ height: 36px;
+ padding: 8px 12px;
+ box-sizing: border-box;
+}
+
+:host .date-picker-toggle-container pf-button.date-picker-calendar-icon:focus {
+ outline: none;
+}
+
+:host .date-picker-toggle-container pf-button.date-picker-calendar-icon:focus-visible {
+ outline: var(--rh-color-accent-base-on-light, #0066cc) auto 2px;
+}
\ No newline at end of file
diff --git a/elements/pf-date-picker/pf-date-picker.ts b/elements/pf-date-picker/pf-date-picker.ts
new file mode 100644
index 0000000000..39a1ab2ced
--- /dev/null
+++ b/elements/pf-date-picker/pf-date-picker.ts
@@ -0,0 +1,658 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { state } from 'lit/decorators/state.js';
+import { property } from 'lit/decorators/property.js';
+import '@patternfly/elements/pf-icon/pf-icon.js';
+import '@patternfly/elements/pf-button/pf-button.js';
+import '@patternfly/elements/pf-popover/pf-popover.js';
+import { PfPopover } from '@patternfly/elements/pf-popover/pf-popover.js';
+import { bound } from '@patternfly/pfe-core/decorators.js';
+import { query } from 'lit/decorators/query.js';
+import { classMap } from 'lit/directives/class-map.js';
+import {
+ days,
+ getDateFormat,
+ getDateValues,
+ getDatePatternFromLocale,
+ getRegexPattern,
+ getMonthNamesFromLocale,
+} from './date-picker-helper.js';
+import styles from './pf-date-picker.css';
+import './pf-previous-button.js';
+import './pf-month-select.js';
+import './pf-year-input.js';
+import './pf-next-button.js';
+import './pf-calendar.js';
+import { PreviousButtonClickEvent } from './pf-previous-button.js';
+import { NextButtonClickEvent } from './pf-next-button.js';
+import { MonthChangeEvent, MonthPopupStateChangeEvent } from './pf-month-select.js';
+import { DayChangeEvent, FocusChangeEvent, KeyboardNavigationFocusEvent } from './pf-calendar.js';
+
+/**
+ * Date Picker
+ * @slot - Place element content here
+ */
+
+interface ErrorMessages {
+ inValid: string;
+ lessThanMinDate: string;
+ greaterThanMaxDate: string;
+}
+
+export class DateChangeEvent extends Event {
+ constructor(public event: Event, public value: Date | null ) {
+ super('selectedDate', { bubbles: true, cancelable: true });
+ }
+}
+
+/*
+ * Date Picker
+ * @slot - Place element content here
+ */
+@customElement('pf-date-picker')
+export class PfDatePicker extends LitElement {
+ static readonly styles = [styles];
+
+ #currentDate = new Date();
+ #errorMessages: ErrorMessages = {
+ inValid: 'Invalid date',
+ lessThanMinDate: 'Date is before the allowable range.',
+ greaterThanMaxDate: 'Date is after the allowable range.',
+ };
+
+ @state() private isDateValid = true; // Checks if the date enetered by user in the input box is valid or not
+ @state() private isValid = true; // Checks if the year enetered by user in the year input box is valid or not
+ @state() private monthExpand = false; // Handles the closing and opening of the Month select
+
+ @query('#date-input') _textInput!: HTMLInputElement; // Date picker input box reference
+ @query('#popover') private _popover!: PfPopover; // Popover reference
+
+
+ // ------------------------Input properties from parent-------------------------------//
+ // -----------[***Only these properties can be used to pass data from parent***] ------------ //
+
+ // Default minimum valid date set as 1st January 1900
+ @property({
+ reflect: true,
+ attribute: 'min-date',
+ }) minDate: Date = new Date(1900, 0, 1);
+
+ // Default maximum valid date set as 31st December 9999
+ @property({
+ reflect: true,
+ attribute: 'max-date',
+ }) maxDate: Date = new Date(9999, 11, 31);
+
+ // Handle date value with a unique time stamp that parent sends to the component.
+ @property({
+ reflect: true,
+ attribute: 'input-date',
+ }) inputDateWithUniqueTimeStamp!: string;
+ // The format: inputDateWithUniqueTimeStamp = (new Date(2024, 3, 2)).toDateString() +'#'+ (Date.now() + Math.random()) //
+
+ // Handles if the date picker is disabled or not
+ @property({
+ reflect: true,
+ attribute: 'disabled',
+ }) isDisabled = false;
+
+ // Language code for date format based on localization
+ @property({
+ reflect: true,
+ attribute: 'localization-language-code',
+ }) localizationLanguageCode!: string;
+
+ // Language code for translation of date input and month names
+ @property({
+ reflect: true,
+ attribute: 'translation-language-code',
+ }) translationLanguageCode!: string;
+
+ // Date format input from parent
+ @property({
+ reflect: true,
+ attribute: 'date-format-input',
+ }) dateFormatInput!: 'MM-DD-YYYY' | 'YYYY-MM-DD' |
+ 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | 'YYYY/DD/MM' | 'DD-MM-YYYY' |
+ 'YYYY-DD-MM' | 'DD.MM.YYYY' | 'MM.DD.YYYY' | 'YYYY.MM.DD' | 'YYYY.DD.MM';
+
+ // Placeholder from parent
+ @property({
+ reflect: true,
+ attribute: 'placeholder',
+ }) placeholderTextWithUniqueCode!: string;
+ // The format: placeholderTextWithUniqueCode = 'placeholder-text' + '#' + Math.random();
+
+ // ----------------------Input properties from parent ends--------------------------------- //
+ // -------------------------------------------------------//
+
+
+ // 'current' refers to the temporary values of Month and Year the user selected before day is selected
+ // and the input box is updated
+ @property() monthNames: string[] = getMonthNamesFromLocale(this.translationLanguageCode);
+ @property() currentMonthSelection: string = this.monthNames[this.#currentDate.getMonth()];
+ @property() currentMonthIndex: number = this.#currentDate.getMonth();
+ @property() currentYear: number = this.#currentDate.getFullYear();
+
+ // 'selected' refers to the active selected values of day, Month and Year the user selected
+ // which is updated in the input box
+ @property() selectedDay!: number | null;
+ @property() selectedMonthIndex: number = this.#currentDate.getMonth();
+ @property() selectedMonthSelection: string = this.monthNames[this.#currentDate.getMonth()];
+ @property() selectedYear: number = this.#currentDate.getFullYear();
+ @property() formattedDate = ''; // The value which is updated in the date-picker input box
+ @property() errorMessage!: string; // Handle the error message on invalid date
+ @property() dateSelected!: Date; // The date selected by the user
+ @property() dayToBeFocused!: number; // Handles the day that needs to be focused
+ @property() dayToBeFocusedRef!: HTMLButtonElement | undefined; // Reference of the day that needs to be focused
+ @property() firstDayToBeFocused!: number; // Handles the day to be focused on popover open
+ @property() dateFormat: string =
+ getDatePatternFromLocale(this.localizationLanguageCode, this.dateFormatInput); // Date format
+
+ @property() inputDate!: Date | null; // Handle and format date input value parent sends to the component
+ #minYear = new Date(this.minDate).getFullYear(); // Minimum Valid Year
+ #maxYear: number = new Date(this.maxDate).getFullYear(); // Maximum Valid Year
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ document.addEventListener('click', this._onOutsideClick);
+ this.#init();
+ if (this.translationLanguageCode) {
+ this.monthNames = getMonthNamesFromLocale(this.translationLanguageCode);
+ }
+ if (this.dateFormatInput || this.localizationLanguageCode) {
+ this.dateFormat = getDatePatternFromLocale(
+ this.localizationLanguageCode,
+ this.dateFormatInput
+ );
+ }
+ }
+
+ willUpdate(changedProperties: Map) {
+ if (changedProperties.has('minDate')) {
+ this.minDate = new Date(this.minDate);
+ }
+
+ if (changedProperties.has('maxDate')) {
+ this.maxDate = new Date(this.maxDate);
+ }
+
+ if (changedProperties.has('inputDateWithUniqueTimeStamp')) {
+ // The unique timestamp is used to re-render the date-picker on date set and date reset
+ this.inputDate = new Date(this.inputDateWithUniqueTimeStamp.split('#')[0]);
+ this.#setInputDate();
+ }
+
+ if (changedProperties.has('placeholderTextWithUniqueCode')) {
+ const [placeholder, uniqueCode] = this.placeholderTextWithUniqueCode.split('#');
+ this.dateFormat = placeholder;
+ }
+ }
+
+ render() {
+ const invalidIconClasses = { isDateInvalid: !this.isDateValid, hidden: this.isDateValid };
+ const invalidTextClasses = { showInvalidText: !this.isDateValid, hidden: this.isDateValid };
+ const invalidInputClasses = { invalidDateInput: !this.isDateValid };
+
+
+ return html`
+
+ `;
+ }
+
+ async #init() {
+ await this.updateComplete;
+ /* The setTimeout function is added to allow the thread to wait until the page has
+ fully loaded before executing the #removePFPopoverDialogFromDOM() function. */
+ setTimeout(()=>{
+ this.#removePFPopoverDialogFromDOM();
+ }, 100);
+ this.#setInputDate();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ document.removeEventListener('click', this._onOutsideClick);
+ }
+
+ // Function to handle the closing of popover and month select popup on outside click
+ @bound private _onOutsideClick(event: MouseEvent) {
+ const path = event.composedPath();
+ if (!path.includes(this._popover)) {
+ if (this.monthExpand) {
+ this.monthExpand = false;
+ } else {
+ this._popover.hide();
+ }
+ }
+ }
+
+ // Function to set the date input from parent component
+ #setInputDate() {
+ if (this.inputDate?.toString() === 'Invalid Date') {
+ this.#clearDateSelection();
+ } else if (this.inputDate) {
+ this.selectedDay = new Date(this.inputDate).getDate();
+ this.selectedMonthIndex = new Date(this.inputDate).getMonth();
+ this.selectedYear = new Date(this.inputDate).getFullYear();
+ this.selectedMonthSelection = this.monthNames[this.selectedMonthIndex];
+ this.currentMonthIndex = new Date(this.inputDate).getMonth();
+ this.currentMonthSelection = this.monthNames[this.currentMonthIndex];
+ this.currentYear = new Date(this.inputDate).getFullYear();
+ this.dateSelected = this.inputDate;
+ this.#setDayToBeFocused();
+ this.#getFormattedDate();
+ }
+ }
+
+ // Function to clear date selected
+ #clearDateSelection() {
+ this.selectedDay = null;
+ this.currentMonthIndex = this.#currentDate.getMonth();
+ this.currentMonthSelection = this.monthNames[this.currentMonthIndex];
+ this.currentYear = this.#currentDate.getFullYear();
+ this.selectedMonthIndex = this.#currentDate.getMonth();
+ this.selectedYear = this.#currentDate.getFullYear();
+ this.selectedMonthSelection = this.monthNames[this.selectedMonthIndex];
+ this.formattedDate = '';
+ }
+
+ // Function to get the reference of the button that needs to be focused
+ #getDayRefToBeFocused(event: FocusChangeEvent) {
+ this.dayToBeFocusedRef = event.dateToBeFocusedRef;
+ }
+
+ // Function to get the month user selected
+ #getCurrentMonth(event: MonthChangeEvent) {
+ this.currentMonthSelection = event.name;
+ this.currentMonthIndex = event.index;
+ this.#setDayToBeFocused();
+ }
+
+ // Function to get the year user entered
+ #getCurrentYear(event: CustomEvent) {
+ if (event.detail < 100) {
+ this.currentYear = this.#validateYearInput(event.detail);
+ } else {
+ this.currentYear = event.detail;
+ }
+ this.#setDayToBeFocused();
+ }
+
+ // Function to get the year user entered on KeyUp/KeyDown
+ #getCurrentStepValue(event: CustomEvent) {
+ this.currentYear = this.#validateYearInput(event.detail);
+ this.#setDayToBeFocused();
+ }
+
+ // Function to check if the year entered by the user is valid or not
+ #validateYearInput(year: number) {
+ this.#minYear = new Date(this.minDate).getFullYear();
+ this.#maxYear = new Date(this.maxDate).getFullYear();
+ const yearInput: number = year;
+
+ this.isValid = yearInput > this.#minYear && yearInput < this.#maxYear;
+ if (this.isValid) {
+ return yearInput;
+ } else if (yearInput < this.#minYear) {
+ let inputYear: number;
+ const date: Date = new Date(year, this.currentMonthIndex, this.selectedDay ?
+ this.selectedDay : this.#currentDate.getDate());
+
+ if (date.getFullYear() >= this.#minYear) {
+ inputYear = date.getFullYear();
+ } else {
+ inputYear = year;
+ }
+ return inputYear;
+ } else if (yearInput > this.#maxYear) {
+ return this.#maxYear;
+ } else {
+ return this.currentYear;
+ }
+ }
+
+ // Function to get the year and month on previous button click
+ #getPreviousMonthAndYear(event: PreviousButtonClickEvent) {
+ this.currentMonthSelection = this.monthNames[event.month];
+ this.currentMonthIndex = event.month;
+ this.currentYear = event.year;
+ this.#setDayToBeFocused();
+ }
+
+ // Function to get the year and month on next button click
+ #getNextMonthAndYear(event: NextButtonClickEvent) {
+ this.currentMonthSelection = this.monthNames[event.month];
+ this.currentMonthIndex = event.month;
+ this.currentYear = event.year;
+ this.#setDayToBeFocused();
+ }
+
+ // Function to get the day to be focused on KeyDown
+ #getFocusDate(event: KeyboardNavigationFocusEvent) {
+ this.currentMonthSelection = this.monthNames[event.month];
+ this.currentMonthIndex = event.month;
+ this.currentYear = event.year;
+ this.dayToBeFocused = event.day;
+ }
+
+ // Function to get the selected day and set and update the input box
+ #getDaySelected(event: DayChangeEvent) {
+ this.selectedDay = event.day;
+ this.currentMonthSelection = this.monthNames[event.month];
+ this.currentMonthIndex = event.month;
+ this.selectedMonthIndex = event.month;
+ this.selectedYear = event.year;
+ this.currentYear = event.year;
+ this.selectedMonthSelection = this.monthNames[event.month];
+ this.monthExpand = false;
+ this.#getFormattedDate();
+ this.#dispatchSelectedDate(event);
+ this.isDateValid = true;
+ this._popover.hide();
+ }
+
+ // Function to dispatch selected date values to the parent component
+ #dispatchSelectedDate(event: Event) {
+ if (this.selectedDay) {
+ this.dateSelected = new Date(this.selectedYear, this.selectedMonthIndex, this.selectedDay);
+ this.dispatchEvent(new DateChangeEvent(event, this.dateSelected));
+ } else {
+ this.dispatchEvent(new DateChangeEvent(event, null));
+ }
+ }
+
+ // Function to get the month select expansion state
+ #getMonthExpandState(event: MonthPopupStateChangeEvent) {
+ this.monthExpand = event.isExpanded;
+ }
+
+ // Function to handle opening of Popover
+ #openPopover() {
+ this.#addPFPopoverDialogToDOM();
+ if (this.isDateValid) {
+ this.currentMonthIndex = this.selectedMonthIndex;
+ this.currentYear = this.selectedYear;
+ this.currentMonthSelection = this.selectedMonthSelection;
+ if (this.selectedDay) {
+ this.#getFormattedDate();
+ this.firstDayToBeFocused = this.selectedDay;
+ } else {
+ this.firstDayToBeFocused = this.#currentDate.getDate();
+ }
+ } else {
+ this.currentMonthIndex = this.#currentDate.getMonth();
+ this.currentYear = this.#currentDate.getFullYear();
+ this.currentMonthSelection = this.monthNames[this.currentMonthIndex];
+ this.firstDayToBeFocused = this.#currentDate.getDate();
+ this.selectedDay = null;
+ }
+
+ this.#setDayToBeFocused();
+ setTimeout(() => {
+ this.dayToBeFocusedRef?.focus();
+ }, 50);
+ }
+
+ // Function to set the day to be focused
+ #setDayToBeFocused() {
+ const totalDays: number = new Date(this.currentYear, this.currentMonthIndex + 1, 0).getDate();
+ if (this.isDateValid && this.selectedDay) {
+ this.dayToBeFocused = this.selectedDay <= totalDays ? this.selectedDay : totalDays;
+ } else {
+ this.dayToBeFocused = this.#currentDate.getDate();
+ }
+ }
+
+ // Function to handle click on date input box
+ #dateInputClick() {
+ this._popover.hide();
+ setTimeout(() => {
+ this._textInput.focus();
+ }, 1);
+ }
+
+ // Function to handle date input by user
+ #onInput(event: Event) {
+ const { value } = event.target as HTMLInputElement;
+ this.formattedDate = value;
+
+ this.isDateValid = this.#isValidDate(value);
+
+ if (this.isDateValid && value !== '') {
+ const dateValues = getDateValues(value, this.localizationLanguageCode, this.dateFormatInput);
+ this.selectedDay = dateValues.day;
+ this.currentMonthIndex = dateValues.month - 1;
+ this.currentMonthSelection = this.monthNames[this.currentMonthIndex];
+ this.currentYear = dateValues.year;
+ this.selectedMonthIndex = dateValues.month - 1;
+ this.selectedYear = dateValues.year;
+ this.selectedMonthSelection = this.monthNames[this.selectedMonthIndex];
+ this.#dispatchSelectedDate(event);
+ } else if (this.isDateValid && value === '') {
+ this.selectedDay = null;
+ this.currentMonthIndex = this.#currentDate.getMonth();
+ this.currentMonthSelection = this.monthNames[this.currentMonthIndex];
+ this.currentYear = this.#currentDate.getFullYear();
+ this.selectedMonthIndex = this.#currentDate.getMonth();
+ this.selectedYear = this.#currentDate.getFullYear();
+ this.selectedMonthSelection = this.monthNames[this.selectedMonthIndex];
+ this.#dispatchSelectedDate(event);
+ }
+ }
+
+ // Function to check if the date user entered is valid or not
+ #isValidDate(dateString: string) {
+ let isValid = true;
+ if (dateString) {
+ const monthLength: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+ // const regex = /^[0-9./-]*$/g; // Commented for reference
+
+ // Parse the date parts to integers
+ const dateValues = getDateValues(
+ dateString,
+ this.localizationLanguageCode,
+ this.dateFormatInput
+ );
+ const selectedDayInput: number = dateValues.day;
+ const selectedMonthInput: number = dateValues.month;
+ const selectedYearInput: number = dateValues.year;
+ const date: Date = new Date(selectedYearInput, selectedMonthInput - 1, selectedDayInput);
+ const regex = new RegExp(
+ getRegexPattern(this.localizationLanguageCode, this.dateFormatInput)
+ );
+
+ if (date.toString() === 'Invalid Date') {
+ this.errorMessage = this.#errorMessages.inValid;
+ return false;
+ }
+ if (date < this.minDate) {
+ this.errorMessage = this.#errorMessages.lessThanMinDate;
+ return false;
+ }
+ if (date > this.maxDate) {
+ this.errorMessage = this.#errorMessages.greaterThanMaxDate;
+ return false;
+ }
+
+ // if (!dateString.match(regex)) { // Commented for reference
+ if (!regex.test(dateString)) {
+ isValid = false;
+ this.errorMessage = this.#errorMessages.inValid;
+ return isValid;
+ }
+
+ // Check the ranges of month and year
+ if (selectedYearInput < this.#minYear || selectedYearInput > this.#maxYear
+ || selectedMonthInput === 0 || selectedMonthInput > 12) {
+ this.errorMessage = this.#errorMessages.inValid;
+ return false;
+ }
+
+ // Adjust for leap years
+ if (selectedYearInput % 400 === 0 || (
+ selectedYearInput % 100 !== 0 && selectedYearInput % 4 === 0)) {
+ monthLength[1] = 29;
+ }
+
+ // Check the range of the day
+ isValid = selectedDayInput > 0 && selectedDayInput <= monthLength[selectedMonthInput - 1];
+ if (!isValid) {
+ this.errorMessage = this.#errorMessages.inValid;
+ }
+ } else if (dateString === '') {
+ isValid = true;
+ }
+
+ return isValid;
+ }
+
+ // Function to get the formatted date string according to date format selected in parent component
+ #getFormattedDate() {
+ const monthSelected: number = this.currentMonthIndex + 1; // Months start at 0!
+ const daySelected: number = this.selectedDay ? this.selectedDay : this.#currentDate.getDate();
+ const yearSelected: number = this.selectedYear;
+ let dd: string = daySelected.toString();
+ let mm: string = monthSelected.toString();
+ const yyyy: string = yearSelected.toString();
+
+ if (daySelected < 10) {
+ dd = `0${dd}`;
+ }
+ if (monthSelected < 10) {
+ mm = `0${mm}`;
+ }
+
+ this.formattedDate = getDateFormat(
+ dd,
+ mm,
+ yyyy,
+ this.localizationLanguageCode,
+ this.dateFormatInput
+ );
+ }
+
+ /* The functions #removePFPopoverDialogFromDOM() and #addPFPopoverDialogToDOM() are added in in order to prevent
+ the responsiveness issue created by pf-popover and can be removed once https://github.com/patternfly/patternfly-elements/issues/2648 is addressed.
+
+ Issue: The pf-popover implementation maintains the popover dialog element in the DOM even when it is not visible or active.
+ This persistent presence in the DOM causes unintended visual effects, including spacing inconsistencies and
+ horizontal scroll issues, particularly when the popover is positioned at the far right edge of the page. */
+
+ #removePFPopoverDialogFromDOM() {
+ this._popover?.shadowRoot?.querySelector('dialog#popover')?.
+ setAttribute('style', 'display:none');
+ }
+
+ #addPFPopoverDialogToDOM() {
+ this._popover?.shadowRoot?.querySelector('dialog#popover')?.
+ setAttribute('style', 'display:block');
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-date-picker': PfDatePicker;
+ }
+}
diff --git a/elements/pf-date-picker/pf-month-select.css b/elements/pf-date-picker/pf-month-select.css
new file mode 100644
index 0000000000..d90b65dc7c
--- /dev/null
+++ b/elements/pf-date-picker/pf-month-select.css
@@ -0,0 +1,74 @@
+:host {
+ display: block;
+}
+
+.month-select-container {
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+ position: relative;
+ width: 140px;
+}
+
+pf-button#date-picker-month-select {
+ width: 140px;
+ background: transparent;
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+ text-align: left;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: relative;
+ box-sizing: border-box;
+ height: 36px;
+}
+
+.date-month-select-icon {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ left: 92px;
+ width: 36px;
+ height: 34px;
+}
+
+div#date-picker-month-select-popup {
+ width: 140px;
+ background: var(--rh-color-white, #ffffff);
+ box-shadow: 0 0.25rem 0.5rem 0 rgba(3, 3, 3, 0.12), 0 0 0.25rem 0 rgba(3, 3, 3, 0.06);
+ position: absolute;
+ top: 36px;
+ left: 0;
+ z-index: 1;
+}
+
+div#date-picker-month-select-popup ul {
+ margin-block-start: 0;
+ margin-block-end: 0;
+ padding-inline-start: 0;
+ list-style: none;
+ padding: 8px 0;
+}
+
+div#date-picker-month-select-popup pf-button {
+ --pf-c-button--m-plain--Color: var(--rh-color-canvas-black, #151515);
+ --pf-c-button--m-plain--hover--BackgroundColor: var(--rh-color-blue-50, #e7f1fa);
+
+ width: 140px;
+ margin-bottom: .125rem;
+}
+
+div#date-picker-month-select-popup pf-button {
+ background: var(--rh-color-white, #ffffff);
+ text-align: left;
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+ width: 140px;
+ border-radius: 0;
+ box-sizing: border-box;
+}
+
+div#date-picker-month-select-popup pf-button:hover {
+ background:#f0f0f0;
+}
+
+pf-button:focus, pf-button:focus-visible {
+ outline: var(--rh-color-accent-base-on-light, #0066cc) auto 2px;
+}
\ No newline at end of file
diff --git a/elements/pf-date-picker/pf-month-select.ts b/elements/pf-date-picker/pf-month-select.ts
new file mode 100644
index 0000000000..32a4c7d309
--- /dev/null
+++ b/elements/pf-date-picker/pf-month-select.ts
@@ -0,0 +1,152 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { property } from 'lit/decorators/property.js';
+import { bound } from '@patternfly/pfe-core/decorators.js';
+import { query } from 'lit/decorators/query.js';
+import styles from './pf-month-select.css';
+import { createRef, ref } from 'lit/directives/ref.js';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { getMonthNamesFromLocale } from './date-picker-helper.js';
+
+export class MonthChangeEvent extends Event {
+ constructor(public event: Event, public name: string, public index: number) {
+ super('currentMonth', { bubbles: true, cancelable: true });
+ }
+}
+
+export class MonthPopupStateChangeEvent extends Event {
+ constructor(public event: Event, public isExpanded: boolean) {
+ super('monthExpandState', { bubbles: true, cancelable: true });
+ }
+}
+
+/**
+ * Month Select
+ * @slot - Place element content here
+ */
+@customElement('pf-month-select')
+export class PfMonthSelect extends LitElement {
+ static readonly styles = [styles];
+
+ // Input properties from the parent
+ @property() translationLanguageCode!: string;
+ @property() monthNames: string[] = getMonthNamesFromLocale(this.translationLanguageCode);
+ @property() currentDate: Date = new Date();
+ @property() currentMonthName: string = this.monthNames[this.currentDate.getMonth()];
+ @property() currentMonthIndex: number = this.currentDate.getMonth();
+ @property() isMonthExpanded = false;
+ @property() monthToBeFocused!: number;
+
+ focusMonthRef: any = createRef(); // Reference to the month that needs to be focused on keyboard navigation
+
+ @query('#date-picker-month-select') private datePickerMonthToggle!: HTMLButtonElement;
+
+ connectedCallback() {
+ super.connectedCallback();
+ document.addEventListener('click', this._onOutsideClick);
+ if (this.translationLanguageCode) {
+ this.monthNames = getMonthNamesFromLocale(this.translationLanguageCode);
+ this.currentMonthName = this.monthNames[this.currentDate.getMonth()];
+ }
+ }
+
+ render() {
+ return html`
+
+
+
+ ${this.currentMonthName}
+
+
+
+
+ `;
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ document.removeEventListener('click', this._onOutsideClick);
+ }
+
+ // Function to focus month on keyboard navigation
+ updated(changedProperties: Map) {
+ if (changedProperties.has('monthToBeFocused')) {
+ const month: HTMLButtonElement | undefined = this.focusMonthRef.value!;
+ month?.focus();
+ }
+
+ if (changedProperties.has('translationLanguageCode')) {
+ this.monthNames = getMonthNamesFromLocale(this.translationLanguageCode);
+ this.currentMonthName = this.monthNames[this.currentMonthIndex];
+ }
+ }
+
+ // Function to handle closing of month select popup on clicking outside
+ @bound private _onOutsideClick(event: MouseEvent) {
+ const path = event.composedPath();
+ if (!path.includes(this.datePickerMonthToggle) && this.isMonthExpanded) {
+ this.isMonthExpanded = false;
+ }
+ }
+
+ // Function to dispatch selected month
+ #selectMonth(event: Event, monthName: string, monthIndex: number) {
+ this.currentMonthIndex = monthIndex;
+ if (monthName && (monthName.toString() !== this.currentMonthName)) {
+ this.currentMonthName = monthName.toString();
+ this.dispatchEvent(
+ new MonthChangeEvent(event, this.currentMonthName, this.currentMonthIndex)
+ );
+ }
+ this.isMonthExpanded = !this.isMonthExpanded;
+ }
+
+ // Function to handle focus on keyboard navigation
+ #onMonthSelectKeydown(event: KeyboardEvent) {
+ switch (event.key) {
+ case 'ArrowUp':
+ if (this.monthToBeFocused > 0) {
+ this.monthToBeFocused = this.monthToBeFocused - 1;
+ } else {
+ this.monthToBeFocused = 11;
+ }
+ break;
+ case 'ArrowDown':
+ if (this.monthToBeFocused < 11) {
+ this.monthToBeFocused = this.monthToBeFocused + 1;
+ } else {
+ this.monthToBeFocused = 0;
+ }
+ break;
+ }
+ }
+
+ // Function to dispatch value to handle the opening and closing of month select popup
+ #showMonthSelect(event: Event) {
+ this.isMonthExpanded = !this.isMonthExpanded;
+ this.monthToBeFocused = -1;
+ this.dispatchEvent(new MonthPopupStateChangeEvent(event, this.isMonthExpanded));
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-month-select': PfMonthSelect;
+ }
+}
diff --git a/elements/pf-date-picker/pf-next-button.css b/elements/pf-date-picker/pf-next-button.css
new file mode 100644
index 0000000000..9fa3db4714
--- /dev/null
+++ b/elements/pf-date-picker/pf-next-button.css
@@ -0,0 +1,21 @@
+:host {
+ display: block;
+}
+
+.date-previous-next-button {
+ --pf-c-button--m-plain--Color: #6a6e73;
+
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--rh-color-white, #ffffff);
+ border: none;
+ line-height: 0.5rem;
+ box-sizing: border-box;
+}
+
+pf-button:focus, pf-button:focus-visible {
+ outline: var(--rh-color-accent-base-on-light, #0066cc) auto 2px;
+}
\ No newline at end of file
diff --git a/elements/pf-date-picker/pf-next-button.ts b/elements/pf-date-picker/pf-next-button.ts
new file mode 100644
index 0000000000..a611905613
--- /dev/null
+++ b/elements/pf-date-picker/pf-next-button.ts
@@ -0,0 +1,52 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { property } from 'lit/decorators/property.js';
+
+import styles from './pf-next-button.css';
+
+export class NextButtonClickEvent extends Event {
+ constructor(public event: Event, public month: number, public year: number) {
+ super('nextMonthAndYear', { bubbles: true, cancelable: true });
+ }
+}
+
+/**
+ * Next Button
+ * @slot - Place element content here
+ */
+@customElement('pf-next-button')
+export class PfNextButton extends LitElement {
+ static readonly styles = [styles];
+
+ // Input properties from the parent
+ @property() currentYear: number = (new Date).getFullYear();
+ @property() currentMonth: number = (new Date).getMonth();
+
+ render() {
+ return html`
+
+
+
+ `;
+ }
+
+ // Function to dispatch month and year
+ #nextMonth(event: Event) {
+ // Increment the current month by one
+ this.currentMonth++;
+
+ // If the current month is greater than 11 (December), set it to 0 (January) and increment the current year
+ if (this.currentMonth > 11) {
+ this.currentMonth = 0;
+ this.currentYear++;
+ }
+
+ this.dispatchEvent(new NextButtonClickEvent(event, this.currentMonth, this.currentYear));
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-next-button': PfNextButton;
+ }
+}
diff --git a/elements/pf-date-picker/pf-previous-button.css b/elements/pf-date-picker/pf-previous-button.css
new file mode 100644
index 0000000000..f73131f05d
--- /dev/null
+++ b/elements/pf-date-picker/pf-previous-button.css
@@ -0,0 +1,21 @@
+:host {
+ display: block;
+}
+
+.date-previous-next-button {
+ --pf-c-button--m-plain--Color: #6a6e73;
+
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--rh-color-white, #ffffff);
+ border: none;
+ line-height: 0.5rem;
+ box-sizing: border-box;
+}
+
+pf-button:focus, pf-button:focus-visible {
+ outline: var(--rh-color-accent-base-on-light, #0066cc) auto 2px;
+}
\ No newline at end of file
diff --git a/elements/pf-date-picker/pf-previous-button.ts b/elements/pf-date-picker/pf-previous-button.ts
new file mode 100644
index 0000000000..8352b1c1f4
--- /dev/null
+++ b/elements/pf-date-picker/pf-previous-button.ts
@@ -0,0 +1,50 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { property } from 'lit/decorators/property.js';
+import styles from './pf-previous-button.css';
+
+export class PreviousButtonClickEvent extends Event {
+ constructor(public event: Event, public month: number, public year: number) {
+ super('previousMonthAndYear', { bubbles: true, cancelable: true });
+ }
+}
+/**
+ * Previous Button
+ * @slot - Place element content here
+ */
+@customElement('pf-previous-button')
+export class PfPreviousButton extends LitElement {
+ static readonly styles = [styles];
+
+ // Input properties from the parent
+ @property() currentYear: number = (new Date).getFullYear();
+ @property() currentMonth: number = (new Date).getMonth();
+
+ render() {
+ return html`
+
+
+
+ `;
+ }
+
+ // Function to dispatch month and year
+ #previousMonth(event: Event) {
+ // Decrement the current month by one
+ this.currentMonth--;
+
+ // If the current month is less than 0, set it to 11 (December) and decrement the current year
+ if (this.currentMonth < 0) {
+ this.currentMonth = 11;
+ this.currentYear--;
+ }
+
+ this.dispatchEvent(new PreviousButtonClickEvent(event, this.currentMonth, this.currentYear));
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-previous-button': PfPreviousButton;
+ }
+}
diff --git a/elements/pf-date-picker/pf-year-input.css b/elements/pf-date-picker/pf-year-input.css
new file mode 100644
index 0000000000..0bf1506dc7
--- /dev/null
+++ b/elements/pf-date-picker/pf-year-input.css
@@ -0,0 +1,21 @@
+:host {
+ display: block;
+}
+
+.date-year-input-container input {
+ height: 36px;
+ box-sizing: border-box;
+ border-radius: 0;
+ border: 1px solid #f0f0f0;
+ border-left: none;
+ border-bottom: 1px solid #8b8d90;
+ padding: 6px 8px;
+ font-size: var(--rh-font-size-body-text-md, 1rem);
+ font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif);
+ width: 80px;
+}
+
+.date-year-input-container input:focus-visible {
+ border-left: 2px;
+ outline: var(--rh-color-accent-base-on-light, #0066cc) auto 2px;
+}
\ No newline at end of file
diff --git a/elements/pf-date-picker/pf-year-input.ts b/elements/pf-date-picker/pf-year-input.ts
new file mode 100644
index 0000000000..61c66e9fb8
--- /dev/null
+++ b/elements/pf-date-picker/pf-year-input.ts
@@ -0,0 +1,86 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { property } from 'lit/decorators/property.js';
+import { query } from 'lit/decorators/query.js';
+
+import styles from './pf-year-input.css';
+
+/**
+ * Year Input
+ * @slot - Place element content here
+ */
+@customElement('pf-year-input')
+export class PfYearInput extends LitElement {
+ static readonly styles = [styles];
+
+ // Input properties from the parent
+ @property() currentYear: number = (new Date).getFullYear();
+ @property() isValid = true;
+ @property() minDate!: Date;
+ @property() maxDate!: Date;
+
+ #minYear = new Date(this.minDate).getFullYear();
+ #maxYear = new Date(this.maxDate).getFullYear();
+
+ @query('#year-input') _numberInput!: HTMLInputElement;
+
+ render() {
+ return html`
+
+
+
+ `;
+ }
+
+ // Function to hanlde year input on input
+ #OnInput(event: Event) {
+ const { value } = event.target as HTMLInputElement;
+ this.#dispatchInputYear(parseInt(value), 'setCurrentYear');
+ }
+
+ // Function to handle year input on step
+ #onChange(event: Event) {
+ const { value } = event.target as HTMLInputElement;
+ this.#dispatchInputYear(parseInt(value), 'currentYearOnStep');
+ }
+
+ // Function to dispatch year input
+ #dispatchInputYear(value: number, eventName: string) {
+ if (isNaN(value)) {
+ this.currentYear = 0;
+ } else {
+ this.currentYear = value;
+ }
+
+ const options = {
+ detail: this.currentYear,
+ bubbles: true,
+ composed: true,
+ };
+ this.dispatchEvent(new CustomEvent(eventName, options));
+ }
+
+ // Function to handle year input on keyUp
+ #onKeyUp(event: KeyboardEvent) {
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
+ this.#dispatchInputYear(this.currentYear, 'currentYearOnStep');
+ }
+ }
+}
+
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-year-input': PfYearInput;
+ }
+}
diff --git a/elements/pf-date-picker/test/pf-date-picker.e2e.ts b/elements/pf-date-picker/test/pf-date-picker.e2e.ts
new file mode 100644
index 0000000000..b99ec8b803
--- /dev/null
+++ b/elements/pf-date-picker/test/pf-date-picker.e2e.ts
@@ -0,0 +1,12 @@
+import { test } from '@playwright/test';
+import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js';
+
+const tagName = 'pf-date-picker';
+
+test.describe(tagName, () => {
+ test('snapshot', async ({ page }) => {
+ const componentPage = new PfeDemoPage(page, tagName);
+ await componentPage.navigate();
+ await componentPage.snapshot();
+ });
+});
diff --git a/elements/pf-date-picker/test/pf-date-picker.spec.ts b/elements/pf-date-picker/test/pf-date-picker.spec.ts
new file mode 100644
index 0000000000..d6d969dee3
--- /dev/null
+++ b/elements/pf-date-picker/test/pf-date-picker.spec.ts
@@ -0,0 +1,21 @@
+import { expect, html } from '@open-wc/testing';
+import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js';
+import { PfDatePicker } from '@patternfly/elements/pf-date-picker/pf-date-picker.js';
+
+describe('', function() {
+ describe('simply instantiating', function() {
+ let element: PfDatePicker;
+ it('imperatively instantiates', function() {
+ expect(document.createElement('pf-date-picker')).to.be.an.instanceof(PfDatePicker);
+ });
+
+ it('should upgrade', async function() {
+ element = await createFixture(html` `);
+ const klass = customElements.get('pf-date-picker');
+ expect(element)
+ .to.be.an.instanceOf(klass)
+ .and
+ .to.be.an.instanceOf(PfDatePicker);
+ });
+ });
+});
diff --git a/elements/pf-tabs/pf-tab-panel.ts b/elements/pf-tabs/pf-tab-panel.ts
index a81b028ca2..d70f883310 100644
--- a/elements/pf-tabs/pf-tab-panel.ts
+++ b/elements/pf-tabs/pf-tab-panel.ts
@@ -11,9 +11,7 @@ import styles from './pf-tab-panel.css';
/**
* @slot - Tab panel content
- *
* @cssprop {} --pf-c-tab-content--m-light-300 {@default `#f0f0f0`}
- *
* @csspart container - container for the panel content
*/
@customElement('pf-tab-panel')