feat: add scroll indicators to dropdown menu content
This commit is contained in:
@@ -20,7 +20,7 @@ export function DesktopMenu({ menuActions, trigger }: DesktopMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="max-h-screen overflow-y-auto">
|
<DropdownMenuContent className="max-h-[50vh] overflow-y-auto">
|
||||||
{menuActions.map((action, index) => {
|
{menuActions.map((action, index) => {
|
||||||
const Icon = action.icon
|
const Icon = action.icon
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +32,10 @@ export function DesktopMenu({ menuActions, trigger }: DesktopMenuProps) {
|
|||||||
<Icon />
|
<Icon />
|
||||||
{action.label}
|
{action.label}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuSubContent className="max-h-screen overflow-y-auto">
|
<DropdownMenuSubContent
|
||||||
|
className="max-h-[50vh] overflow-y-auto"
|
||||||
|
showScrollButtons
|
||||||
|
>
|
||||||
{action.subMenu.map((subAction, subIndex) => (
|
{action.subMenu.map((subAction, subIndex) => (
|
||||||
<div key={subIndex}>
|
<div key={subIndex}>
|
||||||
{subAction.separator && subIndex > 0 && <DropdownMenuSeparator />}
|
{subAction.separator && subIndex > 0 && <DropdownMenuSeparator />}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export default function PostRelaySelector({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuContent align="start" className="max-w-96">
|
<DropdownMenuContent align="start" className="max-w-96 max-h-[50vh]" showScrollButtons>
|
||||||
{content}
|
{content}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
import { Check, ChevronRight, Circle } from 'lucide-react'
|
import { Check, ChevronDown, ChevronRight, ChevronUp, Circle } from 'lucide-react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -39,36 +39,175 @@ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayNam
|
|||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> & {
|
||||||
>(({ className, ...props }, ref) => (
|
showScrollButtons?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, showScrollButtons = true, ...props }, ref) => {
|
||||||
|
const [canScrollUp, setCanScrollUp] = React.useState(false)
|
||||||
|
const [canScrollDown, setCanScrollDown] = React.useState(false)
|
||||||
|
const contentRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
const scrollAreaRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => contentRef.current!)
|
||||||
|
|
||||||
|
const checkScrollability = React.useCallback(() => {
|
||||||
|
const scrollArea = scrollAreaRef.current
|
||||||
|
if (!scrollArea) return
|
||||||
|
|
||||||
|
setCanScrollUp(scrollArea.scrollTop > 0)
|
||||||
|
setCanScrollDown(scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollUp = () => {
|
||||||
|
scrollAreaRef.current?.scroll({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollDown = () => {
|
||||||
|
scrollAreaRef.current?.scroll({
|
||||||
|
top: scrollAreaRef.current.scrollHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={contentRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'relative z-50 min-w-52 overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
|
onAnimationEnd={() => {
|
||||||
|
if (showScrollButtons) {
|
||||||
|
checkScrollability()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
collisionPadding={10}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
))
|
{showScrollButtons && canScrollUp && (
|
||||||
|
<div className="absolute top-0 inset-x-0 z-10 flex items-center justify-center bg-popover">
|
||||||
|
<button
|
||||||
|
onClick={scrollUp}
|
||||||
|
onMouseEnter={scrollUp}
|
||||||
|
className="flex items-center justify-center w-full h-6 hover:bg-accent rounded-sm transition-colors"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={scrollAreaRef}
|
||||||
|
className={cn('p-1 overflow-y-auto', className)}
|
||||||
|
onScroll={checkScrollability}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showScrollButtons && canScrollDown && (
|
||||||
|
<div className="absolute bottom-0 inset-x-0 flex items-center justify-center bg-popover">
|
||||||
|
<button
|
||||||
|
onClick={scrollDown}
|
||||||
|
onMouseEnter={scrollDown}
|
||||||
|
className="flex items-center justify-center w-full h-6 hover:bg-accent rounded-sm transition-colors"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuPrimitive.SubContent>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
showScrollButtons?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, sideOffset = 4, showScrollButtons = false, ...props }, ref) => {
|
||||||
|
const [canScrollUp, setCanScrollUp] = React.useState(false)
|
||||||
|
const [canScrollDown, setCanScrollDown] = React.useState(false)
|
||||||
|
const contentRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
const scrollAreaRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => contentRef.current!)
|
||||||
|
|
||||||
|
const checkScrollability = React.useCallback(() => {
|
||||||
|
const scrollArea = scrollAreaRef.current
|
||||||
|
if (!scrollArea) return
|
||||||
|
|
||||||
|
setCanScrollUp(scrollArea.scrollTop > 0)
|
||||||
|
setCanScrollDown(scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollUp = () => {
|
||||||
|
scrollAreaRef.current?.scroll({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollDown = () => {
|
||||||
|
scrollAreaRef.current?.scroll({
|
||||||
|
top: scrollAreaRef.current.scrollHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={contentRef}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-52 overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'relative z-50 min-w-52 overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
|
onAnimationEnd={() => {
|
||||||
|
if (showScrollButtons) {
|
||||||
|
checkScrollability()
|
||||||
|
}
|
||||||
|
}}
|
||||||
collisionPadding={10}
|
collisionPadding={10}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{showScrollButtons && canScrollUp && (
|
||||||
|
<div className="absolute top-0 inset-x-0 z-10 flex items-center justify-center bg-popover">
|
||||||
|
<button
|
||||||
|
onClick={scrollUp}
|
||||||
|
onMouseEnter={scrollUp}
|
||||||
|
className="flex items-center justify-center w-full h-6 hover:bg-accent rounded-sm transition-colors"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={scrollAreaRef}
|
||||||
|
className={cn('p-1 overflow-y-auto', className)}
|
||||||
|
onScroll={checkScrollability}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showScrollButtons && canScrollDown && (
|
||||||
|
<div className="absolute bottom-0 inset-x-0 flex items-center justify-center bg-popover">
|
||||||
|
<button
|
||||||
|
onClick={scrollDown}
|
||||||
|
onMouseEnter={scrollDown}
|
||||||
|
className="flex items-center justify-center w-full h-6 hover:bg-accent rounded-sm transition-colors"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuPrimitive.Content>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
|||||||
Reference in New Issue
Block a user