@natoe/colab 0.1.4 → 0.1.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.d.mts CHANGED
@@ -734,17 +734,22 @@ interface MessageActionsMenuProps {
734
734
  onPin: (messageId: string) => void;
735
735
  onUnpin: (messageId: string) => void;
736
736
  onCopy?: (text: string) => void;
737
- /** True when the menu should show (hover / open state managed by parent) */
738
- visible: boolean;
739
737
  /** Whether we've hit the pin limit — if true, pin is disabled */
740
738
  pinDisabled?: boolean;
739
+ /** Anchor the popover to the left of the trigger (use for own bubbles) */
740
+ anchorRight?: boolean;
741
741
  className?: string;
742
742
  }
743
743
  /**
744
- * Inline action menu for a single message.
745
- * Parent controls visibility (typically via hover state in MessageBubble).
744
+ * Self-contained "⋯" trigger + popover for per-message actions. Replaces
745
+ * the previous hover-only floating menu so touch users (and anyone with
746
+ * a non-pointer device) can reach Reply / Pin / Copy.
747
+ *
748
+ * Menu rows are icon + text so the affordance reads cold; the trigger is
749
+ * a 36px tap target. Closes on outside-click, Escape, or after selecting
750
+ * an action.
746
751
  */
747
- declare function MessageActionsMenu({ message, onReply, onPin, onUnpin, onCopy, visible, pinDisabled, className, }: MessageActionsMenuProps): react_jsx_runtime.JSX.Element | null;
752
+ declare function MessageActionsMenu({ message, onReply, onPin, onUnpin, onCopy, pinDisabled, anchorRight, className, }: MessageActionsMenuProps): react_jsx_runtime.JSX.Element;
748
753
 
749
754
  interface ReplyPreviewProps {
750
755
  /** The message being replied to */
package/dist/index.d.ts CHANGED
@@ -734,17 +734,22 @@ interface MessageActionsMenuProps {
734
734
  onPin: (messageId: string) => void;
735
735
  onUnpin: (messageId: string) => void;
736
736
  onCopy?: (text: string) => void;
737
- /** True when the menu should show (hover / open state managed by parent) */
738
- visible: boolean;
739
737
  /** Whether we've hit the pin limit — if true, pin is disabled */
740
738
  pinDisabled?: boolean;
739
+ /** Anchor the popover to the left of the trigger (use for own bubbles) */
740
+ anchorRight?: boolean;
741
741
  className?: string;
742
742
  }
743
743
  /**
744
- * Inline action menu for a single message.
745
- * Parent controls visibility (typically via hover state in MessageBubble).
744
+ * Self-contained "⋯" trigger + popover for per-message actions. Replaces
745
+ * the previous hover-only floating menu so touch users (and anyone with
746
+ * a non-pointer device) can reach Reply / Pin / Copy.
747
+ *
748
+ * Menu rows are icon + text so the affordance reads cold; the trigger is
749
+ * a 36px tap target. Closes on outside-click, Escape, or after selecting
750
+ * an action.
746
751
  */
747
- declare function MessageActionsMenu({ message, onReply, onPin, onUnpin, onCopy, visible, pinDisabled, className, }: MessageActionsMenuProps): react_jsx_runtime.JSX.Element | null;
752
+ declare function MessageActionsMenu({ message, onReply, onPin, onUnpin, onCopy, pinDisabled, anchorRight, className, }: MessageActionsMenuProps): react_jsx_runtime.JSX.Element;
748
753
 
749
754
  interface ReplyPreviewProps {
750
755
  /** The message being replied to */
package/dist/index.js CHANGED
@@ -1379,91 +1379,247 @@ var styles3 = {
1379
1379
  textDecoration: "underline"
1380
1380
  }
1381
1381
  };
1382
+ function ReplyIcon({ size = 18, color = "currentColor" }) {
1383
+ return /* @__PURE__ */ jsxRuntime.jsx(
1384
+ "svg",
1385
+ {
1386
+ xmlns: "http://www.w3.org/2000/svg",
1387
+ width: size,
1388
+ height: size,
1389
+ viewBox: "0 0 24 24",
1390
+ fill: color,
1391
+ "aria-hidden": "true",
1392
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z" })
1393
+ }
1394
+ );
1395
+ }
1396
+ function PinIcon({ size = 14, color = "currentColor" }) {
1397
+ return /* @__PURE__ */ jsxRuntime.jsx(
1398
+ "svg",
1399
+ {
1400
+ xmlns: "http://www.w3.org/2000/svg",
1401
+ width: size,
1402
+ height: size,
1403
+ viewBox: "0 0 24 24",
1404
+ fill: color,
1405
+ "aria-hidden": "true",
1406
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z" })
1407
+ }
1408
+ );
1409
+ }
1410
+ function CopyIcon({ size = 18, color = "currentColor" }) {
1411
+ return /* @__PURE__ */ jsxRuntime.jsx(
1412
+ "svg",
1413
+ {
1414
+ xmlns: "http://www.w3.org/2000/svg",
1415
+ width: size,
1416
+ height: size,
1417
+ viewBox: "0 0 24 24",
1418
+ fill: color,
1419
+ "aria-hidden": "true",
1420
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" })
1421
+ }
1422
+ );
1423
+ }
1424
+ function MoreIcon({ size = 18, color = "currentColor" }) {
1425
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1426
+ "svg",
1427
+ {
1428
+ xmlns: "http://www.w3.org/2000/svg",
1429
+ width: size,
1430
+ height: size,
1431
+ viewBox: "0 0 24 24",
1432
+ fill: color,
1433
+ "aria-hidden": "true",
1434
+ children: [
1435
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "5", r: "2" }),
1436
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "2" }),
1437
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "19", r: "2" })
1438
+ ]
1439
+ }
1440
+ );
1441
+ }
1382
1442
  function MessageActionsMenu({
1383
1443
  message,
1384
1444
  onReply,
1385
1445
  onPin,
1386
1446
  onUnpin,
1387
1447
  onCopy,
1388
- visible,
1389
1448
  pinDisabled = false,
1449
+ anchorRight = false,
1390
1450
  className
1391
1451
  }) {
1392
- if (!visible) return null;
1452
+ const [isOpen, setIsOpen] = React4.useState(false);
1453
+ const wrapperRef = React4.useRef(null);
1454
+ const triggerRef = React4.useRef(null);
1393
1455
  const isPinned = !!message.isPinned;
1394
1456
  const canCopy = message.type === "text" && !!message.body;
1395
- const handleCopy = () => {
1396
- if (!onCopy) return;
1397
- onCopy(message.body);
1457
+ React4.useEffect(() => {
1458
+ if (!isOpen) return;
1459
+ const handleClick = (e) => {
1460
+ if (!wrapperRef.current?.contains(e.target)) {
1461
+ setIsOpen(false);
1462
+ }
1463
+ };
1464
+ const handleKey = (e) => {
1465
+ if (e.key === "Escape") {
1466
+ setIsOpen(false);
1467
+ triggerRef.current?.focus();
1468
+ }
1469
+ };
1470
+ document.addEventListener("mousedown", handleClick);
1471
+ document.addEventListener("keydown", handleKey);
1472
+ return () => {
1473
+ document.removeEventListener("mousedown", handleClick);
1474
+ document.removeEventListener("keydown", handleKey);
1475
+ };
1476
+ }, [isOpen]);
1477
+ const run = (fn) => {
1478
+ setIsOpen(false);
1479
+ fn();
1398
1480
  };
1399
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles4.container, children: [
1481
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrapperRef, className, style: styles4.wrapper, children: [
1400
1482
  /* @__PURE__ */ jsxRuntime.jsx(
1401
1483
  "button",
1402
1484
  {
1403
- onClick: () => onReply(message),
1404
- style: styles4.button,
1405
- title: "Reply",
1485
+ ref: triggerRef,
1406
1486
  type: "button",
1407
- children: "\u21A9"
1487
+ onClick: (e) => {
1488
+ e.stopPropagation();
1489
+ setIsOpen((v) => !v);
1490
+ },
1491
+ "aria-label": "Message actions",
1492
+ "aria-haspopup": "menu",
1493
+ "aria-expanded": isOpen ? "true" : "false",
1494
+ title: "Message actions",
1495
+ style: styles4.trigger,
1496
+ children: /* @__PURE__ */ jsxRuntime.jsx(MoreIcon, { size: 18, color: "#4b5563" })
1408
1497
  }
1409
1498
  ),
1410
- isPinned ? /* @__PURE__ */ jsxRuntime.jsx(
1411
- "button",
1412
- {
1413
- onClick: () => onUnpin(message.id),
1414
- style: styles4.button,
1415
- title: "Unpin",
1416
- type: "button",
1417
- children: "\u{1F4CC}"
1418
- }
1419
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1420
- "button",
1499
+ isOpen && /* @__PURE__ */ jsxRuntime.jsxs(
1500
+ "div",
1421
1501
  {
1422
- onClick: () => onPin(message.id),
1502
+ role: "menu",
1503
+ "aria-label": "Message actions",
1423
1504
  style: {
1424
- ...styles4.button,
1425
- opacity: pinDisabled ? 0.4 : 1,
1426
- cursor: pinDisabled ? "not-allowed" : "pointer"
1505
+ ...styles4.menu,
1506
+ ...anchorRight ? styles4.menuRight : styles4.menuLeft
1427
1507
  },
1428
- disabled: pinDisabled,
1429
- title: pinDisabled ? "Pin limit reached (max 3)" : "Pin",
1430
- type: "button",
1431
- children: "\u{1F4CC}"
1432
- }
1433
- ),
1434
- canCopy && onCopy && /* @__PURE__ */ jsxRuntime.jsx(
1435
- "button",
1436
- {
1437
- onClick: handleCopy,
1438
- style: styles4.button,
1439
- title: "Copy text",
1440
- type: "button",
1441
- children: "\u2398"
1508
+ children: [
1509
+ /* @__PURE__ */ jsxRuntime.jsxs(
1510
+ "button",
1511
+ {
1512
+ type: "button",
1513
+ role: "menuitem",
1514
+ onClick: () => run(() => onReply(message)),
1515
+ style: styles4.item,
1516
+ children: [
1517
+ /* @__PURE__ */ jsxRuntime.jsx(ReplyIcon, { size: 18, color: "#374151" }),
1518
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Reply" })
1519
+ ]
1520
+ }
1521
+ ),
1522
+ isPinned ? /* @__PURE__ */ jsxRuntime.jsxs(
1523
+ "button",
1524
+ {
1525
+ type: "button",
1526
+ role: "menuitem",
1527
+ onClick: () => run(() => onUnpin(message.id)),
1528
+ style: styles4.item,
1529
+ children: [
1530
+ /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1531
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Unpin" })
1532
+ ]
1533
+ }
1534
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
1535
+ "button",
1536
+ {
1537
+ type: "button",
1538
+ role: "menuitem",
1539
+ onClick: () => run(() => onPin(message.id)),
1540
+ disabled: pinDisabled,
1541
+ style: {
1542
+ ...styles4.item,
1543
+ ...pinDisabled ? styles4.itemDisabled : {}
1544
+ },
1545
+ title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1546
+ children: [
1547
+ /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1548
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1549
+ ]
1550
+ }
1551
+ ),
1552
+ canCopy && onCopy && /* @__PURE__ */ jsxRuntime.jsxs(
1553
+ "button",
1554
+ {
1555
+ type: "button",
1556
+ role: "menuitem",
1557
+ onClick: () => run(() => onCopy(message.body)),
1558
+ style: styles4.item,
1559
+ children: [
1560
+ /* @__PURE__ */ jsxRuntime.jsx(CopyIcon, { size: 16, color: "#374151" }),
1561
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Copy text" })
1562
+ ]
1563
+ }
1564
+ )
1565
+ ]
1442
1566
  }
1443
1567
  )
1444
1568
  ] });
1445
1569
  }
1446
1570
  var styles4 = {
1447
- container: {
1571
+ wrapper: {
1572
+ position: "relative",
1573
+ display: "inline-block"
1574
+ },
1575
+ trigger: {
1576
+ width: "36px",
1577
+ height: "36px",
1448
1578
  display: "flex",
1449
- gap: "2px",
1450
- padding: "2px",
1579
+ alignItems: "center",
1580
+ justifyContent: "center",
1581
+ backgroundColor: "transparent",
1582
+ border: "none",
1583
+ borderRadius: "50%",
1584
+ cursor: "pointer",
1585
+ color: "#4b5563"
1586
+ },
1587
+ menu: {
1588
+ position: "absolute",
1589
+ top: "100%",
1590
+ marginTop: "4px",
1591
+ minWidth: "160px",
1451
1592
  backgroundColor: "#ffffff",
1452
1593
  border: "1px solid #e5e7eb",
1453
- borderRadius: "20px",
1454
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.08)"
1594
+ borderRadius: "10px",
1595
+ boxShadow: "0 6px 18px rgba(0, 0, 0, 0.12)",
1596
+ padding: "4px",
1597
+ zIndex: 20
1455
1598
  },
1456
- button: {
1457
- width: "24px",
1458
- height: "24px",
1599
+ menuLeft: {
1600
+ left: 0
1601
+ },
1602
+ menuRight: {
1603
+ right: 0
1604
+ },
1605
+ item: {
1459
1606
  display: "flex",
1460
1607
  alignItems: "center",
1461
- justifyContent: "center",
1462
- fontSize: "12px",
1608
+ gap: "10px",
1609
+ width: "100%",
1610
+ minHeight: "40px",
1611
+ padding: "8px 12px",
1612
+ fontSize: "14px",
1613
+ color: "#111827",
1463
1614
  backgroundColor: "transparent",
1464
1615
  border: "none",
1465
- borderRadius: "50%",
1466
- cursor: "pointer"
1616
+ borderRadius: "6px",
1617
+ cursor: "pointer",
1618
+ textAlign: "left"
1619
+ },
1620
+ itemDisabled: {
1621
+ color: "#9ca3af",
1622
+ cursor: "not-allowed"
1467
1623
  }
1468
1624
  };
1469
1625
  function FileIcon({ size = 18, color = "currentColor" }) {
@@ -1480,20 +1636,6 @@ function FileIcon({ size = 18, color = "currentColor" }) {
1480
1636
  }
1481
1637
  );
1482
1638
  }
1483
- function PinIcon({ size = 14, color = "currentColor" }) {
1484
- return /* @__PURE__ */ jsxRuntime.jsx(
1485
- "svg",
1486
- {
1487
- xmlns: "http://www.w3.org/2000/svg",
1488
- width: size,
1489
- height: size,
1490
- viewBox: "0 0 24 24",
1491
- fill: color,
1492
- "aria-hidden": "true",
1493
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z" })
1494
- }
1495
- );
1496
- }
1497
1639
  function AlertIcon({ size = 14, color = "currentColor" }) {
1498
1640
  return /* @__PURE__ */ jsxRuntime.jsx(
1499
1641
  "svg",
@@ -1558,12 +1700,26 @@ function MessageBubble({
1558
1700
  pinDisabled = false,
1559
1701
  className
1560
1702
  }) {
1561
- const [isHovered, setIsHovered] = React4.useState(false);
1562
1703
  if (message.type === "system") {
1563
1704
  return /* @__PURE__ */ jsxRuntime.jsx(SystemBubble, { message });
1564
1705
  }
1565
1706
  const hasActions = !!(onReply || onPin || onUnpin || onCopy);
1566
- return /* @__PURE__ */ jsxRuntime.jsx(
1707
+ const actionsSlot = hasActions ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles5.actionsSlot, children: /* @__PURE__ */ jsxRuntime.jsx(
1708
+ MessageActionsMenu,
1709
+ {
1710
+ message,
1711
+ onReply: onReply ?? (() => {
1712
+ }),
1713
+ onPin: onPin ?? (() => {
1714
+ }),
1715
+ onUnpin: onUnpin ?? (() => {
1716
+ }),
1717
+ onCopy,
1718
+ pinDisabled,
1719
+ anchorRight: !isOwn
1720
+ }
1721
+ ) }) : null;
1722
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1567
1723
  "div",
1568
1724
  {
1569
1725
  className,
@@ -1571,10 +1727,9 @@ function MessageBubble({
1571
1727
  ...styles5.wrapper,
1572
1728
  justifyContent: isOwn ? "flex-end" : "flex-start"
1573
1729
  },
1574
- onMouseEnter: () => setIsHovered(true),
1575
- onMouseLeave: () => setIsHovered(false),
1576
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles5.bubbleWrapper, children: [
1577
- /* @__PURE__ */ jsxRuntime.jsxs(
1730
+ children: [
1731
+ isOwn && actionsSlot,
1732
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles5.bubbleWrapper, children: /* @__PURE__ */ jsxRuntime.jsxs(
1578
1733
  "div",
1579
1734
  {
1580
1735
  style: {
@@ -1633,33 +1788,9 @@ function MessageBubble({
1633
1788
  )
1634
1789
  ]
1635
1790
  }
1636
- ),
1637
- hasActions && /* @__PURE__ */ jsxRuntime.jsx(
1638
- "div",
1639
- {
1640
- style: {
1641
- ...styles5.actionsWrapper,
1642
- [isOwn ? "right" : "left"]: "100%",
1643
- [isOwn ? "marginRight" : "marginLeft"]: "6px"
1644
- },
1645
- children: /* @__PURE__ */ jsxRuntime.jsx(
1646
- MessageActionsMenu,
1647
- {
1648
- message,
1649
- onReply: onReply ?? (() => {
1650
- }),
1651
- onPin: onPin ?? (() => {
1652
- }),
1653
- onUnpin: onUnpin ?? (() => {
1654
- }),
1655
- onCopy,
1656
- visible: isHovered,
1657
- pinDisabled
1658
- }
1659
- )
1660
- }
1661
- )
1662
- ] })
1791
+ ) }),
1792
+ !isOwn && actionsSlot
1793
+ ]
1663
1794
  }
1664
1795
  );
1665
1796
  }
@@ -1756,6 +1887,8 @@ function escapeRegex2(string) {
1756
1887
  var styles5 = {
1757
1888
  wrapper: {
1758
1889
  display: "flex",
1890
+ alignItems: "flex-end",
1891
+ gap: "4px",
1759
1892
  marginBottom: "8px",
1760
1893
  paddingLeft: "16px",
1761
1894
  paddingRight: "16px"
@@ -1764,6 +1897,10 @@ var styles5 = {
1764
1897
  position: "relative",
1765
1898
  maxWidth: "75%"
1766
1899
  },
1900
+ actionsSlot: {
1901
+ flexShrink: 0,
1902
+ alignSelf: "flex-end"
1903
+ },
1767
1904
  bubble: {
1768
1905
  padding: "10px 14px",
1769
1906
  borderRadius: "12px",
@@ -1839,12 +1976,6 @@ var styles5 = {
1839
1976
  cursor: "pointer",
1840
1977
  padding: "0 2px"
1841
1978
  },
1842
- actionsWrapper: {
1843
- position: "absolute",
1844
- top: "50%",
1845
- transform: "translateY(-50%)",
1846
- zIndex: 10
1847
- },
1848
1979
  audioContainer: {
1849
1980
  display: "flex",
1850
1981
  alignItems: "center",
@@ -2494,7 +2625,18 @@ function MessageInput({
2494
2625
  type: "file",
2495
2626
  onChange: handleFileSelect,
2496
2627
  style: { display: "none" },
2497
- accept: [...SUPPORTED_IMAGE_TYPES, "application/pdf", ".doc", ".docx"].join(",")
2628
+ accept: [
2629
+ ...SUPPORTED_IMAGE_TYPES,
2630
+ "application/pdf",
2631
+ ".doc",
2632
+ ".docx",
2633
+ ".xls",
2634
+ ".xlsx",
2635
+ ".csv",
2636
+ ".txt",
2637
+ ".heic",
2638
+ ".heif"
2639
+ ].join(",")
2498
2640
  }
2499
2641
  ),
2500
2642
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3506,6 +3648,7 @@ function ExpandIcon({ size = 18, color = "currentColor" }) {
3506
3648
  }
3507
3649
  );
3508
3650
  }
3651
+ var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3509
3652
  function CollabPopup({
3510
3653
  orderId,
3511
3654
  patientData,
@@ -3527,6 +3670,8 @@ function CollabPopup({
3527
3670
  const [isMinimized, setIsMinimized] = React4.useState(false);
3528
3671
  const dragOffset = React4.useRef({ x: 0, y: 0 });
3529
3672
  const containerRef = React4.useRef(null);
3673
+ const previouslyFocused = React4.useRef(null);
3674
+ const titleId = React4.useId();
3530
3675
  const handleMouseDown = React4.useCallback(
3531
3676
  (e) => {
3532
3677
  if (!e.target.closest("[data-drag-handle]")) return;
@@ -3541,13 +3686,44 @@ function CollabPopup({
3541
3686
  );
3542
3687
  React4.useEffect(() => {
3543
3688
  if (!isOpen) return;
3689
+ previouslyFocused.current = document.activeElement;
3690
+ const focusFirst = () => {
3691
+ const root = containerRef.current;
3692
+ if (!root) return;
3693
+ const first = root.querySelector(FOCUSABLE_SELECTOR);
3694
+ (first ?? root).focus();
3695
+ };
3696
+ const raf = requestAnimationFrame(focusFirst);
3544
3697
  const handleKey = (e) => {
3545
3698
  if (e.key === "Escape") {
3546
3699
  onClose();
3700
+ return;
3701
+ }
3702
+ if (e.key !== "Tab") return;
3703
+ const root = containerRef.current;
3704
+ if (!root) return;
3705
+ const focusables = Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR));
3706
+ if (focusables.length === 0) {
3707
+ e.preventDefault();
3708
+ return;
3709
+ }
3710
+ const first = focusables[0];
3711
+ const last = focusables[focusables.length - 1];
3712
+ const active = document.activeElement;
3713
+ if (e.shiftKey && active === first) {
3714
+ e.preventDefault();
3715
+ last.focus();
3716
+ } else if (!e.shiftKey && active === last) {
3717
+ e.preventDefault();
3718
+ first.focus();
3547
3719
  }
3548
3720
  };
3549
3721
  window.addEventListener("keydown", handleKey);
3550
- return () => window.removeEventListener("keydown", handleKey);
3722
+ return () => {
3723
+ cancelAnimationFrame(raf);
3724
+ window.removeEventListener("keydown", handleKey);
3725
+ previouslyFocused.current?.focus?.();
3726
+ };
3551
3727
  }, [isOpen, onClose]);
3552
3728
  React4.useEffect(() => {
3553
3729
  if (!isDragging) return;
@@ -3573,6 +3749,10 @@ function CollabPopup({
3573
3749
  {
3574
3750
  ref: containerRef,
3575
3751
  className,
3752
+ role: "dialog",
3753
+ "aria-modal": "true",
3754
+ "aria-labelledby": titleId,
3755
+ tabIndex: -1,
3576
3756
  style: {
3577
3757
  ...styles13.container,
3578
3758
  left: position.x,
@@ -3584,7 +3764,7 @@ function CollabPopup({
3584
3764
  onMouseDown: handleMouseDown,
3585
3765
  children: [
3586
3766
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-drag-handle": true, style: styles13.titleBar, children: [
3587
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.titleText, children: buildChannelName(patientData) }),
3767
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: titleId, style: styles13.titleText, children: buildChannelName(patientData) }),
3588
3768
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleActions, children: [
3589
3769
  /* @__PURE__ */ jsxRuntime.jsx(
3590
3770
  "button",