Dropdown scrolling

This commit is contained in:
Gregory Schier
2023-02-25 23:33:07 -08:00
parent bf691017fd
commit 640d2a1bb4
3 changed files with 81 additions and 25 deletions

View File

@@ -2,7 +2,16 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { DropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu'; import { DropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { CheckIcon } from '@radix-ui/react-icons'; import { CheckIcon } from '@radix-ui/react-icons';
import { forwardRef, HTMLAttributes, ReactNode } from 'react'; import {
ForwardedRef,
forwardRef,
HTMLAttributes,
ReactNode,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
interface DropdownMenuRadioProps { interface DropdownMenuRadioProps {
@@ -51,11 +60,15 @@ export function DropdownMenuRadio({
export interface DropdownProps { export interface DropdownProps {
children: ReactNode; children: ReactNode;
items: { items: (
label: string; | {
onSelect?: () => void; label: string;
disabled?: boolean; onSelect?: () => void;
}[]; disabled?: boolean;
leftSlot?: ReactNode;
}
| '-----'
)[];
} }
export function Dropdown({ children, items }: DropdownProps) { export function Dropdown({ children, items }: DropdownProps) {
@@ -64,11 +77,22 @@ export function Dropdown({ children, items }: DropdownProps) {
<DropdownMenuTrigger>{children}</DropdownMenuTrigger> <DropdownMenuTrigger>{children}</DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
{items.map((item, i) => ( {items.map((item, i) => {
<DropdownMenuItem key={i} onSelect={() => item.onSelect?.()} disabled={item.disabled}> if (item === '-----') {
{item.label} return <DropdownMenuSeparator key={i} />;
</DropdownMenuItem> } else {
))} return (
<DropdownMenuItem
key={i}
onSelect={() => item.onSelect?.()}
disabled={item.disabled}
leftSlot={item.leftSlot}
>
{item.label}
</DropdownMenuItem>
);
}
})}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenuPortal> </DropdownMenuPortal>
</DropdownMenu.Root> </DropdownMenu.Root>
@@ -94,13 +118,25 @@ function DropdownMenuPortal({ children }: DropdownMenuPortalProps) {
const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenuContentProps>( const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenuContentProps>(
function DropdownMenuContent( function DropdownMenuContent(
{ className, children, ...props }: DropdownMenu.DropdownMenuContentProps, { className, children, ...props }: DropdownMenu.DropdownMenuContentProps,
ref, ref: ForwardedRef<HTMLDivElement>,
) { ) {
const divRef = useRef<HTMLDivElement>(null);
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(ref, () => divRef.current);
// Calculate the max height so we can scroll
const styles = useMemo(() => {
if (divRef.current === null) return;
const windowBox = document.documentElement.getBoundingClientRect();
const menuBox = divRef.current.getBoundingClientRect();
return { maxHeight: windowBox.height - menuBox.top - 5 };
}, [divRef.current]);
return ( return (
<DropdownMenu.Content <DropdownMenu.Content
ref={ref} ref={divRef}
align="start" align="start"
className={classnames(className, dropdownMenuClasses, 'm-0.5')} className={classnames(className, dropdownMenuClasses, 'overflow-auto m-1')}
style={styles}
{...props} {...props}
> >
{children} {children}
@@ -216,14 +252,14 @@ function DropdownMenuLabel({ className, children, ...props }: DropdownMenu.Dropd
); );
} }
// function DropdownMenuSeparator({ className, ...props }: DropdownMenu.DropdownMenuSeparatorProps) { function DropdownMenuSeparator({ className, ...props }: DropdownMenu.DropdownMenuSeparatorProps) {
// return ( return (
// <DropdownMenu.Separator <DropdownMenu.Separator
// className={classnames(className, 'h-[1px] bg-gray-400 bg-opacity-30 my-1')} className={classnames(className, 'h-[1px] bg-gray-400 bg-opacity-30 my-1')}
// {...props} {...props}
// /> />
// ); );
// } }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
children, children,

View File

@@ -1,10 +1,12 @@
import { import {
ArchiveIcon, ArchiveIcon,
CameraIcon, CameraIcon,
CheckIcon,
GearIcon, GearIcon,
HomeIcon, HomeIcon,
MoonIcon, MoonIcon,
PaperPlaneIcon, PaperPlaneIcon,
QuestionMarkIcon,
SunIcon, SunIcon,
TriangleDownIcon, TriangleDownIcon,
UpdateIcon, UpdateIcon,
@@ -20,6 +22,8 @@ type IconName =
| 'triangle-down' | 'triangle-down'
| 'paper-plane' | 'paper-plane'
| 'update' | 'update'
| 'question'
| 'check'
| 'sun' | 'sun'
| 'moon'; | 'moon';
@@ -28,11 +32,13 @@ const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
'triangle-down': TriangleDownIcon, 'triangle-down': TriangleDownIcon,
archive: ArchiveIcon, archive: ArchiveIcon,
camera: CameraIcon, camera: CameraIcon,
check: CheckIcon,
gear: GearIcon, gear: GearIcon,
home: HomeIcon, home: HomeIcon,
update: UpdateIcon, update: UpdateIcon,
sun: SunIcon, sun: SunIcon,
moon: MoonIcon, moon: MoonIcon,
question: QuestionMarkIcon,
}; };
export interface IconProps { export interface IconProps {
@@ -43,7 +49,7 @@ export interface IconProps {
} }
export function Icon({ icon, spin, size = 'md', className }: IconProps) { export function Icon({ icon, spin, size = 'md', className }: IconProps) {
const Component = icons[icon]; const Component = icons[icon] ?? icons.question;
return ( return (
<Component <Component
className={classnames( className={classnames(

View File

@@ -2,10 +2,11 @@ import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { HStack, VStack } from './Stacks'; import { HStack, VStack } from './Stacks';
import Editor from './Editor/Editor'; import Editor from './Editor/Editor';
import { useMemo } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { WindowDragRegion } from './WindowDragRegion'; import { WindowDragRegion } from './WindowDragRegion';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { Icon } from './Icon';
interface Props { interface Props {
requestId: string; requestId: string;
@@ -13,11 +14,18 @@ interface Props {
} }
export function ResponsePane({ requestId, error }: Props) { export function ResponsePane({ requestId, error }: Props) {
const [activeResponseId, setActiveResponseId] = useState<string | null>(null);
const responses = useResponses(requestId); const responses = useResponses(requestId);
const response = responses.data[0]; const response = activeResponseId
? responses.data.find((r) => r.id === activeResponseId)
: responses.data[0];
const deleteResponse = useDeleteResponse(response); const deleteResponse = useDeleteResponse(response);
const deleteAllResponses = useDeleteAllResponses(response?.requestId); const deleteAllResponses = useDeleteAllResponses(response?.requestId);
useEffect(() => {
setActiveResponseId(null);
}, [responses.data?.length]);
const contentType = useMemo( const contentType = useMemo(
() => () =>
response?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? 'text/plain', response?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? 'text/plain',
@@ -47,6 +55,12 @@ export function ResponsePane({ requestId, error }: Props) {
onSelect: deleteAllResponses.mutate, onSelect: deleteAllResponses.mutate,
disabled: responses.data.length === 0, disabled: responses.data.length === 0,
}, },
'-----',
...responses.data.map((r) => ({
label: r.status + ' - ' + r.elapsed,
leftSlot: response?.id === r.id ? <Icon icon="check" /> : <></>,
onSelect: () => setActiveResponseId(r.id),
})),
]} ]}
> >
<IconButton icon="gear" className="ml-auto" size="sm" /> <IconButton icon="gear" className="ml-auto" size="sm" />