infinity-ui-elements 1.8.32 → 1.8.40

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,95 @@ 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",
2087
+ "Feb",
2088
+ "Mar",
2089
+ "Apr",
2090
+ "May",
2091
+ "Jun",
2092
+ "Jul",
2093
+ "Aug",
2094
+ "Sep",
2095
+ "Oct",
2096
+ "Nov",
2097
+ "Dec",
2098
+ ];
2099
+ const monthNamesFull = [
2100
+ "January",
2101
+ "February",
2102
+ "March",
2103
+ "April",
2104
+ "May",
2105
+ "June",
2106
+ "July",
2107
+ "August",
2108
+ "September",
2109
+ "October",
2110
+ "November",
2111
+ "December",
2112
+ ];
2113
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2114
+ const dayNamesFull = [
2115
+ "Sunday",
2116
+ "Monday",
2117
+ "Tuesday",
2118
+ "Wednesday",
2119
+ "Thursday",
2120
+ "Friday",
2121
+ "Saturday",
2122
+ ];
2123
+ // Pad numbers with leading zeros
2124
+ const pad = (n, length = 2) => {
2125
+ const str = n.toString();
2126
+ if (str.length >= length)
2127
+ return str;
2128
+ return "0".repeat(length - str.length) + str;
2129
+ };
2130
+ let formatted = format;
2131
+ // Replace format patterns
2132
+ formatted = formatted.replace(/YYYY/g, year.toString());
2133
+ formatted = formatted.replace(/YY/g, year.toString().slice(-2));
2134
+ formatted = formatted.replace(/MMMM/g, monthNamesFull[month - 1]);
2135
+ formatted = formatted.replace(/MMM/g, monthNames[month - 1]);
2136
+ formatted = formatted.replace(/MM/g, pad(month));
2137
+ formatted = formatted.replace(/M/g, month.toString());
2138
+ formatted = formatted.replace(/DDDD/g, dayNamesFull[date.getDay()]);
2139
+ formatted = formatted.replace(/DDD/g, dayNames[date.getDay()]);
2140
+ formatted = formatted.replace(/DD/g, pad(day));
2141
+ formatted = formatted.replace(/D/g, day.toString());
2142
+ return formatted;
2143
+ };
2144
+ 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
2145
  const [uncontrolledValue, setUncontrolledValue] = React.useState(parseDate(defaultValue));
2082
2146
  const [isOpen, setIsOpen] = React.useState(false);
2083
2147
  const datePickerRef = React.useRef(null);
2084
2148
  const calendarRef = React.useRef(null);
2085
2149
  const [dropdownPlacement, setDropdownPlacement] = React.useState("bottom");
2150
+ const [isInsideModal, setIsInsideModal] = React.useState(false);
2151
+ const [position, setPosition] = React.useState({
2152
+ top: 0,
2153
+ left: 0,
2154
+ width: 0,
2155
+ bottom: 0,
2156
+ });
2157
+ const [calendarHeight, setCalendarHeight] = React.useState(300); // Default height estimate
2086
2158
  const value = controlledValue !== undefined
2087
2159
  ? parseDate(controlledValue)
2088
2160
  : uncontrolledValue;
2089
2161
  const hasValue = value !== null;
2162
+ // Create a formatter function that uses format prop if provided, otherwise formatDate
2163
+ const formatDateValue = React.useCallback((date) => {
2164
+ if (format) {
2165
+ return formatDateByPattern(date, format);
2166
+ }
2167
+ return formatDate(date);
2168
+ }, [format, formatDate]);
2090
2169
  // Determine which helper text to show
2091
2170
  const displayHelperText = errorText || successText || helperText;
2092
2171
  const currentValidationState = errorText
@@ -2158,13 +2237,74 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2158
2237
  setDropdownPlacement("top");
2159
2238
  }
2160
2239
  }, []);
2240
+ // Check if date picker is inside a modal
2241
+ React.useEffect(() => {
2242
+ if (isOpen && datePickerRef.current) {
2243
+ let element = datePickerRef.current;
2244
+ let foundModal = false;
2245
+ while (element && !foundModal) {
2246
+ const styles = window.getComputedStyle(element);
2247
+ const zIndex = parseInt(styles.zIndex, 10);
2248
+ // Check if element has modal z-index (10000) or is a modal container
2249
+ if (zIndex === 10000 || element.getAttribute("role") === "dialog") {
2250
+ foundModal = true;
2251
+ setIsInsideModal(true);
2252
+ break;
2253
+ }
2254
+ element = element.parentElement;
2255
+ }
2256
+ if (!foundModal) {
2257
+ setIsInsideModal(false);
2258
+ }
2259
+ }
2260
+ }, [isOpen]);
2261
+ // Update position when calendar opens or window resizes
2262
+ React.useEffect(() => {
2263
+ if (isOpen && datePickerRef.current) {
2264
+ const updatePosition = () => {
2265
+ const rect = datePickerRef.current?.getBoundingClientRect();
2266
+ if (rect) {
2267
+ setPosition({
2268
+ top: rect.top,
2269
+ left: rect.left,
2270
+ width: rect.width,
2271
+ bottom: rect.bottom,
2272
+ });
2273
+ // Update dropdown placement based on available space
2274
+ updateDropdownPlacement();
2275
+ }
2276
+ };
2277
+ updatePosition();
2278
+ window.addEventListener("resize", updatePosition);
2279
+ window.addEventListener("scroll", updatePosition, true);
2280
+ return () => {
2281
+ window.removeEventListener("resize", updatePosition);
2282
+ window.removeEventListener("scroll", updatePosition, true);
2283
+ };
2284
+ }
2285
+ }, [isOpen, updateDropdownPlacement]);
2161
2286
  React.useEffect(() => {
2162
2287
  if (!isOpen)
2163
2288
  return;
2164
2289
  if (typeof window === "undefined")
2165
2290
  return;
2166
- let rafId = requestAnimationFrame(updateDropdownPlacement);
2167
- const handleUpdate = () => updateDropdownPlacement();
2291
+ // Use requestAnimationFrame to ensure calendar is rendered before calculating placement
2292
+ let rafId = requestAnimationFrame(() => {
2293
+ updateDropdownPlacement();
2294
+ });
2295
+ const handleUpdate = () => {
2296
+ updateDropdownPlacement();
2297
+ // Also update position when scrolling/resizing
2298
+ if (datePickerRef.current) {
2299
+ const rect = datePickerRef.current.getBoundingClientRect();
2300
+ setPosition({
2301
+ top: rect.top,
2302
+ left: rect.left,
2303
+ width: rect.width,
2304
+ bottom: rect.bottom,
2305
+ });
2306
+ }
2307
+ };
2168
2308
  window.addEventListener("resize", handleUpdate);
2169
2309
  window.addEventListener("scroll", handleUpdate, true);
2170
2310
  return () => {
@@ -2175,14 +2315,44 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2175
2315
  }, [isOpen, updateDropdownPlacement]);
2176
2316
  React.useEffect(() => {
2177
2317
  if (isOpen) {
2178
- updateDropdownPlacement();
2318
+ // Delay to ensure calendar is rendered
2319
+ const timer = setTimeout(() => {
2320
+ updateDropdownPlacement();
2321
+ }, 0);
2322
+ return () => clearTimeout(timer);
2179
2323
  }
2180
2324
  }, [isOpen, updateDropdownPlacement]);
2325
+ // Measure calendar height and adjust position after render
2326
+ React.useLayoutEffect(() => {
2327
+ if (isOpen && calendarRef.current && datePickerRef.current) {
2328
+ const measuredHeight = calendarRef.current.offsetHeight;
2329
+ setCalendarHeight(measuredHeight);
2330
+ const rect = datePickerRef.current.getBoundingClientRect();
2331
+ // Recalculate placement if needed based on actual calendar height
2332
+ const spaceBelow = window.innerHeight - rect.bottom;
2333
+ const spaceAbove = rect.top;
2334
+ if (measuredHeight > spaceBelow && spaceAbove > spaceBelow) {
2335
+ setDropdownPlacement("top");
2336
+ }
2337
+ else {
2338
+ setDropdownPlacement("bottom");
2339
+ }
2340
+ // Update position
2341
+ setPosition({
2342
+ top: rect.top,
2343
+ left: rect.left,
2344
+ width: rect.width,
2345
+ bottom: rect.bottom,
2346
+ });
2347
+ }
2348
+ }, [isOpen]);
2181
2349
  // Close calendar when clicking outside
2182
2350
  React.useEffect(() => {
2183
2351
  const handleClickOutside = (event) => {
2184
2352
  if (datePickerRef.current &&
2185
- !datePickerRef.current.contains(event.target)) {
2353
+ !datePickerRef.current.contains(event.target) &&
2354
+ calendarRef.current &&
2355
+ !calendarRef.current.contains(event.target)) {
2186
2356
  handleOpenChange(false);
2187
2357
  }
2188
2358
  };
@@ -2230,9 +2400,23 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2230
2400
  ? "text-feedback-ink-positive-intense"
2231
2401
  : currentValidationState === "negative"
2232
2402
  ? "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) => {
2403
+ : "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"
2404
+ ? "default"
2405
+ : currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" }), typeof document !== "undefined" &&
2406
+ isOpen &&
2407
+ !isDisabled &&
2408
+ (() => {
2409
+ // Calculate calendar position using fixed positioning (viewport-relative)
2410
+ const gap = 4; // 4px gap between trigger and calendar
2411
+ const calendarTop = dropdownPlacement === "bottom"
2412
+ ? position.bottom + gap
2413
+ : position.top - calendarHeight - gap;
2414
+ const calendarPopup = (jsx("div", { ref: calendarRef, style: {
2415
+ position: "fixed",
2416
+ top: `${calendarTop}px`,
2417
+ left: `${position.left}px`,
2418
+ zIndex: isInsideModal ? 10001 : 9999,
2419
+ }, 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
2420
  const weekdayNames = [
2237
2421
  "Su",
2238
2422
  "Mo",
@@ -2243,9 +2427,9 @@ const DatePicker = React.forwardRef(({ className, value: controlledValue, defaul
2243
2427
  "Sa",
2244
2428
  ];
2245
2429
  return weekdayNames[date.getDay()];
2246
- } }) }) }))] }), jsx(FormFooter, { helperText: displayHelperText, validationState: currentValidationState === "none"
2247
- ? "default"
2248
- : currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" })] }));
2430
+ } }) }) }));
2431
+ return createPortal(calendarPopup, document.body);
2432
+ })()] }));
2249
2433
  });
2250
2434
  DatePicker.displayName = "DatePicker";
2251
2435
 
@@ -3697,7 +3881,7 @@ const selectTriggerVariants = cva("flex items-center gap-1 transition-all font-f
3697
3881
  isDisabled: false,
3698
3882
  },
3699
3883
  });
3700
- const SelectTextField = React.forwardRef(({ textValue: controlledTextValue, defaultTextValue, onTextChange, selectOptions = [], selectValue: controlledSelectValue, defaultSelectValue, onSelectChange, selectPlaceholder = "Select", selectTriggerClassName, selectMenuClassName, selectMenuWidth = "auto", selectSectionHeading, selectEmptyTitle = "No options available", selectEmptyDescription = "There are no options to select from.", selectEmptyIcon, label, helperText, errorText, successText, validationState = "none", isDisabled = false, isRequired = false, isOptional = false, size = "medium", containerClassName, labelClassName, inputClassName, className, ...textFieldProps }, ref) => {
3884
+ const SelectTextField = React.forwardRef(({ textValue: controlledTextValue, defaultTextValue, onTextChange, selectOptions = [], selectValue: controlledSelectValue, defaultSelectValue, onSelectChange, selectPlaceholder = "Select", selectTriggerClassName, selectMenuClassName, selectMenuWidth = "auto", selectSectionHeading, selectEmptyTitle = "No options available", selectEmptyDescription = "There are no options to select from.", selectEmptyIcon, position = "suffix", label, helperText, errorText, successText, validationState = "none", isDisabled = false, isRequired = false, isOptional = false, size = "medium", containerClassName, labelClassName, inputClassName, className, ...textFieldProps }, ref) => {
3701
3885
  const [uncontrolledTextValue, setUncontrolledTextValue] = React.useState(defaultTextValue || "");
3702
3886
  const [uncontrolledSelectValue, setUncontrolledSelectValue] = React.useState(defaultSelectValue);
3703
3887
  const [isSelectOpen, setIsSelectOpen] = React.useState(false);
@@ -3852,12 +4036,14 @@ const SelectTextField = React.forwardRef(({ textValue: controlledTextValue, defa
3852
4036
  gap: "gap-3",
3853
4037
  },
3854
4038
  };
3855
- // Create the select suffix component
3856
- const selectSuffix = (jsxs("div", { className: "relative flex items-center h-full", children: [jsxs("div", { ref: selectRef, className: cn(selectTriggerVariants({
4039
+ // Create the select component (prefix or suffix)
4040
+ const selectComponent = (jsxs("div", { className: cn("relative flex items-stretch h-full"), children: [jsxs("div", { ref: selectRef, className: cn(selectTriggerVariants({
3857
4041
  size,
3858
4042
  validationState: currentValidationState,
3859
4043
  isDisabled,
3860
- }), "border-l border-action-outline-neutral-faded pl-2 ml-2 h-full flex items-center", selectTriggerClassName), onClick: !isDisabled ? toggleSelectOpen : undefined, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isSelectOpen, "aria-disabled": isDisabled, children: [jsx("span", { className: cn("text-left truncate max-w-[120px] whitespace-nowrap", !selectedOption && "text-surface-ink-neutral-muted", isDisabled && "text-surface-ink-neutral-disabled"), children: selectedOption?.label || selectPlaceholder }), jsx(ChevronDown, { className: cn("shrink-0 transition-transform", size === "small"
4044
+ }), "h-full flex items-center self-stretch", position === "prefix"
4045
+ ? "border-r border-action-outline-neutral-faded"
4046
+ : "border-l border-action-outline-neutral-faded", selectTriggerClassName, position === "prefix" ? "pr-4" : "pl-4"), onClick: !isDisabled ? toggleSelectOpen : undefined, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isSelectOpen, "aria-disabled": isDisabled, children: [jsx("span", { className: cn("text-left truncate max-w-[120px] whitespace-nowrap", !selectedOption && "text-surface-ink-neutral-muted", isDisabled && "text-surface-ink-neutral-disabled"), children: selectedOption?.label || selectPlaceholder }), jsx(ChevronDown, { className: cn("shrink-0 transition-transform", size === "small"
3861
4047
  ? "w-3 h-3"
3862
4048
  : size === "medium"
3863
4049
  ? "w-3.5 h-3.5"
@@ -3867,10 +4053,12 @@ const SelectTextField = React.forwardRef(({ textValue: controlledTextValue, defa
3867
4053
  ? "text-feedback-ink-positive-intense"
3868
4054
  : currentValidationState === "negative"
3869
4055
  ? "text-feedback-ink-negative-subtle"
3870
- : "text-surface-ink-neutral-muted", isSelectOpen && "transform rotate-180") })] }), isSelectOpen && !isDisabled && (jsx("div", { ref: dropdownContainerRef, className: cn("absolute z-50 right-0", dropdownPlacement === "bottom"
3871
- ? "top-full mt-1"
4056
+ : "text-surface-ink-neutral-muted", isSelectOpen && "transform rotate-180") })] }), isSelectOpen && !isDisabled && (jsx("div", { ref: dropdownContainerRef, className: cn("absolute z-50", position === "prefix" ? "left-[-12px]" : "right-[-12px]", dropdownPlacement === "bottom"
4057
+ ? "top-[30px] mt-1"
3872
4058
  : "bottom-full mb-1"), children: jsx(DropdownMenu, { items: menuItems, sectionHeading: selectSectionHeading, isEmpty: selectOptions.length === 0, emptyTitle: selectEmptyTitle, emptyDescription: selectEmptyDescription, emptyIcon: selectEmptyIcon, disableFooter: true, onClose: () => handleSelectOpenChange(false), className: selectMenuClassName, width: widthStyle }) }))] }));
3873
- return (jsxs("div", { ref: componentRef, className: cn("w-full flex flex-col", sizeConfig[size].gap, containerClassName), children: [label && (jsx(FormHeader, { label: label, size: size, isRequired: isRequired, isOptional: isOptional, infoHeading: textFieldProps.infoHeading, infoDescription: textFieldProps.infoDescription, LinkComponent: textFieldProps.LinkComponent, linkText: textFieldProps.linkText, linkHref: textFieldProps.linkHref, onLinkClick: textFieldProps.onLinkClick, htmlFor: textFieldProps.id, className: "mb-2", labelClassName: labelClassName })), jsx(TextField, { ref: ref, value: textValue, onChange: handleTextChange, suffix: selectSuffix, size: size, validationState: currentValidationState, isDisabled: isDisabled, isRequired: isRequired, isOptional: isOptional, containerClassName: "gap-0", className: className, inputClassName: inputClassName, ...textFieldProps }), jsx(FormFooter, { helperText: displayHelperText, validationState: currentValidationState === "none"
4059
+ return (jsxs("div", { ref: componentRef, className: cn("w-full flex flex-col", sizeConfig[size].gap, containerClassName), children: [label && (jsx(FormHeader, { label: label, size: size, isRequired: isRequired, isOptional: isOptional, infoHeading: textFieldProps.infoHeading, infoDescription: textFieldProps.infoDescription, LinkComponent: textFieldProps.LinkComponent, linkText: textFieldProps.linkText, linkHref: textFieldProps.linkHref, onLinkClick: textFieldProps.onLinkClick, htmlFor: textFieldProps.id, className: "mb-2", labelClassName: labelClassName })), jsx(TextField, { ref: ref, value: textValue, onChange: handleTextChange, ...(position === "prefix"
4060
+ ? { prefix: selectComponent }
4061
+ : { suffix: selectComponent }), size: size, validationState: currentValidationState, isDisabled: isDisabled, isRequired: isRequired, isOptional: isOptional, containerClassName: "gap-0", className: className, inputClassName: inputClassName, ...textFieldProps }), jsx(FormFooter, { helperText: displayHelperText, validationState: currentValidationState === "none"
3874
4062
  ? "default"
3875
4063
  : currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" })] }));
3876
4064
  });