mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
Refactor the Dropdown into a function component (#4932)
* add unit test for dropdown component * refactor dropdown to function component wip * use useLayoutEffect for updating the position * update types to use DropdownHandle * remove unused forcedposition * remove dropup * add warning about dropdown container * add useCallback/useMemo * re-export the container id for tests * split the state
This commit is contained in:
parent
5f250334f6
commit
526bfc35c8
@ -0,0 +1,118 @@
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { Dropdown, dropdownsContainerId } from '../dropdown/dropdown';
|
||||
import { DropdownButton } from '../dropdown/dropdown-button';
|
||||
import { DropdownItem } from '../dropdown/dropdown-item';
|
||||
|
||||
const prepareDom = () => {
|
||||
const dropdownsContainer = document.createElement('div');
|
||||
dropdownsContainer.setAttribute('id', dropdownsContainerId);
|
||||
|
||||
dropdownsContainer.style.position = 'fixed';
|
||||
dropdownsContainer.style.right = '-90000px';
|
||||
dropdownsContainer.style.left = '-90000px';
|
||||
dropdownsContainer.style.width = '100vw';
|
||||
dropdownsContainer.style.height = '100vh';
|
||||
|
||||
document.body.appendChild(dropdownsContainer);
|
||||
};
|
||||
|
||||
describe('Dropdown', () => {
|
||||
beforeEach(() => {
|
||||
prepareDom();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should render a dropdown', async () => {
|
||||
const onSelect = jest.fn();
|
||||
|
||||
const options = [
|
||||
{ id: 1, label: 'List of Numbers', value: [1, 2, 3] },
|
||||
{ id: 2, label: 'Another List of Numbers', value: [4, 5, 6] },
|
||||
{ id: 3, label: 'List of more Numbers', value: [7, 8, 9] },
|
||||
];
|
||||
|
||||
const { queryByText } = render(
|
||||
<Dropdown>
|
||||
<DropdownButton>
|
||||
Open <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
{options.map(option => (
|
||||
<DropdownItem
|
||||
key={option.id}
|
||||
onClick={onSelect}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
const button = queryByText('Open');
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
const option2 = queryByText(options[1].label);
|
||||
|
||||
fireEvent.click(option2);
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(options[1].value, expect.any(Object));
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('handle navigation via keyboard', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = jest.fn();
|
||||
|
||||
const options = [
|
||||
{ id: 1, label: 'List of Numbers', value: [1, 2, 3] },
|
||||
{ id: 2, label: 'Another List of Numbers', value: [4, 5, 6] },
|
||||
{ id: 3, label: 'List of more Numbers', value: [7, 8, 9] },
|
||||
];
|
||||
|
||||
const { queryByText, queryByTitle } = render(
|
||||
<Dropdown>
|
||||
<DropdownButton>
|
||||
Open <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
{options.map(option => (
|
||||
<DropdownItem
|
||||
key={option.id}
|
||||
title={option.label}
|
||||
onClick={onSelect}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
// Click the open button
|
||||
const button = queryByText('Open');
|
||||
fireEvent.click(button);
|
||||
|
||||
// Navigate with the arrows to the second option
|
||||
await user.keyboard('[ArrowDown]');
|
||||
await user.keyboard('[ArrowDown]');
|
||||
|
||||
const parent = queryByTitle(options[1].label)?.parentElement;
|
||||
expect(parent).toHaveClass('active');
|
||||
|
||||
// Press enter on the second option
|
||||
await user.keyboard('[Enter]');
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(options[1].value, expect.any(Object));
|
||||
|
||||
// The dropdown button should regain focus
|
||||
expect(button).toHaveFocus();
|
||||
|
||||
});
|
||||
});
|
@ -1,10 +1,20 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import { any, equals } from 'ramda';
|
||||
import React, { CSSProperties, Fragment, PureComponent, ReactNode } from 'react';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
forwardRef,
|
||||
Fragment,
|
||||
isValidElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../../common/constants';
|
||||
import { hotKeyRefs } from '../../../../common/hotkeys';
|
||||
import { executeHotKey } from '../../../../common/hotkeys-listener';
|
||||
import { fuzzyMatch } from '../../../../common/misc';
|
||||
@ -12,7 +22,6 @@ import { KeydownBinder } from '../../keydown-binder';
|
||||
import { DropdownButton } from './dropdown-button';
|
||||
import { DropdownDivider } from './dropdown-divider';
|
||||
import { DropdownItem } from './dropdown-item';
|
||||
export const dropdownsContainerId = 'dropdowns-container';
|
||||
|
||||
export interface DropdownProps {
|
||||
children: ReactNode;
|
||||
@ -26,454 +35,455 @@ export interface DropdownProps {
|
||||
beside?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
open: boolean;
|
||||
dropUp: boolean;
|
||||
filter: string;
|
||||
filterVisible: boolean;
|
||||
filterItems?: number[] | null;
|
||||
filterActiveIndex: number;
|
||||
forcedPosition?: { x: number; y: number } | null;
|
||||
uniquenessKey: number;
|
||||
}
|
||||
export const dropdownsContainerId = 'dropdowns-container';
|
||||
|
||||
const isComponent = (match: string) => (child: ReactNode) => any(equals(match), [
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.name,
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.displayName,
|
||||
]);
|
||||
const isComponent = (match: string) => (child: ReactNode) =>
|
||||
any(equals(match), [
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.name,
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.displayName,
|
||||
]);
|
||||
|
||||
const isDropdownItem = isComponent(DropdownItem.name);
|
||||
const isDropdownButton = isComponent(DropdownButton.name);
|
||||
const isDropdownDivider = isComponent(DropdownDivider.name);
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class Dropdown extends PureComponent<DropdownProps, State> {
|
||||
private _node: HTMLDivElement | null = null;
|
||||
private _dropdownList: HTMLDivElement | null = null;
|
||||
private _filter: HTMLInputElement | null = null;
|
||||
// This walks the children tree and returns the dropdown specific components.
|
||||
// It allows us to use arrays, fragments etc.
|
||||
const _getFlattenedChildren = (children: ReactNode[] | ReactNode) => {
|
||||
let newChildren: ReactNode[] = [];
|
||||
// Ensure children is an array
|
||||
const flatChildren = Array.isArray(children) ? children : [children];
|
||||
|
||||
state: State = {
|
||||
open: false,
|
||||
dropUp: false,
|
||||
// Filter Stuff
|
||||
filter: '',
|
||||
filterVisible: false,
|
||||
filterItems: null,
|
||||
filterActiveIndex: 0,
|
||||
// Position
|
||||
forcedPosition: null,
|
||||
// Use this to force new menu every time dropdown opens
|
||||
uniquenessKey: 0,
|
||||
};
|
||||
for (const child of flatChildren) {
|
||||
if (!child) {
|
||||
// Ignore null components
|
||||
continue;
|
||||
}
|
||||
|
||||
_setRef(node: HTMLDivElement) {
|
||||
this._node = node;
|
||||
}
|
||||
|
||||
_handleCheckFilterSubmit(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter') {
|
||||
// Listen for the Enter key and "click" on the active list item
|
||||
const selector = `li[data-filter-index="${this.state.filterActiveIndex}"] button`;
|
||||
|
||||
const button = this._dropdownList?.querySelector(selector);
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
button?.click();
|
||||
if (isValidElement(child) && child.type === Fragment) {
|
||||
newChildren = [
|
||||
...newChildren,
|
||||
..._getFlattenedChildren(child.props.children),
|
||||
];
|
||||
} else if (Array.isArray(child)) {
|
||||
newChildren = [...newChildren, ..._getFlattenedChildren(child)];
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
_handleChangeFilter(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const newFilter = event.target.value;
|
||||
return newChildren;
|
||||
};
|
||||
|
||||
// Nothing to do if the filter didn't change
|
||||
if (newFilter === this.state.filter) {
|
||||
return;
|
||||
}
|
||||
export interface DropdownHandle {
|
||||
show: (
|
||||
filterVisible?: boolean,
|
||||
) => void;
|
||||
hide: () => void;
|
||||
toggle: (filterVisible?: boolean) => void;
|
||||
}
|
||||
|
||||
// Filter the list items that are filterable (have data-filter-index property)
|
||||
const filterItems: number[] = [];
|
||||
export const Dropdown = forwardRef<DropdownHandle, DropdownProps>(
|
||||
({ right, outline, className, style, children, beside, onOpen, onHide, wide }, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
// @TODO: This is a hack to force new menu every time dropdown opens
|
||||
const [uniquenessKey, setUniquenessKey] = useState(0);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [filterVisible, setFilterVisible] = useState(false);
|
||||
const [filterItems, setFilterItems] = useState<number[] | null>(null);
|
||||
const [filterActiveIndex, setFilterActiveIndex] = useState(0);
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION convert to array or use querySelectorAll().forEach
|
||||
for (const listItem of this._dropdownList.querySelectorAll('li')) {
|
||||
if (!listItem.hasAttribute('data-filter-index')) {
|
||||
continue;
|
||||
}
|
||||
const dropdownContainerRef = useRef<HTMLDivElement>(null);
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null);
|
||||
const filterInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const match = fuzzyMatch(newFilter, listItem.textContent || '');
|
||||
const _handleCheckFilterSubmit = useCallback((
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.key === 'Enter') {
|
||||
// Listen for the Enter key and "click" on the active list item
|
||||
const selector = `li[data-filter-index="${filterActiveIndex}"] button`;
|
||||
|
||||
if (!newFilter || match) {
|
||||
const filterIndex = listItem.getAttribute('data-filter-index');
|
||||
if (filterIndex) {
|
||||
filterItems.push(parseInt(filterIndex, 10));
|
||||
const button = dropdownListRef.current?.querySelector(selector);
|
||||
|
||||
if (button instanceof HTMLButtonElement) {
|
||||
button.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [filterActiveIndex]);
|
||||
|
||||
this.setState({
|
||||
filter: newFilter,
|
||||
filterItems: newFilter ? filterItems : null,
|
||||
filterActiveIndex: filterItems[0] || -1,
|
||||
filterVisible: this.state.filterVisible ? true : newFilter.length > 0,
|
||||
});
|
||||
}
|
||||
const _handleChangeFilter = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newFilter = event.target.value;
|
||||
|
||||
_handleDropdownNavigation(event: KeyboardEvent) {
|
||||
const { key, shiftKey } = event;
|
||||
// Handle tab and arrows to move up and down dropdown entries
|
||||
const { filterItems, filterActiveIndex } = this.state;
|
||||
// Nothing to do if the filter didn't change
|
||||
if (newFilter === filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['Tab', 'ArrowDown', 'ArrowUp'].includes(key)) {
|
||||
event.preventDefault();
|
||||
const items = filterItems || [];
|
||||
// Filter the list items that are filterable (have data-filter-index property)
|
||||
const filterItems: number[] = [];
|
||||
|
||||
const filterableItems = dropdownListRef.current?.querySelectorAll('li');
|
||||
|
||||
if (filterableItems instanceof NodeList) {
|
||||
for (const listItem of filterableItems) {
|
||||
if (!listItem.hasAttribute('data-filter-index')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = fuzzyMatch(newFilter, listItem.textContent || '');
|
||||
|
||||
if (!newFilter || match) {
|
||||
const filterIndex = listItem.getAttribute('data-filter-index');
|
||||
|
||||
if (!filterItems) {
|
||||
// @ts-expect-error -- TSCONVERSION convert to array or use querySelectorAll().forEach
|
||||
for (const li of this._dropdownList.querySelectorAll('li')) {
|
||||
if (li.hasAttribute('data-filter-index')) {
|
||||
const filterIndex = li.getAttribute('data-filter-index');
|
||||
if (filterIndex) {
|
||||
items.push(parseInt(filterIndex, 10));
|
||||
filterItems.push(parseInt(filterIndex, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(newFilter);
|
||||
setFilterItems(newFilter ? filterItems : null);
|
||||
setFilterActiveIndex(filterItems[0] || -1);
|
||||
setFilterVisible(filterVisible || newFilter.length > 0);
|
||||
}
|
||||
}, [filter, filterVisible]);
|
||||
|
||||
const _handleDropdownNavigation = useCallback((event: KeyboardEvent) => {
|
||||
const { key, shiftKey } = event;
|
||||
// Handle tab and arrows to move up and down dropdown entries
|
||||
if (['Tab', 'ArrowDown', 'ArrowUp'].includes(key)) {
|
||||
event.preventDefault();
|
||||
const items = filterItems || [];
|
||||
|
||||
if (!filterItems) {
|
||||
const filterableItems = dropdownListRef.current?.querySelectorAll('li');
|
||||
|
||||
if (filterableItems instanceof NodeList) {
|
||||
for (const li of filterableItems) {
|
||||
if (li.hasAttribute('data-filter-index')) {
|
||||
const filterIndex = li.getAttribute('data-filter-index');
|
||||
if (filterIndex) {
|
||||
items.push(parseInt(filterIndex, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const i = items.indexOf(filterActiveIndex);
|
||||
|
||||
if (key === 'ArrowUp' || (key === 'Tab' && shiftKey)) {
|
||||
const nextI = i > 0 ? items[i - 1] : items[items.length - 1];
|
||||
setFilterActiveIndex(nextI);
|
||||
} else {
|
||||
setFilterActiveIndex(items[i + 1] || items[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const i = items.indexOf(filterActiveIndex);
|
||||
filterInputRef.current?.focus();
|
||||
}, [filterActiveIndex, filterItems]);
|
||||
|
||||
if (key === 'ArrowUp' || (key === 'Tab' && shiftKey)) {
|
||||
const nextI = i > 0 ? items[i - 1] : items[items.length - 1];
|
||||
this.setState({
|
||||
filterActiveIndex: nextI,
|
||||
});
|
||||
const _handleBodyKeyDown = (event: KeyboardEvent) => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch all key presses (like global app hotkeys) if we're open
|
||||
event.stopPropagation();
|
||||
|
||||
_handleDropdownNavigation(event);
|
||||
|
||||
executeHotKey(event, hotKeyRefs.CLOSE_DROPDOWN, () => {
|
||||
hide();
|
||||
});
|
||||
};
|
||||
|
||||
const isNearBottomOfScreen = () => {
|
||||
if (!dropdownContainerRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodyHeight = document.body.getBoundingClientRect().height;
|
||||
const dropdownTop = dropdownContainerRef.current.getBoundingClientRect().top;
|
||||
|
||||
return dropdownTop > bodyHeight - 200;
|
||||
};
|
||||
|
||||
// Recalculate the position of the dropdown
|
||||
useLayoutEffect(() => {
|
||||
if (!open || !dropdownListRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute the size of all the menus
|
||||
const dropdownBtnRect = dropdownContainerRef.current?.getBoundingClientRect();
|
||||
if (!dropdownBtnRect) {
|
||||
return;
|
||||
}
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const dropdownListRect = dropdownListRef.current.getBoundingClientRect();
|
||||
|
||||
// Reset all the things so we can start fresh
|
||||
dropdownListRef.current.style.left = 'initial';
|
||||
dropdownListRef.current.style.right = 'initial';
|
||||
dropdownListRef.current.style.top = 'initial';
|
||||
dropdownListRef.current.style.bottom = 'initial';
|
||||
dropdownListRef.current.style.minWidth = 'initial';
|
||||
dropdownListRef.current.style.maxWidth = 'initial';
|
||||
|
||||
const screenMargin = 6;
|
||||
if (right || wide) {
|
||||
// Prevent dropdown from squishing against left side of screen
|
||||
const rightMargin = Math.max(
|
||||
dropdownListRect.width + screenMargin,
|
||||
dropdownBtnRect.right
|
||||
);
|
||||
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
dropdownListRef.current.style.right = `${
|
||||
bodyRect.width - rightMargin + offset
|
||||
}px`;
|
||||
dropdownListRef.current.style.maxWidth = `${Math.min(
|
||||
dropdownListRect.width,
|
||||
rightMargin + offset
|
||||
)}px`;
|
||||
}
|
||||
|
||||
if (!right || wide) {
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
// Prevent dropdown from squishing against right side of screen
|
||||
const leftMargin = Math.min(
|
||||
bodyRect.width - dropdownListRect.width - screenMargin,
|
||||
dropdownBtnRect.left
|
||||
);
|
||||
dropdownListRef.current.style.left = `${leftMargin + offset}px`;
|
||||
dropdownListRef.current.style.maxWidth = `${Math.min(
|
||||
dropdownListRect.width,
|
||||
bodyRect.width - leftMargin - offset
|
||||
)}px`;
|
||||
}
|
||||
|
||||
if (isNearBottomOfScreen()) {
|
||||
dropdownListRef.current.style.bottom = `${
|
||||
bodyRect.height - dropdownBtnRect.top
|
||||
}px`;
|
||||
dropdownListRef.current.style.maxHeight = `${
|
||||
dropdownBtnRect.top - screenMargin
|
||||
}px`;
|
||||
} else {
|
||||
this.setState({
|
||||
filterActiveIndex: items[i + 1] || items[0],
|
||||
});
|
||||
dropdownListRef.current.style.top = `${dropdownBtnRect.bottom}px`;
|
||||
dropdownListRef.current.style.maxHeight = `${
|
||||
bodyRect.height - dropdownBtnRect.bottom - screenMargin
|
||||
}px`;
|
||||
}
|
||||
}
|
||||
}, [beside, open, right, wide]);
|
||||
|
||||
this._filter?.focus();
|
||||
}
|
||||
const _handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggle();
|
||||
};
|
||||
|
||||
_handleBodyKeyDown(event: KeyboardEvent) {
|
||||
if (!this.state.open) {
|
||||
return;
|
||||
}
|
||||
const _handleMouseDown = (event: React.MouseEvent) => {
|
||||
// Intercept mouse down so that clicks don't trigger things like drag and drop.
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
// Catch all key presses (like global app hotkeys) if we're open
|
||||
event.stopPropagation();
|
||||
const hide = useCallback(() => {
|
||||
// Focus the dropdown button after hiding
|
||||
if (dropdownContainerRef.current) {
|
||||
const button = dropdownContainerRef.current.querySelector('button');
|
||||
|
||||
this._handleDropdownNavigation(event);
|
||||
|
||||
executeHotKey(event, hotKeyRefs.CLOSE_DROPDOWN, () => {
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
_checkSizeAndPosition() {
|
||||
if (!this.state.open || !this._dropdownList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get dropdown menu
|
||||
const dropdownList = this._dropdownList;
|
||||
|
||||
// Compute the size of all the menus
|
||||
// @ts-expect-error -- TSCONVERSION should exit if node is not defined
|
||||
let dropdownBtnRect = this._node.getBoundingClientRect();
|
||||
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const dropdownListRect = dropdownList.getBoundingClientRect();
|
||||
const { forcedPosition } = this.state;
|
||||
|
||||
if (forcedPosition) {
|
||||
// @ts-expect-error -- TSCONVERSION missing properties
|
||||
dropdownBtnRect = {
|
||||
left: forcedPosition.x,
|
||||
right: bodyRect.width - forcedPosition.x,
|
||||
top: forcedPosition.y,
|
||||
bottom: bodyRect.height - forcedPosition.y,
|
||||
width: 100,
|
||||
height: 10,
|
||||
};
|
||||
}
|
||||
|
||||
// Should it drop up?
|
||||
const bodyHeight = bodyRect.height;
|
||||
const dropdownTop = dropdownBtnRect.top;
|
||||
const dropUp = dropdownTop > bodyHeight - 200;
|
||||
// Reset all the things so we can start fresh
|
||||
this._dropdownList.style.left = 'initial';
|
||||
this._dropdownList.style.right = 'initial';
|
||||
this._dropdownList.style.top = 'initial';
|
||||
this._dropdownList.style.bottom = 'initial';
|
||||
this._dropdownList.style.minWidth = 'initial';
|
||||
this._dropdownList.style.maxWidth = 'initial';
|
||||
const screenMargin = 6;
|
||||
const { right, wide } = this.props;
|
||||
|
||||
if (right || wide) {
|
||||
const { right: originalRight } = dropdownBtnRect;
|
||||
// Prevent dropdown from squishing against left side of screen
|
||||
const right = Math.max(dropdownListRect.width + screenMargin, originalRight);
|
||||
const { beside } = this.props;
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
this._dropdownList.style.right = `${bodyRect.width - right + offset}px`;
|
||||
this._dropdownList.style.maxWidth = `${Math.min(dropdownListRect.width, right + offset)}px`;
|
||||
}
|
||||
|
||||
if (!right || wide) {
|
||||
const { left: originalLeft } = dropdownBtnRect;
|
||||
const { beside } = this.props;
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
// Prevent dropdown from squishing against right side of screen
|
||||
const left = Math.min(bodyRect.width - dropdownListRect.width - screenMargin, originalLeft);
|
||||
this._dropdownList.style.left = `${left + offset}px`;
|
||||
this._dropdownList.style.maxWidth = `${Math.min(
|
||||
dropdownListRect.width,
|
||||
bodyRect.width - left - offset,
|
||||
)}px`;
|
||||
}
|
||||
|
||||
if (dropUp) {
|
||||
const { top } = dropdownBtnRect;
|
||||
this._dropdownList.style.bottom = `${bodyRect.height - top}px`;
|
||||
this._dropdownList.style.maxHeight = `${top - screenMargin}px`;
|
||||
} else {
|
||||
const { bottom } = dropdownBtnRect;
|
||||
this._dropdownList.style.top = `${bottom}px`;
|
||||
this._dropdownList.style.maxHeight = `${bodyRect.height - bottom - screenMargin}px`;
|
||||
}
|
||||
}
|
||||
|
||||
_handleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
static _handleMouseDown(event: React.MouseEvent) {
|
||||
// Intercept mouse down so that clicks don't trigger things like drag and drop.
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
_addDropdownListRef(dropdownList: HTMLDivElement) {
|
||||
this._dropdownList = dropdownList;
|
||||
}
|
||||
|
||||
_addFilterRef(filter: HTMLInputElement) {
|
||||
this._filter = filter;
|
||||
|
||||
// Automatically focus the filter element when mounted so we can start typing
|
||||
if (this._filter) {
|
||||
this._filter.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: children should not be 'any'.
|
||||
_getFlattenedChildren(children: any) {
|
||||
let newChildren: ReactNode[] = [];
|
||||
// Ensure children is an array
|
||||
children = Array.isArray(children) ? children : [children];
|
||||
|
||||
for (const child of children) {
|
||||
if (!child) {
|
||||
// Ignore null components
|
||||
continue;
|
||||
button?.focus();
|
||||
}
|
||||
|
||||
if (child.type === Fragment) {
|
||||
newChildren = [...newChildren, ...this._getFlattenedChildren(child.props.children)];
|
||||
} else if (Array.isArray(child)) {
|
||||
newChildren = [...newChildren, ...this._getFlattenedChildren(child)];
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
setOpen(false);
|
||||
|
||||
return newChildren;
|
||||
}
|
||||
onHide?.();
|
||||
}, [onHide]);
|
||||
|
||||
componentDidUpdate() {
|
||||
this._checkSizeAndPosition();
|
||||
}
|
||||
const show = useCallback(
|
||||
(
|
||||
filterVisible = false,
|
||||
) => {
|
||||
setOpen(true);
|
||||
setFilterVisible(filterVisible);
|
||||
setFilter('');
|
||||
setFilterItems(null);
|
||||
setFilterActiveIndex(-1);
|
||||
setUniquenessKey(uniquenessKey + 1);
|
||||
|
||||
hide() {
|
||||
// Focus the dropdown button after hiding
|
||||
if (this._node) {
|
||||
const button = this._node.querySelector('button');
|
||||
onOpen?.();
|
||||
},
|
||||
[onOpen, uniquenessKey]
|
||||
);
|
||||
|
||||
button?.focus();
|
||||
}
|
||||
const toggle = useCallback(
|
||||
(filterVisible = false) => {
|
||||
if (open) {
|
||||
hide();
|
||||
} else {
|
||||
show(filterVisible);
|
||||
}
|
||||
},
|
||||
[hide, open, show]
|
||||
);
|
||||
|
||||
this.setState({
|
||||
open: false,
|
||||
});
|
||||
this.props.onHide?.();
|
||||
}
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
show,
|
||||
hide,
|
||||
toggle,
|
||||
}),
|
||||
[hide, show, toggle]
|
||||
);
|
||||
|
||||
show(filterVisible = false, forcedPosition: { x: number; y: number } | null = null) {
|
||||
const bodyHeight = document.body.getBoundingClientRect().height;
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION _node can be undefined
|
||||
const dropdownTop = this._node.getBoundingClientRect().top;
|
||||
|
||||
const dropUp = dropdownTop > bodyHeight - 200;
|
||||
this.setState({
|
||||
open: true,
|
||||
dropUp,
|
||||
forcedPosition,
|
||||
filterVisible,
|
||||
filter: '',
|
||||
filterItems: null,
|
||||
filterActiveIndex: -1,
|
||||
uniquenessKey: this.state.uniquenessKey + 1,
|
||||
});
|
||||
this.props.onOpen?.();
|
||||
}
|
||||
|
||||
toggle(filterVisible = false) {
|
||||
if (this.state.open) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(filterVisible);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { right, outline, wide, className, style, children } = this.props;
|
||||
const {
|
||||
dropUp,
|
||||
open,
|
||||
uniquenessKey,
|
||||
filterVisible,
|
||||
filterActiveIndex,
|
||||
filterItems,
|
||||
filter,
|
||||
} = this.state;
|
||||
const classes = classnames('dropdown', className, {
|
||||
'dropdown--wide': wide,
|
||||
'dropdown--open': open,
|
||||
});
|
||||
|
||||
const menuClasses = classnames({
|
||||
// eslint-disable-next-line camelcase
|
||||
dropdown__menu: true,
|
||||
'theme--dropdown__menu': true,
|
||||
'dropdown__menu--open': open,
|
||||
'dropdown__menu--outlined': outline,
|
||||
'dropdown__menu--up': dropUp,
|
||||
'dropdown__menu--up': isNearBottomOfScreen(),
|
||||
'dropdown__menu--right': right,
|
||||
});
|
||||
const dropdownButtons: ReactNode[] = [];
|
||||
const dropdownItems: ReactNode[] = [];
|
||||
|
||||
const allChildren = this._getFlattenedChildren(children);
|
||||
const dropdownChildren = useMemo(() => {
|
||||
const dropdownButtons: ReactNode[] = [];
|
||||
const dropdownItems: ReactNode[] = [];
|
||||
|
||||
const visibleChildren = allChildren.filter((child, i) => {
|
||||
if (isDropdownItem(child)) {
|
||||
return true;
|
||||
}
|
||||
const allChildren = _getFlattenedChildren(children);
|
||||
|
||||
// It's visible if its index is in the filterItems
|
||||
return !filterItems || filterItems.includes(i);
|
||||
});
|
||||
|
||||
for (let i = 0; i < allChildren.length; i++) {
|
||||
const child = allChildren[i];
|
||||
|
||||
if (isDropdownButton(child)) {
|
||||
dropdownButtons.push(child);
|
||||
} else if (isDropdownItem(child)) {
|
||||
const active = i === filterActiveIndex;
|
||||
const hide = !visibleChildren.includes(child);
|
||||
dropdownItems.push(
|
||||
<li
|
||||
key={i}
|
||||
data-filter-index={i}
|
||||
className={classnames({
|
||||
active,
|
||||
hide,
|
||||
})}
|
||||
>
|
||||
{child}
|
||||
</li>,
|
||||
);
|
||||
} else if (isDropdownDivider(child)) {
|
||||
const currentIndex = visibleChildren.indexOf(child);
|
||||
const nextChild = visibleChildren[currentIndex + 1];
|
||||
|
||||
// Only show the divider if the next child is a DropdownItem
|
||||
if (nextChild && isDropdownItem(nextChild)) {
|
||||
dropdownItems.push(<li key={i}>{child}</li>);
|
||||
const visibleChildren = allChildren.filter((child, i) => {
|
||||
if (!isDropdownItem(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let finalChildren: React.ReactNode = [];
|
||||
|
||||
if (dropdownButtons.length !== 1) {
|
||||
console.error(`Dropdown needs exactly one DropdownButton! Got ${dropdownButtons.length}`, {
|
||||
allChildren,
|
||||
// It's visible if its index is in the filterItems
|
||||
return !filterItems || filterItems.includes(i);
|
||||
});
|
||||
} else {
|
||||
const noResults = filter && filterItems && filterItems.length === 0;
|
||||
finalChildren = [
|
||||
dropdownButtons[0],
|
||||
ReactDOM.createPortal(
|
||||
<div
|
||||
key="item"
|
||||
className={menuClasses}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<div className="dropdown__backdrop theme--transparent-overlay" />
|
||||
<div
|
||||
key={uniquenessKey}
|
||||
ref={this._addDropdownListRef}
|
||||
tabIndex={-1}
|
||||
className={classnames('dropdown__list', {
|
||||
'dropdown__list--filtering': filterVisible,
|
||||
|
||||
for (let i = 0; i < allChildren.length; i++) {
|
||||
const child = allChildren[i];
|
||||
|
||||
if (isDropdownButton(child)) {
|
||||
dropdownButtons.push(child);
|
||||
} else if (isDropdownItem(child)) {
|
||||
const active = i === filterActiveIndex;
|
||||
const hide = !visibleChildren.includes(child);
|
||||
dropdownItems.push(
|
||||
<li
|
||||
key={i}
|
||||
data-filter-index={i}
|
||||
className={classnames({
|
||||
active,
|
||||
hide,
|
||||
})}
|
||||
>
|
||||
<div className="form-control dropdown__filter">
|
||||
<i className="fa fa-search" />
|
||||
<input
|
||||
type="text"
|
||||
onChange={this._handleChangeFilter}
|
||||
ref={this._addFilterRef}
|
||||
onKeyPress={this._handleCheckFilterSubmit}
|
||||
/>
|
||||
</div>
|
||||
{noResults && <div className="text-center pad warning">No match :(</div>}
|
||||
<ul
|
||||
className={classnames({
|
||||
hide: noResults,
|
||||
{child}
|
||||
</li>
|
||||
);
|
||||
} else if (isDropdownDivider(child)) {
|
||||
const currentIndex = visibleChildren.indexOf(child);
|
||||
const nextChild = visibleChildren[currentIndex + 1];
|
||||
|
||||
// Only show the divider if the next child is a DropdownItem
|
||||
if (nextChild && isDropdownItem(nextChild)) {
|
||||
dropdownItems.push(<li key={i}>{child}</li>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let finalChildren: React.ReactNode = [];
|
||||
|
||||
if (dropdownButtons.length !== 1) {
|
||||
console.error(
|
||||
`Dropdown needs exactly one DropdownButton! Got ${dropdownButtons.length}`,
|
||||
{
|
||||
allChildren,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const noResults = filter && filterItems && filterItems.length === 0;
|
||||
const dropdownsContainer = document.getElementById(dropdownsContainerId);
|
||||
|
||||
if (!dropdownsContainer) {
|
||||
console.error('Dropdown: a #dropdowns-container element is required for a dropdown to render properly');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
finalChildren = [
|
||||
dropdownButtons[0],
|
||||
ReactDOM.createPortal(
|
||||
<div key="item" className={menuClasses} aria-hidden={!open}>
|
||||
<div className="dropdown__backdrop theme--transparent-overlay" />
|
||||
<div
|
||||
key={uniquenessKey}
|
||||
ref={dropdownListRef}
|
||||
tabIndex={-1}
|
||||
className={classnames('dropdown__list', {
|
||||
'dropdown__list--filtering': filterVisible,
|
||||
})}
|
||||
>
|
||||
{dropdownItems}
|
||||
</ul>
|
||||
</div>
|
||||
</div>,
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
document.getElementById(dropdownsContainerId),
|
||||
),
|
||||
];
|
||||
}
|
||||
<div className="form-control dropdown__filter">
|
||||
<i className="fa fa-search" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus={open}
|
||||
onChange={_handleChangeFilter}
|
||||
ref={filterInputRef}
|
||||
onKeyPress={_handleCheckFilterSubmit}
|
||||
/>
|
||||
</div>
|
||||
{noResults && (
|
||||
<div className="text-center pad warning">{'No match :('}</div>
|
||||
)}
|
||||
<ul
|
||||
className={classnames({
|
||||
hide: noResults,
|
||||
})}
|
||||
>
|
||||
{dropdownItems}
|
||||
</ul>
|
||||
</div>
|
||||
</div>,
|
||||
dropdownsContainer
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return finalChildren;
|
||||
}, [_handleChangeFilter, _handleCheckFilterSubmit, children, filter, filterActiveIndex, filterItems, filterVisible, menuClasses, open, uniquenessKey]);
|
||||
|
||||
return (
|
||||
<KeydownBinder stopMetaPropagation onKeydown={this._handleBodyKeyDown} disabled={!open}>
|
||||
<KeydownBinder
|
||||
stopMetaPropagation
|
||||
onKeydown={_handleBodyKeyDown}
|
||||
disabled={!open}
|
||||
>
|
||||
<div
|
||||
style={style}
|
||||
className={classes}
|
||||
ref={this._setRef}
|
||||
onClick={this._handleClick}
|
||||
ref={dropdownContainerRef}
|
||||
onClick={_handleClick}
|
||||
tabIndex={-1}
|
||||
onMouseDown={Dropdown._handleMouseDown}
|
||||
onMouseDown={_handleMouseDown}
|
||||
>
|
||||
{finalChildren}
|
||||
{dropdownChildren}
|
||||
</div>
|
||||
</KeydownBinder>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Dropdown.displayName = 'Dropdown';
|
||||
|
@ -5,7 +5,7 @@ import { hotKeyRefs } from '../../../common/hotkeys';
|
||||
import { executeHotKey } from '../../../common/hotkeys-listener';
|
||||
import type { Environment } from '../../../models/environment';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownHint } from '../base/dropdown/dropdown-hint';
|
||||
@ -32,7 +32,7 @@ export const EnvironmentsDropdown: FC<Props> = ({
|
||||
hotKeyRegistry,
|
||||
workspace,
|
||||
}) => {
|
||||
const dropdownRef = useRef<Dropdown>(null);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
const handleShowEnvironmentModal = useCallback(() => {
|
||||
showModal(WorkspaceEnvironmentsEditModal, workspace);
|
||||
}, [workspace]);
|
||||
|
@ -21,7 +21,7 @@ import type {
|
||||
UpdateGitRepositoryCallback,
|
||||
} from '../../redux/modules/git';
|
||||
import * as gitActions from '../../redux/modules/git';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownItem } from '../base/dropdown/dropdown-item';
|
||||
@ -55,7 +55,7 @@ interface State {
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
class GitSyncDropdown extends PureComponent<Props, State> {
|
||||
_dropdown: Dropdown | null = null;
|
||||
_dropdown: DropdownHandle | null = null;
|
||||
|
||||
state: State = {
|
||||
initializing: false,
|
||||
@ -66,7 +66,7 @@ class GitSyncDropdown extends PureComponent<Props, State> {
|
||||
branches: [],
|
||||
};
|
||||
|
||||
_setDropdownRef(dropdown: Dropdown) {
|
||||
_setDropdownRef(dropdown: DropdownHandle) {
|
||||
this._dropdown = dropdown;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { forwardRef, useCallback, useState } from 'react';
|
||||
|
||||
import * as constants from '../../../common/constants';
|
||||
import { METHOD_GRPC } from '../../../common/constants';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownItem } from '../base/dropdown/dropdown-item';
|
||||
@ -19,7 +19,7 @@ interface Props {
|
||||
showGrpc?: boolean;
|
||||
}
|
||||
|
||||
export const MethodDropdown = forwardRef<Dropdown, Props>(({
|
||||
export const MethodDropdown = forwardRef<DropdownHandle, Props>(({
|
||||
className,
|
||||
method,
|
||||
onChange,
|
||||
|
@ -15,7 +15,7 @@ import { incrementDeletedRequests } from '../../../models/stats';
|
||||
import type { RequestAction } from '../../../plugins';
|
||||
import { getRequestActions } from '../../../plugins';
|
||||
import * as pluginContexts from '../../../plugins/context/index';
|
||||
import { Dropdown, DropdownProps } from '../base/dropdown/dropdown';
|
||||
import { type DropdownHandle, type DropdownProps, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownHint } from '../base/dropdown/dropdown-hint';
|
||||
@ -37,7 +37,7 @@ interface Props extends Pick<DropdownProps, 'right'> {
|
||||
requestGroup?: RequestGroup;
|
||||
}
|
||||
|
||||
export const RequestActionsDropdown = forwardRef<Dropdown, Props>(({
|
||||
export const RequestActionsDropdown = forwardRef<DropdownHandle, Props>(({
|
||||
activeEnvironment,
|
||||
activeProject,
|
||||
handleCopyAsCurl,
|
||||
@ -49,6 +49,7 @@ export const RequestActionsDropdown = forwardRef<Dropdown, Props>(({
|
||||
isPinned,
|
||||
request,
|
||||
requestGroup,
|
||||
right,
|
||||
}, ref) => {
|
||||
const [actionPlugins, setActionPlugins] = useState<RequestAction[]>([]);
|
||||
const [loadingActions, setLoadingActions] = useState<Record<string, boolean>>({});
|
||||
@ -109,7 +110,7 @@ export const RequestActionsDropdown = forwardRef<Dropdown, Props>(({
|
||||
// Can only generate code for regular requests, not gRPC requests
|
||||
const canGenerateCode = isRequest(request);
|
||||
return (
|
||||
<Dropdown ref={ref} onOpen={onOpen}>
|
||||
<Dropdown right={right} ref={ref} onOpen={onOpen}>
|
||||
<DropdownButton>
|
||||
<i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
|
@ -13,7 +13,7 @@ import * as pluginContexts from '../../../plugins/context/index';
|
||||
import { createRequest, CreateRequestType } from '../../hooks/create-request';
|
||||
import { createRequestGroup } from '../../hooks/create-request-group';
|
||||
import { selectActiveEnvironment, selectActiveProject, selectActiveWorkspace } from '../../redux/selectors';
|
||||
import { Dropdown, DropdownProps } from '../base/dropdown/dropdown';
|
||||
import { type DropdownHandle, type DropdownProps, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownHint } from '../base/dropdown/dropdown-hint';
|
||||
@ -42,7 +42,7 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
|
||||
}, ref) => {
|
||||
const [actionPlugins, setActionPlugins] = useState<RequestGroupAction[]>([]);
|
||||
const [loadingActions, setLoadingActions] = useState< Record<string, boolean>>({});
|
||||
const dropdownRef = useRef<Dropdown>(null);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
|
||||
const activeProject = useSelector(selectActiveProject);
|
||||
const activeEnvironment = useSelector(selectActiveEnvironment);
|
||||
|
@ -7,7 +7,7 @@ import { decompressObject } from '../../../common/misc';
|
||||
import type { Environment } from '../../../models/environment';
|
||||
import type { RequestVersion } from '../../../models/request-version';
|
||||
import type { Response } from '../../../models/response';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownItem } from '../base/dropdown/dropdown-item';
|
||||
@ -42,7 +42,7 @@ export const ResponseHistoryDropdown: FC<Props> = ({
|
||||
requestVersions,
|
||||
responses,
|
||||
}) => {
|
||||
const dropdownRef = useRef<Dropdown>(null);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
|
||||
const now = new Date();
|
||||
const categories: Record<string, Response[]> = {
|
||||
|
@ -14,7 +14,7 @@ import { ConfigGenerator, getConfigGenerators, getWorkspaceActions } from '../..
|
||||
import * as pluginContexts from '../../../plugins/context';
|
||||
import { selectIsLoading } from '../../redux/modules/global';
|
||||
import { selectActiveApiSpec, selectActiveEnvironment, selectActiveProject, selectActiveWorkspace, selectActiveWorkspaceName, selectSettings } from '../../redux/selectors';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownHint } from '../base/dropdown/dropdown-hint';
|
||||
@ -36,7 +36,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
const [actionPlugins, setActionPlugins] = useState<WorkspaceAction[]>([]);
|
||||
const [configGeneratorPlugins, setConfigGeneratorPlugins] = useState<ConfigGenerator[]>([]);
|
||||
const [loadingActions, setLoadingActions] = useState<Record<string, boolean>>({});
|
||||
const dropdownRef = useRef<Dropdown>(null);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
|
||||
const handlePluginClick = useCallback(async ({ action, plugin, label }: WorkspaceAction, workspace: Workspace) => {
|
||||
setLoadingActions({ ...loadingActions, [label]: true });
|
||||
|
@ -6,7 +6,7 @@ import { hotKeyRefs } from '../../common/hotkeys';
|
||||
import { executeHotKey } from '../../common/hotkeys-listener';
|
||||
import type { Request } from '../../models/request';
|
||||
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
|
||||
import { Dropdown } from './base/dropdown/dropdown';
|
||||
import { type DropdownHandle, Dropdown } from './base/dropdown/dropdown';
|
||||
import { DropdownButton } from './base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from './base/dropdown/dropdown-divider';
|
||||
import { DropdownHint } from './base/dropdown/dropdown-hint';
|
||||
@ -51,8 +51,8 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
uniquenessKey,
|
||||
}, ref) => {
|
||||
|
||||
const methodDropdownRef = useRef<Dropdown>(null);
|
||||
const dropdownRef = useRef<Dropdown>(null);
|
||||
const methodDropdownRef = useRef<DropdownHandle>(null);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
const inputRef = useRef<OneLineEditor>(null);
|
||||
|
||||
const focusInput = useCallback(() => {
|
||||
|
@ -13,7 +13,7 @@ import { RequestGroup } from '../../../models/request-group';
|
||||
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
|
||||
import { createRequest } from '../../hooks/create-request';
|
||||
import { selectActiveEnvironment, selectActiveProject, selectActiveWorkspace } from '../../redux/selectors';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import type { DropdownHandle } from '../base/dropdown/dropdown';
|
||||
import { Editable } from '../base/editable';
|
||||
import { Highlight } from '../base/highlight';
|
||||
import { RequestActionsDropdown } from '../dropdowns/request-actions-dropdown';
|
||||
@ -99,7 +99,7 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
|
||||
|
||||
const [renderedUrl, setRenderedUrl] = useState('');
|
||||
|
||||
const requestActionsDropdown = useRef<Dropdown>(null);
|
||||
const requestActionsDropdown = useRef<DropdownHandle>(null);
|
||||
|
||||
const handleShowRequestActions = useCallback((event: MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
|
Loading…
Reference in New Issue
Block a user