| ---
|
| import { Icon } from "astro-icon/components";
|
| import DropdownItem from "@/components/common/DropdownItem.astro";
|
| import DropdownPanel from "@/components/common/DropdownPanel.astro";
|
| import { LinkPresets } from "@/constants/link-presets";
|
| import { LinkPreset, type NavBarLink } from "@/types/config";
|
| import { url } from "@/utils/url-utils";
|
|
|
| interface Props {
|
| link: NavBarLink;
|
| class?: string;
|
| }
|
|
|
| const { link, class: className } = Astro.props;
|
|
|
|
|
| if (!link) {
|
| return null;
|
| }
|
|
|
|
|
| const processedLink = {
|
| ...link,
|
| children: link.children
|
| ?.map((child: NavBarLink | LinkPreset): NavBarLink | null => {
|
| if (typeof child === "number") {
|
|
|
| if (child in LinkPresets) {
|
| return LinkPresets[child as LinkPreset];
|
| }
|
| return null;
|
| }
|
| return child;
|
| })
|
| .filter((child): child is NavBarLink => child !== null),
|
| };
|
|
|
| const hasChildren = processedLink.children && processedLink.children.length > 0;
|
| ---
|
|
|
| <div class:list={["dropdown-container", className]} data-dropdown>
|
| {hasChildren ? (
|
| <button
|
| class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95 dropdown-trigger"
|
| aria-expanded="false"
|
| aria-haspopup="true"
|
| data-dropdown-trigger
|
| >
|
| <div class="flex items-center">
|
| {processedLink.icon && <Icon name={processedLink.icon} class="text-[1.1rem] mr-2 navbar-icon" />}
|
| {processedLink.name}
|
| <Icon name="material-symbols:keyboard-arrow-down-rounded" class="text-[1.25rem] transition-transform duration-200 dropdown-arrow ml-1" />
|
| </div>
|
| </button>
|
| <div class="dropdown-menu" data-dropdown-menu>
|
| <DropdownPanel class="dropdown-content">
|
| {processedLink.children?.map((child, index) => (
|
| <DropdownItem
|
| href={child.external ? child.url : url(child.url)}
|
| target={child.external ? "_blank" : undefined}
|
| isLast={index === (processedLink.children?.length || 0) - 1}
|
| class="dropdown-item"
|
| >
|
| {child.icon && <Icon name={child.icon} class="text-[1.25rem] mr-3 navbar-icon" />}
|
| <span>{child.name}</span>
|
| {child.external && (
|
| <Icon name="fa7-solid:arrow-up-right-from-square" class="text-[0.75rem] text-black/25 dark:text-white/25 ml-auto" />
|
| )}
|
| </DropdownItem>
|
| ))}
|
| </DropdownPanel>
|
| </div>
|
| ) : (
|
| <a
|
| aria-label={processedLink.name}
|
| href={processedLink.external ? processedLink.url : url(processedLink.url)}
|
| target={processedLink.external ? "_blank" : null}
|
| class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95"
|
| >
|
| <div class="flex items-center">
|
| {processedLink.icon && <Icon name={processedLink.icon} class="text-[1.1rem] mr-2 navbar-icon" />}
|
| {processedLink.name}
|
| {processedLink.external && <Icon name="fa7-solid:arrow-up-right-from-square" class="text-[0.875rem] transition -translate-y-px ml-1 text-black/20 dark:text-white/20" />}
|
| </div>
|
| </a>
|
| )}
|
| </div>
|
|
|
| <style>
|
| @reference "../../styles/main.css";
|
|
|
| .dropdown-container {
|
| @apply relative;
|
| }
|
|
|
| .dropdown-menu {
|
| @apply absolute top-full left-0 pt-2 opacity-0 invisible pointer-events-none transition-all duration-200 ease-out transform translate-y-[-8px] z-50;
|
| }
|
|
|
| .dropdown-container:hover .dropdown-menu,
|
| .dropdown-container:focus-within .dropdown-menu {
|
| @apply opacity-100 visible pointer-events-auto translate-y-0;
|
| }
|
|
|
| .dropdown-container:hover .dropdown-arrow,
|
| .dropdown-container:focus-within .dropdown-arrow {
|
| @apply rotate-180;
|
| }
|
|
|
| .dropdown-content {
|
| @apply min-w-[12rem];
|
| }
|
|
|
|
|
| @media (max-width: 768px) {
|
| .dropdown-container {
|
| @apply hidden;
|
| }
|
| }
|
| </style>
|
|
|
| <script>
|
|
|
| document.addEventListener('DOMContentLoaded', function() {
|
| const dropdowns = document.querySelectorAll('[data-dropdown]');
|
|
|
| dropdowns.forEach(dropdown => {
|
| const trigger = dropdown.querySelector('[data-dropdown-trigger]') as HTMLElement | null;
|
| const menu = dropdown.querySelector('[data-dropdown-menu]');
|
| const items = dropdown.querySelectorAll('.dropdown-item') as NodeListOf<HTMLElement>;
|
|
|
| if (!trigger || !menu) return;
|
|
|
|
|
| trigger.addEventListener('keydown', function(e: KeyboardEvent) {
|
| if (e.key === 'Enter' || e.key === ' ') {
|
| e.preventDefault();
|
| toggleDropdown(dropdown, trigger, menu);
|
| } else if (e.key === 'ArrowDown') {
|
| e.preventDefault();
|
| openDropdown(dropdown, trigger, menu);
|
| if (items.length > 0) {
|
| items[0].focus();
|
| }
|
| } else if (e.key === 'Escape') {
|
| closeDropdown(dropdown, trigger, menu);
|
| }
|
| });
|
|
|
|
|
| items.forEach((item, index) => {
|
| item.addEventListener('keydown', function(e: KeyboardEvent) {
|
| if (e.key === 'ArrowDown') {
|
| e.preventDefault();
|
| const nextIndex = (index + 1) % items.length;
|
| items[nextIndex].focus();
|
| } else if (e.key === 'ArrowUp') {
|
| e.preventDefault();
|
| const prevIndex = (index - 1 + items.length) % items.length;
|
| items[prevIndex].focus();
|
| } else if (e.key === 'Escape') {
|
| closeDropdown(dropdown, trigger, menu);
|
| trigger.focus();
|
| }
|
| });
|
| });
|
| });
|
|
|
|
|
| document.addEventListener('click', function(e) {
|
| dropdowns.forEach(dropdown => {
|
| if (!dropdown.contains(e.target as Node)) {
|
| const trigger = dropdown.querySelector('[data-dropdown-trigger]');
|
| const menu = dropdown.querySelector('[data-dropdown-menu]');
|
| if (trigger && menu) {
|
| closeDropdown(dropdown, trigger, menu);
|
| }
|
| }
|
| });
|
| });
|
| });
|
|
|
| function toggleDropdown(_dropdown: Element, trigger: Element, menu: Element) {
|
| const isOpen = trigger.getAttribute('aria-expanded') === 'true';
|
| if (isOpen) {
|
| closeDropdown(_dropdown, trigger, menu);
|
| } else {
|
| openDropdown(_dropdown, trigger, menu);
|
| }
|
| }
|
|
|
| function openDropdown(_dropdown: Element, trigger: Element, menu: Element) {
|
| trigger.setAttribute('aria-expanded', 'true');
|
| menu.classList.remove('opacity-0', 'invisible', 'pointer-events-none', 'translate-y-[-8px]');
|
| menu.classList.add('opacity-100', 'visible', 'pointer-events-auto', 'translate-y-0');
|
| }
|
|
|
| function closeDropdown(_dropdown: Element, trigger: Element, menu: Element) {
|
| trigger.setAttribute('aria-expanded', 'false');
|
| menu.classList.add('opacity-0', 'invisible', 'pointer-events-none', 'translate-y-[-8px]');
|
| menu.classList.remove('opacity-100', 'visible', 'pointer-events-auto', 'translate-y-0');
|
| }
|
| </script> |