@postnord/web-components
Version:
PostNord Web Components
1,106 lines (1,105 loc) • 84.3 kB
JavaScript
/*!
* Built with Stencil
* By PostNord.
*/
import { h, Host, Mixin, forceUpdate, } from "@stencil/core";
import { awaitTopbar, uuidv4, en, getTotalHeightOffset, getMenuWidth } from "../../../index";
import { animateHeightFactory } from "../../../globals/mixins/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. {@since v7.6.0}
* @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. {@since v7.6.0}
* @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. {@since v7.6.0}
*/
export class PnDatePicker extends Mixin(animateHeightFactory) {
constructor() {
super();
}
id = `pn-date-picker-${uuidv4()}`;
idStart = `${this.id}-from`;
idEnd = `${this.id}-to`;
idStartButton = `${this.id}-from-button`;
idEndButton = `${this.id}-to-button`;
idHelper = `${this.id}-helper`;
idError = `${this.id}-error`;
idCalendar = `${this.id}-calendar`;
mo;
calendarElement;
today = getToday();
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;
selectingEnd = false;
grid;
viewYearStart = null;
dateViewYear;
dateViewMonth;
dateViewDate;
showHelperSlot;
showErrorSlot;
/** 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;
/** Manually set language; this will be inherited from the topbar. */
language = undefined;
/** Set a predefined value for the from date (input value). @see {@link format} @category Native attributes */
start = '';
/**
* Set a predefined value for the from date. (input value end)
*
* @see {@link range}
* @see {@link format}
* @category Native attributes
*/
end = '';
/** HTML input name @since v7.6.0 @category Native attributes*/
name;
/** HTML input name @since v7.25.0 @category Native attributes */
nameEnd;
/**
* Placeholder for the input field (defaults to the format prop).
* @see {@link format}
* @category Native attributes
**/
placeholder;
/**
* Placeholder for end date (defaults to the format prop).
* @see {@link format}
* @deprecated Use `placeholder-end` instead
* @category Native attributes
**/
endPlaceholder;
/**
* Placeholder for end date (defaults to the format prop).
* @see {@link format}
* @since v7.25.0
* @category Native attributes
**/
placeholderEnd;
/** Set the input `autocomplete` attribute. @category Native attributes */
autocomplete;
/** Set the input `list` attribute for the first date input. @since v7.6.0 @category Native attributes */
list;
/** Set the input `list` attribute for the second date input. @since v7.6.0 @category Native attributes */
listEnd;
/** Set the HTML pattern prop on the input elements. Make sure it matches the format. @since v7.6.0 @category Native attributes */
pattern;
/** Set the date picker as required. @category Native attributes */
required = false;
/** Set the date picker as readonly. @since v7.6.0 @category Native attributes */
readonly = false;
/** Set the date picker as disabled. @since v7.6.0 @category Native attributes */
disabled = false;
/**
* 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.
*
* @since v7.6.0
* @see {@link https://day.js.org/docs/en/display/format Day.js format documentation.}
* @category Features
*/
format = 'YYYY-MM-DD';
/** Disable the automatic insertion of separators when typing in the input. @since v7.6.0 @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;
/** If you use a format with an unknown length, disable the max length. @since 7.11.3 @category Features */
disableMaxLength = false;
/** 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;
/** Show weekend numbers to the left of the calendar grid. @since v7.6.0 @category Features */
weekNumbers = false;
/** Set the date picker label as compact. If used, the `placeholder` will no longer be displayed. @since v7.21.0 @category Features */
compact = false;
/** Make the calendar open upwards by default. Opens downwards if there is not enough space. @since v7.6.0 @category Features */
calendarUp = false;
/** The calendar grid is shown as default. You can set either `months` or `years` as your first choice. @since v7.6.0 @category Features */
view = 'calendar';
/** Trigger the invalid state without an error message. @since v7.6.0 @category Features */
invalid = false;
/** Set an error message for the date picker. Overwrites the helpertext if used at the same time. @since v7.6.0 @category Features */
error;
/** Set a custom ID for the calendar. If you use `range`, the end input will use `${pn-id}-end`. @since v7.25.0 @category HTML attributes */
pnId;
/**
* Provide the label via an aria attribute.
* We strongly recommend you use the `label-from` prop instead.
* @since v7.25.0
* @category HTML attributes
*/
pnAriaLabel;
/**
* Provide the label for the end input via an aria attribute.
* We strongly recommend you use the `label-to` prop instead.
* @since v7.25.0
* @category HTML attributes
*/
pnAriaLabelEnd;
/**
* Provide the label from another element via its ID.
* We strongly recommend you use the `label-from` prop instead.
* @since v7.25.0
* @category HTML attributes
*/
pnAriaLabelledby;
/**
* Provide the label for the end input from another element via its ID.
* We strongly recommend you use the `label-to` prop instead.
* @since v7.25.0
* @category HTML attributes
*/
pnAriaLabelledbyEnd;
/** Set a custom ID for the calendar. @since v7.6.0 @deprecated Use `pn-id` instead. @category HTML attributes */
dateId;
/**
* 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 Features
**/
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 Features
**/
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.idStart = this.getId();
this.idEnd = `${this.getId()}-end`;
this.idStartButton = `${this.idStart}-from-button`;
this.idEndButton = `${this.idEnd}-to-button`;
this.idHelper = `${this.idStart}-helper`;
this.idError = `${this.idStart}-error`;
this.idCalendar = `${this.idStart}-calendar`;
}
watchView() {
const data = this.getCurrentDateObject();
if (validateDate(data, this.format))
this.updateGrid();
}
watchOpen() {
this.toggleCalendar.emit(this.open);
this.dropdownHandler();
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 horizontal positioning - center calendar relative to host element
const rectCal = this.getRect(this.calendarElement);
const menuWidth = getMenuWidth();
// Calculate center position: host center minus half of calendar width
const hostCenterX = rectHost.x + rectHost.width / 2;
const calendarHalfWidth = rectCal.width / 2;
const idealCenterOffset = hostCenterX - calendarHalfWidth - rectCal.x;
// Check boundaries, accounting for menu width on the left
const calendarLeftEdge = rectCal.x + idealCenterOffset;
const calendarRightEdge = calendarLeftEdge + rectCal.width;
const leftBoundary = menuWidth + 16; // Menu width + buffer
let finalOffset = idealCenterOffset;
// Adjust if calendar would go beyond left edge of viewport or over the menu
if (calendarLeftEdge < leftBoundary) {
finalOffset = leftBoundary - rectCal.x;
}
// Adjust if calendar would go beyond right edge of viewport
else if (calendarRightEdge > innerWidth - 16) {
finalOffset = innerWidth - 16 - rectCal.width - rectCal.x;
}
this.calendarElement.style.setProperty('--pn-calendar-offset-left', `${Math.floor(finalOffset)}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.
* @since v7.6.0
*/
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. @since v7.6.0 */
dateInvalid;
/** Emitted when the calendar is toggled. @since v7.6.0 */
toggleCalendar;
/** Emmitted when you select a new view. @since v7.6.0 */
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.toggleGrid(false);
}
connectedCallback() {
this.mo = new MutationObserver(() => {
forceUpdate(this.hostElement);
this.handleMessage();
});
this.mo.observe(this.hostElement, { childList: true, subtree: true });
}
disconnectedCallback() {
if (this.mo)
this.mo.disconnect();
}
async componentWillLoad() {
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 === undefined)
await awaitTopbar(this.hostElement);
}
getId() {
return this.pnId || this.dateId || this.id;
}
getAriaLabel(end) {
if (end)
return !this.labelTo && !this.pnAriaLabelledbyEnd ? this.pnAriaLabelEnd : null;
return !this.labelFrom && !this.pnAriaLabelledby ? this.pnAriaLabel : null;
}
getAriaLabelledby(end) {
if (end)
return !this.labelTo && !this.pnAriaLabelEnd ? this.pnAriaLabelledbyEnd : null;
return !this.labelFrom && !this.pnAriaLabel ? this.pnAriaLabelledby : null;
}
getPlaceholder(end) {
if (this.compact)
return ' ';
if (end)
return this.placeholderEnd || this.endPlaceholder || this.format;
return this.placeholder || this.format;
}
dropdownHandler() {
if (this.open)
this.openDropdown(this.calendarElement);
else
this.closeDropdown(this.calendarElement);
}
globalEvents = (event) => {
const target = event.target;
const isWithinCalendar = target?.closest(this.hostElement.localName);
if (!isWithinCalendar)
this.toggleGrid(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();
}
toggleGrid(state, selecting) {
this.open = state ?? !this.open;
this.selectingEnd = 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;
}
hideHelpertext() {
return this.hasErrorMessage() || !this.hasHelperText();
}
hideError() {
return !this.hasErrorMessage();
}
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, end = false) {
const value = end ? 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',
'Escape',
];
if (!validCodes.includes(event.code))
return;
event.preventDefault();
if (event.code === 'Escape')
return this.toggleGrid(false);
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, end = false) {
const propName = end ? '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.selectingEnd)
this.end = value;
else
this.start = value;
this.setViewDate({ date });
if (!this.range || this.selectingEnd) {
this.toggleGrid(false);
this.focusToggleCalendarButton();
}
if (this.range && !this.selectingEnd)
this.selectingEnd = 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.idEndButton : this.idStartButton;
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 (event.code === 'Escape') {
event.preventDefault();
event.stopImmediatePropagation();
}
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 });
event.stopImmediatePropagation();
if (event.code === 'Escape') {
event.preventDefault();
this.toggleGrid(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)))))));
}
renderLabel(end = false) {
const id = end ? this.idEnd : this.idStart;
const label = end ? this.labelTo : this.labelFrom;
if (!label)
return null;
return (h("label", { class: "pn-date-picker-label", htmlFor: id, "data-compact": this.compact }, h("span", null, label)));
}
renderInput({ end = false } = {}) {
const id = end ? this.idEnd : this.idStart;
const idButton = end ? this.idEndButton : this.idStartButton;
const value = end ? this.end : this.start;
const list = end ? this.listEnd : this.list;
const name = end ? this.nameEnd : this.name;
const editing = this.open ? (this.selectingEnd ? end : !end) : false;
const defaultText = this.translate('SELECT_DATE');
const textProp = this.range ? (end ? '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() }, !this.compact && this.renderLabel(end), h("div", { class: "pn-date-picker-field" }, h("input", { type: "text", id: id, class: "pn-date-picker-input", name: name, placeholder: this.getPlaceholder(end), autocomplete: this.autocomplete, maxlength: this.disableMaxLength ? null : this.format.length, list: list, pattern: this.pattern, value: value, disabled: this.disabled, required: this.required, readonly: this.readonly, "aria-label": this.getAriaLabel(end), "aria-labelledby": this.getAriaLabelledby(end), "aria-describedby": this.ariaDescribedby(), "aria-invalid": this.hasError()?.toString(), "data-active": editing, "data-compact": this.compact, onInput: e => this.inputHandler(e, end) }), this.compact && this.renderLabel(end), 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.toggleGrid(null, end), onKeyDown: e => this.handleButtonBlur(e) })))));
}
render() {
return (h(Host, { key: 'dbe09316ddf6365ad119afe9fd01a363aa3cf9bf' }, h("div", { key: 'b6aa1c922859c282529ff9e1297aacc79111a0f9', class: "pn-date-picker" }, this.renderInput(), this.range && (h("div", { key: '48d050a834a2df16b33759bbe117a5c378dfa069', class: "pn-date-picker-range-icon test" }, h("pn-icon", { key: '6b6507409ba5eaf3de232dff28785d9c7c0a85f0', icon: arrow_right }))), this.range && this.renderInput({ end: this.range })), h("div", { key: '9a9d31f221aabbf0927bcc8ae602d1330d75b4d3', id: this.idCalendar, class: "pn-date-picker-calendar", role: "dialog", "aria-label": this.translate('CALENDAR_NAVIGATION'), "data-open": this.open, "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: '56137ab966418bda3abd61bbcc12ff004feca5c0', class: "pn-date-picker-wrapper" }, h("nav", { key: '67a2fea072a8e4953fa9803e77d0245760b7195a', class: "pn-date-picker-nav", "aria-labelledby": this.idCalendar }, h("pn-button", { key: '9d9d0aa471baa6d6183a4b3b920cf8d05737e8a1', 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: '57988169fdd38cb05c18c6d4f8e71ded23f97f5f', hidden: !this.viewingCalendar(), small: true, appearance: "light", onPnClick: () => this.setView(MONTHS) }, h("span", { key: '93c98b9aeabdc4b08e59afdaf8655dfe3c440b13', class: "pn-date-picker-month", "data-full": true }, this.translateDateText({ date: 1 }, 'MMMM')), h("span", { key: '9c0b0c3378f00755ef84e5e87426c1a9bedb0651', class: "pn-date-picker-month", "data-abbr": true }, this.translateDateText({ date: 1 }, 'MMM'))), h("h2", { key: '02d40d1aa03dd69f5b344295713ec9c45431b3e9', hidden: this.viewingCalendar(), class: "pn-date-picker-title" }, this.translate(`SELECT_${this.viewType().toUpperCase()}`)), h("pn-button", { key: '4f953719a89967b10c98472b24f732c42cd61e3c', hidden: !this.viewingCalendar(), small: true, appearance: "light", onPnClick: () => this.setView(YEARS) }, h("span", { key: 'a91a0dd940b94d0f71d887508644d6acfa3ef098' }, this.dateViewYear)), h("pn-button", { key: '3eabe547992de22014f3e229c9e7e17eab395d56', 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: '6eaefead16141908688578834b1e6a2ba87e8161', class: "pn-date-picker-chips" }, h("slot", { key: '43bcfa9bb50d6dbd912ae8cfee339b642b9363a5', name: "chips" })), h("nav", { key: 'b22d7112ba5d3d012f5d2617586bd251547f4ed7', class: "pn-date-picker-bottom", hidden: this.viewingCalendar() }, h("pn-button", { key: '41031640dbac5130b3fb9029b36c5e732068c78f', appearance: "light", variant: "outlined", small: true, icon: pn_return, onPnClick: () => this.setView(CALENDAR) }, h("span", { key: '08568a439547db8728c3f110c00854d49ec7eb77' }, this.translate('GO_CALENDAR')))))), h("p", { key: 'e890351198e9eaa85e9db3bcc55bc8bdd48f3538', id: this.idHelper, class: "pn-date-picker-helpertext", hidden: this.hideHelpertext() }, h("span", { key: '0b776595b33ed64cb73b5060d023eb89421ad758' }, this.helpertext), h("slot", { key: 'e9ef3797e3baae78dca7efc2313e1b6cf9c7efd7', name: "helpertext" })), h("p", { key: 'aa43fb6eaf42251bb7dadab0f724edee0e6d42b2', id: this.idError, class: "pn-date-picker-error", role: "alert", hidden: this.hideError() }, h("span", { key: '556bfbb5e5ba697234734eb601445150b23ddcf2' }, this.error), h("slot", { key: '21dd12fd2c3f1ac814699f4785ece567136cedd2', 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": false,
"optional": true,
"docs": {
"tags": [],
"text": "Set a label for the from date."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "label-from"
},
"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,
"reflect": false,
"attribute": "label-to"
},
"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,
"reflect": false,
"attribute": "helpertext"
},
"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",
"referenceLocation": "PnLanguages"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Manually set language; this will be inherited from the topbar."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "language",
"defaultValue": "undefined"
},
"start": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "see",
"text": "{@link format }"
}, {
"name": "category",
"text": "Native attributes"
}],
"text": "Set a predefined value for the from date (input value)."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "start",
"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 }"
}, {
"name": "category",
"text": "Native attributes"
}],
"text": "Set a predefined value for the from date. (input value end)"
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "end",
"defaultValue": "''"
},
"name": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "since",
"text": "v7.6.0"
}, {
"name": "category",
"text": "Native attributes"
}],
"text": "HTML input name"
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "name"
},
"nameEnd": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "since",
"text": "v7.25.0"
}, {
"name": "category",
"text": "Native attributes"
}],
"text": "HTML input name"
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "name-end"
},
"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": "Native attributes"
}],
"text": "Placeholder for the input field (defaults to the format prop)."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "placeholder"
},
"endPlaceholder": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link format }"
}, {
"name": "deprecated",
"text": "Use `placeholder-end` instead"
}, {
"name": "category",
"text": "Native attributes"
}],
"text": "Placeholder for end date (defaults to the format prop)."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "end-placeholder"
},
"placeholderEnd": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link format }"
}, {
"name": "since",
"text": "v7.25.0"
}, {
"name": "category",
"text": "Native attributes"
}],
"text": "Placeholder for end date (defaults to the format prop)."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "placeholder-end"
},
"autocomplete": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",