infinity-ui-elements 1.14.1 → 1.14.3
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/components/BottomSheet/BottomSheet.d.ts +19 -0
- package/dist/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/components/SearchableDropdown/SearchableDropdown.d.ts.map +1 -1
- package/dist/components/SearchableDropdown/SearchableDropdown.stories.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.esm.js +285 -29
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +285 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1410,13 +1410,102 @@ const Divider = React.forwardRef(({ className, orientation = "horizontal", thick
|
|
|
1410
1410
|
});
|
|
1411
1411
|
Divider.displayName = "Divider";
|
|
1412
1412
|
|
|
1413
|
-
|
|
1413
|
+
// Detect iOS
|
|
1414
|
+
const isIOS = () => {
|
|
1415
|
+
if (typeof window === "undefined" || typeof navigator === "undefined")
|
|
1416
|
+
return false;
|
|
1417
|
+
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
1418
|
+
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
|
1419
|
+
};
|
|
1420
|
+
// Detect Android
|
|
1421
|
+
const isAndroid = () => {
|
|
1422
|
+
if (typeof navigator === "undefined")
|
|
1423
|
+
return false;
|
|
1424
|
+
return /Android/.test(navigator.userAgent);
|
|
1425
|
+
};
|
|
1426
|
+
// Get visual viewport height (accounts for keyboard)
|
|
1427
|
+
const getVisualViewportHeight = () => {
|
|
1428
|
+
if (typeof window === "undefined")
|
|
1429
|
+
return 0;
|
|
1430
|
+
// Use visualViewport API if available (better keyboard detection)
|
|
1431
|
+
if (window.visualViewport) {
|
|
1432
|
+
return window.visualViewport.height;
|
|
1433
|
+
}
|
|
1434
|
+
return window.innerHeight;
|
|
1435
|
+
};
|
|
1436
|
+
// Get safe area inset bottom (for devices with home indicator)
|
|
1437
|
+
const getSafeAreaInsetBottom = () => {
|
|
1438
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
1439
|
+
return 0;
|
|
1440
|
+
const root = document.documentElement;
|
|
1441
|
+
const safeAreaInset = getComputedStyle(root).getPropertyValue("--safe-area-inset-bottom");
|
|
1442
|
+
if (safeAreaInset) {
|
|
1443
|
+
return parseInt(safeAreaInset, 10) || 0;
|
|
1444
|
+
}
|
|
1445
|
+
// Fallback: try to read env() value
|
|
1446
|
+
const testDiv = document.createElement("div");
|
|
1447
|
+
testDiv.style.paddingBottom = "env(safe-area-inset-bottom, 0px)";
|
|
1448
|
+
document.body.appendChild(testDiv);
|
|
1449
|
+
const paddingBottom = parseInt(getComputedStyle(testDiv).paddingBottom, 10) || 0;
|
|
1450
|
+
document.body.removeChild(testDiv);
|
|
1451
|
+
return paddingBottom;
|
|
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, stickyContent, stickyContentClassName, }, ref) => {
|
|
1414
1454
|
const sheetRef = React.useRef(null);
|
|
1415
1455
|
const contentRef = ref || sheetRef;
|
|
1416
1456
|
const bodyRef = React.useRef(null);
|
|
1457
|
+
const dragHandleRef = React.useRef(null);
|
|
1458
|
+
const overlayRef = React.useRef(null);
|
|
1417
1459
|
const [isClosing, setIsClosing] = React.useState(false);
|
|
1418
1460
|
const [touchStart, setTouchStart] = React.useState(null);
|
|
1419
1461
|
const [touchEnd, setTouchEnd] = React.useState(null);
|
|
1462
|
+
const [touchStartX, setTouchStartX] = React.useState(null);
|
|
1463
|
+
const [isSwipeGesture, setIsSwipeGesture] = React.useState(false);
|
|
1464
|
+
const [keyboardHeight, setKeyboardHeight] = React.useState(0);
|
|
1465
|
+
const [viewportHeight, setViewportHeight] = React.useState(typeof window !== "undefined" ? window.innerHeight : 0);
|
|
1466
|
+
const [safeAreaBottom, setSafeAreaBottom] = React.useState(0);
|
|
1467
|
+
// Track if user started dragging from the drag handle
|
|
1468
|
+
const isDraggingFromHandle = React.useRef(false);
|
|
1469
|
+
// Store original body styles for restoration
|
|
1470
|
+
const originalBodyStyles = React.useRef(null);
|
|
1471
|
+
// Track scroll position before locking
|
|
1472
|
+
const scrollPosition = React.useRef(0);
|
|
1473
|
+
// Initialize safe area inset
|
|
1474
|
+
React.useEffect(() => {
|
|
1475
|
+
setSafeAreaBottom(getSafeAreaInsetBottom());
|
|
1476
|
+
}, []);
|
|
1477
|
+
// Handle Visual Viewport API for keyboard detection
|
|
1478
|
+
React.useEffect(() => {
|
|
1479
|
+
if (!isOpen || !adjustForKeyboard)
|
|
1480
|
+
return;
|
|
1481
|
+
if (typeof window === "undefined")
|
|
1482
|
+
return;
|
|
1483
|
+
const updateViewport = () => {
|
|
1484
|
+
const newHeight = getVisualViewportHeight();
|
|
1485
|
+
const fullHeight = window.innerHeight;
|
|
1486
|
+
const newKeyboardHeight = Math.max(0, fullHeight - newHeight);
|
|
1487
|
+
setViewportHeight(newHeight);
|
|
1488
|
+
setKeyboardHeight(newKeyboardHeight);
|
|
1489
|
+
};
|
|
1490
|
+
// Use visualViewport API if available
|
|
1491
|
+
if (window.visualViewport) {
|
|
1492
|
+
window.visualViewport.addEventListener("resize", updateViewport);
|
|
1493
|
+
window.visualViewport.addEventListener("scroll", updateViewport);
|
|
1494
|
+
updateViewport();
|
|
1495
|
+
return () => {
|
|
1496
|
+
window.visualViewport?.removeEventListener("resize", updateViewport);
|
|
1497
|
+
window.visualViewport?.removeEventListener("scroll", updateViewport);
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
// Fallback for browsers without visualViewport API
|
|
1502
|
+
window.addEventListener("resize", updateViewport);
|
|
1503
|
+
updateViewport();
|
|
1504
|
+
return () => {
|
|
1505
|
+
window.removeEventListener("resize", updateViewport);
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
}, [isOpen, adjustForKeyboard]);
|
|
1420
1509
|
// Handle escape key
|
|
1421
1510
|
React.useEffect(() => {
|
|
1422
1511
|
if (!isOpen || !closeOnEscape || !onClose)
|
|
@@ -1429,80 +1518,245 @@ const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, foo
|
|
|
1429
1518
|
document.addEventListener("keydown", handleEscape);
|
|
1430
1519
|
return () => document.removeEventListener("keydown", handleEscape);
|
|
1431
1520
|
}, [isOpen, closeOnEscape, onClose]);
|
|
1432
|
-
//
|
|
1521
|
+
// Robust body scroll lock for iOS and Android
|
|
1433
1522
|
React.useEffect(() => {
|
|
1434
|
-
if (isOpen)
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1523
|
+
if (!isOpen)
|
|
1524
|
+
return;
|
|
1525
|
+
const iOS = isIOS();
|
|
1526
|
+
// Store current scroll position
|
|
1527
|
+
scrollPosition.current = window.pageYOffset || document.documentElement.scrollTop;
|
|
1528
|
+
// Store original styles
|
|
1529
|
+
originalBodyStyles.current = {
|
|
1530
|
+
overflow: document.body.style.overflow,
|
|
1531
|
+
position: document.body.style.position,
|
|
1532
|
+
top: document.body.style.top,
|
|
1533
|
+
left: document.body.style.left,
|
|
1534
|
+
right: document.body.style.right,
|
|
1535
|
+
width: document.body.style.width,
|
|
1536
|
+
height: document.body.style.height,
|
|
1537
|
+
};
|
|
1538
|
+
// Apply scroll lock
|
|
1539
|
+
document.body.style.overflow = "hidden";
|
|
1540
|
+
// iOS requires position fixed to prevent background scroll
|
|
1541
|
+
if (iOS) {
|
|
1542
|
+
document.body.style.position = "fixed";
|
|
1543
|
+
document.body.style.top = `-${scrollPosition.current}px`;
|
|
1544
|
+
document.body.style.left = "0";
|
|
1545
|
+
document.body.style.right = "0";
|
|
1546
|
+
document.body.style.width = "100%";
|
|
1547
|
+
}
|
|
1548
|
+
// Prevent touchmove on overlay to stop iOS rubber-banding
|
|
1549
|
+
const preventTouchMove = (e) => {
|
|
1550
|
+
const target = e.target;
|
|
1551
|
+
// Allow scrolling inside the body content
|
|
1552
|
+
if (bodyRef.current?.contains(target)) {
|
|
1553
|
+
const scrollTop = bodyRef.current.scrollTop;
|
|
1554
|
+
const scrollHeight = bodyRef.current.scrollHeight;
|
|
1555
|
+
const clientHeight = bodyRef.current.clientHeight;
|
|
1556
|
+
// Check if at scroll boundaries
|
|
1557
|
+
const isAtTop = scrollTop <= 0;
|
|
1558
|
+
const isAtBottom = scrollTop + clientHeight >= scrollHeight;
|
|
1559
|
+
// Only prevent default if at boundaries and trying to scroll further
|
|
1560
|
+
if ((isAtTop && e.touches[0].clientY > (touchStart || 0)) ||
|
|
1561
|
+
(isAtBottom && e.touches[0].clientY < (touchStart || 0))) {
|
|
1562
|
+
// Allow the swipe-to-close gesture
|
|
1563
|
+
if (!isDraggingFromHandle.current) {
|
|
1564
|
+
e.preventDefault();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
// Prevent scroll on overlay and other areas
|
|
1570
|
+
e.preventDefault();
|
|
1571
|
+
};
|
|
1572
|
+
document.addEventListener("touchmove", preventTouchMove, { passive: false });
|
|
1440
1573
|
return () => {
|
|
1441
|
-
document.
|
|
1574
|
+
document.removeEventListener("touchmove", preventTouchMove);
|
|
1575
|
+
// Restore original styles
|
|
1576
|
+
if (originalBodyStyles.current) {
|
|
1577
|
+
document.body.style.overflow = originalBodyStyles.current.overflow;
|
|
1578
|
+
document.body.style.position = originalBodyStyles.current.position;
|
|
1579
|
+
document.body.style.top = originalBodyStyles.current.top;
|
|
1580
|
+
document.body.style.left = originalBodyStyles.current.left;
|
|
1581
|
+
document.body.style.right = originalBodyStyles.current.right;
|
|
1582
|
+
document.body.style.width = originalBodyStyles.current.width;
|
|
1583
|
+
}
|
|
1584
|
+
// Restore scroll position for iOS
|
|
1585
|
+
if (iOS) {
|
|
1586
|
+
window.scrollTo(0, scrollPosition.current);
|
|
1587
|
+
}
|
|
1442
1588
|
};
|
|
1443
1589
|
}, [isOpen]);
|
|
1444
1590
|
// Reset closing state when opening
|
|
1445
1591
|
React.useEffect(() => {
|
|
1446
1592
|
if (isOpen) {
|
|
1447
1593
|
setIsClosing(false);
|
|
1594
|
+
setKeyboardHeight(0);
|
|
1448
1595
|
}
|
|
1449
1596
|
}, [isOpen]);
|
|
1450
|
-
//
|
|
1597
|
+
// Scroll focused input into view when keyboard opens
|
|
1451
1598
|
React.useEffect(() => {
|
|
1452
|
-
if (isOpen
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1599
|
+
if (!isOpen || !scrollToFocusedInput || !adjustForKeyboard)
|
|
1600
|
+
return;
|
|
1601
|
+
const handleFocusIn = (e) => {
|
|
1602
|
+
const target = e.target;
|
|
1603
|
+
if (!target || !bodyRef.current)
|
|
1604
|
+
return;
|
|
1605
|
+
// Check if target is an input/textarea inside the body
|
|
1606
|
+
if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
|
|
1607
|
+
bodyRef.current.contains(target)) {
|
|
1608
|
+
// Wait for keyboard to open
|
|
1609
|
+
setTimeout(() => {
|
|
1610
|
+
// Scroll the input into view within the body container
|
|
1611
|
+
const targetRect = target.getBoundingClientRect();
|
|
1612
|
+
const bodyRect = bodyRef.current?.getBoundingClientRect();
|
|
1613
|
+
if (!bodyRect)
|
|
1614
|
+
return;
|
|
1615
|
+
// Calculate if input is below visible area (considering keyboard)
|
|
1616
|
+
const visibleBottom = viewportHeight - keyboardHeight - 20; // 20px buffer
|
|
1617
|
+
if (targetRect.bottom > visibleBottom) {
|
|
1618
|
+
// Scroll to bring input into view
|
|
1619
|
+
const scrollAmount = targetRect.bottom - visibleBottom + 40;
|
|
1620
|
+
bodyRef.current?.scrollBy({
|
|
1621
|
+
top: scrollAmount,
|
|
1622
|
+
behavior: "smooth",
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}, 100);
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
document.addEventListener("focusin", handleFocusIn);
|
|
1629
|
+
return () => document.removeEventListener("focusin", handleFocusIn);
|
|
1630
|
+
}, [isOpen, scrollToFocusedInput, adjustForKeyboard, viewportHeight, keyboardHeight]);
|
|
1631
|
+
// Reset scroll to top when content changes (for filtered results)
|
|
1632
|
+
React.useEffect(() => {
|
|
1633
|
+
if (!isOpen || !bodyRef.current)
|
|
1634
|
+
return;
|
|
1635
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
1636
|
+
requestAnimationFrame(() => {
|
|
1637
|
+
if (bodyRef.current) {
|
|
1638
|
+
bodyRef.current.scrollTop = 0;
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1460
1641
|
}, [children, isOpen]);
|
|
1461
1642
|
// Handle close with animation
|
|
1462
|
-
const handleClose = () => {
|
|
1643
|
+
const handleClose = React.useCallback(() => {
|
|
1463
1644
|
if (!onClose)
|
|
1464
1645
|
return;
|
|
1646
|
+
// Blur any focused input to dismiss keyboard
|
|
1647
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
1648
|
+
document.activeElement.blur();
|
|
1649
|
+
}
|
|
1465
1650
|
setIsClosing(true);
|
|
1466
1651
|
// Wait for animation to complete before calling onClose
|
|
1467
1652
|
setTimeout(() => {
|
|
1468
1653
|
onClose();
|
|
1469
1654
|
setIsClosing(false);
|
|
1470
1655
|
}, 300); // Match animation duration
|
|
1471
|
-
};
|
|
1656
|
+
}, [onClose]);
|
|
1472
1657
|
// Handle overlay click
|
|
1473
1658
|
const handleOverlayClick = (e) => {
|
|
1474
1659
|
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
|
1475
1660
|
handleClose();
|
|
1476
1661
|
}
|
|
1477
1662
|
};
|
|
1478
|
-
//
|
|
1663
|
+
// Touch handling for swipe-to-close (only from drag handle or when scrolled to top)
|
|
1479
1664
|
const handleTouchStart = (e) => {
|
|
1480
1665
|
if (!closeOnSwipeDown)
|
|
1481
1666
|
return;
|
|
1667
|
+
const touch = e.targetTouches[0];
|
|
1482
1668
|
setTouchEnd(null);
|
|
1483
|
-
setTouchStart(
|
|
1669
|
+
setTouchStart(touch.clientY);
|
|
1670
|
+
setTouchStartX(touch.clientX);
|
|
1671
|
+
setIsSwipeGesture(false);
|
|
1672
|
+
// Check if drag started from the drag handle
|
|
1673
|
+
isDraggingFromHandle.current = dragHandleRef.current?.contains(e.target) || false;
|
|
1484
1674
|
};
|
|
1485
1675
|
const handleTouchMove = (e) => {
|
|
1486
|
-
if (!closeOnSwipeDown)
|
|
1676
|
+
if (!closeOnSwipeDown || touchStart === null)
|
|
1487
1677
|
return;
|
|
1488
|
-
|
|
1678
|
+
const touch = e.targetTouches[0];
|
|
1679
|
+
const currentY = touch.clientY;
|
|
1680
|
+
const currentX = touch.clientX;
|
|
1681
|
+
setTouchEnd(currentY);
|
|
1682
|
+
// Determine if this is a vertical swipe gesture (vs horizontal scroll)
|
|
1683
|
+
if (!isSwipeGesture && touchStartX !== null) {
|
|
1684
|
+
const deltaY = Math.abs(currentY - touchStart);
|
|
1685
|
+
const deltaX = Math.abs(currentX - touchStartX);
|
|
1686
|
+
// If vertical movement is greater, it's a swipe gesture
|
|
1687
|
+
if (deltaY > 10 || deltaX > 10) {
|
|
1688
|
+
setIsSwipeGesture(deltaY > deltaX);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1489
1691
|
};
|
|
1490
1692
|
const handleTouchEnd = () => {
|
|
1491
|
-
if (!closeOnSwipeDown ||
|
|
1693
|
+
if (!closeOnSwipeDown || touchStart === null || touchEnd === null) {
|
|
1694
|
+
resetTouchState();
|
|
1492
1695
|
return;
|
|
1696
|
+
}
|
|
1493
1697
|
const distance = touchEnd - touchStart;
|
|
1494
|
-
const isSwipeDown = distance >
|
|
1495
|
-
|
|
1698
|
+
const isSwipeDown = distance > 80; // Minimum swipe distance
|
|
1699
|
+
// Only close if:
|
|
1700
|
+
// 1. Swiped from drag handle, OR
|
|
1701
|
+
// 2. Body is scrolled to top and user swiped down
|
|
1702
|
+
const bodyScrolledToTop = (bodyRef.current?.scrollTop || 0) <= 0;
|
|
1703
|
+
const shouldClose = isSwipeDown && isSwipeGesture && (isDraggingFromHandle.current || bodyScrolledToTop);
|
|
1704
|
+
if (shouldClose) {
|
|
1496
1705
|
handleClose();
|
|
1497
1706
|
}
|
|
1707
|
+
resetTouchState();
|
|
1708
|
+
};
|
|
1709
|
+
const resetTouchState = () => {
|
|
1498
1710
|
setTouchStart(null);
|
|
1499
1711
|
setTouchEnd(null);
|
|
1712
|
+
setTouchStartX(null);
|
|
1713
|
+
setIsSwipeGesture(false);
|
|
1714
|
+
isDraggingFromHandle.current = false;
|
|
1500
1715
|
};
|
|
1716
|
+
// Handle Android back button
|
|
1717
|
+
React.useEffect(() => {
|
|
1718
|
+
if (!isOpen || !isAndroid())
|
|
1719
|
+
return;
|
|
1720
|
+
const handlePopState = (e) => {
|
|
1721
|
+
e.preventDefault();
|
|
1722
|
+
handleClose();
|
|
1723
|
+
};
|
|
1724
|
+
// Push a dummy state so back button can be caught
|
|
1725
|
+
window.history.pushState({ bottomSheet: true }, "");
|
|
1726
|
+
window.addEventListener("popstate", handlePopState);
|
|
1727
|
+
return () => {
|
|
1728
|
+
window.removeEventListener("popstate", handlePopState);
|
|
1729
|
+
// Clean up the dummy state if still present
|
|
1730
|
+
if (window.history.state?.bottomSheet) {
|
|
1731
|
+
window.history.back();
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
}, [isOpen, handleClose]);
|
|
1501
1735
|
// Don't render if not open and not closing
|
|
1502
1736
|
if (!isOpen && !isClosing)
|
|
1503
1737
|
return null;
|
|
1504
1738
|
const hasHeader = title || description;
|
|
1505
|
-
|
|
1739
|
+
// Calculate dynamic max height based on keyboard
|
|
1740
|
+
const dynamicMaxHeight = keyboardHeight > 0
|
|
1741
|
+
? `calc(${viewportHeight}px - ${safeAreaBottom}px - 20px)`
|
|
1742
|
+
: 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
|
|
1747
|
+
height: keyboardHeight > 0 ? `${viewportHeight}px` : "100%",
|
|
1748
|
+
}, 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: {
|
|
1751
|
+
maxHeight: dynamicMaxHeight,
|
|
1752
|
+
// Add safe area padding at the bottom for devices with home indicator
|
|
1753
|
+
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" }) })), 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 overflow-y-auto overscroll-contain",
|
|
1755
|
+
// Smooth scrolling and momentum scroll for iOS
|
|
1756
|
+
"-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
|
+
// Add safe area padding to footer
|
|
1758
|
+
paddingBottom: `max(env(safe-area-inset-bottom, 0px), ${safeAreaBottom}px)`,
|
|
1759
|
+
}, 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 })] }))] })] }));
|
|
1506
1760
|
});
|
|
1507
1761
|
BottomSheet.displayName = "BottomSheet";
|
|
1508
1762
|
|
|
@@ -4613,8 +4867,10 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
|
|
|
4613
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
|
|
4614
4868
|
? true
|
|
4615
4869
|
: false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"), maxHeight: `${position.maxHeight}px`, unstyled: isMobile }));
|
|
4870
|
+
// Search field component for mobile BottomSheet
|
|
4871
|
+
const mobileSearchField = (jsx(TextField, { value: searchValue, onChange: handleSearchChange, onKeyDown: handleKeyDown, containerClassName: "mb-0", placeholder: textFieldProps.placeholder || "Search...", autoFocus: true, ...textFieldProps }));
|
|
4616
4872
|
// Mobile: BottomSheet, Desktop: Regular Dropdown
|
|
4617
|
-
const dropdownMenu = showDropdown && (isMobile ? (
|
|
4873
|
+
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: {
|
|
4618
4874
|
position: "fixed",
|
|
4619
4875
|
...(position.top !== undefined && { top: `${position.top}px` }),
|
|
4620
4876
|
...(position.bottom !== undefined && {
|