infinity-ui-elements 1.14.3 → 1.14.5

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.js CHANGED
@@ -1566,32 +1566,55 @@ const BottomSheet = React__namespace.forwardRef(({ isOpen, onClose, title, descr
1566
1566
  document.body.style.right = "0";
1567
1567
  document.body.style.width = "100%";
1568
1568
  }
1569
+ // Track touch position for scroll boundary detection
1570
+ let lastTouchY = 0;
1571
+ const handleTouchStartForScroll = (e) => {
1572
+ lastTouchY = e.touches[0].clientY;
1573
+ };
1569
1574
  // Prevent touchmove on overlay to stop iOS rubber-banding
1575
+ // But always allow scrolling inside body content
1570
1576
  const preventTouchMove = (e) => {
1571
1577
  const target = e.target;
1572
- // Allow scrolling inside the body content
1578
+ const currentTouchY = e.touches[0].clientY;
1579
+ const touchDelta = currentTouchY - lastTouchY;
1580
+ lastTouchY = currentTouchY;
1581
+ // Always allow scrolling inside the body content
1573
1582
  if (bodyRef.current?.contains(target)) {
1574
1583
  const scrollTop = bodyRef.current.scrollTop;
1575
1584
  const scrollHeight = bodyRef.current.scrollHeight;
1576
1585
  const clientHeight = bodyRef.current.clientHeight;
1577
- // Check if at scroll boundaries
1578
- const isAtTop = scrollTop <= 0;
1579
- const isAtBottom = scrollTop + clientHeight >= scrollHeight;
1580
- // Only prevent default if at boundaries and trying to scroll further
1581
- if ((isAtTop && e.touches[0].clientY > (touchStart || 0)) ||
1582
- (isAtBottom && e.touches[0].clientY < (touchStart || 0))) {
1583
- // Allow the swipe-to-close gesture
1584
- if (!isDraggingFromHandle.current) {
1586
+ const canScroll = scrollHeight > clientHeight;
1587
+ // If content is scrollable, allow normal scrolling
1588
+ if (canScroll) {
1589
+ const isAtTop = scrollTop <= 0;
1590
+ const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
1591
+ const isScrollingDown = touchDelta > 0; // finger moving down = scrolling up
1592
+ const isScrollingUp = touchDelta < 0; // finger moving up = scrolling down
1593
+ // Only prevent at boundaries when trying to scroll beyond
1594
+ if ((isAtTop && isScrollingDown) || (isAtBottom && isScrollingUp)) {
1595
+ // At boundary, prevent overscroll but allow swipe-to-close from drag handle
1596
+ if (isDraggingFromHandle.current) {
1597
+ return; // Allow swipe gesture
1598
+ }
1585
1599
  e.preventDefault();
1586
1600
  }
1601
+ // Otherwise, allow normal scrolling
1602
+ return;
1587
1603
  }
1604
+ // Content not scrollable, allow touch events to pass through
1605
+ return;
1606
+ }
1607
+ // Also allow scrolling in sticky content area
1608
+ if (target.closest('[data-sticky-content]')) {
1588
1609
  return;
1589
1610
  }
1590
- // Prevent scroll on overlay and other areas
1611
+ // Prevent scroll on overlay and other areas (to prevent background scroll)
1591
1612
  e.preventDefault();
1592
1613
  };
1614
+ document.addEventListener("touchstart", handleTouchStartForScroll, { passive: true });
1593
1615
  document.addEventListener("touchmove", preventTouchMove, { passive: false });
1594
1616
  return () => {
1617
+ document.removeEventListener("touchstart", handleTouchStartForScroll);
1595
1618
  document.removeEventListener("touchmove", preventTouchMove);
1596
1619
  // Restore original styles
1597
1620
  if (originalBodyStyles.current) {
@@ -1608,11 +1631,24 @@ const BottomSheet = React__namespace.forwardRef(({ isOpen, onClose, title, descr
1608
1631
  }
1609
1632
  };
1610
1633
  }, [isOpen]);
1611
- // Reset closing state when opening
1634
+ // Reset closing state and scroll position when opening
1612
1635
  React__namespace.useEffect(() => {
1613
1636
  if (isOpen) {
1614
1637
  setIsClosing(false);
1615
1638
  setKeyboardHeight(0);
1639
+ // Reset scroll to top when opening - use multiple timeouts for iOS reliability
1640
+ const resetScroll = () => {
1641
+ if (bodyRef.current) {
1642
+ bodyRef.current.scrollTop = 0;
1643
+ }
1644
+ };
1645
+ // Immediate reset
1646
+ resetScroll();
1647
+ // After animation starts
1648
+ requestAnimationFrame(resetScroll);
1649
+ // After animation completes
1650
+ const timer = setTimeout(resetScroll, 350);
1651
+ return () => clearTimeout(timer);
1616
1652
  }
1617
1653
  }, [isOpen]);
1618
1654
  // Scroll focused input into view when keyboard opens
@@ -1650,16 +1686,25 @@ const BottomSheet = React__namespace.forwardRef(({ isOpen, onClose, title, descr
1650
1686
  return () => document.removeEventListener("focusin", handleFocusIn);
1651
1687
  }, [isOpen, scrollToFocusedInput, adjustForKeyboard, viewportHeight, keyboardHeight]);
1652
1688
  // Reset scroll to top when content changes (for filtered results)
1689
+ // This ensures empty states and filtered results are always visible
1653
1690
  React__namespace.useEffect(() => {
1654
1691
  if (!isOpen || !bodyRef.current)
1655
1692
  return;
1656
1693
  // Use requestAnimationFrame to ensure DOM has updated
1657
1694
  requestAnimationFrame(() => {
1658
1695
  if (bodyRef.current) {
1696
+ // Always scroll to top to show content (including empty states)
1659
1697
  bodyRef.current.scrollTop = 0;
1698
+ // If keyboard is open, ensure content is in visible area
1699
+ if (keyboardHeight > 0) {
1700
+ // Force a re-render of the scroll position
1701
+ bodyRef.current.style.scrollBehavior = 'auto';
1702
+ bodyRef.current.scrollTop = 0;
1703
+ bodyRef.current.style.scrollBehavior = '';
1704
+ }
1660
1705
  }
1661
1706
  });
1662
- }, [children, isOpen]);
1707
+ }, [children, isOpen, keyboardHeight]);
1663
1708
  // Handle close with animation
1664
1709
  const handleClose = React__namespace.useCallback(() => {
1665
1710
  if (!onClose)
@@ -1758,21 +1803,28 @@ const BottomSheet = React__namespace.forwardRef(({ isOpen, onClose, title, descr
1758
1803
  return null;
1759
1804
  const hasHeader = title || description;
1760
1805
  // Calculate dynamic max height based on keyboard
1806
+ // When keyboard is open, limit height to fit in visible viewport above keyboard
1761
1807
  const dynamicMaxHeight = keyboardHeight > 0
1762
- ? `calc(${viewportHeight}px - ${safeAreaBottom}px - 20px)`
1808
+ ? `${Math.min(viewportHeight - 20, viewportHeight * 0.9)}px`
1763
1809
  : maxHeight;
1764
- return (jsxRuntime.jsxs("div", { className: cn("fixed inset-0 z-9999 flex items-end justify-center",
1765
- // Use visual viewport height on mobile when keyboard is open
1766
- keyboardHeight > 0 && "items-start pt-[env(safe-area-inset-top)]", className), style: {
1767
- // Use visual viewport height when keyboard is open
1810
+ return (jsxRuntime.jsxs("div", { className: cn("fixed z-9999 flex items-end justify-center",
1811
+ // Always position at bottom, container height adjusts for keyboard
1812
+ className), style: {
1813
+ // Position container to fill visible viewport (above keyboard)
1814
+ top: 0,
1815
+ left: 0,
1816
+ right: 0,
1817
+ // Use visual viewport height when keyboard is open to stay above keyboard
1768
1818
  height: keyboardHeight > 0 ? `${viewportHeight}px` : "100%",
1819
+ // On iOS, also set bottom to ensure proper positioning
1820
+ ...(keyboardHeight === 0 && { bottom: 0 }),
1769
1821
  }, role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title, "aria-describedby": ariaDescribedBy, children: [jsxRuntime.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" }), jsxRuntime.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",
1770
- // Ensure bottom sheet is at the bottom when keyboard is open
1771
- keyboardHeight > 0 && "mt-auto", contentClassName), style: {
1822
+ // Always position at bottom of container
1823
+ "mt-auto", contentClassName), style: {
1772
1824
  maxHeight: dynamicMaxHeight,
1773
1825
  // Add safe area padding at the bottom for devices with home indicator
1774
1826
  paddingBottom: footer ? 0 : `max(env(safe-area-inset-bottom, 0px), ${safeAreaBottom}px)`,
1775
- }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [showDragHandle && variant === "default" && (jsxRuntime.jsx("div", { ref: dragHandleRef, className: "flex justify-center pt-3 pb-2 cursor-grab active:cursor-grabbing touch-none", children: jsxRuntime.jsx("div", { className: "w-10 h-1 bg-neutral-300 rounded-full" }) })), hasHeader && (jsxRuntime.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: [jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [title && (jsxRuntime.jsx(Text, { as: "h2", variant: "body", size: "large", weight: "semibold", color: "default", children: title })), description && (jsxRuntime.jsx(Text, { as: "p", variant: "body", size: "small", weight: "regular", color: "subtle", className: "mt-1", children: description }))] }), showCloseButton && onClose && (jsxRuntime.jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet", className: "shrink-0" }))] })), !hasHeader && showCloseButton && onClose && (jsxRuntime.jsx("div", { className: cn("absolute z-10", variant === "default" && "top-4 right-4"), children: jsxRuntime.jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet" }) })), stickyContent && (jsxRuntime.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 })), jsxRuntime.jsx("div", { ref: bodyRef, className: cn("flex-1 overflow-y-auto overscroll-contain",
1827
+ }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [showDragHandle && variant === "default" && (jsxRuntime.jsx("div", { ref: dragHandleRef, className: "flex justify-center pt-3 pb-2 cursor-grab active:cursor-grabbing touch-none", children: jsxRuntime.jsx("div", { className: "w-10 h-1 bg-neutral-300 rounded-full" }) })), hasHeader && (jsxRuntime.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: [jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [title && (jsxRuntime.jsx(Text, { as: "h2", variant: "body", size: "large", weight: "semibold", color: "default", children: title })), description && (jsxRuntime.jsx(Text, { as: "p", variant: "body", size: "small", weight: "regular", color: "subtle", className: "mt-1", children: description }))] }), showCloseButton && onClose && (jsxRuntime.jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet", className: "shrink-0" }))] })), !hasHeader && showCloseButton && onClose && (jsxRuntime.jsx("div", { className: cn("absolute z-10", variant === "default" && "top-4 right-4"), children: jsxRuntime.jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet" }) })), stickyContent && (jsxRuntime.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 })), jsxRuntime.jsx("div", { ref: bodyRef, className: cn("flex-1 min-h-0 overflow-y-auto overscroll-contain",
1776
1828
  // Smooth scrolling and momentum scroll for iOS
1777
1829
  "-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 && (jsxRuntime.jsxs("div", { className: "flex flex-col shrink-0", style: {
1778
1830
  // Add safe area padding to footer
@@ -4885,11 +4937,14 @@ const SearchableDropdown = React__namespace.forwardRef(({ className, items = [],
4885
4937
  });
4886
4938
  const showDropdown = isOpen && searchValue.length >= minSearchLength;
4887
4939
  // Render dropdown menu content
4888
- const renderDropdownContent = () => (jsxRuntime.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
4940
+ const renderDropdownContent = () => (jsxRuntime.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
4889
4941
  ? true
4890
- : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"), maxHeight: `${position.maxHeight}px`, unstyled: isMobile }));
4942
+ : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"),
4943
+ // On mobile, let BottomSheet body handle scrolling; on desktop use calculated maxHeight
4944
+ maxHeight: isMobile ? "none" : `${position.maxHeight}px`, unstyled: isMobile }));
4891
4945
  // Search field component for mobile BottomSheet
4892
- const mobileSearchField = (jsxRuntime.jsx(TextField, { value: searchValue, onChange: handleSearchChange, onKeyDown: handleKeyDown, containerClassName: "mb-0", placeholder: textFieldProps.placeholder || "Search...", autoFocus: true, ...textFieldProps }));
4946
+ // Note: No autoFocus - keyboard only appears when user taps the field
4947
+ const mobileSearchField = (jsxRuntime.jsx(TextField, { value: searchValue, onChange: handleSearchChange, onKeyDown: handleKeyDown, containerClassName: "mb-0", placeholder: textFieldProps.placeholder || "Search...", ...textFieldProps }));
4893
4948
  // Mobile: BottomSheet, Desktop: Regular Dropdown
4894
4949
  const dropdownMenu = showDropdown && (isMobile ? (jsxRuntime.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() })) : (jsxRuntime.jsx("div", { ref: menuRef, style: {
4895
4950
  position: "fixed",