| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import React, { useMemo, useState } from "react"; |
| 2 | +import { createPortal } from "react-dom"; | |
| 2 | 3 | import { |
| 3 | 4 | Table as UITable, |
| 4 | 5 | TableBody, |
| 5 | 6 | TableCell, |
| @@ -146,8 +147,78 @@ | ||
| 146 | 147 | return () => document.removeEventListener("click", handleClickOutside); |
| 147 | 148 | } |
| 148 | 149 | }, [openDropdownId]); |
| 149 | 150 | |
| 151 | + const getVisibleActions = (item: any) => | |
| 152 | + actions.filter((a) => !a.condition || a.condition(item)); | |
| 153 | + | |
| 154 | + const openActionsMenu = ( | |
| 155 | + e: React.MouseEvent<HTMLButtonElement>, | |
| 156 | + item: any, | |
| 157 | + ) => { | |
| 158 | + e.preventDefault(); | |
| 159 | + e.stopPropagation(); | |
| 160 | + const itemId = getItemId(item); | |
| 161 | + const rect = e.currentTarget.getBoundingClientRect(); | |
| 162 | + const visibleActions = getVisibleActions(item); | |
| 163 | + const dropdownHeight = visibleActions.length * 40 + 16; | |
| 164 | + const spaceBelow = window.innerHeight - rect.bottom; | |
| 165 | + const spaceAbove = rect.top; | |
| 166 | + const shouldPositionAbove = | |
| 167 | + spaceBelow < dropdownHeight && spaceAbove > dropdownHeight; | |
| 168 | + | |
| 169 | + // position:fixed uses viewport coordinates; getBoundingClientRect is viewport-relative | |
| 170 | + setDropdownPosition({ | |
| 171 | + top: shouldPositionAbove | |
| 172 | + ? Math.max(8, rect.top - dropdownHeight) | |
| 173 | + : rect.bottom, | |
| 174 | + right: window.innerWidth - rect.right, | |
| 175 | + }); | |
| 176 | + setOpenDropdownId(openDropdownId === itemId ? null : itemId); | |
| 177 | + }; | |
| 178 | + | |
| 179 | + const renderActionsDropdown = (item: any) => { | |
| 180 | + const itemId = getItemId(item); | |
| 181 | + if (openDropdownId !== itemId || !dropdownPosition) return null; | |
| 182 | + | |
| 183 | + return createPortal( | |
| 184 | + <div | |
| 185 | + className="fixed min-w-[180px] w-max bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-md py-1" | |
| 186 | + style={{ | |
| 187 | + top: `${dropdownPosition.top}px`, | |
| 188 | + right: `${dropdownPosition.right}px`, | |
| 189 | + zIndex: 999999, | |
| 190 | + }} | |
| 191 | + data-dropdown-content | |
| 192 | + onClick={(e) => e.stopPropagation()} | |
| 193 | + > | |
| 194 | + {getVisibleActions(item).map((action) => ( | |
| 195 | + <button | |
| 196 | + key={action.key} | |
| 197 | + type="button" | |
| 198 | + onClick={(e) => { | |
| 199 | + e.preventDefault(); | |
| 200 | + e.stopPropagation(); | |
| 201 | + action.onClick(item); | |
| 202 | + setOpenDropdownId(null); | |
| 203 | + }} | |
| 204 | + className={`w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors cursor-pointer whitespace-nowrap ${ | |
| 205 | + action.variant === "destructive" | |
| 206 | + ? "text-red-600 dark:text-red-400" | |
| 207 | + : action.variant === "outline" | |
| 208 | + ? "text-blue-600 dark:text-blue-400" | |
| 209 | + : "text-gray-700 dark:text-gray-300" | |
| 210 | + }`} | |
| 211 | + > | |
| 212 | + {action.icon} | |
| 213 | + {action.label} | |
| 214 | + </button> | |
| 215 | + ))} | |
| 216 | + </div>, | |
| 217 | + document.body, | |
| 218 | + ); | |
| 219 | + }; | |
| 220 | + | |
| 150 | 221 | // Render skeleton loading state |
| 151 | 222 | const renderSkeleton = () => ( |
| 152 | 223 | <UITable> |
| 153 | 224 | <TableHeader> |
| @@ -498,38 +569,9 @@ | ||
| 498 | 569 | <div className="relative inline-block"> |
| 499 | 570 | <Button |
| 500 | 571 | variant="ghost" |
| 501 | 572 | size="icon" |
| 502 | - onClick={(e) => { | |
| 503 | - e.preventDefault(); | |
| 504 | - e.stopPropagation(); | |
| 505 | - const button = e.currentTarget; | |
| 506 | - const rect = button.getBoundingClientRect(); | |
| 507 | - | |
| 508 | - // Calculate dropdown height (estimate based on number of actions) | |
| 509 | - const dropdownHeight = | |
| 510 | - actions.filter((a) => !a.condition || a.condition(item)) | |
| 511 | - .length * | |
| 512 | - 40 + | |
| 513 | - 16; | |
| 514 | - const spaceBelow = window.innerHeight - rect.bottom; | |
| 515 | - const spaceAbove = rect.top; | |
| 516 | - | |
| 517 | - // Position dropdown above if not enough space below | |
| 518 | - const shouldPositionAbove = | |
| 519 | - spaceBelow < dropdownHeight && | |
| 520 | - spaceAbove > dropdownHeight; | |
| 521 | - | |
| 522 | - setDropdownPosition({ | |
| 523 | - top: shouldPositionAbove | |
| 524 | - ? rect.top + window.scrollY - dropdownHeight | |
| 525 | - : rect.bottom + window.scrollY, | |
| 526 | - right: window.innerWidth - rect.right + window.scrollX, | |
| 527 | - }); | |
| 528 | - setOpenDropdownId( | |
| 529 | - openDropdownId === itemId ? null : itemId, | |
| 530 | - ); | |
| 531 | - }} | |
| 573 | + onClick={(e) => openActionsMenu(e, item)} | |
| 532 | 574 | className="h-8 w-8 hover:bg-gray-100 dark:hover:bg-gray-700" |
| 533 | 575 | aria-label={__("More actions", "yatra")} |
| 534 | 576 | data-dropdown-trigger |
| 535 | 577 | > |
| @@ -534,48 +576,9 @@ | ||
| 534 | 576 | data-dropdown-trigger |
| 535 | 577 | > |
| 536 | 578 | <MoreVertical className="w-4 h-4" /> |
| 537 | 579 | </Button> |
| 538 | - | |
| 539 | - {openDropdownId === itemId && dropdownPosition && ( | |
| 540 | - <div | |
| 541 | - className="fixed min-w-[180px] w-max bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-2xl py-1" | |
| 542 | - style={{ | |
| 543 | - top: `${dropdownPosition.top}px`, | |
| 544 | - right: `${dropdownPosition.right}px`, | |
| 545 | - zIndex: 999999, | |
| 546 | - }} | |
| 547 | - data-dropdown-content | |
| 548 | - onClick={(e) => e.stopPropagation()} | |
| 549 | - > | |
| 550 | - {actions | |
| 551 | - .filter( | |
| 552 | - (action) => !action.condition || action.condition(item), | |
| 553 | - ) | |
| 554 | - .map((action) => ( | |
| 555 | - <button | |
| 556 | - key={action.key} | |
| 557 | - type="button" | |
| 558 | - onClick={(e) => { | |
| 559 | - e.preventDefault(); | |
| 560 | - e.stopPropagation(); | |
| 561 | - action.onClick(item); | |
| 562 | - setOpenDropdownId(null); | |
| 563 | - }} | |
| 564 | - className={`w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors cursor-pointer whitespace-nowrap ${ | |
| 565 | - action.variant === "destructive" | |
| 566 | - ? "text-red-600 dark:text-red-400" | |
| 567 | - : action.variant === "outline" | |
| 568 | - ? "text-blue-600 dark:text-blue-400" | |
| 569 | - : "text-gray-700 dark:text-gray-300" | |
| 570 | - }`} | |
| 571 | - > | |
| 572 | - {action.icon} | |
| 573 | - {action.label} | |
| 574 | - </button> | |
| 575 | - ))} | |
| 576 | - </div> | |
| 577 | - )} | |
| 580 | + {renderActionsDropdown(item)} | |
| 578 | 581 | </div> |
| 579 | 582 | </TableCell> |
| 580 | 583 | )} |
| 581 | 584 | </TableRow> |
| @@ -631,35 +634,9 @@ | ||
| 631 | 634 | <div className="relative inline-block"> |
| 632 | 635 | <Button |
| 633 | 636 | variant="ghost" |
| 634 | 637 | size="icon" |
| 635 | - onClick={(e) => { | |
| 636 | - e.preventDefault(); | |
| 637 | - e.stopPropagation(); | |
| 638 | - const button = e.currentTarget; | |
| 639 | - const rect = button.getBoundingClientRect(); | |
| 640 | - | |
| 641 | - // Calculate dropdown height (estimate based on number of actions) | |
| 642 | - const dropdownHeight = | |
| 643 | - actions.filter((a) => !a.condition || a.condition(item)) | |
| 644 | - .length * | |
| 645 | - 40 + | |
| 646 | - 16; | |
| 647 | - const spaceBelow = window.innerHeight - rect.bottom; | |
| 648 | - const spaceAbove = rect.top; | |
| 649 | - | |
| 650 | - // Position dropdown above if not enough space below | |
| 651 | - const shouldPositionAbove = | |
| 652 | - spaceBelow < dropdownHeight && spaceAbove > dropdownHeight; | |
| 653 | - | |
| 654 | - setDropdownPosition({ | |
| 655 | - top: shouldPositionAbove | |
| 656 | - ? rect.top + window.scrollY - dropdownHeight | |
| 657 | - : rect.bottom + window.scrollY, | |
| 658 | - right: window.innerWidth - rect.right + window.scrollX, | |
| 659 | - }); | |
| 660 | - setOpenDropdownId(openDropdownId === itemId ? null : itemId); | |
| 661 | - }} | |
| 638 | + onClick={(e) => openActionsMenu(e, item)} | |
| 662 | 639 | className="h-8 w-8 hover:bg-gray-100 dark:hover:bg-gray-700" |
| 663 | 640 | aria-label={__("More actions", "yatra")} |
| 664 | 641 | data-dropdown-trigger |
| 665 | 642 | > |
| @@ -664,48 +641,9 @@ | ||
| 664 | 641 | data-dropdown-trigger |
| 665 | 642 | > |
| 666 | 643 | <MoreVertical className="w-4 h-4" /> |
| 667 | 644 | </Button> |
| 668 | - | |
| 669 | - {openDropdownId === itemId && dropdownPosition && ( | |
| 670 | - <div | |
| 671 | - className="fixed min-w-[180px] w-max bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-2xl py-1" | |
| 672 | - style={{ | |
| 673 | - top: `${dropdownPosition.top}px`, | |
| 674 | - right: `${dropdownPosition.right}px`, | |
| 675 | - zIndex: 999999, | |
| 676 | - }} | |
| 677 | - data-dropdown-content | |
| 678 | - onClick={(e) => e.stopPropagation()} | |
| 679 | - > | |
| 680 | - {actions | |
| 681 | - .filter( | |
| 682 | - (action) => !action.condition || action.condition(item), | |
| 683 | - ) | |
| 684 | - .map((action) => ( | |
| 685 | - <button | |
| 686 | - key={action.key} | |
| 687 | - type="button" | |
| 688 | - onClick={(e) => { | |
| 689 | - e.preventDefault(); | |
| 690 | - e.stopPropagation(); | |
| 691 | - action.onClick(item); | |
| 692 | - setOpenDropdownId(null); | |
| 693 | - }} | |
| 694 | - className={`w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors cursor-pointer whitespace-nowrap ${ | |
| 695 | - action.variant === "destructive" | |
| 696 | - ? "text-red-600 dark:text-red-400" | |
| 697 | - : action.variant === "outline" | |
| 698 | - ? "text-blue-600 dark:text-blue-400" | |
| 699 | - : "text-gray-700 dark:text-gray-300" | |
| 700 | - }`} | |
| 701 | - > | |
| 702 | - {action.icon} | |
| 703 | - {action.label} | |
| 704 | - </button> | |
| 705 | - ))} | |
| 706 | - </div> | |
| 707 | - )} | |
| 645 | + {renderActionsDropdown(item)} | |
| 708 | 646 | </div> |
| 709 | 647 | </TableCell> |
| 710 | 648 | )} |
| 711 | 649 | </TableRow> |