Skip to content

Instantly share code, notes, and snippets.

@leidison
Created February 5, 2025 22:43
Show Gist options
  • Save leidison/bcfd9c164f59ebb9fe35d9a558938c56 to your computer and use it in GitHub Desktop.
Save leidison/bcfd9c164f59ebb9fe35d9a558938c56 to your computer and use it in GitHub Desktop.
Apresentando tabs ocultas em dropdown com Shadcn
import ErrorDisplay from '@/base/components/error-display'
import { Loading } from '@/base/components/loading'
import { NavigationHistory } from '@/base/components/navigation-history'
import { NavigationTabs } from '@/base/components/navigation-tabs'
import { PageHeader } from '@/base/components/page-header'
import { useFindOneEmployee } from '@/base/http/generated'
import { useEmployee } from '@/employee/context/employee'
import { useEffect, useMemo } from 'react'
import { Outlet, useParams } from 'react-router'
export function Component() {
const { employeeId } = useParams()
const { employee, setEmployee } = useEmployee()
const { data, error, isLoading, refetch }: any = useFindOneEmployee(
employeeId!,
)
const tabs = useMemo(
() => [
{ title: 'Dados', to: `/colaboradores/${employeeId}` },
{
title: 'Documentos',
to: `/colaboradores/${employeeId}/documentos`,
},
{ title: 'Unidades', to: `/colaboradores/${employeeId}/unidades` },
{ title: 'Escala', to: `/colaboradores/${employeeId}/escala` },
],
[employeeId],
)
useEffect(() => {
setEmployee(data)
}, [data, setEmployee])
if (error) {
return <ErrorDisplay error={error} refetch={refetch} />
}
if (isLoading || !employee) {
return (
<div className="flex justify-center items-center h-full">
<Loading />
</div>
)
}
return (
<>
<NavigationHistory
items={[
{ title: 'Listagem de colaboradores', to: '/colaboradores' },
{ title: 'Editar' },
]}
/>
<PageHeader title={employee.name + ''} descriptionElipsis={true}>
{employee?.data?.description}
</PageHeader>
<NavigationTabs items={tabs} />
<Outlet />
</>
)
}
import { Tabs, TabsList, TabsTrigger } from '@/base/components/ui/tabs'
import clsx from 'clsx'
import { CheckIcon, MoreVerticalIcon } from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Link, useLocation } from 'react-router'
import { Button } from './ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from './ui/dropdown-menu'
type Item = { title: string; to: string }
type Props = {
items: Item[]
}
export function NavigationTabs({ items }: Props) {
const location = useLocation()
const containerRef = useRef<HTMLDivElement>(null)
const [hiddenItemsIndex, setHiddenItemsIndex] = useState<number[]>([])
const hiddenItems = useMemo(
() => hiddenItemsIndex.map((index) => items[index]),
[hiddenItemsIndex],
)
const showMoreButton = useMemo(() => {
return !!hiddenItemsIndex.length
}, [hiddenItemsIndex])
const active = useMemo(() => {
return [...items]
.sort((a, b) => b.to.length - a.to.length)
.find((item) => location.pathname.startsWith(item.to))?.to
}, [items, location.pathname])
const checkVisibility = () => {
const container = containerRef.current
if (!container) return
const containerRect = container.getBoundingClientRect()
const buttons = container.querySelectorAll('a')
const notFullyVisible: number[] = []
buttons.forEach((button, index) => {
const buttonRect = button.getBoundingClientRect()
// Verifica se o botão está completamente fora do container
if (
// Botão ultrapassa à direita
buttonRect.right > containerRect.right ||
// Botão ultrapassa à esquerda
buttonRect.left < containerRect.left
) {
notFullyVisible.push(index)
}
})
setHiddenItemsIndex(notFullyVisible)
}
const scrollToActiveTab = () => {
const container = containerRef.current
if (!container) return
const activeButton = container.querySelector(
'a[data-selected="true"]',
) as HTMLElement | null
if (activeButton) {
// Calcula a posição do botão ativo em relação ao container
const buttonRect = activeButton.getBoundingClientRect()
const containerRect = container.getBoundingClientRect()
// Calcula a posição central do botão em relação ao container
const buttonCenter = buttonRect.left + buttonRect.width / 2
const containerCenter = containerRect.left + containerRect.width / 2
// Scroll para centralizar o botão ativo
container.scrollTo({
left: container.scrollLeft + (buttonCenter - containerCenter), // Centraliza o botão
behavior: 'smooth',
})
}
}
useEffect(() => {
const container = containerRef.current
if (!container) return
// Verificar a visibilidade ao montar, ao redimensionar e ao scrollar
checkVisibility()
window.addEventListener('resize', checkVisibility)
container.addEventListener('scroll', checkVisibility)
// Rolar até a tab ativa ao carregar a tela
scrollToActiveTab()
// Limpar os listeners ao desmontar o componente
return () => {
window.removeEventListener('resize', checkVisibility)
container.removeEventListener('scroll', checkVisibility)
}
}, [items, location.pathname])
return (
<div className="flex flex-row ">
<div
ref={containerRef}
className="flex-1 relative scroll-x-invisible h-12"
>
<Tabs defaultValue={active} value={active} className="absolute">
<TabsList>
{items.map((item, index) => (
<TabsTrigger
key={item.to}
value={item.to}
asChild
className={clsx({
dark: active,
'opacity-50': hiddenItemsIndex.includes(index),
})}
>
<Link
data-index={index}
data-selected={item.to === active}
to={item.to}
>
{item.title}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
{showMoreButton && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="link" size="icon">
<MoreVerticalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Outros menus</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{hiddenItems.map((item) => (
<DropdownMenuItem key={item.to} asChild>
<Link to={item.to}>
{item.title}
{item.to === active && (
<DropdownMenuShortcut>
<CheckIcon />
</DropdownMenuShortcut>
)}
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
)
}
@leidison
Copy link
Author

leidison commented Feb 5, 2025

Exemplo não expandido (mobile)

image

Exemplo expandido (mobile)

image

Exemplo com todos a mostra

image

OBS

O scroll horizontal continua funcionando.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment