UNPKG

@postnord/web-components

Version:

PostNord Web Components

1,132 lines 68.5 kB
/*! * 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