Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/all-taxis-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudoperators/juno-ui-components": patch
---

Decouple components public interfaces from the third party library types.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import flatpickr from "flatpickr"
import { FormHint } from "../FormHint/"
import { Icon } from "../Icon"
import { Label } from "../Label"
import { Options, DateOption, DateLimit } from "flatpickr/dist/types/options"
import { key as LocaleKey, CustomLocale } from "flatpickr/dist/types/locale"
import type { DateOption, DateLimit, LocaleKey, CustomLocale, DateChangeHandler } from "./DateTimePicker.types"
import { mapDateOption, mapDateLimits, mapLocale } from "./DateTimePicker.mappers"
import { Options } from "flatpickr/dist/types/options"

import "./datetimepicker.css"

Expand Down Expand Up @@ -197,31 +198,55 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
onBlur && onBlur(theDate.selectedDate, theDate.selectedDateStr)
}

const handleChange = (selectedDate: Date[], dateStr: string, _instance: flatpickr.Instance) => {
const handleChange: InternalDateChangeHandler = (
selectedDate: Date[],
dateStr: string,
_instance: flatpickr.Instance
) => {
setTheDate({ selectedDate: selectedDate, selectedDateStr: dateStr })
onChange && onChange(selectedDate, dateStr)
}

const handleClose = (selectedDate: Date[], dateStr: string, _instance: flatpickr.Instance) => {
const handleClose: InternalDateChangeHandler = (
selectedDate: Date[],
dateStr: string,
_instance: flatpickr.Instance
) => {
setIsOpen(false)
onClose && onClose(selectedDate, dateStr)
}

const handleMonthChange = (selectedDate: Date[], dateStr: string, _instance: flatpickr.Instance) => {
const handleMonthChange: InternalDateChangeHandler = (
selectedDate: Date[],
dateStr: string,
_instance: flatpickr.Instance
) => {
setTheDate({ selectedDate: selectedDate, selectedDateStr: dateStr })
onMonthChange && onMonthChange(selectedDate, dateStr)
}

const handleOpen = (selectedDate: Date[], dateStr: string, _instance: flatpickr.Instance) => {
const handleOpen: InternalDateChangeHandler = (
selectedDate: Date[],
dateStr: string,
_instance: flatpickr.Instance
) => {
setIsOpen(true)
onOpen && onOpen(selectedDate, dateStr)
}

const handleReady = (selectedDate: Date[], dateStr: string, _instance: flatpickr.Instance) => {
const handleReady: InternalDateChangeHandler = (
selectedDate: Date[],
dateStr: string,
_instance: flatpickr.Instance
) => {
onReady && onReady(selectedDate, dateStr)
}

const handleYearChange = (selectedDate: Date[], dateStr: string, _instance: flatpickr.Instance) => {
const handleYearChange: InternalDateChangeHandler = (
selectedDate: Date[],
dateStr: string,
_instance: flatpickr.Instance
) => {
setTheDate({ selectedDate: selectedDate, selectedDateStr: dateStr })
onYearChange && onYearChange(selectedDate, dateStr)
}
Expand Down Expand Up @@ -272,13 +297,13 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
defaultDate: defaultDate || defaultValue,
defaultHour: defaultHour,
defaultMinute: defaultMinute,
disable: disable,
disable: disable.length > 0 ? mapDateLimits(disable) : [],
enableSeconds: enableSeconds,
enableTime: enableTime,
hourIncrement: hourIncrement,
locale: locale || "default",
maxDate: maxDate || undefined,
minDate: minDate || undefined,
locale: locale ? mapLocale(locale) : "default",
maxDate: maxDate ? mapDateOption(maxDate) : undefined,
minDate: minDate ? mapDateOption(minDate) : undefined,
minuteIncrement: minuteIncrement,
mode: mode,
monthSelectorType,
Expand Down Expand Up @@ -441,23 +466,23 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
}, [dateFormat])

useEffect(() => {
flatpickrInstanceRef.current?.set("disable", disable)
flatpickrInstanceRef.current?.set("disable", disable.length > 0 ? mapDateLimits(disable) : [])
}, [disable])

useEffect(() => {
flatpickrInstanceRef.current?.set("hourIncrement", hourIncrement)
}, [hourIncrement])

useEffect(() => {
flatpickrInstanceRef.current?.set("locale", locale || "default")
flatpickrInstanceRef.current?.set("locale", locale ? mapLocale(locale) : "default")
}, [locale])

useEffect(() => {
flatpickrInstanceRef.current?.set("maxDate", maxDate)
flatpickrInstanceRef.current?.set("maxDate", maxDate ? mapDateOption(maxDate) : undefined)
}, [maxDate])

useEffect(() => {
flatpickrInstanceRef.current?.set("minDate", minDate)
flatpickrInstanceRef.current?.set("minDate", minDate ? mapDateOption(minDate) : undefined)
}, [minDate])

useEffect(() => {
Expand All @@ -470,10 +495,15 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({

// Update the flatpickr instance whenever the value prop (or any of its aliases) changes, and force the flatpickr instance to fire onChange event. These props may contain an array of one or multiple objects. These will never pass React's identity comparison, and will be regarded as a new object with any render regardless of their contents, thus creating an endless loop by updating the flatpickr instance updating the parent state (via onChange above) updating the flatpickr instance (…). We prevent this by checking on the stringified versions of the props in the dependency array.
useEffect(() => {
flatpickrInstanceRef.current?.setDate(
(value || defaultDate || defaultValue) as DateOption | DateOption[],
true // enforce firing change event that in turn will update our state via handleChange.
)
const dateValue = value || defaultDate || defaultValue
if (dateValue) {
const flatpickrValue = Array.isArray(dateValue) ? dateValue.map(mapDateOption) : mapDateOption(dateValue)

flatpickrInstanceRef.current?.setDate(
flatpickrValue,
true // enforce firing change event that in turn will update our state via handleChange.
)
}
}, [stringifiedValue, stringifiedDefaultDate, stringifiedDefaultValue])

useEffect(() => {
Expand Down Expand Up @@ -553,8 +583,8 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
)
}

// eslint-disable-next-line no-unused-vars
type DateChangeHandler = (dates?: Date[], dateStr?: string, instance?: flatpickr.Instance) => void
// Internal type that includes flatpickr instance for internal use
type InternalDateChangeHandler = (_selectedDate: Date[], _dateStr: string, _instance: flatpickr.Instance) => void

export interface DateTimePickerProps
extends Omit<React.HTMLAttributes<HTMLInputElement>, "defaultValue" | "onFocus" | "onBlur" | "onChange"> {
Expand Down Expand Up @@ -585,7 +615,7 @@ export interface DateTimePickerProps
/**
* Pass an array of dates, date strings, date ranges or functions to disable dates. More on disabling dates: https://flatpickr.js.org/examples/#disabling-specific-dates
*/
disable?: DateLimit<DateOption>[]
disable?: DateLimit[]
/** Whether the DateTimePicker is disabled */
disabled?: boolean
/** Whether to show seconds when showing a time picker. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Internal type mappers to convert between DateTimePicker public types and flatpickr types.
* This file is NOT exported in index.ts - it's used only internally within DateTimePicker.
*
* These mappers provide a boundary between our public API (DateTimePicker types) and
* the internal implementation (flatpickr types), preventing type leakage.
*/

import { DateOption as FlatpickrDateOption, DateLimit as FlatpickrDateLimit } from "flatpickr/dist/types/options"
import { key as FlatpickrLocaleKey, CustomLocale as FlatpickrCustomLocale } from "flatpickr/dist/types/locale"
import type { DateOption, DateLimit, LocaleKey, CustomLocale } from "./DateTimePicker.types"

/**
* Maps public DateTimePicker date option to flatpickr DateOption.
* Both types are structurally identical (string | Date | number),
* but TypeScript treats them as distinct types.
*/
export function mapDateOption(date: DateOption): FlatpickrDateOption {
return date as unknown as FlatpickrDateOption
}

/**
* Maps public DateTimePicker date limit to flatpickr DateLimit.
* Handles simple dates, date ranges, and filter functions.
*/
export function mapDateLimit(limit: DateLimit): FlatpickrDateLimit<FlatpickrDateOption> {
// If it's a function, map it directly
if (typeof limit === "function") {
return limit as unknown as FlatpickrDateLimit<FlatpickrDateOption>
}

// If it's a date range object { from, to }
if (typeof limit === "object" && limit !== null && !(limit instanceof Date)) {
if ("from" in limit && "to" in limit) {
return {
from: mapDateOption(limit.from),
to: mapDateOption(limit.to),
} as FlatpickrDateLimit<FlatpickrDateOption>
}
}

// Otherwise it's a simple date option
return mapDateOption(limit as DateOption) as unknown as FlatpickrDateLimit<FlatpickrDateOption>
}

/**
* Maps array of public DateTimePicker date limits to flatpickr DateLimit array.
*/
export function mapDateLimits(limits: DateLimit[]): FlatpickrDateLimit<FlatpickrDateOption>[] {
return limits.map(mapDateLimit)
}

/**
* Maps public DateTimePicker locale to flatpickr locale.
* Handles both locale key strings and custom locale objects.
*/
export function mapLocale(
locale: LocaleKey | Partial<CustomLocale>
): FlatpickrLocaleKey | Partial<FlatpickrCustomLocale> {
// If it's a string locale key, map directly
if (typeof locale === "string") {
return locale as unknown as FlatpickrLocaleKey
}

// If it's a custom locale object, map it
// Since both types are structurally compatible, we can safely cast
return locale as unknown as Partial<FlatpickrCustomLocale>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Custom types to replace flatpickr types and eliminate type leakage.
* These types mirror the functionality of flatpickr without depending on its type definitions.
*/

/**
* Date option type that can represent a date in various formats.
* Replaces flatpickr's DateOption type.
*/
export type DateOption = string | Date | number

/**
* Date limit function type for disabling specific dates.
* Replaces flatpickr's DateLimit type.
*/
export type DateLimit = DateOption | { from: DateOption; to: DateOption } | ((_date: Date) => boolean)

/**
* Locale key type for predefined locales.
* Replaces flatpickr's LocaleKey type.
*/
export type LocaleKey =
| "ar"
| "at"
| "az"
| "be"
| "bg"
| "bn"
| "bs"
| "ca"
| "cat"
| "ckb"
| "cs"
| "cy"
| "da"
| "de"
| "default"
| "en"
| "eo"
| "es"
| "et"
| "fa"
| "fi"
| "fo"
| "fr"
| "gr"
| "he"
| "hi"
| "hr"
| "hu"
| "hy"
| "id"
| "is"
| "it"
| "ja"
| "ka"
| "ko"
| "km"
| "kz"
| "lt"
| "lv"
| "mk"
| "mn"
| "ms"
| "my"
| "nl"
| "nn"
| "no"
| "pa"
| "pl"
| "pt"
| "ro"
| "ru"
| "si"
| "sk"
| "sl"
| "sq"
| "sr"
| "sv"
| "th"
| "tr"
| "uk"
| "vn"
| "zh"
| "uz"
| "uz_latn"
| "zh_tw"

type Locale = {
weekdays: {
shorthand: [string, string, string, string, string, string, string]
longhand: [string, string, string, string, string, string, string]
}
months: {
shorthand: [string, string, string, string, string, string, string, string, string, string, string, string]
longhand: [string, string, string, string, string, string, string, string, string, string, string, string]
}
daysInMonth: [number, number, number, number, number, number, number, number, number, number, number, number]
firstDayOfWeek: number
ordinal: (_nth: number) => string
rangeSeparator: string
weekAbbreviation: string
scrollTitle: string
toggleTitle: string
amPM: [string, string]
yearAriaLabel: string
monthAriaLabel: string
hourAriaLabel: string
minuteAriaLabel: string
time_24hr: boolean
}

/**
* Custom locale configuration object.
* Replaces flatpickr's CustomLocale type.
*/
export interface CustomLocale {
ordinal?: Locale["ordinal"]
daysInMonth?: Locale["daysInMonth"]
firstDayOfWeek?: Locale["firstDayOfWeek"]
rangeSeparator?: Locale["rangeSeparator"]
weekAbbreviation?: Locale["weekAbbreviation"]
toggleTitle?: Locale["toggleTitle"]
scrollTitle?: Locale["scrollTitle"]
yearAriaLabel?: string
monthAriaLabel?: string
hourAriaLabel?: string
minuteAriaLabel?: string
amPM?: Locale["amPM"]
time_24hr?: Locale["time_24hr"]
weekdays: {
shorthand: [string, string, string, string, string, string, string]
longhand: [string, string, string, string, string, string, string]
}
months: {
shorthand: [string, string, string, string, string, string, string, string, string, string, string, string]
longhand: [string, string, string, string, string, string, string, string, string, string, string, string]
}
}

/**
* Date change handler type.
* Provides the same interface as flatpickr's onChange handlers but without flatpickr.Instance dependency.
*/
export type DateChangeHandler = (_dates?: Date[], _dateStr?: string) => void
Loading
Loading