infinity-ui-elements 1.8.32 → 1.8.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -6,9 +6,9 @@ import { PulseLoader, ClipLoader } from 'react-spinners';
6
6
  import { clsx } from 'clsx';
7
7
  import { twMerge } from 'tailwind-merge';
8
8
  import { ExternalLink, Calendar, X, Loader2, Search, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
9
+ import { createPortal } from 'react-dom';
9
10
  import Calendar$1 from 'react-calendar';
10
11
  import 'react-calendar/dist/Calendar.css';
11
- import { createPortal } from 'react-dom';
12
12
  import { flexRender } from '@tanstack/react-table';
13
13
 
14
14
  /**
@@ -2077,16 +2077,64 @@ const formatDateDefault = (date) => {
2077
2077
  day: "numeric",
2078
2078
  });
2079
2079
  };
2080
- const DatePicker = React.forwardRef(({ className, value: controlledValue, defaultValue, onChange, placeholder = "Select a date", label, helperText, errorText, successText, validationState = "none", isDisabled = false, isRequired = false, isOptional = false, size = "medium", showClearButton = true, onClear, containerClassName, labelClassName, triggerClassName, calendarClassName, minDate, maxDate, formatDate = formatDateDefault, infoHeading, infoDescription, LinkComponent, linkText, linkHref, onLinkClick, ...props }, ref) => {
2080
+ // Helper function to format date based on format string
2081
+ const formatDateByPattern = (date, format) => {
2082
+ const day = date.getDate();
2083
+ const month = date.getMonth() + 1; // getMonth() returns 0-11
2084
+ const year = date.getFullYear();
2085
+ const monthNames = [
2086
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
2087
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
2088
+ ];
2089
+ const monthNamesFull = [
2090
+ "January", "February", "March", "April", "May", "June",
2091
+ "July", "August", "September", "October", "November", "December"
2092
+ ];
2093
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2094
+ const dayNamesFull = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
2095
+ // Pad numbers with leading zeros
2096
+ const pad = (n, length = 2) => {
2097
+ return n.toString().padStart(length, "0");
2098
+ };
2099
+ let formatted = format;
2100
+ // Replace format patterns
2101
+ formatted = formatted.replace(/YYYY/g, year.toString());
2102
+ formatted = formatted.replace(/YY/g, year.toString().slice(-2));
2103
+ formatted = formatted.replace(/MMMM/g, monthNamesFull[month - 1]);
2104
+ formatted = formatted.replace(/MMM/g, monthNames[month - 1]);
2105
+ formatted = formatted.replace(/MM/g, pad(month));
2106
+ formatted = formatted.replace(/M/g, month.toString());
2107
+ formatted = formatted.replace(/DDDD/g, dayNamesFull[date.getDay()]);
2108
+ formatted = formatted.replace(/DDD/g, dayNames[date.getDay()]);
2109
+ formatted = formatted.replace(/DD/g, pad(day));
2110
+ formatted = formatted.replace(/D/g, day.toString());
2111
+ return formatted;
2112
+ };
2113
+ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaultValue, onChange, placeholder = "Select a date", label, helperText, errorText, successText, validationState = "none", isDisabled = false, isRequired = false, isOptional = false, size = "medium", showClearButton = true, onClear, containerClassName, labelClassName, triggerClassName, calendarClassName, minDate, maxDate, formatDate = formatDateDefault, format, infoHeading, infoDescription, LinkComponent, linkText, linkHref, onLinkClick, ...props }, ref) => {
2081
2114
  const [uncontrolledValue, setUncontrolledValue] = React.useState(parseDate(defaultValue));
2082
2115
  const [isOpen, setIsOpen] = React.useState(false);
2083
2116
  const datePickerRef = React.useRef(null);
2084
2117
  const calendarRef = React.useRef(null);
2085
2118
  const [dropdownPlacement, setDropdownPlacement] = React.useState("bottom");
2119
+ const [isInsideModal, setIsInsideModal] = React.useState(false);
2120
+ const [position, setPosition] = React.useState({
2121
+ top: 0,
2122
+ left: 0,
2123
+ width: 0,
2124
+ bottom: 0,
2125
+ });
2126
+ const [calendarHeight, setCalendarHeight] = React.useState(300); // Default height estimate
2086
2127
  const value = controlledValue !== undefined
2087
2128
  ? parseDate(controlledValue)
2088
2129
  : uncontrolledValue;
2089
2130
  const hasValue = value !== null;
2131
+ // Create a formatter function that uses format prop if provided, otherwise formatDate
2132
+ const formatDateValue = React.useCallback((date) => {
2133
+ if (format) {
2134
+ return formatDateByPattern(date, format);
2135
+ }
2136
+ return formatDate(date);
2137
+ }, [format, formatDate]);
2090
2138
  // Determine which helper text to show
2091
2139
  const displayHelperText = errorText || successText || helperText;
2092
2140
  const currentValidationState = errorText
@@ -2158,13 +2206,74 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2158
2206
  setDropdownPlacement("top");
2159
2207
  }
2160
2208
  }, []);
2209
+ // Check if date picker is inside a modal
2210
+ React.useEffect(() => {
2211
+ if (isOpen && datePickerRef.current) {
2212
+ let element = datePickerRef.current;
2213
+ let foundModal = false;
2214
+ while (element && !foundModal) {
2215
+ const styles = window.getComputedStyle(element);
2216
+ const zIndex = parseInt(styles.zIndex, 10);
2217
+ // Check if element has modal z-index (10000) or is a modal container
2218
+ if (zIndex === 10000 || element.getAttribute("role") === "dialog") {
2219
+ foundModal = true;
2220
+ setIsInsideModal(true);
2221
+ break;
2222
+ }
2223
+ element = element.parentElement;
2224
+ }
2225
+ if (!foundModal) {
2226
+ setIsInsideModal(false);
2227
+ }
2228
+ }
2229
+ }, [isOpen]);
2230
+ // Update position when calendar opens or window resizes
2231
+ React.useEffect(() => {
2232
+ if (isOpen && datePickerRef.current) {
2233
+ const updatePosition = () => {
2234
+ const rect = datePickerRef.current?.getBoundingClientRect();
2235
+ if (rect) {
2236
+ setPosition({
2237
+ top: rect.top,
2238
+ left: rect.left,
2239
+ width: rect.width,
2240
+ bottom: rect.bottom,
2241
+ });
2242
+ // Update dropdown placement based on available space
2243
+ updateDropdownPlacement();
2244
+ }
2245
+ };
2246
+ updatePosition();
2247
+ window.addEventListener("resize", updatePosition);
2248
+ window.addEventListener("scroll", updatePosition, true);
2249
+ return () => {
2250
+ window.removeEventListener("resize", updatePosition);
2251
+ window.removeEventListener("scroll", updatePosition, true);
2252
+ };
2253
+ }
2254
+ }, [isOpen, updateDropdownPlacement]);
2161
2255
  React.useEffect(() => {
2162
2256
  if (!isOpen)
2163
2257
  return;
2164
2258
  if (typeof window === "undefined")
2165
2259
  return;
2166
- let rafId = requestAnimationFrame(updateDropdownPlacement);
2167
- const handleUpdate = () => updateDropdownPlacement();
2260
+ // Use requestAnimationFrame to ensure calendar is rendered before calculating placement
2261
+ let rafId = requestAnimationFrame(() => {
2262
+ updateDropdownPlacement();
2263
+ });
2264
+ const handleUpdate = () => {
2265
+ updateDropdownPlacement();
2266
+ // Also update position when scrolling/resizing
2267
+ if (datePickerRef.current) {
2268
+ const rect = datePickerRef.current.getBoundingClientRect();
2269
+ setPosition({
2270
+ top: rect.top,
2271
+ left: rect.left,
2272
+ width: rect.width,
2273
+ bottom: rect.bottom,
2274
+ });
2275
+ }
2276
+ };
2168
2277
  window.addEventListener("resize", handleUpdate);
2169
2278
  window.addEventListener("scroll", handleUpdate, true);
2170
2279
  return () => {
@@ -2175,14 +2284,44 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2175
2284
  }, [isOpen, updateDropdownPlacement]);
2176
2285
  React.useEffect(() => {
2177
2286
  if (isOpen) {
2178
- updateDropdownPlacement();
2287
+ // Delay to ensure calendar is rendered
2288
+ const timer = setTimeout(() => {
2289
+ updateDropdownPlacement();
2290
+ }, 0);
2291
+ return () => clearTimeout(timer);
2179
2292
  }
2180
2293
  }, [isOpen, updateDropdownPlacement]);
2294
+ // Measure calendar height and adjust position after render
2295
+ React.useLayoutEffect(() => {
2296
+ if (isOpen && calendarRef.current && datePickerRef.current) {
2297
+ const measuredHeight = calendarRef.current.offsetHeight;
2298
+ setCalendarHeight(measuredHeight);
2299
+ const rect = datePickerRef.current.getBoundingClientRect();
2300
+ // Recalculate placement if needed based on actual calendar height
2301
+ const spaceBelow = window.innerHeight - rect.bottom;
2302
+ const spaceAbove = rect.top;
2303
+ if (measuredHeight > spaceBelow && spaceAbove > spaceBelow) {
2304
+ setDropdownPlacement("top");
2305
+ }
2306
+ else {
2307
+ setDropdownPlacement("bottom");
2308
+ }
2309
+ // Update position
2310
+ setPosition({
2311
+ top: rect.top,
2312
+ left: rect.left,
2313
+ width: rect.width,
2314
+ bottom: rect.bottom,
2315
+ });
2316
+ }
2317
+ }, [isOpen]);
2181
2318
  // Close calendar when clicking outside
2182
2319
  React.useEffect(() => {
2183
2320
  const handleClickOutside = (event) => {
2184
2321
  if (datePickerRef.current &&
2185
- !datePickerRef.current.contains(event.target)) {
2322
+ !datePickerRef.current.contains(event.target) &&
2323
+ calendarRef.current &&
2324
+ !calendarRef.current.contains(event.target)) {
2186
2325
  handleOpenChange(false);
2187
2326
  }
2188
2327
  };
@@ -2230,9 +2369,23 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2230
2369
  ? "text-feedback-ink-positive-intense"
2231
2370
  : currentValidationState === "negative"
2232
2371
  ? "text-feedback-ink-negative-subtle"
2233
- : "text-surface-ink-neutral-muted") }), jsx("span", { className: cn("flex-1 text-left truncate", !hasValue && "text-surface-ink-neutral-muted", isDisabled && "text-surface-ink-neutral-disabled"), children: hasValue && value ? formatDate(value) : placeholder }), showClearButton && hasValue && !isDisabled && (jsx("button", { type: "button", onClick: handleClear, className: "shrink-0 flex items-center justify-center text-surface-ink-neutral-muted hover:text-surface-ink-neutral-normal transition-colors", tabIndex: -1, "aria-label": "Clear date", children: jsx(X, { className: "w-4 h-4" }) })), isOpen && !isDisabled && (jsx("div", { ref: calendarRef, className: cn("absolute z-50 left-0 bg-surface-fill-neutral-intense rounded-large shadow-lg p-4", dropdownPlacement === "bottom"
2234
- ? "top-full mt-1"
2235
- : "bottom-full mb-1", calendarClassName), onClick: (e) => e.stopPropagation(), children: jsx("div", { className: "react-calendar-wrapper w-fit", children: jsx(Calendar$1, { onChange: handleCalendarChange, value: value ?? null, minDate: minDateParsed ?? undefined, maxDate: maxDateParsed ?? undefined, locale: "en-US", formatShortWeekday: (locale, date) => {
2372
+ : "text-surface-ink-neutral-muted") }), jsx("span", { className: cn("flex-1 text-left truncate", !hasValue && "text-surface-ink-neutral-muted", isDisabled && "text-surface-ink-neutral-disabled"), children: hasValue && value ? formatDateValue(value) : placeholder }), showClearButton && hasValue && !isDisabled && (jsx("button", { type: "button", onClick: handleClear, className: "shrink-0 flex items-center justify-center text-surface-ink-neutral-muted hover:text-surface-ink-neutral-normal transition-colors", tabIndex: -1, "aria-label": "Clear date", children: jsx(X, { className: "w-4 h-4" }) }))] }), jsx(FormFooter, { helperText: displayHelperText, validationState: currentValidationState === "none"
2373
+ ? "default"
2374
+ : currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" }), typeof document !== "undefined" &&
2375
+ isOpen &&
2376
+ !isDisabled &&
2377
+ (() => {
2378
+ // Calculate calendar position using fixed positioning (viewport-relative)
2379
+ const gap = 4; // 4px gap between trigger and calendar
2380
+ const calendarTop = dropdownPlacement === "bottom"
2381
+ ? position.bottom + gap
2382
+ : position.top - calendarHeight - gap;
2383
+ const calendarPopup = (jsx("div", { ref: calendarRef, style: {
2384
+ position: "fixed",
2385
+ top: `${calendarTop}px`,
2386
+ left: `${position.left}px`,
2387
+ zIndex: isInsideModal ? 10001 : 9999,
2388
+ }, className: cn("bg-surface-fill-neutral-intense rounded-large shadow-lg p-4 w-fit", calendarClassName), onClick: (e) => e.stopPropagation(), children: jsx("div", { className: "react-calendar-wrapper w-fit", children: jsx(Calendar$1, { onChange: handleCalendarChange, value: value ?? null, minDate: minDateParsed ?? undefined, maxDate: maxDateParsed ?? undefined, locale: "en-US", formatShortWeekday: (locale, date) => {
2236
2389
  const weekdayNames = [
2237
2390
  "Su",
2238
2391
  "Mo",
@@ -2243,9 +2396,9 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2243
2396
  "Sa",
2244
2397
  ];
2245
2398
  return weekdayNames[date.getDay()];
2246
- } }) }) }))] }), jsx(FormFooter, { helperText: displayHelperText, validationState: currentValidationState === "none"
2247
- ? "default"
2248
- : currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" })] }));
2399
+ } }) }) }));
2400
+ return createPortal(calendarPopup, document.body);
2401
+ })()] }));
2249
2402
  });
2250
2403
  DatePicker.displayName = "DatePicker";
2251
2404