@postnord/web-components
Version:
PostNord Web Components
1,132 lines • 68.5 kB
JavaScript
/*!
* Built with Stencil
* By PostNord.
*/
import { h, Host } from "@stencil/core";
import { awaitTopbar, uuidv4, en, getTotalHeightOffset } from "../../../index";
import { translations } from "./translations";
import { CALENDAR, MONTHS, YEARS, validateDate, isBefore, isAfter, selectedDate, getDateObject, getDiff, getToday, getDate, getReadableDate, getGrid, setYear, setMonth, navigateGrid, } from "../../../globals/date/index";
import { calendar, arrow_left, arrow_right, pn_return } from "pn-design-assets/pn-assets/icons.js";
/**
* The date picker allows a single or a range of dates to be selected.
*
* Based on the `format` prop, separators will automatically be added if you type the date.
*
* You can navigate the calendar grid with your keyboard.
*
* @nativeInput Use the `input` event to listen to content being modified by the user. It is emitted everytime a user writes or removes content in the input.
* @nativeChange The `change` event is emitted when the input loses focus, the user clicks `Enter` or makes a selection (such as auto complete or suggestions).
*
* @slot chips - Introduce some quick date selectors underneath the calendar grid. Use the `pn-choice-chip` component.
* @slot helpertext - You can use this slot instead of the prop `helpertext`. Recommended, only if you need to include additional HTML markup. Such as a `pn-text-link`. Use a `span` element to wrap the text and link.
* @slot error - You can use this slot instead of the prop `error`. Recommended, only if you need to include additional HTML markup. Such as a `pn-text-link`. Use a `span` element to wrap the text and link.
*/
export class PnDatePicker {
id = `pn-date-picker-${uuidv4()}`;
idFrom = `${this.id}-from`;
idTo = `${this.id}-to`;
idFromButton = `${this.id}-from-button`;
idToButton = `${this.id}-to-button`;
idHelper = `${this.id}-helper`;
idError = `${this.id}-error`;
idCalendar = `${this.id}-calendar`;
calendarElement;
today = getToday();
animation;
separators = [];
separatorRegex = /[^a-zA-Z\d\s:]/g;
listMonths = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
listWeek = [1, 2, 3, 4, 5, 6, 0];
hostElement;
open = false;
openUp = false;
selectingTo = false;
grid;
viewYearStart = null;
dateViewYear;
dateViewMonth;
dateViewDate;
showHelperSlot;
showErrorSlot;
isClosing = false;
isExpanding = false;
/** Set a label for the from date. */
labelFrom;
/** Set a label for the to date. @see {@link range} */
labelTo;
/** Provide a helpertext for the date input. */
helpertext;
/** Set a predefined value for the from date. @see {@link format} */
start = '';
/**
* Set a predefined value for the from date.
*
* @see {@link range}
* @see {@link format}
*/
end = '';
/**
* Set the date format of the value.
*
* While you can set any date value from the Dayjs documentation,
* we strongly recommend you pick a simple format that you can also type manually.
*
* @see {@link https://day.js.org/docs/en/display/format Day.js format documentation.}
*/
format = 'YYYY-MM-DD';
/** Manually set language; this will be inherited from the topbar. */
language = null;
/** Set a custom ID for the calendar. @category HTML attributes */
dateId = this.id;
/** HTML input name @category HTML attributes */
name;
/**
* Placeholder for the input field (defaults to the format prop).
* @see {@link format}
* @category HTML attributes
**/
placeholder = this.format;
/**
* Placeholder for end date (defaults to the format prop).
* @see {@link format}
* @category HTML attributes
**/
endPlaceholder = this.format;
/** Set the input `autocomplete` attribute. @category HTML attributes */
autocomplete;
/** Set the input `list` attribute for the first date input. @category HTML attributes */
list;
/** Set the input `list` attribute for the second date input. @category HTML attributes */
listEnd;
/** Set the HTML pattern prop on the input elements. Make sure it matches the format. @category HTML attributes */
pattern;
/** Allow the selection of a date range. @category Features */
range = false;
/**
* Set a limit on how many days one may select.
* By default, you can select an unlimited range.
*
* @todo Create a range limit function.
* @see {@link range}
*
* @category Features
* @hide true
**/
rangeLimit;
/** The calendar grid is shown as default. You can set either `months` or `years` as your first choice. @category Features */
view = CALENDAR;
/** Make the calendar open upwards by default. Opens downwards if there is not enough space. @category Features */
calendarUp = false;
/** Show weekend numbers to the left of the calendar grid. @category Features */
weekNumbers = false;
/** Disable the automatic insertion of separators when typing in the input. @category Features */
disableTypeAhead = false;
/** Remove the option to select dates on weekends. @category Features */
disableWeekends = false;
/**
* Individual dates you want to disable. Use a comma separated string.
*
* Remember to use the same format that you have in the `format` prop.
* @see {@link format}
* @example "YYYY-MM-DD,YYYY-MM-DD"
* @category Features
**/
disabledDates;
/** Set the date picker as required. @category State */
required = false;
/** Set the date picker as readonly. @category State */
readonly = false;
/** Set the date picker as disabled. @category State */
disabled = false;
/** Set an error message for the date picker. Overwrites the helpertext if used at the same time. @category Validation */
error;
/** Trigger the invalid state without an error message. @category Validation */
invalid = false;
/**
* Earliest date possible, this will determine how many years back the date picker will show.
*
* Remember to use the same format that you have in the `format` prop.
* @see {@link format}
* @example "2024-05-25"
* @category Min/max date
**/
minDate = null;
watchMin() {
if (this.minDate === null)
return;
if (!validateDate(this.minDate, this.format))
this.minDate = null;
}
/**
* Latest date possible, this will determine how many years forward the date picker will show.
*
* Remember to use the same format that you have in the `format` prop.
* @see {@link format}
* @example "2024-06-25"
* @category Min/max date
**/
maxDate = null;
watchMax() {
if (this.maxDate === null)
return;
if (!validateDate(this.maxDate, this.format))
this.maxDate = null;
}
watchValue() {
if (!validateDate(this.start, this.format))
return this.dateInvalid.emit({ start: this.start });
const { year, month, date } = getDateObject(this.start, this.format);
this.setViewYear({ year });
this.setViewMonth({ month });
this.setViewDate({ date });
if (this.range && isAfter(this.start, this.end, this.format, 'date')) {
this.end = '';
}
this.emitSelection();
}
watchValueTo() {
if (!this.range)
return;
if (!validateDate(this.end, this.format))
return this.dateInvalid.emit({ end: this.end });
const date = getDate(this.end, this.format);
this.setViewYear({ year: date.year() });
this.setViewMonth({ month: date.month() });
this.setViewDate({ date: date.date() });
if (isAfter(this.start, this.end, this.format, 'date')) {
const value = this.start;
this.start = this.end;
this.end = value;
}
this.emitSelection();
}
handleFormat() {
this.separators.length = 0;
this.format
.split('')
.forEach((item, index) => this.separatorRegex.exec(item) && this.separators.push({ name: item, index }));
}
watchId() {
this.idFrom = `${this.dateId}-from`;
this.idTo = `${this.dateId}-to`;
this.idFromButton = `${this.id}-from-button`;
this.idToButton = `${this.id}-to-button`;
this.idHelper = `${this.dateId}-helper`;
this.idError = `${this.dateId}-error`;
this.idCalendar = `${this.dateId}-calendar`;
}
watchView() {
const data = this.getCurrentDateObject();
if (validateDate(data, this.format))
this.updateGrid();
}
watchOpen() {
this.toggleCalendar.emit(this.open);
this.gridHandler();
if (this.open)
this.addGlobalEventListeners();
else
return this.removeGlobalEventListeners();
this.calendarElement.style.removeProperty('--pn-calendar-offset-left');
this.openUp = this.calendarUp;
requestAnimationFrame(() => {
const rectHost = this.getRect(this.hostElement);
const { scrollHeight } = this.calendarElement;
const { innerHeight, innerWidth } = window;
const offsetTop = getTotalHeightOffset();
const spaceUpwards = rectHost.y - offsetTop;
const spaceDownwards = innerHeight - rectHost.bottom;
const fitUpwards = spaceUpwards > scrollHeight;
const fitDownwards = spaceDownwards > scrollHeight;
const openTop = (this.openUp && (fitUpwards || spaceUpwards > spaceDownwards)) || (!fitDownwards && fitUpwards);
this.openUp = openTop;
// Calc y-axis
const rectCal = this.getRect(this.calendarElement);
const widthMinusRight = innerWidth - rectCal.right;
const offset = 0 > widthMinusRight ? widthMinusRight - 8 : 0;
this.calendarElement.style.setProperty('--pn-calendar-offset-left', `${Math.floor(offset)}px`);
});
}
handleMessage() {
this.checkSlottedHelper();
this.checkSlottedError();
}
handleView() {
this.currentView.emit(this.view);
}
/**
* Use the new `dateSelection`. Its here for compatibility. Will be removed in v8.
* @deprecated Use the new `dateSelection`. Will be removed in v8.
**/
dateselection;
/** Emits on valid date selection. Either if the user selects a date in the calendar or writes it manually. */
dateSelection;
emitSelection() {
const data = {
start: this.start,
};
if (this.range) {
const days = getDiff(this.start, this.end, this.format);
data.end = this.end;
data.days = typeof days === 'number' ? days + 1 : null;
}
this.dateSelection.emit(data);
this.dateselection.emit(data);
}
/** Emitted when an invalid value is set. This can only be done if the user writes in the input itself. */
dateInvalid;
/** Emitted when the calendar is toggled. */
toggleCalendar;
/** Emmitted when you select a new view. */
currentView;
/**
* If the select is open and you resize the window.
* Remove all css props and disable the animations entierly.
**/
handleResize() {
if (!this.open)
return;
this.toggle(false);
}
async componentWillLoad() {
this.watchMin();
this.watchMax();
this.watchId();
this.handleFormat();
this.handleMessage();
const valid = validateDate(this.start || this.end, this.format);
const data = valid && getDateObject(this.start || this.end, this.format);
const { year, month, date } = getDateObject(this.today);
this.setViewDate({ date: data.date || date });
this.setViewMonth({ month: data.month || month });
this.setViewYear({ year: data.year || year });
if (this.language === null)
await awaitTopbar(this.hostElement);
}
// Animation
gridHandler() {
if (this.open)
this.openGrid();
else
this.closeGrid();
}
openGrid() {
requestAnimationFrame(() => {
const { clientHeight, scrollHeight } = this.calendarElement;
const height = this.isClosing ? clientHeight : 0;
this.calendarElement.style.height = `${scrollHeight}px`;
this.isExpanding = true;
this.animate(true, `${height}px`, `${this.calendarElement.scrollHeight}px`);
});
}
closeGrid() {
const { scrollHeight, clientHeight } = this.calendarElement;
const height = this.isExpanding ? clientHeight : scrollHeight;
this.calendarElement.style.height = `0px`;
this.isClosing = true;
this.animate(false, `${height}px`, `0px`);
}
animate(open, startHeight, endHeight) {
this.cancelAnimations();
this.animation = this.calendarElement.animate({
height: [startHeight, endHeight],
}, {
duration: 400,
easing: 'cubic-bezier(0.6, 0, 0.2, 1)',
});
this.animation.onfinish = () => this.animationFinish();
this.animation.oncancel = () => (open ? (this.isExpanding = false) : (this.isClosing = false));
}
animationFinish() {
this.cancelAnimations();
this.calendarElement.style.height = this.isClosing ? '0px' : '';
this.isClosing = false;
this.isExpanding = false;
}
cancelAnimations() {
if (this.animation)
this.animation.cancel();
}
// End of Animation
globalEvents = (event) => {
const target = event.target;
const isWithinCalendar = target?.closest(this.hostElement.localName);
if (!isWithinCalendar)
this.toggle(false);
};
addGlobalEventListeners() {
const root = this.hostElement.getRootNode();
root.addEventListener('click', this.globalEvents);
}
removeGlobalEventListeners() {
const root = this.hostElement.getRootNode();
root.removeEventListener('click', this.globalEvents);
}
translate(prop) {
return translations?.[prop?.toUpperCase()]?.[this.language || en] || prop?.toUpperCase();
}
translateDateText(customDate, format) {
return getReadableDate({ ...this.getCurrentDateObject(), ...customDate }, this.language, format);
}
getRect(element) {
return element.getBoundingClientRect();
}
toggle(state, selecting) {
this.open = state ?? !this.open;
this.selectingTo = selecting;
}
hasHelperText() {
return this.helpertext?.length > 0 || this.showHelperSlot;
}
/** If any `error` text is present, either via prop/slot. */
hasErrorMessage() {
return this.error?.length > 0 || this.showErrorSlot;
}
/** If any `error` is active, either via the prop `invalid` or `error` prop/slot. */
hasError() {
return this.hasErrorMessage() || this.invalid || this.showErrorSlot;
}
checkSlottedHelper() {
const slottedHelper = this.hostElement.querySelector('[slot=helpertext]')?.textContent;
this.showHelperSlot = !!slottedHelper?.length;
}
checkSlottedError() {
const slottedError = this.hostElement.querySelector('[slot=error]')?.textContent;
this.showErrorSlot = !!slottedError?.length;
}
viewingCalendar() {
return this.view === CALENDAR;
}
viewingMonth() {
return this.view === MONTHS;
}
viewingYears() {
return this.view === YEARS;
}
viewType() {
return this.viewingCalendar() ? 'date' : this.viewingMonth() ? 'month' : 'year';
}
isBeforeMax(data) {
if (this.maxDate)
return isBefore(data, this.maxDate, this.format, this.viewType());
return true;
}
isAfterMin(data) {
if (this.minDate)
return isAfter(data, this.minDate, this.format, this.viewType());
return true;
}
isDisabledWeekend(day) {
if (!this.viewingCalendar())
return false;
return this.disableWeekends ? day >= 6 : false;
}
isDisabledDate(data) {
if (!this.disabledDates?.length || !this.viewingCalendar())
return false;
const list = this.disabledDates.split(',');
return !!list.find(disabledDate => selectedDate(disabledDate, data, this.format, this.viewType()));
}
isDisabled(data) {
const isAfterMaxDate = !this.isBeforeMax(data);
const isBeforeMinDate = !this.isAfterMin(data);
const weekendDisable = this.viewingCalendar() && this.isDisabledWeekend(data.day);
const manualDisable = this.isDisabledDate(data);
const disabled = isAfterMaxDate || isBeforeMinDate || weekendDisable || manualDisable;
return {
disabled,
manualDisable,
weekendDisable,
minMaxDisable: isAfterMaxDate || isBeforeMinDate,
};
}
updateGrid() {
this.grid = getGrid(this.dateViewYear, this.dateViewMonth);
}
getCurrentViewDate(data = this.getCurrentDateObject()) {
return getReadableDate(data, this.language, 'MMMM YYYY');
}
isSelected(data, to = false) {
const value = to ? this.end : this.start;
return selectedDate(value, this.getCurrentDateObject(data), this.format, this.viewType());
}
isToday(data) {
return selectedDate(this.today.format(this.format), this.getCurrentDateObject(data), this.format, this.viewType());
}
getCurrentDateObject({ year = this.dateViewYear, month = this.dateViewMonth, date = this.dateViewDate, day, } = {}) {
return {
year,
month,
date,
day,
};
}
/** Handle keyboard navigation in the calendar grid. */
calendarKeyboardNavigation(event, data, disabled = false) {
const validCodes = [
'Enter',
'Space',
'ArrowRight',
'ArrowLeft',
'ArrowUp',
'ArrowDown',
'Home',
'End',
'PageDown',
'PageUp',
];
if (!validCodes.includes(event.code))
return;
event.preventDefault();
const select = !disabled && event.code.match(/^(Enter|Space)$/);
if (select && this.viewingYears())
return this.setViewYear({ year: data.year, reset: true });
if (select && this.viewingMonth())
return this.setViewMonth({ month: data.month, grid: true, reset: true });
if (select && this.viewingCalendar())
return this.setValue(data.date);
const goToDate = this.navDirection(event, data);
if (!goToDate)
return;
const { year, month, date } = goToDate;
this.setViewYear({ year });
this.setViewMonth({ month, grid: true });
this.setViewDate({ date });
this.resetFocus();
}
navDirection(event, data) {
const { code } = event;
const nextDate = navigateGrid(code, data, this.disableWeekends, this.viewType());
const date = getDateObject(nextDate);
return date;
}
getYearGrid() {
const list = [];
let oldestInList = this.viewYearStart - 7;
for (let i = 0; 15 > i; i++) {
list.push(oldestInList++);
}
return list;
}
/** Defaults to the calendar view. */
setView(view) {
this.view = view;
requestAnimationFrame(() => this.focusCalendar());
}
setNavView(data) {
if (this.viewingYears())
return this.setViewYear({ ...data, grid: true });
return this.setViewMonth(data);
}
setViewYear({ year = this.dateViewYear, minus = false, plus = false, reset = false, grid = false, }) {
const nextYear = setYear({ year, minus, plus });
const start = this.viewYearStart;
const max = start + 7;
const min = start - 7;
if (grid) {
const minusVal = this.viewYearStart - 15;
const plusVal = this.viewYearStart + 15;
this.viewYearStart = minus ? minusVal : plusVal;
this.dateViewYear = this.viewYearStart;
}
else
this.dateViewYear = nextYear;
if (start === null || nextYear > max || min > nextYear)
this.viewYearStart = nextYear;
if (reset)
this.setView(CALENDAR);
}
setViewMonth({ month = this.dateViewMonth, minus = false, plus = false, reset = false, grid = false, }) {
const nextMonth = setMonth({ month, minus, plus });
this.dateViewMonth = nextMonth;
if (!grid && month === 0 && minus)
this.setViewYear({ minus: true });
if (!grid && month === 11 && plus)
this.setViewYear({ plus: true });
if (reset)
this.setView(CALENDAR);
}
setViewDate({ date }) {
this.dateViewDate = date;
}
handleSeparator(event) {
let value = event.target.value;
const foundSeparator = this.separators.find(({ index }) => index === value.length);
if (foundSeparator?.name) {
value += foundSeparator.name;
}
return value;
}
inputHandler(event, to = false) {
const propName = to ? 'end' : 'start';
const value = this.disableTypeAhead ? event.target.value : this.handleSeparator(event);
this[propName] = value;
}
setValue(date) {
const value = getReadableDate(this.getCurrentDateObject({ date }), this.language, this.format);
if (this.selectingTo)
this.end = value;
else
this.start = value;
this.setViewDate({ date });
if (!this.range || this.selectingTo) {
this.toggle(false);
this.focusToggleCalendarButton();
}
if (this.range && !this.selectingTo)
this.selectingTo = true;
}
getDayAttributes(dateObject, blank) {
const data = this.getCurrentDateObject(dateObject);
const { disabled } = this.isDisabled(data);
if (blank)
return { 'data-blank': true };
function capitalize(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
const type = this.viewType();
const value = data[type];
const disabledProp = type !== 'date' ? 'disabled' : 'aria-disabled';
const selectedProp = type !== 'date' ? 'aria-pressed' : 'aria-selected';
const selectedFrom = this.isSelected(data);
const selectedTo = this.isSelected(data, true);
const selected = selectedFrom || selectedTo;
const after = this.range && isAfter(data, this.start, this.format, this.viewType());
const before = this.range && isBefore(data, this.end, this.format, this.viewType());
const isBetween = after && before;
const isDisabled = disabled && (type === 'date' || !selected);
const tabbable = this[`dateView${capitalize(type)}`] === value;
const props = {
'onKeyDown': (e) => this.calendarKeyboardNavigation(e, data, isDisabled),
'tabindex': tabbable ? '0' : '-1',
[selectedProp]: (selected || isBetween)?.toString(),
'aria-current': this.isToday(data) ? 'date' : null,
'data-active': isDisabled ? null : selected,
'data-today': this.isToday(data),
'data-option': 'true',
[`data-${type}`]: value,
[disabledProp]: isDisabled ? 'true' : null,
};
const singleDate = selectedDate(this.start, this.end, this.format, 'date');
if (this.range && !singleDate) {
props['data-range'] = isBetween;
props['data-range-from'] = selectedFrom && this.end !== '';
props['data-range-to'] = selectedTo && this.start !== '';
}
else {
props['data-single'] = true;
}
if (this.viewingCalendar())
props.onClick = () => this.setValue(data.date);
if (this.viewingMonth())
props.onClick = () => this.setViewMonth({ month: data.month, reset: true });
if (this.viewingYears())
props.onClick = () => this.setViewYear({ year: data.year, reset: true });
if (isDisabled)
delete props.onClick;
return props;
}
/** Focus the button toggling the calendar. Handles the start/from date on its own. */
focusToggleCalendarButton() {
requestAnimationFrame(() => {
const id = this.range ? this.idToButton : this.idFromButton;
this.hostElement.querySelector(`#${id}`).focus({ preventScroll: true });
});
}
focusCalendar() {
requestAnimationFrame(() => {
const element = this.calendarTabElement({ first: true, grid: true });
element?.focus({ preventScroll: true });
});
}
resetFocus() {
if (this.open)
return this.focusCalendar();
else
this.focusToggleCalendarButton();
}
handleButtonBlur(event) {
if (this.open && event.key === 'Tab')
return this.focusCalendar();
}
/**
* This function queries all tabbable elements inside the calendar popup.
* Since we allow slotted content it important that we have a function that takes all elements into account.
* With the `first` and `grid` argument, you can decide which one you want to get.
* There are fallbacks so you should never get an empty list of elements.
*/
calendarTabElement({ first, grid }) {
const focusableElements = ':is(input, select, button:not([tabindex="-1"]), td[tabindex="0"])';
const elements = this.calendarElement.querySelectorAll(focusableElements);
const list = Array.from(elements).filter(({ localName, offsetParent }) => focusableElements.includes(localName) && offsetParent !== null);
const gridEl = list.find(({ dataset, tabIndex }) => dataset.option && tabIndex === 0);
if (grid && gridEl)
return gridEl;
if (first)
return list.shift();
else
return list.pop();
}
/**
* We need to listen to the `Esc` and `Tab` key for the entire calendar popup.
* Regardless if you focus the grid, the nav buttons or slotted content,
* the popup will close if you press `Esc`. We also need to reset the focus when the user tabs.
*/
handleCalendarTabEsc(event) {
const target = event.target;
const tabElement = this.calendarTabElement({ first: event.shiftKey });
if (event.code === 'Escape') {
this.toggle(false);
this.focusToggleCalendarButton();
return;
}
if (event.code === 'Tab' && target.isEqualNode(tabElement)) {
event.preventDefault();
this.calendarTabElement({ first: !event.shiftKey }).focus({ preventScroll: true });
}
}
ariaDescribedby() {
const list = [];
if (this.hasErrorMessage())
list.push(this.idError);
else if (this.hasHelperText())
list.push(this.idHelper);
return list.length ? list.join(' ') : null;
}
/** Renders the date calendar grid. */
renderDateGrid() {
return (h("table", { role: "grid", class: "pn-date-picker-table", "aria-multiselectable": this.range ? 'true' : null }, h("caption", { class: "pn-date-picker-sr-only", key: this.getCurrentViewDate() }, this.getCurrentViewDate()), h("thead", { class: "pn-date-picker-thead" }, h("tr", { class: "pn-date-picker-tr" }, this.weekNumbers && h("th", { class: "pn-date-picker-th", scope: "col", "aria-hidden": "true" }), this.listWeek.map(index => (h("th", { class: "pn-date-picker-th", scope: "col", abbr: this.translateDateText({ day: index }, 'dddd') }, this.translateDateText({ day: index }, 'ddd')))))), h("tbody", { class: "pn-date-picker-tbody" }, this.grid?.map(({ week, list }) => (h("tr", { key: `${this.dateViewYear}-${week}`, class: "pn-date-picker-tr" }, this.weekNumbers && (h("td", { class: "pn-date-picker-td", "data-blank": true, "data-week": true, title: `${this.translate('WEEK_NAME')} ${week}`, "aria-hidden": "true" }, h("span", { class: "pn-date-picker-td-week" }, week))), list.map(({ day, date, blank }) => (h("td", { key: `${this.dateViewYear}-${this.dateViewMonth}-${date}`, class: "pn-date-picker-td", ...this.getDayAttributes({ date, day }, blank) }, h("span", { class: "pn-date-picker-td-text" }, date))))))))));
}
/** Renders the month calendar grid. */
renderMonthGrid() {
return (h("ul", { class: "pn-date-picker-list" }, this.listMonths.map(month => (h("li", { key: month, class: "pn-date-picker-item", "data-item": "month" }, h("button", { type: "button", class: "pn-date-picker-button", ...this.getDayAttributes({ month }, false) }, h("span", { class: "pn-date-picker-month", "data-full": true }, this.translateDateText({ month }, 'MMMM')), h("span", { class: "pn-date-picker-month", "data-abbr": true }, this.translateDateText({ month }, 'MMM'))))))));
}
/** Renders the year calendar grid. */
renderYearGrid() {
return (h("ul", { class: "pn-date-picker-list" }, this.getYearGrid()?.map(year => (h("li", { key: year, class: "pn-date-picker-item", "data-item": "year" }, h("button", { type: "button", class: "pn-date-picker-button", ...this.getDayAttributes({ year }, false) }, h("span", null, year)))))));
}
renderInput({ to = false } = {}) {
const id = to ? this.idTo : this.idFrom;
const idButton = to ? this.idToButton : this.idFromButton;
const label = to ? this.labelTo : this.labelFrom;
const value = to ? this.end : this.start;
const placeholder = to ? this.endPlaceholder : this.placeholder;
const list = to ? this.listEnd : this.list;
const editing = this.open ? (this.selectingTo ? to : !to) : false;
const defaultText = this.translate('SELECT_DATE');
const textProp = this.range ? (to ? 'END_' : 'START_') : '';
const dateText = this.translate(`SELECTED_${textProp}DATE`);
let textButton = defaultText;
if (value) {
textButton += `, ${dateText.replace('{date}', value)}`;
}
const showButton = !(this.disabled || this.readonly);
return (h("div", { class: "pn-date-picker-container", "data-error": this.hasError() }, h("label", { class: "pn-date-picker-label", htmlFor: id }, h("span", null, label)), h("div", { class: "pn-date-picker-field" }, h("input", { type: "text", id: id, class: "pn-date-picker-input", name: this.name, placeholder: placeholder, autocomplete: this.autocomplete, maxlength: this.format.length, list: list, pattern: this.pattern, value: value, disabled: this.disabled, required: this.required, readonly: this.readonly, "aria-describedby": this.ariaDescribedby(), onInput: e => this.inputHandler(e, to), "data-active": editing }), showButton && (h("pn-button", { class: "pn-date-picker-toggle", buttonId: idButton, icon: calendar, iconOnly: true, appearance: "light", arialabel: textButton, ariaexpanded: this.open.toString(), ariacontrols: this.idCalendar, "data-active": this.open, "data-input": true, small: true, onPnClick: () => this.toggle(null, to), onKeyDown: e => this.handleButtonBlur(e) })))));
}
render() {
return (h(Host, { key: 'd123c0e7ef8ff2fd2a1a3c49de721f711b944524' }, h("div", { key: 'cd422a8b305a632cb28c08cab376f715b597cae9', class: "pn-date-picker" }, this.renderInput(), this.range && (h("div", { key: 'e4b2642c001b5a5b2d8d8067ddccacb0999d428d', class: "pn-date-picker-range-icon test" }, h("pn-icon", { key: '95162c088247aa9d10966fb9876c5f1bb39ccd96', icon: arrow_right }))), this.range && this.renderInput({ to: this.range })), h("div", { key: '777742a9408dcaa680e5168bb1259fceb8820094', id: this.idCalendar, class: "pn-date-picker-calendar", role: "dialog", "aria-label": this.translate('CALENDAR_NAVIGATION'), "data-open": this.open, "data-moving": this.isClosing || this.isExpanding, "data-direction": this.openUp ? 'top' : 'bottom', "data-range": this.range, style: { height: '0px' }, ref: el => (this.calendarElement = el), onKeyDown: e => this.handleCalendarTabEsc(e) }, h("div", { key: 'f0df0c7b6d1c15835363643ccec42470122f112c', class: "pn-date-picker-wrapper" }, h("nav", { key: '8df1e8a97d7858a7e69f05adb2746a02a4753a2b', class: "pn-date-picker-nav", "aria-labelledby": this.idCalendar }, h("pn-button", { key: '12e09bc691fa00220a0fec43b237a4b92a5945d1', hidden: this.viewingMonth(), small: true, appearance: "light", arialabel: this.translate(`PREVIOUS_${this.viewType().toUpperCase()}`), icon: arrow_left, iconOnly: true, onPnClick: () => this.setNavView({ minus: true }) }), h("pn-button", { key: 'c22945eac17cbf12269c54b77d200e9828781f49', hidden: !this.viewingCalendar(), small: true, appearance: "light", onPnClick: () => this.setView(MONTHS) }, h("span", { key: '882732031f60e197340677d9cf4d189118765ab4', class: "pn-date-picker-month", "data-full": true }, this.translateDateText({}, 'MMMM')), h("span", { key: '91a4b3e9947d1823e9618b5a2be09d21c0f8a023', class: "pn-date-picker-month", "data-abbr": true }, this.translateDateText({}, 'MMM'))), h("h2", { key: 'c364e8947b7c4116f7b4dd5bdefe3f24de0f54b4', hidden: this.viewingCalendar(), class: "pn-date-picker-title" }, this.translate(`SELECT_${this.viewType().toUpperCase()}`)), h("pn-button", { key: '226bdb077262de9ca537831b6fd16466a6471e31', hidden: !this.viewingCalendar(), small: true, appearance: "light", onPnClick: () => this.setView(YEARS) }, h("span", { key: '8345010a01ca45f1a46e405074ed735cd9633fca' }, this.dateViewYear)), h("pn-button", { key: '2e147ef9336dd93b82907939dd22865b59019d6b', hidden: this.viewingMonth(), small: true, appearance: "light", arialabel: this.translate(`NEXT_${this.viewType().toUpperCase()}`), icon: arrow_right, iconOnly: true, onPnClick: () => this.setNavView({ plus: true }) })), this.viewingYears() && this.renderYearGrid(), this.viewingMonth() && this.renderMonthGrid(), this.viewingCalendar() && this.renderDateGrid(), h("aside", { key: '89a5cd70f66c5eb68ccb8e53f92ea7a44b9b74be', class: "pn-date-picker-chips" }, h("slot", { key: 'd7ec62adf206939ede1e83f05d40b1204e9c4917', name: "chips" })), h("nav", { key: '5a53c0d58c0df54cb2fb8ce7910887c3d40d7bdb', class: "pn-date-picker-bottom", hidden: this.viewingCalendar() }, h("pn-button", { key: '66829d9c3d0bb8619e4f754c202847dbff9d8a64', appearance: "light", variant: "outlined", small: true, icon: pn_return, onPnClick: () => this.setView(CALENDAR) }, h("span", { key: '8c5e969443cf0f399c3875d1d44f65acc66603c8' }, this.translate('GO_CALENDAR')))))), h("p", { key: '0b2e3a6bc70ecee6b555351fcee8b116543029df', id: this.idHelper, class: "pn-date-picker-helpertext", hidden: !this.hasHelperText() || this.hasError() }, h("span", { key: 'c97d5cce6402d47b1b7d5988d21551de7f2c86db' }, this.helpertext), h("slot", { key: '2e700d98e3104824ac6fec70e95141c715437dfc', name: "helpertext" })), h("p", { key: '2fcdbae6dd8b0fd379b31066371b2e9b2784797b', id: this.idError, class: "pn-date-picker-error", role: "alert", hidden: !this.hasErrorMessage() }, h("span", { key: '0f3efed5e56bf439cdce972d3ed3b86c09d017c2' }, this.error), h("slot", { key: 'fc52f3e6ee3a579b9012fc1c5edc9dd4ad019fb0', name: "error" }))));
}
static get is() { return "pn-date-picker"; }
static get originalStyleUrls() {
return {
"$": ["pn-date-picker.scss"]
};
}
static get styleUrls() {
return {
"$": ["pn-date-picker.css"]
};
}
static get properties() {
return {
"labelFrom": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": true,
"optional": false,
"docs": {
"tags": [],
"text": "Set a label for the from date."
},
"getter": false,
"setter": false,
"attribute": "label-from",
"reflect": false
},
"labelTo": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link range }"
}],
"text": "Set a label for the to date."
},
"getter": false,
"setter": false,
"attribute": "label-to",
"reflect": false
},
"helpertext": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Provide a helpertext for the date input."
},
"getter": false,
"setter": false,
"attribute": "helpertext",
"reflect": false
},
"start": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "see",
"text": "{@link format }"
}],
"text": "Set a predefined value for the from date."
},
"getter": false,
"setter": false,
"attribute": "start",
"reflect": false,
"defaultValue": "''"
},
"end": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "see",
"text": "{@link range }"
}, {
"name": "see",
"text": "{@link format }"
}],
"text": "Set a predefined value for the from date."
},
"getter": false,
"setter": false,
"attribute": "end",
"reflect": false,
"defaultValue": "''"
},
"format": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "see",
"text": "{@link https://day.js.org/docs/en/display/format Day.js format documentation.}"
}],
"text": "Set the date format of the value.\n\nWhile you can set any date value from the Dayjs documentation,\nwe strongly recommend you pick a simple format that you can also type manually."
},
"getter": false,
"setter": false,
"attribute": "format",
"reflect": false,
"defaultValue": "'YYYY-MM-DD'"
},
"language": {
"type": "string",
"mutable": false,
"complexType": {
"original": "PnLanguages",
"resolved": "\"\" | \"da\" | \"en\" | \"fi\" | \"no\" | \"sv\"",
"references": {
"PnLanguages": {
"location": "import",
"path": "@/index",
"id": "src/index.ts::PnLanguages"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Manually set language; this will be inherited from the topbar."
},
"getter": false,
"setter": false,
"attribute": "language",
"reflect": false,
"defaultValue": "null"
},
"dateId": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "category",
"text": "HTML attributes"
}],
"text": "Set a custom ID for the calendar."
},
"getter": false,
"setter": false,
"attribute": "date-id",
"reflect": false,
"defaultValue": "this.id"
},
"name": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "HTML attributes"
}],
"text": "HTML input name"
},
"getter": false,
"setter": false,
"attribute": "name",
"reflect": false
},
"placeholder": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link format }"
}, {
"name": "category",
"text": "HTML attributes"
}],
"text": "Placeholder for the input field (defaults to the format prop)."
},
"getter": false,
"setter": false,
"attribute": "placeholder",
"reflect": false,
"defaultValue": "this.format"
},
"endPlaceholder": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link format }"
}, {
"name": "category",
"text": "HTML attributes"
}],
"text": "Placeholder for end date (defaults to the format prop)."
},
"getter": false,
"setter": false,
"attribute": "end-placeholder",
"reflect": false,
"defaultValue": "this.format"
},
"autocomplete": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "HTML attributes"
}],
"text": "Set the input `autocomplete` attribute."
},
"getter": false,
"setter": false,
"attribute": "autocomplete",
"reflect": false
},
"list": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "HTML attributes"
}],
"text": "Set the input `list` attribute for the first date input."
},
"getter": false,
"setter": false,
"attribute": "list",
"reflect": false
},
"listEnd": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "HTML attributes"
}],
"text": "Set the input `list` attribute for the second date input."
},
"getter": false,
"setter": false,
"attribute": "list-end",
"reflect": false
},
"pattern": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "HTML attributes"
}],
"text": "Set the HTML pattern prop on the input elements. Make sure it matches the format."
},
"getter": false,
"setter": false,
"attribute": "pattern",
"reflect": false
},
"range": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "Allow the selection of a date range."
},
"getter": false,
"setter": false,
"attribute": "range",
"reflect": false,
"defaultValue": "false"
},
"rangeLimit": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "todo",
"text": "Create a range limit function."
}, {
"name": "see",
"text": "{@link range }"
}, {
"name": "category",
"text": "Features"
}, {
"name": "hide",
"text": "true"
}],
"text": "Set a limit on how