Skip to main content
Glama
index.tsx7.45 kB
import type { FC, HTMLAttributes } from 'react'; import { cn } from '../../utils/cn'; import { Button, type ButtonProps } from '../Button'; import { MaxHeightSmoother } from '../MaxHeightSmoother'; /** * Props for the DropDown component */ export interface DropDownProps extends HTMLAttributes<HTMLDivElement> { /** * Unique identifier that links the trigger and panel for accessibility. * This is used to generate proper ARIA attributes. * @example "user-menu" * @example "language-selector" */ identifier: string; } export type DropDownType = FC<DropDownProps> & { Trigger: FC<TriggerProps>; Panel: FC<PanelProps>; }; /** * DropDown Component * * A compound component that provides dropdown/popover functionality with flexible trigger mechanisms. * Supports hover, focus, and controlled visibility states with proper accessibility features. * * @example * ```tsx * // Basic hover dropdown * <DropDown identifier="menu"> * <DropDown.Trigger identifier="menu"> * Open Menu * </DropDown.Trigger> * <DropDown.Panel identifier="menu" isOverable> * <div>Menu content</div> * </DropDown.Panel> * </DropDown> * * // Focus-based dropdown for accessibility * <DropDown identifier="accessible-menu"> * <DropDown.Trigger identifier="accessible-menu"> * Keyboard Accessible Menu * </DropDown.Trigger> * <DropDown.Panel identifier="accessible-menu" isFocusable> * <div>Accessible content</div> * </DropDown.Panel> * </DropDown> * * // Controlled dropdown * <DropDown identifier="controlled"> * <DropDown.Trigger identifier="controlled"> * Controlled Menu * </DropDown.Trigger> * <DropDown.Panel identifier="controlled" isHidden={!isOpen}> * <div>Controlled content</div> * </DropDown.Panel> * </DropDown> * ``` * * @component * @accessibility * - Uses proper ARIA attributes (aria-haspopup, aria-labelledby, etc.) * - Supports keyboard navigation with focus management * - Screen reader compatible with proper role and labeling * - Maintains focus trap within dropdown when needed */ export const DropDown: DropDownType = ({ children, className, identifier, ...props }) => ( <div className={cn(`group/dropdown relative flex`, className)} aria-label={`DropDown ${identifier}`} id={`dropdown-container-${identifier}`} {...props} > {children} </div> ); /** * Props for the DropDown.Trigger component */ export interface TriggerProps extends Partial<ButtonProps> { /** * Unique identifier that matches the parent DropDown identifier * @example "user-menu" */ identifier: string; } /** * DropDown.Trigger Component * * The clickable/focusable element that controls the dropdown panel visibility. * Built on top of the Button component with enhanced dropdown-specific behaviors. * * @example * ```tsx * <DropDown.Trigger identifier="menu"> * <div>Click to open</div> * </DropDown.Trigger> * ``` * * @component * @accessibility * - Automatically generates appropriate ARIA attributes * - Maintains proper focus management across browsers * - Works with keyboard navigation (Tab, Enter, Space) * - Announces dropdown state to screen readers * * @note Don't nest Button components inside the Trigger - it's already a button */ const Trigger: FC<TriggerProps> = ({ children, identifier, className, label, ...props }) => ( <Button className={cn([ 'w-full cursor-pointer', 'group-focus-within/dropdown:bg-current/20 group-focus-within/dropdown:ring-4', className, ])} label={label ?? `Open panel ${identifier}`} aria-haspopup="true" aria-controls={`dropdown-panel-${identifier}`} id={`dropdown-trigger-${identifier}`} onClick={(e) => { // Ensure focus behavior is consistent across all mobile browsers (e.currentTarget as HTMLButtonElement).focus(); }} variant="none" {...props} > {children} </Button> ); /** * Alignment options for the dropdown panel relative to the trigger */ export enum DropDownAlign { /** Align panel to the start (left in LTR, right in RTL) of the trigger */ START = 'start', /** Align panel to the end (right in LTR, left in RTL) of the trigger */ END = 'end', } /** * Props for the DropDown.Panel component */ export interface PanelProps extends HTMLAttributes<HTMLDivElement> { /** * Whether the panel should be visible when the trigger is focused. * Enables keyboard accessibility for the dropdown. * @default false */ isFocusable?: boolean; /** * Controls panel visibility explicitly. * - `true`: Panel is hidden * - `false`: Panel is visible * - `undefined`: Panel visibility controlled by hover/focus states * @default undefined */ isHidden?: boolean; /** * Whether the panel should be visible when hovering over the trigger. * Provides quick access via mouse interaction. * @default false */ isOverable?: boolean; /** * Unique identifier that matches the parent DropDown identifier * @example "user-menu" */ identifier: string; /** * Horizontal alignment of the panel relative to the trigger * @default DropDownAlign.START */ align?: DropDownAlign | `${DropDownAlign}`; } /** * DropDown.Panel Component * * The content area that appears when the dropdown is triggered. * Supports multiple trigger methods (hover, focus, controlled) with smooth animations. * * @example * ```tsx * // Hover-triggered panel * <DropDown.Panel identifier="menu" isOverable> * <div>Content appears on hover</div> * </DropDown.Panel> * * // Focus-triggered panel (accessible) * <DropDown.Panel identifier="menu" isFocusable> * <div>Content appears on focus</div> * </DropDown.Panel> * * // Controlled panel * <DropDown.Panel identifier="menu" isHidden={!isOpen}> * <div>Content visibility controlled externally</div> * </DropDown.Panel> * * // Right-aligned panel * <DropDown.Panel identifier="menu" align={DropDownAlign.END} isOverable> * <div>Right-aligned content</div> * </DropDown.Panel> * ``` * * @component * @accessibility * - Proper ARIA attributes (role, aria-labelledby, aria-hidden) * - Smooth height transitions with MaxHeightSmoother * - Keyboard navigation support when isFocusable is enabled * - Screen reader announcements for state changes */ const Panel: FC<PanelProps> = ({ children, isHidden = undefined, isOverable = false, isFocusable = false, align = DropDownAlign.START, identifier, className, ...props }) => ( <div className={cn( 'absolute top-[calc(100%+0.5rem)] z-100 min-w-full', align === DropDownAlign.START && 'left-0', align === DropDownAlign.END && 'right-0', className )} aria-hidden={isHidden} role="region" aria-labelledby={`dropdown-trigger-${identifier}`} id={`dropdown-panel-${identifier}`} > <MaxHeightSmoother isHidden={isHidden} className={cn( 'overflow-x-visible', isHidden === false && 'invisible', isHidden === true && 'visible', isOverable && 'group-hover/dropdown:visible group-hover/dropdown:grid-rows-[1fr]', isFocusable && 'group-focus-within/dropdown:visible group-focus-within/dropdown:grid-rows-[1fr]' )} {...props} > {children} </MaxHeightSmoother> </div> ); DropDown.Trigger = Trigger; DropDown.Panel = Panel;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server