UNPKG

@postnord/web-components

Version:
1,106 lines (1,105 loc) 84.3 kB
/*! * 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",