import React, { useState, useRef, useMemo, useCallback } from 'react'; import { IconCaretDown } from '@tabler/icons'; import MenuDropdown from 'ui/MenuDropdown'; import StyledWrapper from './StyledWrapper'; import { useTheme } from 'providers/Theme'; const STANDARD_METHODS = Object.freeze(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT']); const KEY = Object.freeze({ ENTER: 'Enter', ESCAPE: 'Escape' }); const DEFAULT_METHOD = 'GET'; const TriggerButton = ({ method, methodSpanRef, showCaret, ...props }) => { return ( ); }; const HttpMethodSelector = ({ method = DEFAULT_METHOD, onMethodSelect, showCaret = false }) => { const [isCustomMode, setIsCustomMode] = useState(false); const inputRef = useRef(); const selectedMethodRef = useRef(method); const methodSpanRef = useRef(); const [previousMethodWidth, setPreviousMethodWidth] = useState(null); const { theme } = useTheme(); const methodColor = useMemo(() => { const colorMap = { ...theme.request.methods, ...theme.request }; return colorMap[method.toLocaleLowerCase()]; }, [method, theme]); const blurInput = () => inputRef.current?.blur(); const handleInputChange = (e) => { const val = e.target.value.toUpperCase(); onMethodSelect(val); }; const handleMethodSelect = useCallback((verb) => { onMethodSelect(verb); selectedMethodRef.current = verb; setIsCustomMode(false); blurInput(); }, [onMethodSelect]); const handleBlur = (e) => { // Keep the current value when blurring let currentValue = ''; if (e.target.value && e.target.value.length > 0) { currentValue = e.target.value.toUpperCase(); selectedMethodRef.current = currentValue; } else { currentValue = selectedMethodRef.current; } onMethodSelect(currentValue); setIsCustomMode(false); }; const handleAddCustomMethod = useCallback(() => { // Capture the width of the current method span before switching to custom mode if (methodSpanRef.current) { setPreviousMethodWidth(methodSpanRef.current.offsetWidth); } setIsCustomMode(true); onMethodSelect(''); setTimeout(() => { inputRef.current?.focus(); inputRef.current?.select(); }, 0); }, [onMethodSelect]); const handleKeyDown = (e) => { switch (e.key) { case KEY.ESCAPE: { setIsCustomMode(false); blurInput(); e.preventDefault(); e.stopPropagation(); return; } case KEY.ENTER: { onMethodSelect(e.target.value ? e.target.value.toUpperCase() : DEFAULT_METHOD); setIsCustomMode(false); blurInput(); return; } default: { return; } } }; // Convert STANDARD_METHODS to MenuDropdown items format const menuItems = useMemo(() => { const items = STANDARD_METHODS.map((verb) => ({ id: verb.toLowerCase(), label: verb, onClick: () => handleMethodSelect(verb) })); // Add "Add Custom" item items.push({ id: 'add-custom', label: '+ Add Custom', onClick: handleAddCustomMethod, className: 'font-normal mt-1 text-link' }); return items; }, [handleMethodSelect, handleAddCustomMethod]); // Determine selected item ID (only if method is a standard method) const selectedItemId = useMemo(() => { if (isCustomMode || !STANDARD_METHODS.includes(method)) { return null; } return method.toLowerCase(); }, [method, isCustomMode]); // If in custom mode, render input field instead of dropdown if (isCustomMode) { // Calculate width based on content length (in ch units), clamped to max 16ch // Add 1ch for cursor space const contentWidth = Math.min(method.length + 1, 16); // Use previous method width as minimum, content-based width as current const minWidthPx = previousMethodWidth ? `${previousMethodWidth}px` : '5ch'; // Use calc to add padding space (px-2 = 0.5rem per side = 1rem total) to the ch width const currentWidth = `calc(${Math.max(contentWidth, 1)}ch + 1rem)`; return (
); } return (
); }; export default HttpMethodSelector;