mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 14:44:07 +00:00
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:
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user