UNPKG

bits-ui

Version:

The headless components for Svelte.

871 lines (870 loc) 32.1 kB
import { getLocalTimeZone, isSameDay, isSameMonth, isToday, } from "@internationalized/date"; import { DEV } from "esm-env"; import { onMount, untrack } from "svelte"; import { attachRef, DOMContext, } from "svelte-toolbelt"; import { Context, watch } from "runed"; import { getAriaDisabled, getAriaHidden, getAriaReadonly, getAriaSelected, getDataDisabled, getDataReadonly, getDataSelected, getDataUnavailable, } from "../../internal/attrs.js"; import { useId } from "../../internal/use-id.js"; import { getAnnouncer } from "../../internal/date-time/announcer.js"; import { createFormatter } from "../../internal/date-time/formatter.js"; import { calendarAttrs, createAccessibleHeading, createMonths, getCalendarElementProps, getCalendarHeadingValue, getDateWithPreviousTime, getDefaultYears, getIsNextButtonDisabled, getIsPrevButtonDisabled, getWeekdays, handleCalendarKeydown, handleCalendarNextPage, handleCalendarPrevPage, shiftCalendarFocus, useEnsureNonDisabledPlaceholder, useMonthViewOptionsSync, useMonthViewPlaceholderSync, } from "../../internal/date-time/calendar-helpers.svelte.js"; import { getDateValueType, isBefore, toDate } from "../../internal/date-time/utils.js"; export const CalendarRootContext = new Context("Calendar.Root | RangeCalender.Root"); export class CalendarRootState { static create(opts) { return CalendarRootContext.set(new CalendarRootState(opts)); } opts; visibleMonths = $derived.by(() => this.months.map((month) => month.value)); formatter; accessibleHeadingId = useId(); domContext; attachment; months = $state([]); announcer; constructor(opts) { this.opts = opts; this.attachment = attachRef(this.opts.ref); this.domContext = new DOMContext(opts.ref); this.announcer = getAnnouncer(null); this.formatter = createFormatter({ initialLocale: this.opts.locale.current, monthFormat: this.opts.monthFormat, yearFormat: this.opts.yearFormat, }); this.setMonths = this.setMonths.bind(this); this.nextPage = this.nextPage.bind(this); this.prevPage = this.prevPage.bind(this); this.prevYear = this.prevYear.bind(this); this.nextYear = this.nextYear.bind(this); this.setYear = this.setYear.bind(this); this.setMonth = this.setMonth.bind(this); this.isOutsideVisibleMonths = this.isOutsideVisibleMonths.bind(this); this.isDateDisabled = this.isDateDisabled.bind(this); this.isDateSelected = this.isDateSelected.bind(this); this.shiftFocus = this.shiftFocus.bind(this); this.handleCellClick = this.handleCellClick.bind(this); this.handleMultipleUpdate = this.handleMultipleUpdate.bind(this); this.handleSingleUpdate = this.handleSingleUpdate.bind(this); this.onkeydown = this.onkeydown.bind(this); this.getBitsAttr = this.getBitsAttr.bind(this); onMount(() => { this.announcer = getAnnouncer(this.domContext.getDocument()); }); this.months = createMonths({ dateObj: this.opts.placeholder.current, weekStartsOn: this.opts.weekStartsOn.current, locale: this.opts.locale.current, fixedWeeks: this.opts.fixedWeeks.current, numberOfMonths: this.opts.numberOfMonths.current, }); this.#setupInitialFocusEffect(); this.#setupAccessibleHeadingEffect(); this.#setupFormatterEffect(); /** * Updates the displayed months based on changes in the placeholder value. */ useMonthViewPlaceholderSync({ placeholder: this.opts.placeholder, getVisibleMonths: () => this.visibleMonths, weekStartsOn: this.opts.weekStartsOn, locale: this.opts.locale, fixedWeeks: this.opts.fixedWeeks, numberOfMonths: this.opts.numberOfMonths, setMonths: (months) => (this.months = months), }); /** * Updates the displayed months based on changes in the options values, * which determines the month to show in the calendar. */ useMonthViewOptionsSync({ fixedWeeks: this.opts.fixedWeeks, locale: this.opts.locale, numberOfMonths: this.opts.numberOfMonths, placeholder: this.opts.placeholder, setMonths: this.setMonths, weekStartsOn: this.opts.weekStartsOn, }); /** * Update the accessible heading's text content when the `fullCalendarLabel` * changes. */ watch(() => this.fullCalendarLabel, (label) => { const node = this.domContext.getElementById(this.accessibleHeadingId); if (!node) return; node.textContent = label; }); /** * Synchronize the placeholder value with the current value. */ watch(() => this.opts.value.current, () => { const value = this.opts.value.current; if (Array.isArray(value) && value.length) { const lastValue = value[value.length - 1]; if (lastValue && this.opts.placeholder.current !== lastValue) { this.opts.placeholder.current = lastValue; } } else if (!Array.isArray(value) && value && this.opts.placeholder.current !== value) { this.opts.placeholder.current = value; } }); useEnsureNonDisabledPlaceholder({ placeholder: opts.placeholder, defaultPlaceholder: opts.defaultPlaceholder, isDateDisabled: opts.isDateDisabled, maxValue: opts.maxValue, minValue: opts.minValue, ref: opts.ref, }); } setMonths(months) { this.months = months; } /** * This derived state holds an array of localized day names for the current * locale and calendar view. It dynamically syncs with the 'weekStartsOn' option, * updating its content when the option changes. Using this state to render the * calendar's days of the week is strongly recommended, as it guarantees that * the days are correctly formatted for the current locale and calendar view. */ weekdays = $derived.by(() => { return getWeekdays({ months: this.months, formatter: this.formatter, weekdayFormat: this.opts.weekdayFormat.current, }); }); initialPlaceholderYear = $derived.by(() => untrack(() => this.opts.placeholder.current.year)); defaultYears = $derived.by(() => { return getDefaultYears({ minValue: this.opts.minValue.current, maxValue: this.opts.maxValue.current, placeholderYear: this.initialPlaceholderYear, }); }); #setupInitialFocusEffect() { $effect(() => { const initialFocus = untrack(() => this.opts.initialFocus.current); if (initialFocus) { // focus the first `data-focused` day node const firstFocusedDay = this.opts.ref.current?.querySelector(`[data-focused]`); if (firstFocusedDay) { firstFocusedDay.focus(); } } }); } #setupAccessibleHeadingEffect() { $effect(() => { if (!this.opts.ref.current) return; const removeHeading = createAccessibleHeading({ calendarNode: this.opts.ref.current, label: this.fullCalendarLabel, accessibleHeadingId: this.accessibleHeadingId, }); return removeHeading; }); } #setupFormatterEffect() { $effect.pre(() => { if (this.formatter.getLocale() === this.opts.locale.current) return; this.formatter.setLocale(this.opts.locale.current); }); } /** * Navigates to the next page of the calendar. */ nextPage() { handleCalendarNextPage({ fixedWeeks: this.opts.fixedWeeks.current, locale: this.opts.locale.current, numberOfMonths: this.opts.numberOfMonths.current, pagedNavigation: this.opts.pagedNavigation.current, setMonths: this.setMonths, setPlaceholder: (date) => (this.opts.placeholder.current = date), weekStartsOn: this.opts.weekStartsOn.current, months: this.months, }); } /** * Navigates to the previous page of the calendar. */ prevPage() { handleCalendarPrevPage({ fixedWeeks: this.opts.fixedWeeks.current, locale: this.opts.locale.current, numberOfMonths: this.opts.numberOfMonths.current, pagedNavigation: this.opts.pagedNavigation.current, setMonths: this.setMonths, setPlaceholder: (date) => (this.opts.placeholder.current = date), weekStartsOn: this.opts.weekStartsOn.current, months: this.months, }); } nextYear() { this.opts.placeholder.current = this.opts.placeholder.current.add({ years: 1 }); } prevYear() { this.opts.placeholder.current = this.opts.placeholder.current.subtract({ years: 1 }); } setYear(year) { this.opts.placeholder.current = this.opts.placeholder.current.set({ year }); } setMonth(month) { this.opts.placeholder.current = this.opts.placeholder.current.set({ month }); } isNextButtonDisabled = $derived.by(() => { return getIsNextButtonDisabled({ maxValue: this.opts.maxValue.current, months: this.months, disabled: this.opts.disabled.current, }); }); isPrevButtonDisabled = $derived.by(() => { return getIsPrevButtonDisabled({ minValue: this.opts.minValue.current, months: this.months, disabled: this.opts.disabled.current, }); }); isInvalid = $derived.by(() => { const value = this.opts.value.current; const isDateDisabled = this.opts.isDateDisabled.current; const isDateUnavailable = this.opts.isDateUnavailable.current; if (Array.isArray(value)) { if (!value.length) return false; for (const date of value) { if (isDateDisabled(date)) return true; if (isDateUnavailable(date)) return true; } } else { if (!value) return false; if (isDateDisabled(value)) return true; if (isDateUnavailable(value)) return true; } return false; }); headingValue = $derived.by(() => { this.opts.monthFormat.current; this.opts.yearFormat.current; return getCalendarHeadingValue({ months: this.months, formatter: this.formatter, locale: this.opts.locale.current, }); }); fullCalendarLabel = $derived.by(() => { return `${this.opts.calendarLabel.current} ${this.headingValue}`; }); isOutsideVisibleMonths(date) { return !this.visibleMonths.some((month) => isSameMonth(date, month)); } isDateDisabled(date) { if (this.opts.isDateDisabled.current(date) || this.opts.disabled.current) return true; const minValue = this.opts.minValue.current; const maxValue = this.opts.maxValue.current; if (minValue && isBefore(date, minValue)) return true; if (maxValue && isBefore(maxValue, date)) return true; return false; } isDateSelected(date) { const value = this.opts.value.current; if (Array.isArray(value)) { return value.some((d) => isSameDay(d, date)); } else if (!value) { return false; } return isSameDay(value, date); } shiftFocus(node, add) { return shiftCalendarFocus({ node, add, placeholder: this.opts.placeholder, calendarNode: this.opts.ref.current, isPrevButtonDisabled: this.isPrevButtonDisabled, isNextButtonDisabled: this.isNextButtonDisabled, months: this.months, numberOfMonths: this.opts.numberOfMonths.current, }); } #isMultipleSelectionValid(selectedDates) { // only validate for multiple type and when maxDays is set if (this.opts.type.current !== "multiple") return true; if (!this.opts.maxDays.current) return true; const selectedCount = selectedDates.length; if (this.opts.maxDays.current && selectedCount > this.opts.maxDays.current) return false; return true; } handleCellClick(_, date) { if (this.opts.readonly.current || this.opts.isDateDisabled.current?.(date) || this.opts.isDateUnavailable.current?.(date)) { return; } const prev = this.opts.value.current; const multiple = this.opts.type.current === "multiple"; if (multiple) { if (Array.isArray(prev) || prev === undefined) { this.opts.value.current = this.handleMultipleUpdate(prev, date); } } else if (!Array.isArray(prev)) { const next = this.handleSingleUpdate(prev, date); if (!next) { this.announcer.announce("Selected date is now empty.", "polite", 5000); } else { this.announcer.announce(`Selected Date: ${this.formatter.selectedDate(next, false)}`, "polite"); } this.opts.value.current = getDateWithPreviousTime(next, prev); if (next !== undefined) { this.opts.onDateSelect?.current?.(); } } } handleMultipleUpdate(prev, date) { if (!prev) { const newSelection = [date]; return this.#isMultipleSelectionValid(newSelection) ? newSelection : [date]; } if (!Array.isArray(prev)) { if (DEV) throw new Error("Invalid value for multiple prop."); return; } const index = prev.findIndex((d) => isSameDay(d, date)); const preventDeselect = this.opts.preventDeselect.current; if (index === -1) { // adding a new date - check if it would be valid const newSelection = [...prev, date]; if (this.#isMultipleSelectionValid(newSelection)) { return newSelection; } else { // reset to just the newly selected date when constraints are violated return [date]; } } else if (preventDeselect) { return prev; } else { const next = prev.filter((d) => !isSameDay(d, date)); if (!next.length) { this.opts.placeholder.current = date; return undefined; } return next; } } handleSingleUpdate(prev, date) { if (Array.isArray(prev)) { if (DEV) throw new Error("Invalid value for single prop."); } if (!prev) return date; const preventDeselect = this.opts.preventDeselect.current; if (!preventDeselect && isSameDay(prev, date)) { this.opts.placeholder.current = date; return undefined; } return date; } onkeydown(event) { handleCalendarKeydown({ event, handleCellClick: this.handleCellClick, shiftFocus: this.shiftFocus, placeholderValue: this.opts.placeholder.current, }); } snippetProps = $derived.by(() => ({ months: this.months, weekdays: this.weekdays, })); getBitsAttr = (part) => { return calendarAttrs.getAttr(part); }; props = $derived.by(() => ({ ...getCalendarElementProps({ fullCalendarLabel: this.fullCalendarLabel, id: this.opts.id.current, isInvalid: this.isInvalid, disabled: this.opts.disabled.current, readonly: this.opts.readonly.current, }), [this.getBitsAttr("root")]: "", // onkeydown: this.onkeydown, ...this.attachment, })); } export class CalendarHeadingState { static create(opts) { return new CalendarHeadingState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, "aria-hidden": getAriaHidden(true), "data-disabled": getDataDisabled(this.root.opts.disabled.current), "data-readonly": getDataReadonly(this.root.opts.readonly.current), [this.root.getBitsAttr("heading")]: "", ...this.attachment, })); } const CalendarCellContext = new Context("Calendar.Cell | RangeCalendar.Cell"); export class CalendarCellState { static create(opts) { return CalendarCellContext.set(new CalendarCellState(opts, CalendarRootContext.get())); } opts; root; cellDate = $derived.by(() => toDate(this.opts.date.current)); isUnavailable = $derived.by(() => this.root.opts.isDateUnavailable.current(this.opts.date.current)); isDateToday = $derived.by(() => isToday(this.opts.date.current, getLocalTimeZone())); isOutsideMonth = $derived.by(() => !isSameMonth(this.opts.date.current, this.opts.month.current)); isOutsideVisibleMonths = $derived.by(() => this.root.isOutsideVisibleMonths(this.opts.date.current)); isDisabled = $derived.by(() => this.root.isDateDisabled(this.opts.date.current) || (this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current)); isFocusedDate = $derived.by(() => isSameDay(this.opts.date.current, this.root.opts.placeholder.current)); isSelectedDate = $derived.by(() => this.root.isDateSelected(this.opts.date.current)); labelText = $derived.by(() => this.root.formatter.custom(this.cellDate, { weekday: "long", month: "long", day: "numeric", year: "numeric", })); attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } snippetProps = $derived.by(() => ({ disabled: this.isDisabled, unavailable: this.isUnavailable, selected: this.isSelectedDate, day: `${this.opts.date.current.day}`, })); ariaDisabled = $derived.by(() => { return (this.isDisabled || (this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current) || this.isUnavailable); }); sharedDataAttrs = $derived.by(() => ({ "data-unavailable": getDataUnavailable(this.isUnavailable), "data-today": this.isDateToday ? "" : undefined, "data-outside-month": this.isOutsideMonth ? "" : undefined, "data-outside-visible-months": this.isOutsideVisibleMonths ? "" : undefined, "data-focused": this.isFocusedDate ? "" : undefined, "data-selected": getDataSelected(this.isSelectedDate), "data-value": this.opts.date.current.toString(), "data-type": getDateValueType(this.opts.date.current), "data-disabled": getDataDisabled(this.isDisabled || (this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current)), })); props = $derived.by(() => ({ id: this.opts.id.current, role: "gridcell", "aria-selected": getAriaSelected(this.isSelectedDate), "aria-disabled": getAriaDisabled(this.ariaDisabled), ...this.sharedDataAttrs, [this.root.getBitsAttr("cell")]: "", ...this.attachment, })); } export class CalendarDayState { static create(opts) { return new CalendarDayState(opts, CalendarCellContext.get()); } opts; cell; attachment; constructor(opts, cell) { this.opts = opts; this.cell = cell; this.onclick = this.onclick.bind(this); this.attachment = attachRef(this.opts.ref); } #tabindex = $derived.by(() => (this.cell.isOutsideMonth && this.cell.root.opts.disableDaysOutsideMonth.current) || this.cell.isDisabled ? undefined : this.cell.isFocusedDate ? 0 : -1); onclick(e) { if (this.cell.isDisabled) return; this.cell.root.handleCellClick(e, this.cell.opts.date.current); } snippetProps = $derived.by(() => ({ disabled: this.cell.isDisabled, unavailable: this.cell.isUnavailable, selected: this.cell.isSelectedDate, day: `${this.cell.opts.date.current.day}`, })); props = $derived.by(() => ({ id: this.opts.id.current, role: "button", "aria-label": this.cell.labelText, "aria-disabled": getAriaDisabled(this.cell.ariaDisabled), ...this.cell.sharedDataAttrs, tabindex: this.#tabindex, [this.cell.root.getBitsAttr("day")]: "", // Shared logic for range calendar and calendar "data-bits-day": "", // onclick: this.onclick, ...this.attachment, })); } export class CalendarNextButtonState { static create(opts) { return new CalendarNextButtonState(opts, CalendarRootContext.get()); } opts; root; isDisabled = $derived.by(() => this.root.isNextButtonDisabled); attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.onclick = this.onclick.bind(this); this.attachment = attachRef(this.opts.ref); } onclick(_) { if (this.isDisabled) return; this.root.nextPage(); } props = $derived.by(() => ({ id: this.opts.id.current, role: "button", type: "button", "aria-label": "Next", "aria-disabled": getAriaDisabled(this.isDisabled), "data-disabled": getDataDisabled(this.isDisabled), disabled: this.isDisabled, [this.root.getBitsAttr("next-button")]: "", // onclick: this.onclick, ...this.attachment, })); } export class CalendarPrevButtonState { static create(opts) { return new CalendarPrevButtonState(opts, CalendarRootContext.get()); } opts; root; isDisabled = $derived.by(() => this.root.isPrevButtonDisabled); attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.onclick = this.onclick.bind(this); this.attachment = attachRef(this.opts.ref); } onclick(_) { if (this.isDisabled) return; this.root.prevPage(); } props = $derived.by(() => ({ id: this.opts.id.current, role: "button", type: "button", "aria-label": "Previous", "aria-disabled": getAriaDisabled(this.isDisabled), "data-disabled": getDataDisabled(this.isDisabled), disabled: this.isDisabled, [this.root.getBitsAttr("prev-button")]: "", // onclick: this.onclick, ...this.attachment, })); } export class CalendarGridState { static create(opts) { return new CalendarGridState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, tabindex: -1, role: "grid", "aria-readonly": getAriaReadonly(this.root.opts.readonly.current), "aria-disabled": getAriaDisabled(this.root.opts.disabled.current), "data-readonly": getDataReadonly(this.root.opts.readonly.current), "data-disabled": getDataDisabled(this.root.opts.disabled.current), [this.root.getBitsAttr("grid")]: "", ...this.attachment, })); } export class CalendarGridBodyState { static create(opts) { return new CalendarGridBodyState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, "data-disabled": getDataDisabled(this.root.opts.disabled.current), "data-readonly": getDataReadonly(this.root.opts.readonly.current), [this.root.getBitsAttr("grid-body")]: "", ...this.attachment, })); } export class CalendarGridHeadState { static create(opts) { return new CalendarGridHeadState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, "data-disabled": getDataDisabled(this.root.opts.disabled.current), "data-readonly": getDataReadonly(this.root.opts.readonly.current), [this.root.getBitsAttr("grid-head")]: "", ...this.attachment, })); } export class CalendarGridRowState { static create(opts) { return new CalendarGridRowState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, "data-disabled": getDataDisabled(this.root.opts.disabled.current), "data-readonly": getDataReadonly(this.root.opts.readonly.current), [this.root.getBitsAttr("grid-row")]: "", ...this.attachment, })); } export class CalendarHeadCellState { static create(opts) { return new CalendarHeadCellState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, "data-disabled": getDataDisabled(this.root.opts.disabled.current), "data-readonly": getDataReadonly(this.root.opts.readonly.current), [this.root.getBitsAttr("head-cell")]: "", ...this.attachment, })); } export class CalendarHeaderState { static create(opts) { return new CalendarHeaderState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, "data-disabled": getDataDisabled(this.root.opts.disabled.current), "data-readonly": getDataReadonly(this.root.opts.readonly.current), [this.root.getBitsAttr("header")]: "", ...this.attachment, })); } export class CalendarMonthSelectState { static create(opts) { return new CalendarMonthSelectState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.onchange = this.onchange.bind(this); this.attachment = attachRef(this.opts.ref); } monthItems = $derived.by(() => { this.root.opts.locale.current; const monthNumbers = this.opts.months.current; const monthFormat = this.opts.monthFormat.current; const months = []; for (const month of monthNumbers) { // create a date with the current year and the month to get localized name const date = this.root.opts.placeholder.current.set({ month }); let label; if (typeof monthFormat === "function") { label = monthFormat(month); } else { label = this.root.formatter.custom(toDate(date), { month: monthFormat }); } months.push({ value: month, label, }); } return months; }); currentMonth = $derived.by(() => this.root.opts.placeholder.current.month); isDisabled = $derived.by(() => this.root.opts.disabled.current || this.opts.disabled.current); snippetProps = $derived.by(() => { return { monthItems: this.monthItems, selectedMonthItem: this.monthItems.find((month) => month.value === this.currentMonth), }; }); onchange(event) { if (this.isDisabled) return; const target = event.target; const month = parseInt(target.value, 10); if (!isNaN(month)) { this.root.opts.placeholder.current = this.root.opts.placeholder.current.set({ month }); } } props = $derived.by(() => ({ id: this.opts.id.current, value: this.currentMonth, disabled: this.isDisabled, "data-disabled": getDataDisabled(this.isDisabled), [this.root.getBitsAttr("month-select")]: "", // onchange: this.onchange, ...this.attachment, })); } export class CalendarYearSelectState { static create(opts) { return new CalendarYearSelectState(opts, CalendarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.onchange = this.onchange.bind(this); this.attachment = attachRef(this.opts.ref); } years = $derived.by(() => { if (this.opts.years.current && this.opts.years.current.length) return this.opts.years.current; return this.root.defaultYears; }); yearItems = $derived.by(() => { this.root.opts.locale.current; const yearFormat = this.opts.yearFormat.current; const localYears = []; for (const year of this.years) { // create a date with the year to get localized formatting const date = this.root.opts.placeholder.current.set({ year }); let label; if (typeof yearFormat === "function") { label = yearFormat(year); } else { label = this.root.formatter.custom(toDate(date), { year: yearFormat }); } localYears.push({ value: year, label, }); } return localYears; }); currentYear = $derived.by(() => this.root.opts.placeholder.current.year); isDisabled = $derived.by(() => this.root.opts.disabled.current || this.opts.disabled.current); snippetProps = $derived.by(() => { return { yearItems: this.yearItems, selectedYearItem: this.yearItems.find((year) => year.value === this.currentYear), }; }); onchange(event) { if (this.isDisabled) return; const target = event.target; const year = parseInt(target.value, 10); if (!isNaN(year)) { this.root.opts.placeholder.current = this.root.opts.placeholder.current.set({ year }); } } props = $derived.by(() => ({ id: this.opts.id.current, value: this.currentYear, disabled: this.isDisabled, "data-disabled": getDataDisabled(this.isDisabled), [this.root.getBitsAttr("year-select")]: "", // onchange: this.onchange, ...this.attachment, })); }