UNPKG

@trimble-oss/moduswebcomponents

Version:

Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust

896 lines (891 loc) 52.4 kB
import { p as proxyCustomElement, H, d as createEvent, h, c as Host } from './p-X1tirp06.js'; import { i as inheritAriaAttributes } from './p-VPqXjOQn.js'; import { d as defineCustomElement$6 } from './p-CZDjVvLY.js'; import { d as defineCustomElement$5 } from './p-s6LDESOI.js'; import { d as defineCustomElement$4 } from './p-kjsS48iG.js'; import { d as defineCustomElement$3 } from './p-DtlnhsZ5.js'; import { d as defineCustomElement$2 } from './p-D2LdPRKQ.js'; import { c as createPopper } from './p-BfP9ezJQ.js'; const convertPropsToClasses = ({ bordered, feedback, readOnly, size, }) => { let classes = ''; if (bordered) { classes = `${classes} modus-wc-input-bordered`; } if (feedback) { classes = `${classes} modus-wc-input--${feedback.level}`; } if (readOnly) { classes = `${classes} modus-wc-date-input--readonly`; } if (size) { classes = `${classes} modus-wc-input-${size}`; } return classes.trim(); }; class DatePickerCalendar { constructor(firstDayOfWeek = 0) { this.currentDate = new Date(); this.currentMonthDates = []; this.firstDayOfWeek = 0; // Default to Sunday this.firstDayOfWeek = firstDayOfWeek; const today = new Date(); this.gotoDate(today.getFullYear(), today.getMonth()); } get selectedYear() { return this.currentDate.getFullYear(); } get selectedMonth() { return this.currentDate.getMonth(); } get dates() { return this.currentMonthDates; } addMonthOffset(offset) { this.gotoDate(this.currentDate.getFullYear(), this.currentDate.getMonth() + offset); return this; } gotoDate(year, month) { this.currentDate = new Date(year, month, 1); this.calculateDates(); } getDaysOfWeek(locale, firstDayOfWeek = 0) { /** * Nov 1st, 2020 starts on a Sunday, * assumes weeks start on Sunday, * but is configurable via `firstDayOfWeek`. */ const intl = new Intl.DateTimeFormat(locale, { weekday: 'short' }); const startDate = new Date('11/01/2020'); const daysOfWeek = []; /** * For each day of the week, * get the day name. */ for (let i = firstDayOfWeek; i < firstDayOfWeek + 7; i++) { const currentDate = new Date(startDate); currentDate.setDate(currentDate.getDate() + i); const d = intl.format(currentDate); daysOfWeek.push(d.toUpperCase().startsWith('SA') ? d : d.slice(0, 2)); } return daysOfWeek; } /** * Get ISO week number for a given date * ISO 8601: Week 1 is the week with the year's first Thursday */ getWeekNumber(date, weekStart = 1) { function isoWeek(d) { const temp = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); const day = temp.getUTCDay() || 7; temp.setUTCDate(temp.getUTCDate() + 4 - day); const yearStart = new Date(Date.UTC(temp.getUTCFullYear(), 0, 1)); return Math.ceil(((temp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); } // --- FIND START OF WEEK --- const day = date.getDay(); const diff = (day - weekStart + 7) % 7; const weekStartDate = new Date(date); weekStartDate.setDate(date.getDate() - diff); // --- COLLECT ALL WEEK NUMBERS --- const weekCounts = {}; for (let i = 0; i < 7; i++) { const d = new Date(weekStartDate); d.setDate(weekStartDate.getDate() + i); const w = isoWeek(d); weekCounts[w] = (weekCounts[w] || 0) + 1; } // --- PICK WEEK WITH MOST OCCURRENCES --- let majorityWeek = 0; let max = 0; for (const [weekStr, count] of Object.entries(weekCounts)) { const week = Number(weekStr); if (count > max) { max = count; majorityWeek = week; } } // Return double-digit week number return majorityWeek.toString().padStart(2, '0'); } calculateDates() { const dates = []; const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth(); // Get first day of current month const firstDayOfMonth = new Date(year, month, 1); const dayOfWeek = firstDayOfMonth.getDay(); // 0 = Sunday, 1 = Monday, etc. // Simple calculation: if month starts on Wednesday (3) and we want Monday (1) as first day, // we need to show 2 days from previous month (3 - 1 = 2) // But if month starts on Sunday (0) and we want Monday (1) as first day, // we need to show 6 days from previous month (0 - 1 = -1, so we add 7: 6) let daysToSubtract = dayOfWeek - this.firstDayOfWeek; if (daysToSubtract < 0) { daysToSubtract += 7; } // Start from the previous month's dates to fill the first week const startDate = new Date(year, month, 1 - daysToSubtract); // Generate 42 dates (6 weeks * 7 days) to ensure consistent 6-row layout for (let i = 0; i < 42; i++) { const date = new Date(startDate); date.setDate(startDate.getDate() + i); dates.push(date); } this.currentMonthDates = dates; } } const modusWcDateCss = ".modus-wc-input--error{border-color:var(--modus-wc-color-error) !important}.modus-wc-input--info{border-color:var(--modus-wc-color-info) !important}.modus-wc-input--success{border-color:var(--modus-wc-color-success) !important}.modus-wc-input--warning{border-color:var(--modus-wc-color-warning) !important}.modus-wc-input-xs{height:var(--modus-wc-size-xs);min-height:var(--modus-wc-size-xs)}.modus-wc-input-sm{height:var(--modus-wc-size-sm);min-height:var(--modus-wc-size-sm)}.modus-wc-input-md{height:var(--modus-wc-size-md);min-height:var(--modus-wc-size-md)}.modus-wc-input-lg{height:var(--modus-wc-size-lg);min-height:var(--modus-wc-size-lg)}.modus-wc-input-xl{height:var(--modus-wc-size-xl);min-height:var(--modus-wc-size-xl)}modus-wc-date .modus-wc-date-input{background-color:inherit}modus-wc-date .modus-wc-date-input .modus-wc-input-label{padding-bottom:var(--modus-wc-spacing-sm)}modus-wc-date .modus-wc-date-input--readonly{background-color:var(--modus-wc-color-base-200)}modus-wc-date{--calendar-grid-columns:repeat(7, 1fr);--calendar-grid-columns-with-week-numbers:auto repeat(7, 1fr);position:relative}modus-wc-date .date-input-container{align-items:center;display:inline-flex;position:relative;width:100%}modus-wc-date .date-input-container .calendar-icon-button{align-items:center;display:flex;justify-content:center;position:absolute;right:var(--modus-wc-spacing-xs)}modus-wc-date .date-input-container .calendar-icon-button :hover{color:var(--modus-wc-color-blue-light)}modus-wc-date .date-input-container .calendar-icon-button :disabled{background-color:transparent;cursor:not-allowed}modus-wc-date .calendar-container{background:var(--modus-wc-color-white);border:1px solid var(--modus-wc-color-gray-4);border-radius:var(--modus-wc-border-radius-md);box-shadow:0 4px 12px rgba(0, 0, 0, 0.15);height:327px;overflow:hidden;position:fixed;width:272px;z-index:9999}modus-wc-date .calendar-container.has-week-numbers{width:320px}modus-wc-date .calendar-container .calendar-header{align-items:center;background-color:var(--modus-wc-color-primary);color:var(--modus-wc-color-white);display:flex;gap:var(--modus-wc-spacing-sm);justify-content:center;padding:var(--modus-wc-spacing-md) var(--modus-wc-spacing-xl)}modus-wc-date .calendar-container .calendar-header .nav-btn i{color:var(--modus-wc-color-white)}modus-wc-date .calendar-container .calendar-header .nav-btn:hover i{color:var(--modus-wc-color-blue-dark)}modus-wc-date .calendar-container .calendar-header .calendar-selects{align-items:center;display:flex;gap:var(--modus-wc-spacing-xs)}modus-wc-date .calendar-container .calendar-header .calendar-selects select{box-shadow:0 1px 2px 0 rgba(0, 0, 0, 0.05)}modus-wc-date .calendar-container .calendar-header .calendar-selects select option{background-color:var(--modus-wc-color-white);color:var(--modus-wc-color-gray-9)}modus-wc-date .calendar-container .calendar-body{background-color:var(--modus-wc-color-base-page);font-size:var(--modus-wc-font-size-md);height:272px;padding:var(--modus-wc-spacing-xl)}modus-wc-date .calendar-container .calendar-body .calendar-days-week{display:grid;gap:var(--modus-wc-spacing-xs);grid-template-columns:var(--calendar-grid-columns);margin-bottom:var(--modus-wc-spacing-sm)}modus-wc-date .calendar-container .calendar-body .calendar-days-week.has-week-numbers{grid-template-columns:var(--calendar-grid-columns-with-week-numbers)}modus-wc-date .calendar-container .calendar-body .calendar-days-week .week-number-header{margin-inline-end:var(--modus-wc-spacing-md);padding:var(--modus-wc-spacing-xs)}modus-wc-date .calendar-container .calendar-body .calendar-days-week .day-header{color:var(--modus-wc-color-base-content-hight-contrast);font-weight:var(--modus-wc-font-weight-bold);padding:var(--modus-wc-spacing-xs);text-align:center}modus-wc-date .calendar-container .calendar-body .calendar-dates{display:grid;grid-template-columns:var(--calendar-grid-columns)}modus-wc-date .calendar-container .calendar-body .calendar-dates.has-week-numbers{grid-template-columns:var(--calendar-grid-columns-with-week-numbers)}modus-wc-date .calendar-container .calendar-body .calendar-dates .week-number{align-items:center;border-inline-end:1px solid color-mix(in sRGB, var(--modus-wc-color-base-inverted) 10%, transparent);display:flex;font-size:var(--modus-wc-font-size-sm);font-weight:var(--modus-wc-font-weight-bold);justify-content:center;padding-inline-end:var(--modus-wc-spacing-sm)}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day{align-items:center;background:none;border:none;border-radius:50%;color:var(--modus-wc-color-base-content-hight-contrast);cursor:pointer;display:flex;height:32px;justify-content:center;width:32px}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day:hover{background-color:var(--modus-wc-color-blue-light);color:var(--modus-wc-color-white)}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day:focus{background-color:var(--modus-wc-color-blue-light);color:var(--modus-wc-color-white);outline:none}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day.current-day{border:2px solid var(--modus-wc-color-blue-light);outline:none}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day.selected{background-color:var(--modus-wc-color-blue-light);color:var(--modus-wc-color-white);font-weight:var(--modus-wc-font-weight-semibold)}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day.other-month{color:var(--modus-wc-color-base-content-high-contrast);opacity:0.5}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day.other-month:hover{background-color:var(--modus-wc-color-gray-2);color:var(--modus-wc-color-gray-6)}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day.disabled{color:var(--modus-wc-color-gray-4)}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day.disabled:hover{background-color:transparent;color:var(--modus-wc-color-gray-4)}modus-wc-date .calendar-container .calendar-body .calendar-dates .calendar-day.disabled:focus{border:none}[data-theme=modus-classic-light] modus-wc-date .modus-wc-date-input.modus-wc-input,[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input.modus-wc-input{border-radius:var(--modus-wc-border-radius-md)}[data-theme=modus-classic-light] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-input-sm,[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-input-sm{font-size:var(--modus-wc-font-size-sm);height:var(--modus-wc-input-height-sm);padding:var(--modus-wc-spacing-sm) var(--modus-wc-spacing-xs)}[data-theme=modus-classic-light] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-input-md,[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-input-md{font-size:var(--modus-wc-font-size-md);height:var(--modus-wc-input-height-md);padding:var(--modus-wc-spacing-sm)}[data-theme=modus-classic-light] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-input-lg,[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-input-lg{font-size:var(--modus-wc-font-size-lg);height:var(--modus-wc-input-height-lg);padding:var(--modus-wc-spacing-md) var(--modus-wc-spacing-sm)}[data-theme=modus-classic-light] modus-wc-date .modus-wc-date-input.modus-wc-input:focus,[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input.modus-wc-input:focus{border-color:var(--modus-wc-color-blue-light);border-width:var(--modus-wc-border-width-sm)}[data-theme=modus-classic-light] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-date-input--readonly,[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-date-input--readonly{background-color:var(--modus-wc-color-base-100)}[data-theme=modus-classic-light] modus-wc-date .modus-wc-date-input.modus-wc-input.modus-wc-input-bordered:not(:disabled):not(:focus){border-color:var(--modus-wc-color-gray-6)}[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input{color-scheme:dark}[data-theme=modus-classic-dark] modus-wc-date .modus-wc-date-input.modus-wc-input:focus{border-color:var(--modus-wc-color-highlight-blue)}[data-theme=connect-light] modus-wc-date .modus-wc-date-input,[data-theme=connect-dark] modus-wc-date .modus-wc-date-input{border-bottom-width:var(--input-bottom-border-width);outline-width:0 !important}[data-theme=connect-light] modus-wc-date .modus-wc-date-input:not(.modus-wc-select,.modus-wc-number-input),[data-theme=connect-dark] modus-wc-date .modus-wc-date-input:not(.modus-wc-select,.modus-wc-number-input){padding:0 var(--modus-wc-spacing-sm)}[data-theme=connect-light] modus-wc-date .modus-wc-date-input:hover,[data-theme=connect-dark] modus-wc-date .modus-wc-date-input:hover{border-bottom-color:var(--modus-wc-color-primary)}[data-theme=connect-light] modus-wc-date .modus-wc-date-input:active,[data-theme=connect-dark] modus-wc-date .modus-wc-date-input:active{border-bottom-color:var(--modus-wc-color-primary)}[data-theme=connect-light] modus-wc-date .modus-wc-date-input:focus,[data-theme=connect-dark] modus-wc-date .modus-wc-date-input:focus{border-bottom-color:var(--modus-wc-color-primary);outline:none}[data-theme=connect-light] modus-wc-date .modus-wc-date-input:focus-within,[data-theme=connect-dark] modus-wc-date .modus-wc-date-input:focus-within{border-bottom-color:var(--modus-wc-color-primary);outline:none}[data-theme=connect-dark] modus-wc-date .calendar-container .calendar-header .nav-btn i,[data-theme=modus-modern-dark] modus-wc-date .calendar-container .calendar-header .nav-btn i,[data-theme=modus-classic-dark] modus-wc-date .calendar-container .calendar-header .nav-btn i{color:var(--modus-wc-color-black)}[data-theme=connect-dark] modus-wc-date .calendar-container .calendar-header .calendar-selects select,[data-theme=modus-modern-dark] modus-wc-date .calendar-container .calendar-header .calendar-selects select,[data-theme=modus-classic-dark] modus-wc-date .calendar-container .calendar-header .calendar-selects select{background-color:unset;color:var(--modus-wc-color-black);font-weight:600}[data-theme=connect-dark] modus-wc-date .calendar-container .calendar-header .calendar-selects select option,[data-theme=modus-modern-dark] modus-wc-date .calendar-container .calendar-header .calendar-selects select option,[data-theme=modus-classic-dark] modus-wc-date .calendar-container .calendar-header .calendar-selects select option{background-color:var(--modus-wc-color-black);color:var(--modus-wc-color-white)}"; const MONTH_SHORT_NAMES = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; const WEEK_START_DAY_MAP = { sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, }; const ModusWcDate$1 = /*@__PURE__*/ proxyCustomElement(class ModusWcDate extends H { constructor() { super(); this.__registerHost(); this.inputBlur = createEvent(this, "inputBlur"); this.inputChange = createEvent(this, "inputChange"); this.inputFocus = createEvent(this, "inputFocus"); this.calendarMonthChange = createEvent(this, "calendarMonthChange"); this.calendarYearChange = createEvent(this, "calendarYearChange"); this.inheritedAttributes = {}; this.popperInstance = null; /** Show the calendar dropdown */ this.showCalendar = false; /** Calendar state object */ this.calendar = new DatePickerCalendar(); /** Currently focused date index in calendar */ this.focusedDateIndex = -1; /** Tracks whether the component currently has focus */ this.hasFocus = false; /** Indicates that the input should have a border. */ this.bordered = true; /** Custom CSS class to apply to the input. */ this.customClass = ''; /** Whether the form control is disabled. */ this.disabled = false; /** Whether the value is editable. */ this.readOnly = false; /** A value is required or must be checked for the form to be submittable. */ this.required = false; /** The size of the input. */ this.size = 'md'; /** The date format for display and input. */ this.format = 'dd-mm-yyyy'; /** The value of the control. */ this.value = ''; /** The first day of the week for the calendar display */ this.weekStartDay = 'sunday'; /** Displays ISO 8601 week numbers in the calendar.Week numbers are calculated with Monday as the first day of the week.*/ this.showWeekNumbers = false; this.handleBlur = (event) => { // Check if focus is moving to an element within the component const relatedTarget = event.relatedTarget; // istanbul ignore next (unreachable code) if (relatedTarget && this.el.contains(relatedTarget)) { // Focus is moving within the component, don't emit blur return; } // Focus is leaving the component this.hasFocus = false; this.syncValueFromInput(); this.inputBlur.emit(event); }; this.handleFocus = (event) => { // Only emit focus if component didn't already have focus if (!this.hasFocus) { this.hasFocus = true; this.inputFocus.emit(event); } }; this.handleInput = (event) => { this.inputChange.emit(event); }; this.handleInputKeyDown = (event) => { if (event.key === 'Enter') { event.preventDefault(); this.syncValueFromInput(); } }; this.setupPopper = () => { if (this.popperInstance) { this.popperInstance.destroy(); } this.popperInstance = createPopper(this.inputRef, this.calendarRef, { placement: 'bottom-start', strategy: 'fixed', modifiers: [ { name: 'offset', options: { offset: [0, 8], }, }, { name: 'flip', options: { fallbackPlacements: ['top-start', 'bottom-end', 'top-end'], }, }, ], }); }; this.toggleCalendar = () => { this.showCalendar = !this.showCalendar; // If opening the calendar and there's a selected date, navigate to it if (this.showCalendar) { const selectedDate = this.parseISODate(this.value); this.ensureCalendarWithinBounds(selectedDate); // Set focus to the selected date if it exists if (selectedDate) { const selectedIndex = this.calendar.dates.findIndex((date) => date && this.compareDate(date, selectedDate) === 0); if (selectedIndex !== -1) { this.focusedDateIndex = selectedIndex; } } // set focus to today else { this.ensureCalendarWithinBounds(new Date()); this.focusedDateIndex = this.calendar.dates.findIndex((date) => date && this.compareDate(date, new Date()) === 0); } } else { // Reset focus when closing this.focusedDateIndex = -1; } // Always ensure input is focused when toggling calendar (opening or closing) if (this.inputRef) { this.inputRef.focus(); } }; this.handleDateSelect = (date) => { if (this.isDateDisabled(date)) { return; } this.value = this.formatISODate(date); // If the selected date is from a different month, navigate to that month // istanbul ignore next (unreachable code) if (date.getMonth() !== this.calendar.selectedMonth || date.getFullYear() !== this.calendar.selectedYear) { const firstDayOfWeek = WEEK_START_DAY_MAP[this.weekStartDay || 'sunday']; const newCalendar = new DatePickerCalendar(firstDayOfWeek); newCalendar.gotoDate(date.getFullYear(), date.getMonth()); this.calendar = newCalendar; } this.showCalendar = false; this.hasFocus = false; this.inputBlur.emit(new FocusEvent('blur', { bubbles: true })); }; this.addMonthOffset = (offset) => { const target = new Date(this.calendar.selectedYear, this.calendar.selectedMonth + offset, 1); this.updateCalendarAndEmitEvents(target.getFullYear(), target.getMonth()); }; this.handleMonthChange = (event) => { event.stopPropagation(); // Try to get the value from the original input event const inputEvent = event.detail; const selectTarget = inputEvent === null || inputEvent === void 0 ? void 0 : inputEvent.target; const monthValue = selectTarget === null || selectTarget === void 0 ? void 0 : selectTarget.value; const newMonth = parseInt(monthValue || '0', 10); const currentYear = this.calendar.selectedYear; if (Number.isNaN(newMonth)) { return; } this.updateCalendarAndEmitEvents(currentYear, newMonth); }; this.handleYearChange = (event) => { event.stopPropagation(); // Try to get the value from the original input event const inputEvent = event.detail; const selectTarget = inputEvent === null || inputEvent === void 0 ? void 0 : inputEvent.target; const yearValue = selectTarget === null || selectTarget === void 0 ? void 0 : selectTarget.value; const newYear = parseInt(yearValue || '0', 10); const currentMonth = this.calendar.selectedMonth; if (Number.isNaN(newYear)) { return; } this.updateCalendarAndEmitEvents(newYear, currentMonth); }; this.handleDateKeyDown = (event, date) => { if (this.isDateDisabled(date)) { return; } if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); this.handleDateSelect(date); } }; } handleMinChange(newValue) { this.minDate = this.parseISODate(newValue); if (this.maxDate && this.minDate && this.minDate > this.maxDate) { this.maxDate = this.cloneDate(this.minDate); } this.ensureValueWithinBounds(); } handleMaxChange(newValue) { this.maxDate = this.parseISODate(newValue); if (this.minDate && this.maxDate && this.maxDate < this.minDate) { this.minDate = this.cloneDate(this.maxDate); } this.ensureValueWithinBounds(); } handleValueChange(newValue) { if (newValue === undefined) { return; } if (!newValue) { if (this.inputRef) { this.inputRef.value = ''; } return; } const parsed = this.parseISODate(newValue); if (!parsed) { if (this.value) { this.value = ''; } return; } const clamped = this.clampDate(parsed); const formatted = this.formatISODate(clamped); if (newValue !== formatted) { this.value = formatted; return; } if (this.inputRef) { this.inputRef.value = formatted; const event = new Event('input', { bubbles: true }); this.inputRef.dispatchEvent(event); } this.ensureCalendarWithinBounds(clamped); } handleWeekStartDayChange() { // Reinitialize calendar with new first day of week const firstDayOfWeek = WEEK_START_DAY_MAP[this.weekStartDay]; this.calendar = new DatePickerCalendar(firstDayOfWeek); // Navigate to currently selected date if exists const selectedDate = this.parseISODate(this.value); if (selectedDate) { this.calendar.gotoDate(selectedDate.getFullYear(), selectedDate.getMonth()); } } componentWillLoad() { if (!this.el.ariaLabel) { this.el.ariaLabel = 'Date input'; } this.inheritedAttributes = inheritAriaAttributes(this.el); // Initialize calendar with the correct first day of week const firstDayOfWeek = WEEK_START_DAY_MAP[this.weekStartDay]; this.calendar = new DatePickerCalendar(firstDayOfWeek); this.handleMinChange(this.min); this.handleMaxChange(this.max); this.handleValueChange(this.value); } componentDidUpdate() { if (this.showCalendar && this.inputRef && this.calendarRef) { this.setupPopper(); } else if (this.popperInstance) { this.popperInstance.destroy(); this.popperInstance = null; } } disconnectedCallback() { if (this.popperInstance) { this.popperInstance.destroy(); this.popperInstance = null; } } getClasses() { const classList = [ 'modus-wc-date-input', 'modus-wc-input', 'modus-wc-w-full', ]; const propClasses = convertPropsToClasses({ bordered: this.bordered, feedback: this.feedback, readOnly: this.readOnly, size: this.size, }); // The order CSS classes are added matters to CSS specificity if (propClasses) classList.push(propClasses); if (this.customClass) classList.push(this.customClass); return classList.join(' '); } handleClickOutside(event) { const path = event.composedPath(); const insideComponent = path.includes(this.el); if (!insideComponent && this.showCalendar) { this.showCalendar = false; this.hasFocus = false; } } handleEscapeKey(event) { if (event.key === 'Escape' && this.showCalendar) { this.showCalendar = false; event.preventDefault(); } } navigateToAdjacentMonth(currentIndex, isUp) { var _a, _b, _c; const currentColumn = currentIndex % 7; // Navigate to previous/next month // Date constructor will normalize out-of-bounds months (e.g., -1 → Dec of prev year, 12 → Jan of next year) this.updateCalendarAndEmitEvents(this.calendar.selectedYear, this.calendar.selectedMonth + (isUp ? -1 : 1)); // Find target date in same column const weekRange = isUp ? [5, 4, 3, 2, 1, 0] : [0, 1, 2, 3, 4, 5]; for (const week of weekRange) { const indexInWeek = week * 7 + currentColumn; // istanbul ignore next (optional chaining) if (indexInWeek < this.calendar.dates.length && ((_a = this.calendar.dates[indexInWeek]) === null || _a === void 0 ? void 0 : _a.getMonth()) === this.calendar.selectedMonth) { this.focusedDateIndex = indexInWeek; return; } } // Fallback to first/last current-month date // istanbul ignore next (fallback scenario) const currentMonthIndices = this.calendar.dates .map((date, index) => (date === null || date === void 0 ? void 0 : date.getMonth()) === this.calendar.selectedMonth ? index : -1) .filter((index) => index !== -1); // istanbul ignore next (fallback scenario) this.focusedDateIndex = isUp ? ((_b = currentMonthIndices[currentMonthIndices.length - 1]) !== null && _b !== void 0 ? _b : this.calendar.dates.length - 1) : ((_c = currentMonthIndices[0]) !== null && _c !== void 0 ? _c : 0); } handleArrowKeys(event) { var _a; if (!this.showCalendar) { return; } const key = event.key; if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) { return; } event.preventDefault(); const totalDates = this.calendar.dates.length; let newIndex = this.focusedDateIndex; // If no date is focused, start with the first date or selected date if (newIndex === -1) { if (this.value) { const selectedDate = this.parseISODate(this.value); if (selectedDate) { newIndex = this.calendar.dates.findIndex((date) => this.compareDate(date, selectedDate) === 0); } } // istanbul ignore next (unreachable code) if (newIndex === -1) { newIndex = 0; } } // Calculate target position for each arrow key let targetIndex = newIndex; let shouldChangeMonth = false; let targetDate = null; // Navigate based on arrow key // istanbul ignore next (unreachable code) switch (key) { case 'ArrowLeft': targetIndex = newIndex - 1; break; case 'ArrowRight': targetIndex = newIndex + 1; break; case 'ArrowUp': targetIndex = newIndex - 7; break; case 'ArrowDown': targetIndex = newIndex + 7; break; } // Check if target index is valid and get the target date if (targetIndex >= 0 && targetIndex < totalDates) { targetDate = this.calendar.dates[targetIndex]; if (targetDate) { // Skip disabled dates - keep moving in the same direction until we find a valid date let searchIndex = targetIndex; const direction = key === 'ArrowLeft' ? -1 : key === 'ArrowRight' ? 1 : key === 'ArrowUp' ? -7 : 7; while (searchIndex >= 0 && searchIndex < totalDates && this.isDateDisabled(this.calendar.dates[searchIndex])) { searchIndex += direction; } // If we found a valid date within bounds if (searchIndex >= 0 && searchIndex < totalDates && this.calendar.dates[searchIndex]) { targetDate = this.calendar.dates[searchIndex]; targetIndex = searchIndex; } else { // No valid date found in this direction, don't move return; } // If target date is from a different month, navigate to that month // istanbul ignore next (optional chaining) if (targetDate.getMonth() !== this.calendar.selectedMonth) { shouldChangeMonth = true; } this.focusedDateIndex = targetIndex; } } else { // Target is outside current calendar, navigate to appropriate month shouldChangeMonth = true; if (key === 'ArrowUp') { // Check if we can navigate to previous month const prevMonthDate = new Date(this.calendar.selectedYear, this.calendar.selectedMonth - 1, 1); if (!this.isDateDisabled(prevMonthDate)) { this.navigateToAdjacentMonth(newIndex, true); shouldChangeMonth = false; // Already handled in helper } } else if (key === 'ArrowDown') { // Check if we can navigate to next month const nextMonthDate = new Date(this.calendar.selectedYear, this.calendar.selectedMonth + 1, 1); if (!this.isDateDisabled(nextMonthDate)) { this.navigateToAdjacentMonth(newIndex, false); shouldChangeMonth = false; // Already handled in helper } } else if (key === 'ArrowLeft') { // Go to previous month's last day const prevMonthDate = new Date(this.calendar.selectedYear, this.calendar.selectedMonth - 1, 1); targetDate = new Date(prevMonthDate.getFullYear(), prevMonthDate.getMonth() + 1, 0); // Last day of previous month // Only navigate if not disabled if (this.isDateDisabled(targetDate)) { return; } } else { // Go to next month's first day targetDate = new Date(this.calendar.selectedYear, this.calendar.selectedMonth + 1, 1); // First day of next month // Only navigate if not disabled if (this.isDateDisabled(targetDate)) { return; } } } // Handle month change if needed if (shouldChangeMonth && targetDate) { this.updateCalendarAndEmitEvents(targetDate.getFullYear(), targetDate.getMonth()); // Find the target date in the new calendar const newTargetIndex = this.calendar.dates.findIndex( // istanbul ignore next (optional chaining) (date) => date && this.compareDate(date, targetDate) === 0); // istanbul ignore next (inequality check) if (newTargetIndex !== -1) { this.focusedDateIndex = newTargetIndex; } else { // Fallback positioning // istanbul ignore next (fallback scenario) if (key === 'ArrowLeft' || key === 'ArrowUp') { // Focus on last current-month date // istanbul ignore next (fallback scenario) const lastCurrentMonthIndex = this.calendar.dates .map((date, index) => date && date.getMonth() === targetDate.getMonth() ? index : -1) .filter((index) => index !== -1) .pop(); // istanbul ignore next (fallback scenario) this.focusedDateIndex = lastCurrentMonthIndex !== undefined ? lastCurrentMonthIndex : this.calendar.dates.length - 1; } else { // Focus on first current-month date // istanbul ignore next (fallback scenario) const firstCurrentMonthIndex = this.calendar.dates.findIndex((date) => date && date.getMonth() === targetDate.getMonth()); // istanbul ignore next (fallback scenario) this.focusedDateIndex = firstCurrentMonthIndex !== -1 ? firstCurrentMonthIndex : 0; } } } // Focus the corresponding button // istanbul ignore next (unreachable code) const dateButtons = (_a = this.calendarRef) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.calendar-day'); if (dateButtons && dateButtons[this.focusedDateIndex]) { // istanbul ignore next (unreachable code) dateButtons[this.focusedDateIndex].focus(); } } renderCalendarHeader() { const currentYear = this.calendar.selectedYear; const currentMonth = this.calendar.selectedMonth; // Generate year options (current year ± 100 years) const yearOptions = []; for (let i = currentYear - 100; i <= currentYear + 100; i++) { yearOptions.push({ value: i.toString(), label: i.toString() }); } // Generate month options const monthOptions = MONTH_SHORT_NAMES.map((month, index) => ({ value: index.toString(), label: month, })); return (h("div", { class: "calendar-header" }, h("modus-wc-button", { type: "button", "aria-label": "Previous", variant: "borderless", shape: "circle", size: "xs", onButtonClick: // istanbul ignore next (unreachable code) () => this.addMonthOffset(-1), class: "nav-btn" }, h("modus-wc-icon", { name: "chevron_left", size: "sm" })), h("div", { class: "calendar-selects" }, h("modus-wc-select", { key: `month-${currentYear}-${currentMonth}`, class: "month-select", value: currentMonth.toString(), options: monthOptions, onInputChange: // istanbul ignore next (unreachable code) (e) => this.handleMonthChange(e), onInputBlur: // istanbul ignore next (unreachable code) (e) => e.stopPropagation(), bordered: false, size: "sm" }), h("modus-wc-select", { key: `year-${currentYear}`, class: "year-select", value: currentYear.toString(), options: yearOptions, onInputChange: // istanbul ignore next (unreachable code) (e) => this.handleYearChange(e), onInputBlur: // istanbul ignore next (unreachable code) (e) => e.stopPropagation(), bordered: false, size: "sm" })), h("modus-wc-button", { type: "button", "aria-label": "Next", variant: "borderless", shape: "circle", size: "xs", onButtonClick: // istanbul ignore next (unreachable code) () => this.addMonthOffset(1), class: "nav-btn" }, h("modus-wc-icon", { name: "chevron_right", size: "sm" })))); } renderCalendarBody() { const today = new Date(); const selectedDate = this.parseISODate(this.value); const currentMonth = this.calendar.selectedMonth; return (h("div", { class: "calendar-body" }, h("div", { class: `calendar-days-week${this.showWeekNumbers ? ' has-week-numbers' : ''}` }, this.showWeekNumbers && h("div", { class: "week-number-header" }), this.calendar .getDaysOfWeek('default', WEEK_START_DAY_MAP[this.weekStartDay]) .map((d) => { return h("div", { class: "day-header" }, d); })), h("div", { class: `calendar-dates${this.showWeekNumbers ? ' has-week-numbers' : ''}` }, this.calendar.dates.map((date, index) => { // Add week number at the start of each row (every 7 days) const weekNumberElement = this.showWeekNumbers && index % 7 === 0 ? (h("div", { class: "week-number", "aria-label": `Week ${this.calendar.getWeekNumber(date, WEEK_START_DAY_MAP[this.weekStartDay])}` }, this.calendar.getWeekNumber(date, WEEK_START_DAY_MAP[this.weekStartDay]))) : null; if (!date) { return weekNumberElement; } const isToday = this.compareDate(date, today) === 0; const isSelected = (selectedDate && this.compareDate(date, selectedDate) === 0) || false; const isCurrentMonth = date.getMonth() === currentMonth; const isDisabled = this.isDateDisabled(date); const button = (h("button", { type: "button", class: { 'calendar-day': true, 'current-day': isToday, selected: isSelected, 'current-month': isCurrentMonth, 'other-month': !isCurrentMonth, disabled: isDisabled, }, disabled: isDisabled, onClick: () => this.handleDateSelect(date), onKeyDown: (e) => this.handleDateKeyDown(e, date), tabIndex: isDisabled ? -1 : 0 }, date.getDate())); // Only create array when week number exists return weekNumberElement ? [weekNumberElement, button] : button; })))); } compareDate(date1, date2) { if (!date1 && !date2) { return 0; } else if (!date1 && date2) { return -1; } else if (date1 && !date2) { return 1; } let delta; delta = date1.getFullYear() - date2.getFullYear(); if (delta !== 0) { return delta; } delta = date1.getMonth() - date2.getMonth(); if (delta !== 0) { return delta; } return date1.getDate() - date2.getDate(); } parseISODate(value) { var _a; if (!value) { return undefined; } let yearStr, monthStr, dayStr; if (this.format === 'MMM DD, YYYY') { // Parse "Jan 01, 2025" format const match = value.match(/^([A-Za-z]{3})\s+(\d{1,2}),\s+(\d{4})$/); if (!match) { return undefined; } const [, monthName, day, year] = match; // Case-insensitive month name lookup monthStr = MONTH_SHORT_NAMES.findIndex((m) => m.toLowerCase() === monthName.toLowerCase()); if (monthStr === -1) { return undefined; } dayStr = day; yearStr = year; } else { // istanbul ignore next (unreachable code) const separator = ((_a = this.format) === null || _a === void 0 ? void 0 : _a.includes('/')) ? '/' : '-'; const parts = value.split(separator); if (parts.length !== 3) { return undefined; } if (this.format === 'dd-mm-yyyy' || this.format === 'dd/mm/yyyy') { [dayStr, monthStr, yearStr] = parts; } else { // yyyy-mm-dd or yyyy/mm/dd [yearStr, monthStr, dayStr] = parts; } } // istanbul ignore next (unreachable code) if (yearStr == null || monthStr == null || dayStr == null) { return undefined; } const year = Number(yearStr); const month = typeof monthStr === 'number' ? monthStr : Number(monthStr) - 1; const day = Number(dayStr); if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) { return undefined; } const date = new Date(year, month, day); if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { return undefined; } return this.cloneDate(date); } formatISODate(date) { const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); switch (this.format) { case 'dd-mm-yyyy': return `${day}-${month}-${year}`; case 'dd/mm/yyyy': return `${day}/${month}/${year}`; case 'yyyy/mm/dd': return `${year}/${month}/${day}`; case 'MMM DD, YYYY': { const monthName = MONTH_SHORT_NAMES[date.getMonth()]; return `${monthName} ${day}, ${year}`; } default: // yyyy-mm-dd return `${year}-${month}-${day}`; } } cloneDate(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } clampDate(date) { let result = this.cloneDate(date); if (this.minDate && result < this.minDate) { result = this.cloneDate(this.minDate); } if (this.maxDate && result > this.maxDate) { result = this.cloneDate(this.maxDate); } return result; } isDateDisabled(date) { if (this.minDate && date < this.minDate) { return true; } if (this.maxDate && date > this.maxDate) { return true; } return false; } ensureValueWithinBounds() { if (!this.value) { return; } const parsed = this.parseISODate(this.value); if (!parsed) { this.value = ''; return; } const clamped = this.clampDate(parsed); const formatted = this.formatISODate(clamped); if (formatted !== this.value) { this.value = formatted; } } ensureCalendarWithinBounds(referenceDate) { // Allow viewing any month, just disable dates outside min/max if (referenceDate) { const firstDayOfWeek = WEEK_START_DAY_MAP[this.weekStartDay]; const newCalendar = new DatePickerCalendar(firstDayOfWeek); newCalendar.gotoDate(referenceDate.getFullYear(), referenceDate.getMonth()); this.calendar = newCalendar; } } setCalendarMonth(year, month) { const firstDayOfWeek = WEEK_START_DAY_MAP[this.weekStartDay]; const newCalendar = new DatePickerCalendar(firstDayOfWeek); newCalendar.gotoDate(year, month); this.calendar = newCalendar; } updateCalendarAndEmitEvents(year, month) { const oldYear = this.calendar.selectedYear; const oldMonth = this.calendar.selectedMonth; this.setCalendarMonth(year, month); // Emit events only if the values actually changed if (month !== oldMonth) { this.calendarMonthChange.emit(month); } if (year !== oldYear) { this.calendarYearChange.emit(year); } } syncValueFromInput() { if (!this.inputRef) { return; } const value = this.inputRef.value.trim(); if (!value) { if (this.value) { this.value = ''; } return; } const parsed = this.parseISODate(value); if (!parsed) { this.inputRef.value = this.value || ''; return; } const clamped = this.clampDate(parsed); const formatted = this.formatISODate(clamped); this.value = formatted; this.inputRef.value = formatted; } render() { return (h(Host, { key: '6f000e9dc71d97f911585d99bff1a5a3055761e3' }, this.label && (h("modus-wc-input-label", { key: 'f687de871654f64a79ae022de727432ff2704f19', forId: this.inputId, labelText: this.label, required: this.required, size: this.size })), h("div", { key: '367647149b48874be5a78bd9f73bf36a1a89acf4', class: "date-input-container" }, h("input", Object.assign({ key: 'fa3bf4d35eb4c1c53117af2e12a3269c390b40d4', ref: (el) => (this.inputRef = el), "aria-disabled": this.disabled, class: this.getClasses(), disabled: this.disabled, id: this.inputId, name: this.name, onBlur: this.handleBlur, onFocus: this.handleFocus, onInput: this.handleInput, onKeyDown: this.handleInputKeyDown, placeholder: this.format, readonly: this.readOnly, required: this.required, tabIndex: this.inputTabIndex, type: "text", value: this.value }, this.inheritedAttributes)), h("modus-wc-button", { key: 'dbf3ae7cb1b6b1178f664b973e518be8d948d52d', "aria-label": "Open calendar", disabled: this.disabled || this.readOnly, variant: "borderless", shape: "circle", size: "xs", color: "tertiary", class: "calendar-icon-button", onButtonClick: // istanbul ignore next (unreachable code) () => this.toggleCalendar() }, h("modus-wc-icon", { key: 'ebac173bb763508b40fd1c84be9878d9299713e2', name: "calendar_blank", size: "sm" }))), this.showCalendar && (h("div", { key: '31cfd0de7895cba19d32e3babe2a39031a9373ed', ref: (el) => (this.calendarRef = el), class: `calendar-container${this.showWeekNumbers ? ' has-week-numbers' : ''}` }, this.renderCalendarHeader(), this.renderCalendarBody())), this.feedback && (h("modus-wc-input-feedback", { key: 'c5dcb5f36dd5aa35ed7f93b528287b3fe8421a10', level: this.feedback.level, message: this.feedback.message, size: this.size })))); } get el() { return this; } static get watchers() { return { "min": ["handleMinChange"], "max": ["handleMaxChange"], "value": ["handleValueChange"], "weekStartDay": ["handleWeekStartDayChange"]