@mirror-physics/fractal-ui 0.2.0-next.84 → 0.2.0-next.86

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
@@ -1427,6 +1427,9 @@ import * as React11 from "react";
1427
1427
  import { Close as Close2, Check as Check2, MagnifyingGlass as MagnifyingGlass2, ChevronDown, ChevronLeft, ChevronRight as ChevronRight2, ChevronUp } from "@mirror-physics/fractal-icons";
1428
1428
 
1429
1429
  // src/components/Dropdown/position.ts
1430
+ function isDropdownTriggerVisible(trigger, bounds) {
1431
+ return trigger.right > bounds.left && trigger.left < bounds.right && trigger.bottom > bounds.top && trigger.top < bounds.bottom;
1432
+ }
1430
1433
  function positionDropdown(trigger, panel, bounds, direction, align) {
1431
1434
  const gap = 4;
1432
1435
  const width = Math.min(panel.width, Math.max(0, bounds.right - bounds.left));
@@ -1497,6 +1500,9 @@ function Dropdown({
1497
1500
  }) {
1498
1501
  const [query, setQuery] = React11.useState("");
1499
1502
  const searchRef = React11.useRef(null);
1503
+ const searchContainerRef = React11.useRef(null);
1504
+ const optionsRef = React11.useRef(null);
1505
+ const [optionsScrolled, setOptionsScrolled] = React11.useState(false);
1500
1506
  const accessibleLabel = triggerLabel ?? fieldLabel ?? "Options";
1501
1507
  const size = controlSize ?? sizeAlias;
1502
1508
  const canClear = clearable && !disabled && (multiple ? selectedValues.length > 0 && Boolean(onSelectionChange) : Boolean(selectedValue) && Boolean(onSelect));
@@ -1534,6 +1540,11 @@ function Dropdown({
1534
1540
  setOpen(false);
1535
1541
  triggerRef.current?.focus();
1536
1542
  }, open);
1543
+ React11.useLayoutEffect(() => {
1544
+ if (!open) return;
1545
+ if (optionsRef.current) optionsRef.current.scrollTop = 0;
1546
+ setOptionsScrolled(false);
1547
+ }, [open, query]);
1537
1548
  React11.useLayoutEffect(() => {
1538
1549
  const root = rootRef.current;
1539
1550
  const trigger = triggerRef.current;
@@ -1559,19 +1570,24 @@ function Dropdown({
1559
1570
  bounds.bottom = Math.min(bounds.bottom, rect.top + ancestor.clientTop + ancestor.clientHeight);
1560
1571
  }
1561
1572
  }
1573
+ const triggerRect = trigger.getBoundingClientRect();
1574
+ if (!isDropdownTriggerVisible(triggerRect, bounds)) {
1575
+ focusOnOpen.current = false;
1576
+ setOpen(false);
1577
+ return false;
1578
+ }
1562
1579
  const padding = Math.max(0, collisionPadding);
1563
1580
  bounds.left += padding;
1564
1581
  bounds.right = Math.max(bounds.left, bounds.right - padding);
1565
1582
  bounds.top += padding;
1566
1583
  bounds.bottom = Math.max(bounds.top, bounds.bottom - padding);
1567
- const triggerRect = trigger.getBoundingClientRect();
1568
1584
  const rootRect = root.getBoundingClientRect();
1569
1585
  const availableWidth = bounds.right - bounds.left;
1570
1586
  const heightLimit = Math.max(0, maxHeight);
1571
1587
  panel.style.maxWidth = `${availableWidth}px`;
1572
1588
  panel.style.minWidth = `${Math.min(triggerRect.width, availableWidth)}px`;
1573
1589
  const panelRect = panel.getBoundingClientRect();
1574
- const desiredHeight = Math.min(heightLimit, panel.scrollHeight + panel.offsetHeight - panel.clientHeight);
1590
+ const desiredHeight = Math.min(heightLimit, (optionsRef.current?.scrollHeight ?? 0) + (searchContainerRef.current?.offsetHeight ?? 0) + parseFloat(getComputedStyle(panel).paddingTop) + parseFloat(getComputedStyle(panel).paddingBottom) + panel.offsetHeight - panel.clientHeight);
1575
1591
  const position = positionDropdown(triggerRect, { width: panelRect.width, height: desiredHeight }, bounds, direction, align);
1576
1592
  const nextStyle = {
1577
1593
  left: position.left - rootRect.left - root.clientLeft + root.scrollLeft,
@@ -1584,8 +1600,9 @@ function Dropdown({
1584
1600
  panel.style[key] = `${nextStyle[key]}px`;
1585
1601
  }
1586
1602
  setPanelStyle((previous) => Object.entries(nextStyle).every(([key, value]) => previous[key] === value) ? previous : nextStyle);
1603
+ return true;
1587
1604
  };
1588
- updatePosition();
1605
+ if (!updatePosition()) return;
1589
1606
  if (searchable && !panel.contains(document.activeElement)) searchRef.current?.focus({ preventScroll: true });
1590
1607
  if (focusOnOpen.current) {
1591
1608
  focusOnOpen.current = false;
@@ -1596,6 +1613,7 @@ function Dropdown({
1596
1613
  const resize = new ResizeObserver(updatePosition);
1597
1614
  resize.observe(root);
1598
1615
  resize.observe(panel);
1616
+ if (optionsRef.current) resize.observe(optionsRef.current);
1599
1617
  for (let ancestor = root.parentElement; ancestor; ancestor = ancestor.parentElement) {
1600
1618
  resize.observe(ancestor);
1601
1619
  }
@@ -1695,11 +1713,11 @@ function Dropdown({
1695
1713
  className: cn("fractal-dropdown-panel", `fractal-dropdown-panel--${size}`, panelClassName),
1696
1714
  style: panelStyle,
1697
1715
  children: [
1698
- searchable && /* @__PURE__ */ jsxs17("div", { className: "fractal-dropdown-search", children: [
1716
+ searchable && /* @__PURE__ */ jsx19("div", { ref: searchContainerRef, className: "fractal-dropdown-search-container", "data-scrolled": optionsScrolled, children: /* @__PURE__ */ jsxs17("div", { className: "fractal-dropdown-search", children: [
1699
1717
  /* @__PURE__ */ jsx19(MagnifyingGlass2, { "aria-hidden": "true" }),
1700
1718
  /* @__PURE__ */ jsx19("input", { ref: searchRef, type: "search", "aria-label": searchPlaceholder, placeholder: searchPlaceholder, value: query, onChange: (event) => setQuery(event.target.value) })
1701
- ] }),
1702
- /* @__PURE__ */ jsxs17("div", { role: "listbox", id: `${menuId}-options`, "aria-label": accessibleLabel, "aria-multiselectable": multiple || void 0, children: [
1719
+ ] }) }),
1720
+ /* @__PURE__ */ jsxs17("div", { ref: optionsRef, className: "fractal-dropdown-options", onScroll: (event) => setOptionsScrolled(event.currentTarget.scrollTop > 0), role: "listbox", id: `${menuId}-options`, "aria-label": accessibleLabel, "aria-multiselectable": multiple || void 0, children: [
1703
1721
  label != null && /* @__PURE__ */ jsx19("div", { className: "fractal-dropdown-label", children: label }),
1704
1722
  visibleItems.map((item, index) => {
1705
1723
  const topDivider = item.border === "top" ? /* @__PURE__ */ jsx19("div", { className: "fractal-dropdown-separator" }) : null;
@@ -1739,9 +1757,9 @@ function Dropdown({
1739
1757
  ),
1740
1758
  bottomDivider
1741
1759
  ] }, item.value);
1742
- })
1743
- ] }),
1744
- !selectableItems.length && /* @__PURE__ */ jsx19("p", { role: "status", className: "fractal-dropdown-label", children: emptyMessage })
1760
+ }),
1761
+ !selectableItems.length && /* @__PURE__ */ jsx19("p", { role: "status", className: "fractal-dropdown-label", children: emptyMessage })
1762
+ ] })
1745
1763
  ]
1746
1764
  }
1747
1765
  )