Better dropdown separator

This commit is contained in:
Gregory Schier
2023-03-25 11:06:05 -07:00
parent 6dc7dc6ad2
commit 06349b8d5b
6 changed files with 73 additions and 61 deletions

View File

@@ -76,7 +76,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) {
label: viewMode === 'pretty' ? 'View Raw' : 'View Prettified', label: viewMode === 'pretty' ? 'View Raw' : 'View Prettified',
onSelect: toggleViewMode, onSelect: toggleViewMode,
}, },
'-----', { type: 'separator' },
{ {
label: 'Clear Response', label: 'Clear Response',
onSelect: deleteResponse.mutate, onSelect: deleteResponse.mutate,
@@ -88,7 +88,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) {
hidden: responses.length <= 1, hidden: responses.length <= 1,
disabled: responses.length === 0, disabled: responses.length === 0,
}, },
'-----', { type: 'separator' },
...responses.slice(0, 10).map((r) => ({ ...responses.slice(0, 10).map((r) => ({
label: r.status + ' - ' + r.elapsed + ' ms', label: r.status + ' - ' + r.elapsed + ' ms',
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>, leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>,

View File

@@ -74,7 +74,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
); );
const sidebarStyles = useMemo(() => ({ width: width.value }), [width.value]); const sidebarStyles = useMemo(() => ({ width: width.value }), [width.value]);
const sidebarWidth = width.value - 1; // Minus 1 for the border
return ( return (
<div className="relative"> <div className="relative">
@@ -89,6 +88,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
> >
<HStack as={WindowDragRegion} alignItems="center" justifyContent="end"> <HStack as={WindowDragRegion} alignItems="center" justifyContent="end">
<IconButton <IconButton
size="sm"
title="Add Request" title="Add Request"
className="mx-1" className="mx-1"
icon="plusCircle" icon="plusCircle"
@@ -101,12 +101,8 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
}} }}
/> />
</HStack> </HStack>
<VStack as="ul" className="relative py-3" draggable={false}> <VStack as="ul" className="relative py-3 overflow-auto" draggable={false}>
<SidebarItems <SidebarItems activeRequestId={activeRequest?.id} requests={requests} />
sidebarWidth={sidebarWidth}
activeRequestId={activeRequest?.id}
requests={requests}
/>
</VStack> </VStack>
<HStack className="mx-1 pb-1" alignItems="center" justifyContent="end"> <HStack className="mx-1 pb-1" alignItems="center" justifyContent="end">
<ToggleThemeButton /> <ToggleThemeButton />
@@ -119,11 +115,9 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
function SidebarItems({ function SidebarItems({
requests, requests,
activeRequestId, activeRequestId,
sidebarWidth,
}: { }: {
requests: HttpRequest[]; requests: HttpRequest[];
activeRequestId?: string; activeRequestId?: string;
sidebarWidth: number;
}) { }) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const updateRequest = useUpdateAnyRequest(); const updateRequest = useUpdateAnyRequest();
@@ -178,7 +172,6 @@ function SidebarItems({
requestName={r.name} requestName={r.name}
workspaceId={r.workspaceId} workspaceId={r.workspaceId}
active={r.id === activeRequestId} active={r.id === activeRequestId}
sidebarWidth={sidebarWidth}
onMove={handleMove} onMove={handleMove}
onEnd={handleEnd} onEnd={handleEnd}
/> />
@@ -194,12 +187,11 @@ type SidebarItemProps = {
requestId: string; requestId: string;
requestName: string; requestName: string;
workspaceId: string; workspaceId: string;
sidebarWidth: number;
active?: boolean; active?: boolean;
}; };
const _SidebarItem = forwardRef(function SidebarItem( const _SidebarItem = forwardRef(function SidebarItem(
{ className, requestName, requestId, workspaceId, active, sidebarWidth }: SidebarItemProps, { className, requestName, requestId, workspaceId, active }: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>, ref: ForwardedRef<HTMLLIElement>,
) { ) {
const updateRequest = useUpdateRequest(requestId); const updateRequest = useUpdateRequest(requestId);
@@ -215,7 +207,6 @@ const _SidebarItem = forwardRef(function SidebarItem(
el?.select(); el?.select();
}, []); }, []);
const itemStyles = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLElement>) => { (e: KeyboardEvent<HTMLElement>) => {
// Hitting enter on active request during keyboard nav will start edit // Hitting enter on active request during keyboard nav will start edit
@@ -242,12 +233,8 @@ const _SidebarItem = forwardRef(function SidebarItem(
); );
return ( return (
<li <li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}>
ref={ref} <div className="relative">
className={classnames(className, 'block group/item px-2 pb-0.5')}
style={itemStyles}
>
<div className="relative w-full">
<Button <Button
color="custom" color="custom"
size="sm" size="sm"
@@ -258,7 +245,6 @@ const _SidebarItem = forwardRef(function SidebarItem(
justify="start" justify="start"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={classnames( className={classnames(
'w-full',
editing && 'focus-within:border-focus', editing && 'focus-within:border-focus',
active active
? 'bg-highlight text-gray-900' ? 'bg-highlight text-gray-900'
@@ -315,7 +301,6 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
requestId, requestId,
workspaceId, workspaceId,
active, active,
sidebarWidth,
onMove, onMove,
onEnd, onEnd,
}: DraggableSidebarItemProps) { }: DraggableSidebarItemProps) {
@@ -358,7 +343,6 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
requestId={requestId} requestId={requestId}
workspaceId={workspaceId} workspaceId={workspaceId}
active={active} active={active}
sidebarWidth={sidebarWidth}
/> />
); );
}); });

View File

@@ -1,8 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { act } from 'react-dom/test-utils';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace'; import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace'; import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { useRoutes } from '../hooks/useRoutes'; import { useRoutes } from '../hooks/useRoutes';
@@ -36,17 +34,24 @@ export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }:
return [ return [
...workspaceItems, ...workspaceItems,
'-----', {
type: 'separator',
label: activeWorkspace?.name,
},
{
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteWorkspace.mutate(),
},
{
type: 'separator',
label: 'Actions',
},
{ {
label: 'New Workspace', label: 'New Workspace',
leftSlot: <Icon icon="plus" />, leftSlot: <Icon icon="plus" />,
onSelect: () => createWorkspace.mutate({ name: 'New Workspace' }), onSelect: () => createWorkspace.mutate({ name: 'New Workspace' }),
}, },
{
label: 'Delete Workspace',
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteWorkspace.mutate(),
},
]; ];
}, [workspaces, activeWorkspaceId]); }, [workspaces, activeWorkspaceId]);

View File

@@ -8,6 +8,7 @@ import { VStack } from './Stacks';
export type DropdownItem = export type DropdownItem =
| { | {
type?: 'default';
label: string; label: string;
disabled?: boolean; disabled?: boolean;
hidden?: boolean; hidden?: boolean;
@@ -15,7 +16,10 @@ export type DropdownItem =
rightSlot?: ReactNode; rightSlot?: ReactNode;
onSelect?: () => void; onSelect?: () => void;
} }
| '-----'; | {
type: 'separator';
label?: string;
};
export interface DropdownProps { export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>; children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
@@ -93,7 +97,7 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
let nextIndex = (currIndex ?? 0) - 1; let nextIndex = (currIndex ?? 0) - 1;
const maxTries = items.length; const maxTries = items.length;
for (let i = 0; i < maxTries; i++) { for (let i = 0; i < maxTries; i++) {
if (items[nextIndex] === '-----') { if (items[nextIndex]?.type === 'separator') {
nextIndex--; nextIndex--;
} else if (nextIndex < 0) { } else if (nextIndex < 0) {
nextIndex = items.length - 1; nextIndex = items.length - 1;
@@ -110,7 +114,7 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
let nextIndex = (currIndex ?? -1) + 1; let nextIndex = (currIndex ?? -1) + 1;
const maxTries = items.length; const maxTries = items.length;
for (let i = 0; i < maxTries; i++) { for (let i = 0; i < maxTries; i++) {
if (items[nextIndex] === '-----') { if (items[nextIndex]?.type === 'separator') {
nextIndex++; nextIndex++;
} else if (nextIndex >= items.length) { } else if (nextIndex >= items.length) {
nextIndex = 0; nextIndex = 0;
@@ -122,26 +126,29 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
}); });
}); });
const containerStyles: CSSProperties = useMemo(() => { const { containerStyles, triangleStyles } = useMemo<{
containerStyles: CSSProperties;
triangleStyles: CSSProperties;
}>(() => {
const docWidth = document.documentElement.getBoundingClientRect().width; const docWidth = document.documentElement.getBoundingClientRect().width;
const spaceRemaining = docWidth - triggerRect.left; const spaceRemaining = docWidth - triggerRect.left;
if (spaceRemaining < 200) { const top = triggerRect?.bottom + 5;
return { const onRight = spaceRemaining < 200;
top: triggerRect?.bottom, const containerStyles = onRight
right: 0, ? { top, right: docWidth - triggerRect?.right }
}; : { top, left: triggerRect?.left };
} const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
return { const triangleStyles = onRight
top: triggerRect?.bottom, ? { right: triggerRect.width / 2, marginRight: '-0.2rem', ...size }
left: triggerRect?.left, : { left: triggerRect.width / 2, marginLeft: '-0.2rem', ...size };
}; return { containerStyles, triangleStyles };
}, [triggerRect]); }, [triggerRect]);
const handleSelect = useCallback( const handleSelect = useCallback(
(i: DropdownItem) => { (i: DropdownItem) => {
onClose(); onClose();
setSelectedIndex(null); setSelectedIndex(null);
if (i !== '-----') { if (i.type !== 'separator') {
i.onSelect?.(); i.onSelect?.();
} }
}, },
@@ -160,6 +167,11 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
style={containerStyles} style={containerStyles}
className={classnames(className, 'pointer-events-auto fixed z-50')} className={classnames(className, 'pointer-events-auto fixed z-50')}
> >
<span
style={triangleStyles}
aria-hidden
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
/>
{containerStyles && ( {containerStyles && (
<VStack <VStack
space={0.5} space={0.5}
@@ -169,11 +181,12 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
className={classnames( className={classnames(
className, className,
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border', 'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
'border-gray-200 overflow-auto m-1', 'border-gray-200 overflow-auto mb-1 mx-0.5',
)} )}
> >
{items.map((item, i) => { {items.map((item, i) => {
if (item === '-----') return <Separator key={i} className="my-1.5" />; if (item.type === 'separator')
return <Separator key={i} className="my-1.5" label={item.label} />;
if (item.hidden) return null; if (item.hidden) return null;
return ( return (
<MenuItem <MenuItem
@@ -211,7 +224,7 @@ function MenuItem({ className, focused, item, onSelect, ...props }: MenuItemProp
[focused], [focused],
); );
if (item === '-----') return <Separator className="my-1.5" />; if (item.type === 'separator') return <Separator className="my-1.5" />;
return ( return (
<button <button

View File

@@ -4,19 +4,26 @@ interface Props {
orientation?: 'horizontal' | 'vertical'; orientation?: 'horizontal' | 'vertical';
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
className?: string; className?: string;
label?: string;
} }
export function Separator({ className, variant = 'primary', orientation = 'horizontal' }: Props) { export function Separator({
className,
variant = 'primary',
orientation = 'horizontal',
label,
}: Props) {
return ( return (
<div <div role="separator" className={classnames(className, 'flex items-center')}>
role="separator" {label && <div className="text-xs text-gray-500 mx-2 whitespace-nowrap">{label}</div>}
className={classnames( <div
className, className={classnames(
variant === 'primary' && 'bg-highlight', variant === 'primary' && 'bg-highlight',
variant === 'secondary' && 'bg-highlightSecondary', variant === 'secondary' && 'bg-highlightSecondary',
orientation === 'horizontal' && 'w-full h-[1px]', orientation === 'horizontal' && 'w-full h-[1px]',
orientation === 'vertical' && 'h-full w-[1px]', orientation === 'vertical' && 'h-full w-[1px]',
)} )}
/> />
</div>
); );
} }

View File

@@ -10,6 +10,9 @@ module.exports = {
opacity: { opacity: {
"disabled": "0.3" "disabled": "0.3"
}, },
fontSize: {
"xs": "0.8rem"
},
height: { height: {
"xs": "1.5rem", "xs": "1.5rem",
"sm": "2.00rem", "sm": "2.00rem",