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

1,256 lines 51.2 kB
import { createPopper } from "@popperjs/core";
import { h, Host, } from "@stencil/core";
import { convertPropsToClasses } from "./modus-wc-date.tailwind";
import { inheritAriaAttributes } from "../utils";
import DatePickerCalendar from "./utils/calendar";
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,
};
/**
 * A customizable date picker component used to create date inputs.
 *
 * Adheres to WCAG 2.2 standards.
 */
export class ModusWcDate {
    constructor() {
        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 }))));
    }
    static get is() { return "modus-wc-date"; }
    static get originalStyleUrls() {
        return {
            "$": ["modus-wc-date.scss"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["modus-wc-date.css"]
        };
    }
    static get properties() {
        return {
            "bordered": {
                "type": "boolean",
                "attribute": "bordered",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Indicates that the input should have a border."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "true"
            },
            "customClass": {
                "type": "string",
                "attribute": "custom-class",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Custom CSS class to apply to the input."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "''"
            },
            "disabled": {
                "type": "boolean",
                "attribute": "disabled",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Whether the form control is disabled."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "feedback": {
                "type": "unknown",
                "attribute": "feedback",
                "mutable": false,
                "complexType": {
                    "original": "IInputFeedbackProp",
                    "resolved": "IInputFeedbackProp | undefined",
                    "references": {
                        "IInputFeedbackProp": {
                            "location": "import",
                            "path": "../types",
                            "id": "src/components/types.ts::IInputFeedbackProp"
                        }
                    }
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Feedback to render below the input."
                },
                "getter": false,
                "setter": false
            },
            "inputId": {
                "type": "string",
                "attribute": "input-id",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The ID of the input element."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "inputTabIndex": {
                "type": "number",
                "attribute": "input-tab-index",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Determine the control's relative ordering for sequential focus navigation (typically with the Tab key)."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "label": {
                "type": "string",
                "attribute": "label",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The text to display within the label."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "max": {
                "type": "string",
                "attribute": "max",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Maximum date value."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "min": {
                "type": "string",
                "attribute": "min",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Minimum date value."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "name": {
                "type": "string",
                "attribute": "name",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Name of the form control. Submitted with the form as part of a name/value pair."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "readOnly": {
                "type": "boolean",
                "attribute": "read-only",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Whether the value is editable."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "required": {
                "type": "boolean",
                "attribute": "required",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "A value is required or must be checked for the form to be submittable."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "size": {
                "type": "string",
                "attribute": "size",
                "mutable": false,
                "complexType": {
                    "original": "ModusSize",
                    "resolved": "\"lg\" | \"md\" | \"sm\" | undefined",
                    "references": {
                        "ModusSize": {
                            "location": "import",
                            "path": "../types",
                            "id": "src/components/types.ts::ModusSize"
                        }
                    }
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The size of the input."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "'md'"
            },
            "format": {
                "type": "string",
                "attribute": "format",
                "mutable": false,
                "complexType": {
                    "original": "| 'yyyy-mm-dd'\n    | 'dd-mm-yyyy'\n    | 'yyyy/mm/dd'\n    | 'dd/mm/yyyy'\n    | 'MMM DD, YYYY'",
                    "resolved": "\"MMM DD, YYYY\" | \"dd-mm-yyyy\" | \"dd/mm/yyyy\" | \"yyyy-mm-dd\" | \"yyyy/mm/dd\" | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The date format for display and input."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "'dd-mm-yyyy'"
            },
            "value": {
                "type": "string",
                "attribute": "value",
                "mutable": true,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The value of the control."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "defaultValue": "''"
            },
            "weekStartDay": {
                "type": "string",
                "attribute": "week-start-day",
                "mutable": false,
                "complexType": {
                    "original": "WeekStartDay",
                    "resolved": "\"friday\" | \"monday\" | \"saturday\" | \"sunday\" | \"thursday\" | \"tuesday\" | \"wednesday\" | undefined",
                    "references": {
                        "WeekStartDay": {
                            "location": "import",
                            "path": "../types",
                            "id": "src/components/types.ts::WeekStartDay"
                        }
                    }
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The first day of the week for the calendar display"
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "'sunday'"
            },
            "showWeekNumbers": {
                "type": "boolean",
                "attribute": "show-week-numbers",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Displays ISO 8601 week numbers in the calendar.Week numbers are calculated with Monday as the first day of the week."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            }
        };
    }
    static get states() {
        return {
            "showCalendar": {},
            "calendar": {},
            "focusedDateIndex": {}
        };
    }
    static get events() {
        return [{
                "method": "inputBlur",
                "name": "inputBlur",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Event emitted when the input loses focus."
                },
                "complexType": {
                    "original": "FocusEvent",
                    "resolved": "FocusEvent",
                    "references": {
                        "FocusEvent": {
                            "location": "global",
                            "id": "global::FocusEvent"
                        }
                    }
                }
            }, {
                "method": "inputChange",
                "name": "inputChange",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Event emitted when the input value changes."
                },
                "complexType": {
                    "original": "InputEvent",
                    "resolved": "InputEvent",
                    "references": {
                        "InputEvent": {
                            "location": "global",
                            "id": "global::InputEvent"
                        }
                    }
                }
            }, {
                "method": "inputFocus",
                "name": "inputFocus",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Event emitted when the input gains focus."
                },
                "complexType": {
                    "original": "FocusEvent",
                    "resolved": "FocusEvent",
                    "references": {
                        "FocusEvent": {
                            "location": "global",
                            "id": "global::FocusEvent"
                        }
                    }
                }
            }, {
                "method": "calendarMonthChange",
                "name": "calendarMonthChange",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Event emitted when the calendar month selection changes."
                },
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                }
            }, {
                "method": "calendarYearChange",
                "name": "calendarYearChange",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Event emitted when the calendar year selection changes."
                },
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                }
            }];
    }
    static get elementRef() { return "el"; }
    static get watchers() {
        return [{
                "propName": "min",
                "methodName": "handleMinChange"
            }, {
                "propName": "max",
                "methodName": "handleMaxChange"
            }, {
                "propName": "value",
                "methodName": "handleValueChange"
            }, {
                "propName": "weekStartDay",
                "methodName": "handleWeekStartDayChange"
            }];
    }
    static get listeners() {
        return [{
                "name": "click",
                "method": "handleClickOutside",
                "target": "document",
                "capture": false,
                "passive": false
            }, {
                "name": "keydown",
                "method": "handleEscapeKey",
                "target": "document",
                "capture": false,
                "passive": false
            }, {
                "name": "keydown",
                "method": "handleArrowKeys",
                "target": undefined,
                "capture": false,
                "passive": false
            }];
    }
}