Add right-click context menu to request tabs with MenuDropdown # (#6502)

* refactor: replace Dropdown with MenuDropdown in RequestTab component; update Dropdown props handling in Dropdown component

* refactor: remove Portal import and simplify menuDropdown rendering in RequestTab component

* refactor: streamline RequestTabMenu functionality and improve tab closing methods with async handling

* refactor: enhance Dropdown and MenuDropdown components with improved props handling and styling adjustments

* refactor: enhance Dropdown and MenuDropdown components by improving structure and removing unused styles

* refactor: update Dropdown and MenuDropdown components to append to sidebar sections container for improved layout

* refactor: integrate dropdownContainerRef for improved MenuDropdown positioning in RequestTabs and Sidebar components

* refactor: update Dropdown component to include 'tippy-box' class for e2e test selections

* refactor: update dropdown item selection logic in selectRequestPaneTab function for improved accuracy

* refactor: add fixed positioning to popperOptions in Collection and CollectionItem components for improved dropdown behavior

---------

Co-authored-by: sanjai <sanjai@usebruno.com>
This commit is contained in:
Abhishek S Lal
2025-12-24 21:08:53 +05:30
committed by GitHub
parent 1f05ffd469
commit 1b8eece173
10 changed files with 321 additions and 433 deletions

View File

@@ -1,153 +1,165 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.dropdown-toggle {
&:hover {
color: black;
min-width: 160px;
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.dropdown.color};
background-color: ${(props) => props.theme.dropdown.bg};
box-shadow: ${(props) => props.theme.shadow.sm};
border-radius: ${(props) => props.theme.border.radius.base};
max-height: 90vh;
overflow-y: auto;
max-width: unset !important;
padding: 0.25rem;
[role="menu"] {
outline: none;
&:focus {
outline: none;
}
&:focus-visible {
outline: none;
}
}
.tippy-box {
min-width: 160px;
font-size: ${(props) => props.theme.font.size.base};
.label-item {
display: flex;
align-items: center;
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.025em;
color: ${(props) => props.theme.dropdown.color};
background-color: ${(props) => props.theme.dropdown.bg};
box-shadow: ${(props) => props.theme.shadow.sm};
border-radius: ${(props) => props.theme.border.radius.base};
max-height: 90vh;
overflow-y: auto;
max-width: unset !important;
padding: 0.25rem;
opacity: 0.6;
margin-top: 0.25rem;
&:first-child {
margin-top: 0;
}
}
.tippy-content {
padding-left: 0;
padding-right: 0;
padding-top: 0;
padding-bottom: 0;
.dropdown-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.275rem 0.625rem;
cursor: pointer;
border-radius: 6px;
margin: 0.0625rem 0;
font-size: 0.8125rem;
[role="menu"] {
outline: none;
&:focus {
outline: none;
}
&:focus-visible {
outline: none;
}
}
.label-item {
display: flex;
align-items: center;
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.025em;
color: ${(props) => props.theme.dropdown.color};
opacity: 0.6;
margin-top: 0.25rem;
&:first-child {
margin-top: 0;
}
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.275rem 0.625rem;
cursor: pointer;
border-radius: 6px;
margin: 0.0625rem 0;
font-size: 0.8125rem;
&.active {
color: ${(props) => props.theme.colors.text.yellow} !important;
.dropdown-icon {
color: ${(props) => props.theme.colors.text.yellow} !important;
}
}
.dropdown-label {
flex: 1;
}
.dropdown-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.dropdown.iconColor};
opacity: 0.8;
}
.dropdown-right-section {
margin-left: auto;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
&:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.selected-focused:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus-visible:not(:disabled) {
outline: none;
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus:not(:focus-visible) {
outline: none;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.delete-item {
color: ${(props) => props.theme.colors.text.danger};
.dropdown-icon {
color: ${(props) => props.theme.colors.text.danger};
}
&:hover {
background-color: ${({ theme }) => {
const hex = theme.colors.text.danger.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
}} !important;
color: ${(props) => props.theme.colors.text.danger} !important;
}
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
margin-top: 0.25rem;
padding-top: 0.375rem;
}
&.dropdown-item-select {
padding-left: 1.5rem;
}
}
.dropdown-separator {
height: 1px;
background-color: ${(props) => props.theme.dropdown.separator};
margin: 0.25rem 0;
&.active {
color: ${(props) => props.theme.colors.text.yellow} !important;
.dropdown-icon {
color: ${(props) => props.theme.colors.text.yellow} !important;
}
}
.dropdown-label {
flex: 1;
}
.dropdown-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.dropdown.iconColor};
opacity: 0.8;
}
.dropdown-right-section {
margin-left: auto;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
&:hover:not(:disabled):not(.disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.selected-focused:not(:disabled):not(.disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus-visible:not(:disabled):not(.disabled) {
outline: none;
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus:not(:focus-visible) {
outline: none;
}
&:disabled,
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.delete-item {
color: ${(props) => props.theme.colors.text.danger};
.dropdown-icon {
color: ${(props) => props.theme.colors.text.danger};
}
&:hover {
background-color: ${({ theme }) => {
const hex = theme.colors.text.danger.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
}} !important;
color: ${(props) => props.theme.colors.text.danger} !important;
}
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
margin-top: 0.25rem;
padding-top: 0.375rem;
}
&.dropdown-item-select {
padding-left: 1.5rem;
}
/* Focused state - applied during keyboard navigation */
&.dropdown-item-focused {
background-color: ${({ theme }) => theme.dropdown.hoverBg};
outline: none;
}
/* Active/selected state - applied to the currently selected item */
&.dropdown-item-active {
color: ${({ theme }) => theme.colors.text.yellow};
background-color: ${({ theme }) => theme.dropdown.activeBg};
font-weight: 500;
.dropdown-icon {
color: ${({ theme }) => theme.colors.text.yellow};
}
}
/* Combined state - when active item is also focused */
&.dropdown-item-active.dropdown-item-focused {
background-color: ${({ theme }) => theme.dropdown.activeHoverBg};
}
/* Focus visible for accessibility */
&:focus-visible {
outline: 2px solid ${({ theme }) => theme.dropdown.focusRing};
outline-offset: -2px;
}
}
.dropdown-separator {
height: 1px;
background-color: ${(props) => props.theme.dropdown.separator};
margin: 0.25rem 0;
}
`;

View File

@@ -2,25 +2,27 @@ import React from 'react';
import Tippy from '@tippyjs/react';
import StyledWrapper from './StyledWrapper';
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, ...props }) => {
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, appendTo, ...props }) => {
// When in controlled mode (visible prop is provided), don't use trigger prop
const tippyProps = visible !== undefined
? { ...props, visible, interactive: true, appendTo: 'parent' }
: { ...props, trigger: 'click', interactive: true, appendTo: 'parent' };
? { ...props, visible, interactive: true, appendTo: appendTo || 'parent' }
: { ...props, trigger: 'click', interactive: true, appendTo: appendTo || 'parent' };
return (
<StyledWrapper className="dropdown" transparent={transparent}>
<Tippy
content={children}
placement={placement || 'bottom-end'}
animation={false}
arrow={false}
onCreate={onCreate}
{...tippyProps}
>
{icon}
</Tippy>
</StyledWrapper>
<Tippy
render={(attrs) => (
<StyledWrapper className="tippy-box dropdown" transparent={transparent} tabIndex={-1} {...attrs}>
{children}
</StyledWrapper>
)}
placement={placement || 'bottom-end'}
animation={false}
arrow={false}
onCreate={onCreate}
{...tippyProps}
>
{icon}
</Tippy>
);
};

View File

@@ -17,7 +17,7 @@ import ConfirmCloseEnvironment from 'components/Environments/ConfirmCloseEnviron
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
import Dropdown from 'components/Dropdown';
import MenuDropdown from 'ui/MenuDropdown';
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
import NewRequest from 'components/Sidebar/NewRequest/index';
import GradientCloseButton from './GradientCloseButton';
@@ -26,11 +26,12 @@ import { closeWsConnection } from 'utils/network/index';
import ExampleTab from '../ExampleTab';
import toast from 'react-hot-toast';
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow }) => {
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow, dropdownContainerRef }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
const tabNameRef = useRef(null);
const tabLabelRef = useRef(null);
const lastOverflowStateRef = useRef(null);
const [showConfirmClose, setShowConfirmClose] = useState(false);
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
@@ -38,8 +39,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
const [showConfirmEnvironmentClose, setShowConfirmEnvironmentClose] = useState(false);
const [showConfirmGlobalEnvironmentClose, setShowConfirmGlobalEnvironmentClose] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const menuDropdownRef = useRef();
const item = findItemInCollection(collection, tab.uid);
@@ -99,17 +99,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
);
};
const handleRightClick = (_event) => {
const menuDropdown = dropdownTippyRef.current;
if (!menuDropdown) {
return;
}
if (menuDropdown.state.isShown) {
menuDropdown.hide();
} else {
menuDropdown.show();
}
const handleRightClick = (event) => {
event.preventDefault();
event.stopPropagation();
menuDropdownRef.current?.show();
};
const handleMouseUp = (e) => {
@@ -383,6 +376,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
/>
)}
<div
ref={tabLabelRef}
className={`flex items-baseline tab-label ${tab.preview ? 'italic' : ''}`}
onContextMenu={handleRightClick}
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
@@ -403,13 +397,13 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
{item.name}
</span>
<RequestTabMenu
onDropdownCreate={onDropdownCreate}
menuDropdownRef={menuDropdownRef}
tabLabelRef={tabLabelRef}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
tabItem={item}
collection={collection}
dropdownTippyRef={dropdownTippyRef}
dispatch={dispatch}
dropdownContainerRef={dropdownContainerRef}
/>
</div>
<GradientCloseButton
@@ -429,10 +423,19 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
);
};
function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, tabIndex, collection, dispatch, dropdownContainerRef }) {
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
// Returns the tab-label's position for dropdown positioning.
// Returns zero-sized rect if element isn't mounted yet (prevents Tippy errors).
const getTabLabelRect = () => {
if (!tabLabelRef.current) {
return { width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0 };
}
return tabLabelRef.current.getBoundingClientRect();
};
const totalTabs = collectionRequestTabs.length || 0;
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
const currentTabItem = findItemInCollection(collection, currentTabUid);
@@ -442,10 +445,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
const hasRightTabs = totalTabs > tabIndex + 1;
const hasOtherTabs = totalTabs > 1;
async function handleCloseTab(event, tabUid) {
event.stopPropagation();
dropdownTippyRef.current.hide();
async function handleCloseTab(tabUid) {
if (!tabUid) {
return;
}
@@ -461,10 +461,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
} catch (err) { }
}
function handleRevertChanges(event) {
event.stopPropagation();
dropdownTippyRef.current.hide();
function handleRevertChanges() {
if (!currentTabUid) {
return;
}
@@ -480,40 +477,96 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
} catch (err) { }
}
function handleCloseOtherTabs(event) {
dropdownTippyRef.current.hide();
async function handleCloseOtherTabs() {
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
await Promise.all(otherTabs.map((tab) => handleCloseTab(tab.uid)));
}
function handleCloseTabsToTheLeft(event) {
dropdownTippyRef.current.hide();
async function handleCloseTabsToTheLeft() {
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
await Promise.all(leftTabs.map((tab) => handleCloseTab(tab.uid)));
}
function handleCloseTabsToTheRight(event) {
dropdownTippyRef.current.hide();
async function handleCloseTabsToTheRight() {
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
await Promise.all(rightTabs.map((tab) => handleCloseTab(tab.uid)));
}
function handleCloseSavedTabs(event) {
event.stopPropagation();
function handleCloseSavedTabs() {
const items = flattenItems(collection?.items);
const savedTabs = items?.filter?.((item) => !hasRequestChanges(item));
const savedTabIds = savedTabs?.map((item) => item.uid) || [];
dispatch(closeTabs({ tabUids: savedTabIds }));
}
function handleCloseAllTabs(event) {
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
async function handleCloseAllTabs() {
await Promise.all(collectionRequestTabs.map((tab) => handleCloseTab(tab.uid)));
}
const menuItems = useMemo(() => [
{
id: 'new-request',
label: 'New Request',
onClick: () => setShowAddNewRequestModal(true)
},
{
id: 'clone-request',
label: 'Clone Request',
onClick: () => setShowCloneRequestModal(true)
},
{
id: 'revert-changes',
label: 'Revert Changes',
onClick: handleRevertChanges,
disabled: !currentTabItem?.draft
},
{
id: 'close',
label: 'Close',
onClick: () => handleCloseTab(currentTabUid)
},
{
id: 'close-others',
label: 'Close Others',
onClick: handleCloseOtherTabs,
disabled: !hasOtherTabs
},
{
id: 'close-left',
label: 'Close to the Left',
onClick: handleCloseTabsToTheLeft,
disabled: !hasLeftTabs
},
{
id: 'close-right',
label: 'Close to the Right',
onClick: handleCloseTabsToTheRight,
disabled: !hasRightTabs
},
{
id: 'close-saved',
label: 'Close Saved',
onClick: handleCloseSavedTabs
},
{
id: 'close-all',
label: 'Close All',
onClick: handleCloseAllTabs
}
], [currentTabUid, currentTabItem, hasOtherTabs, hasLeftTabs, hasRightTabs, collection, collectionRequestTabs, tabIndex, dispatch]);
const menuDropdown = (
<MenuDropdown
ref={menuDropdownRef}
items={menuItems}
placement="bottom-start"
appendTo={dropdownContainerRef?.current || document.body}
getReferenceClientRect={getTabLabelRect}
>
<span></span>
</MenuDropdown>
);
return (
<Fragment>
{showAddNewRequestModal && (
@@ -528,51 +581,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
/>
)}
<Dropdown onCreate={onDropdownCreate} icon={<span></span>} placement="bottom-start">
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowAddNewRequestModal(true);
}}
>
New Request
</button>
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowCloneRequestModal(true);
}}
>
Clone Request
</button>
<button
className="dropdown-item w-full"
onClick={handleRevertChanges}
disabled={!currentTabItem?.draft}
>
Revert Changes
</button>
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
Close
</button>
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
Close Others
</button>
<button disabled={!hasLeftTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheLeft}>
Close to the Left
</button>
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
Close to the Right
</button>
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
Close Saved
</button>
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
Close All
</button>
</Dropdown>
{menuDropdown}
</Fragment>
);
}

View File

@@ -17,6 +17,7 @@ const RequestTabs = () => {
const dispatch = useDispatch();
const tabsRef = useRef();
const scrollContainerRef = useRef();
const collectionTabsRef = useRef();
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [tabOverflowStates, setTabOverflowStates] = useState({});
const [showChevrons, setShowChevrons] = useState(false);
@@ -115,7 +116,7 @@ const RequestTabs = () => {
{collectionRequestTabs && collectionRequestTabs.length ? (
<>
<CollectionToolBar collection={activeCollection} />
<div className="flex items-center pl-2">
<div className="flex items-center pl-2" ref={collectionTabsRef}>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
@@ -158,6 +159,7 @@ const RequestTabs = () => {
folderUid={tab.folderUid}
hasOverflow={tabOverflowStates[tab.uid]}
setHasOverflow={createSetHasOverflow(tab.uid)}
dropdownContainerRef={collectionTabsRef}
/>
</DraggableTab>
);

View File

@@ -53,8 +53,10 @@ import CreateExampleModal from 'components/ResponseExample/CreateExampleModal';
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
import ActionIcon from 'ui/ActionIcon';
import MenuDropdown from 'ui/MenuDropdown';
import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';
const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
const { dropdownContainerRef } = useSidebarAccordion();
const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });
const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual);
@@ -640,8 +642,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
<ActionIcon style={{ width: 16, minWidth: 16 }}>
{isFolder ? (
{isFolder ? (
<ActionIcon style={{ width: 16, minWidth: 16 }}>
<IconChevronRight
size={16}
strokeWidth={2}
@@ -651,7 +654,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
onDoubleClick={handleFolderDoubleClick}
data-testid="folder-chevron"
/>
) : hasExamples ? (
</ActionIcon>
) : hasExamples ? (
<ActionIcon style={{ width: 16, minWidth: 16 }}>
<IconChevronRight
size={16}
strokeWidth={2}
@@ -661,8 +666,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
onDoubleClick={handleExamplesDoubleClick}
data-testid="request-item-chevron"
/>
) : null}
</ActionIcon>
</ActionIcon>
) : null}
<div className="ml-1 flex w-full h-full items-center overflow-hidden">
<CollectionItemIcon item={item} />
<span className="item-name" title={item.name}>
@@ -676,6 +682,8 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
items={buildMenuItems()}
placement="bottom-start"
data-testid="collection-item-menu"
popperOptions={{ strategy: 'fixed' }}
appendTo={dropdownContainerRef?.current || document.body}
>
<ActionIcon className="menu-icon">
<IconDots size={18} className="collection-item-menu-icon" />

View File

@@ -45,8 +45,10 @@ import { sortByNameThenSequence } from 'utils/common/index';
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
import ActionIcon from 'ui/ActionIcon';
import MenuDropdown from 'ui/MenuDropdown';
import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';
const Collection = ({ collection, searchText }) => {
const { dropdownContainerRef } = useSidebarAccordion();
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
@@ -434,6 +436,8 @@ const Collection = ({ collection, searchText }) => {
ref={menuDropdownRef}
items={menuItems}
placement="bottom-start"
appendTo={dropdownContainerRef?.current || document.body}
popperOptions={{ strategy: 'fixed' }}
data-testid="collection-actions"
>
<ActionIcon className="collection-actions">

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
const SidebarAccordionContext = createContext();
@@ -12,6 +12,7 @@ export const useSidebarAccordion = () => {
export const SidebarAccordionProvider = ({ children, defaultExpanded = ['collections'] }) => {
const [expandedSections, setExpandedSections] = useState(new Set(defaultExpanded));
const dropdownContainerRef = useRef(null);
const toggleSection = useCallback((sectionId) => {
setExpandedSections((prev) => {
@@ -52,10 +53,13 @@ export const SidebarAccordionProvider = ({ children, defaultExpanded = ['collect
toggleSection,
setSectionExpanded,
isExpanded,
getExpandedCount
getExpandedCount,
dropdownContainerRef
}}
>
{children}
<div ref={dropdownContainerRef}>
{children}
</div>
</SidebarAccordionContext.Provider>
);
};

View File

@@ -1,150 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.tippy-box {
.tippy-content {
.label-item {
display: flex;
align-items: center;
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.025em;
color: ${(props) => props.theme.dropdown.color};
opacity: 0.6;
margin-top: 0.25rem;
&:first-child {
margin-top: 0;
}
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.275rem 0.625rem;
cursor: pointer;
border-radius: 6px;
margin: 0.0625rem 0;
font-size: 0.8125rem;
&.active {
color: ${(props) => props.theme.colors.text.yellow} !important;
.dropdown-icon {
color: ${(props) => props.theme.colors.text.yellow} !important;
}
}
.dropdown-label {
flex: 1;
}
.dropdown-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.dropdown.iconColor};
opacity: 0.8;
}
.dropdown-right-section {
margin-left: auto;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
&:hover:not(:disabled):not(.disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.selected-focused:not(:disabled):not(.disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus-visible:not(:disabled):not(.disabled) {
outline: none;
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus:not(:focus-visible) {
outline: none;
}
&:disabled,
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.delete-item {
color: ${(props) => props.theme.colors.text.danger};
.dropdown-icon {
color: ${(props) => props.theme.colors.text.danger};
}
&:hover {
background-color: ${({ theme }) => {
const hex = theme.colors.text.danger.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
}} !important;
color: ${(props) => props.theme.colors.text.danger} !important;
}
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
margin-top: 0.25rem;
padding-top: 0.375rem;
}
&.dropdown-item-select {
padding-left: 1.5rem;
}
/* Focused state - applied during keyboard navigation */
&.dropdown-item-focused {
background-color: ${({ theme }) => theme.dropdown.hoverBg};
outline: none;
}
/* Active/selected state - applied to the currently selected item */
&.dropdown-item-active {
color: ${({ theme }) => theme.colors.text.yellow};
background-color: ${({ theme }) => theme.dropdown.activeBg};
font-weight: 500;
.dropdown-icon {
color: ${({ theme }) => theme.colors.text.yellow};
}
}
/* Combined state - when active item is also focused */
&.dropdown-item-active.dropdown-item-focused {
background-color: ${({ theme }) => theme.dropdown.activeHoverBg};
}
/* Focus visible for accessibility */
&:focus-visible {
outline: 2px solid ${({ theme }) => theme.dropdown.focusRing};
outline-offset: -2px;
}
}
.dropdown-separator {
height: 1px;
background-color: ${(props) => props.theme.dropdown.separator};
margin: 0.25rem 0;
}
}
}
`;
export default StyledWrapper;

View File

@@ -1,6 +1,5 @@
import React, { forwardRef, useRef, useCallback, useState, useImperativeHandle, useEffect, useMemo } from 'react';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
// Constants
const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'];
@@ -432,37 +431,35 @@ const MenuDropdown = forwardRef(({
: <div onClick={handleTriggerClick} data-testid={testId}>{children}</div>;
return (
<StyledWrapper>
<Dropdown
onCreate={onDropdownCreate}
icon={triggerElement}
placement={placement}
className={className}
visible={isOpen}
onClickOutside={handleClickOutside}
{...dropdownProps}
>
<div {...(testId && { 'data-testid': testId + '-dropdown' })}>
{header && (
<div className="dropdown-header-container" onClick={handleClickOutside}>
{header}
<div className="dropdown-divider"></div>
</div>
)}
<div role="menu" tabIndex={-1} onKeyDown={handleMenuKeyDown}>
{renderMenuContent()}
<Dropdown
onCreate={onDropdownCreate}
icon={triggerElement}
placement={placement}
className={className}
visible={isOpen}
onClickOutside={handleClickOutside}
{...dropdownProps}
>
<div {...(testId && { 'data-testid': testId + '-dropdown' })}>
{header && (
<div className="dropdown-header-container" onClick={handleClickOutside}>
{header}
<div className="dropdown-divider"></div>
</div>
{footer && (
<>
<div className="dropdown-divider"></div>
<div className="dropdown-footer-container">
{footer}
</div>
</>
)}
)}
<div role="menu" tabIndex={-1} onKeyDown={handleMenuKeyDown}>
{renderMenuContent()}
</div>
</Dropdown>
</StyledWrapper>
{footer && (
<>
<div className="dropdown-divider"></div>
<div className="dropdown-footer-container">
{footer}
</div>
</>
)}
</div>
</Dropdown>
);
});

View File

@@ -650,7 +650,7 @@ const selectRequestPaneTab = async (page: Page, tabName: string) => {
await overflowButton.click();
// Wait for dropdown to appear and click the menu item (overflow tabs are rendered as menuitems)
const dropdownItem = page.locator('.tippy-content').getByRole('menuitem', { name: tabName });
const dropdownItem = page.locator('.tippy-box .dropdown-item').filter({ hasText: tabName });
await expect(dropdownItem).toBeVisible();
await dropdownItem.click();
return;