infinity-ui-elements 1.14.2 → 1.14.4

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
@@ -1450,7 +1450,7 @@ const getSafeAreaInsetBottom = () => {
1450
1450
  document.body.removeChild(testDiv);
1451
1451
  return paddingBottom;
1452
1452
  };
1453
- const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, footer, children, variant = "default", showCloseButton = true, showDragHandle = true, closeOnOverlayClick = true, closeOnEscape = true, closeOnSwipeDown = true, maxHeight = "90vh", className, contentClassName, headerClassName, bodyClassName, footerClassName, overlayClassName, ariaLabel, ariaDescribedBy, adjustForKeyboard = true, scrollToFocusedInput = true, }, ref) => {
1453
+ const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, footer, children, variant = "default", showCloseButton = true, showDragHandle = true, closeOnOverlayClick = true, closeOnEscape = true, closeOnSwipeDown = true, maxHeight = "90vh", className, contentClassName, headerClassName, bodyClassName, footerClassName, overlayClassName, ariaLabel, ariaDescribedBy, adjustForKeyboard = true, scrollToFocusedInput = true, stickyContent, stickyContentClassName, }, ref) => {
1454
1454
  const sheetRef = React.useRef(null);
1455
1455
  const contentRef = ref || sheetRef;
1456
1456
  const bodyRef = React.useRef(null);
@@ -1587,11 +1587,24 @@ const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, foo
1587
1587
  }
1588
1588
  };
1589
1589
  }, [isOpen]);
1590
- // Reset closing state when opening
1590
+ // Reset closing state and scroll position when opening
1591
1591
  React.useEffect(() => {
1592
1592
  if (isOpen) {
1593
1593
  setIsClosing(false);
1594
1594
  setKeyboardHeight(0);
1595
+ // Reset scroll to top when opening - use multiple timeouts for iOS reliability
1596
+ const resetScroll = () => {
1597
+ if (bodyRef.current) {
1598
+ bodyRef.current.scrollTop = 0;
1599
+ }
1600
+ };
1601
+ // Immediate reset
1602
+ resetScroll();
1603
+ // After animation starts
1604
+ requestAnimationFrame(resetScroll);
1605
+ // After animation completes
1606
+ const timer = setTimeout(resetScroll, 350);
1607
+ return () => clearTimeout(timer);
1595
1608
  }
1596
1609
  }, [isOpen]);
1597
1610
  // Scroll focused input into view when keyboard opens
@@ -1629,16 +1642,25 @@ const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, foo
1629
1642
  return () => document.removeEventListener("focusin", handleFocusIn);
1630
1643
  }, [isOpen, scrollToFocusedInput, adjustForKeyboard, viewportHeight, keyboardHeight]);
1631
1644
  // Reset scroll to top when content changes (for filtered results)
1645
+ // This ensures empty states and filtered results are always visible
1632
1646
  React.useEffect(() => {
1633
1647
  if (!isOpen || !bodyRef.current)
1634
1648
  return;
1635
1649
  // Use requestAnimationFrame to ensure DOM has updated
1636
1650
  requestAnimationFrame(() => {
1637
1651
  if (bodyRef.current) {
1652
+ // Always scroll to top to show content (including empty states)
1638
1653
  bodyRef.current.scrollTop = 0;
1654
+ // If keyboard is open, ensure content is in visible area
1655
+ if (keyboardHeight > 0) {
1656
+ // Force a re-render of the scroll position
1657
+ bodyRef.current.style.scrollBehavior = 'auto';
1658
+ bodyRef.current.scrollTop = 0;
1659
+ bodyRef.current.style.scrollBehavior = '';
1660
+ }
1639
1661
  }
1640
1662
  });
1641
- }, [children, isOpen]);
1663
+ }, [children, isOpen, keyboardHeight]);
1642
1664
  // Handle close with animation
1643
1665
  const handleClose = React.useCallback(() => {
1644
1666
  if (!onClose)
@@ -1737,23 +1759,30 @@ const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, foo
1737
1759
  return null;
1738
1760
  const hasHeader = title || description;
1739
1761
  // Calculate dynamic max height based on keyboard
1762
+ // When keyboard is open, limit height to fit in visible viewport above keyboard
1740
1763
  const dynamicMaxHeight = keyboardHeight > 0
1741
- ? `calc(${viewportHeight}px - ${safeAreaBottom}px - 20px)`
1764
+ ? `${Math.min(viewportHeight - 20, viewportHeight * 0.9)}px`
1742
1765
  : maxHeight;
1743
- return (jsxs("div", { className: cn("fixed inset-0 z-9999 flex items-end justify-center",
1744
- // Use visual viewport height on mobile when keyboard is open
1745
- keyboardHeight > 0 && "items-start pt-[env(safe-area-inset-top)]", className), style: {
1746
- // Use visual viewport height when keyboard is open
1766
+ return (jsxs("div", { className: cn("fixed z-9999 flex items-end justify-center",
1767
+ // Always position at bottom, container height adjusts for keyboard
1768
+ className), style: {
1769
+ // Position container to fill visible viewport (above keyboard)
1770
+ top: 0,
1771
+ left: 0,
1772
+ right: 0,
1773
+ // Use visual viewport height when keyboard is open to stay above keyboard
1747
1774
  height: keyboardHeight > 0 ? `${viewportHeight}px` : "100%",
1775
+ // On iOS, also set bottom to ensure proper positioning
1776
+ ...(keyboardHeight === 0 && { bottom: 0 }),
1748
1777
  }, role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title, "aria-describedby": ariaDescribedBy, children: [jsx("div", { ref: overlayRef, className: cn("absolute inset-0 z-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300", isClosing ? "opacity-0" : "opacity-100", overlayClassName), onClick: handleOverlayClick, "aria-hidden": "true" }), jsxs("div", { ref: contentRef, className: cn("relative z-10 w-full transition-transform duration-300", "flex flex-col overflow-hidden", variant === "default" && "bg-white rounded-t-2xl shadow-xl", isClosing ? "animate-slide-out-bottom" : "animate-slide-in-bottom",
1749
- // Ensure bottom sheet is at the bottom when keyboard is open
1750
- keyboardHeight > 0 && "mt-auto", contentClassName), style: {
1778
+ // Always position at bottom of container
1779
+ "mt-auto", contentClassName), style: {
1751
1780
  maxHeight: dynamicMaxHeight,
1752
1781
  // Add safe area padding at the bottom for devices with home indicator
1753
1782
  paddingBottom: footer ? 0 : `max(env(safe-area-inset-bottom, 0px), ${safeAreaBottom}px)`,
1754
- }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [showDragHandle && variant === "default" && (jsx("div", { ref: dragHandleRef, className: "flex justify-center pt-3 pb-2 cursor-grab active:cursor-grabbing touch-none", children: jsx("div", { className: "w-10 h-1 bg-neutral-300 rounded-full" }) })), hasHeader && (jsxs("div", { className: cn("flex items-start justify-between gap-4 shrink-0", variant === "default" && "px-6", variant === "default" && !showDragHandle && "pt-6", variant === "default" && showDragHandle && "pt-2", variant === "default" && !description && "pb-4", variant === "default" && description && "pb-2", headerClassName), children: [jsxs("div", { className: "flex-1 min-w-0", children: [title && (jsx(Text, { as: "h2", variant: "body", size: "large", weight: "semibold", color: "default", children: title })), description && (jsx(Text, { as: "p", variant: "body", size: "small", weight: "regular", color: "subtle", className: "mt-1", children: description }))] }), showCloseButton && onClose && (jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet", className: "shrink-0" }))] })), !hasHeader && showCloseButton && onClose && (jsx("div", { className: cn("absolute z-10", variant === "default" && "top-4 right-4"), children: jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet" }) })), jsx("div", { ref: bodyRef, className: cn("flex-1 overflow-y-auto overscroll-contain",
1783
+ }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [showDragHandle && variant === "default" && (jsx("div", { ref: dragHandleRef, className: "flex justify-center pt-3 pb-2 cursor-grab active:cursor-grabbing touch-none", children: jsx("div", { className: "w-10 h-1 bg-neutral-300 rounded-full" }) })), hasHeader && (jsxs("div", { className: cn("flex items-start justify-between gap-4 shrink-0", variant === "default" && "px-6", variant === "default" && !showDragHandle && "pt-6", variant === "default" && showDragHandle && "pt-2", variant === "default" && !description && "pb-4", variant === "default" && description && "pb-2", headerClassName), children: [jsxs("div", { className: "flex-1 min-w-0", children: [title && (jsx(Text, { as: "h2", variant: "body", size: "large", weight: "semibold", color: "default", children: title })), description && (jsx(Text, { as: "p", variant: "body", size: "small", weight: "regular", color: "subtle", className: "mt-1", children: description }))] }), showCloseButton && onClose && (jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet", className: "shrink-0" }))] })), !hasHeader && showCloseButton && onClose && (jsx("div", { className: cn("absolute z-10", variant === "default" && "top-4 right-4"), children: jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet" }) })), stickyContent && (jsx("div", { className: cn("shrink-0", variant === "default" && "px-6 pb-4", variant === "default" && !hasHeader && !showDragHandle && "pt-4", variant === "default" && !hasHeader && showDragHandle && "pt-2", stickyContentClassName), children: stickyContent })), jsx("div", { ref: bodyRef, className: cn("flex-1 min-h-0 overflow-y-auto overscroll-contain",
1755
1784
  // Smooth scrolling and momentum scroll for iOS
1756
- "-webkit-overflow-scrolling-touch", variant === "default" && "px-6", variant === "default" && hasHeader && "py-4", variant === "default" && !hasHeader && !showDragHandle && "pt-6 pb-4", variant === "default" && !hasHeader && showDragHandle && "pt-2 pb-4", variant === "default" && !footer && "pb-6", bodyClassName), children: children }), footer && (jsxs("div", { className: "flex flex-col shrink-0", style: {
1785
+ "-webkit-overflow-scrolling-touch", variant === "default" && "px-6", variant === "default" && hasHeader && !stickyContent && "py-4", variant === "default" && hasHeader && stickyContent && "pb-4", variant === "default" && !hasHeader && !showDragHandle && !stickyContent && "pt-6 pb-4", variant === "default" && !hasHeader && showDragHandle && !stickyContent && "pt-2 pb-4", variant === "default" && stickyContent && "pt-0 pb-4", variant === "default" && !footer && "pb-6", bodyClassName), children: children }), footer && (jsxs("div", { className: "flex flex-col shrink-0", style: {
1757
1786
  // Add safe area padding to footer
1758
1787
  paddingBottom: `max(env(safe-area-inset-bottom, 0px), ${safeAreaBottom}px)`,
1759
1788
  }, children: [variant === "default" && (jsx(Divider, { thickness: "thin", variant: "muted" })), jsx("div", { className: cn("flex items-center justify-end gap-3", variant === "default" && "px-6 py-4", footerClassName), children: footer })] }))] })] }));
@@ -4864,11 +4893,16 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
4864
4893
  });
4865
4894
  const showDropdown = isOpen && searchValue.length >= minSearchLength;
4866
4895
  // Render dropdown menu content
4867
- const renderDropdownContent = () => (jsx(DropdownMenu, { items: itemsWithHandlers, sectionHeading: sectionHeading, isLoading: isLoading, isEmpty: itemsWithAddNew.length === 0 && !showAddNew, emptyTitle: emptyTitle, emptyDescription: emptyDescription, emptyLinkText: emptyLinkText, onEmptyLinkClick: onEmptyLinkClick, primaryButtonText: primaryButtonText, secondaryButtonText: secondaryButtonText, onPrimaryClick: onPrimaryClick, onSecondaryClick: onSecondaryClick, showChevron: showChevron, emptyIcon: emptyIcon, disableFooter: disableFooter, showFooter: (primaryButtonText || secondaryButtonText) && !disableFooter
4896
+ const renderDropdownContent = () => (jsx(DropdownMenu, { items: itemsWithHandlers, sectionHeading: isMobile ? undefined : sectionHeading, isLoading: isLoading, isEmpty: itemsWithAddNew.length === 0 && !showAddNew, emptyTitle: emptyTitle, emptyDescription: emptyDescription, emptyLinkText: emptyLinkText, onEmptyLinkClick: onEmptyLinkClick, primaryButtonText: primaryButtonText, secondaryButtonText: secondaryButtonText, onPrimaryClick: onPrimaryClick, onSecondaryClick: onSecondaryClick, showChevron: showChevron, emptyIcon: emptyIcon, disableFooter: disableFooter, showFooter: (primaryButtonText || secondaryButtonText) && !disableFooter
4868
4897
  ? true
4869
- : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"), maxHeight: `${position.maxHeight}px`, unstyled: isMobile }));
4898
+ : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"),
4899
+ // On mobile, let BottomSheet body handle scrolling; on desktop use calculated maxHeight
4900
+ maxHeight: isMobile ? "none" : `${position.maxHeight}px`, unstyled: isMobile }));
4901
+ // Search field component for mobile BottomSheet
4902
+ // Note: No autoFocus - keyboard only appears when user taps the field
4903
+ const mobileSearchField = (jsx(TextField, { value: searchValue, onChange: handleSearchChange, onKeyDown: handleKeyDown, containerClassName: "mb-0", placeholder: textFieldProps.placeholder || "Search...", ...textFieldProps }));
4870
4904
  // Mobile: BottomSheet, Desktop: Regular Dropdown
4871
- const dropdownMenu = showDropdown && (isMobile ? (jsxs(BottomSheet, { isOpen: isOpen, onClose: () => setIsOpen(false), title: sectionHeading, variant: "default", showDragHandle: true, closeOnOverlayClick: true, closeOnEscape: true, closeOnSwipeDown: true, adjustForKeyboard: true, scrollToFocusedInput: true, maxHeight: "85vh", children: [jsx("div", { className: "mb-4 sticky top-0 bg-white z-10 -mx-6 px-6 pt-2 pb-4 -mt-4", children: jsx(TextField, { value: searchValue, onChange: handleSearchChange, onKeyDown: handleKeyDown, containerClassName: "mb-0", placeholder: textFieldProps.placeholder || "Search...", autoFocus: true, ...textFieldProps }) }), renderDropdownContent()] })) : (jsx("div", { ref: menuRef, style: {
4905
+ const dropdownMenu = showDropdown && (isMobile ? (jsx(BottomSheet, { isOpen: isOpen, onClose: () => setIsOpen(false), title: sectionHeading, variant: "default", showDragHandle: true, closeOnOverlayClick: true, closeOnEscape: true, closeOnSwipeDown: true, adjustForKeyboard: true, scrollToFocusedInput: true, maxHeight: "85vh", stickyContent: mobileSearchField, children: renderDropdownContent() })) : (jsx("div", { ref: menuRef, style: {
4872
4906
  position: "fixed",
4873
4907
  ...(position.top !== undefined && { top: `${position.top}px` }),
4874
4908
  ...(position.bottom !== undefined && {