Dropdown 下拉菜单
展示用户可选择的操作或选项列表
用法
import { Dropdown } from '@lenso/ui';此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 dropdown-default (Apache-2.0).import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup } from "./_shared";
export function Default() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Copy link" onClick={() => console.log("Selected: copy-link")} /> <ActionItem label="Edit file" onClick={() => console.log("Selected: edit-file")} /> <ActionItem label="Delete file" variant="danger" onClick={() => console.log("Selected: delete-file")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
组件结构
import { Dropdown, Button, Label, Description, Header, Kbd, Separator } from '@lenso/ui';
export default () => ( <Dropdown> <Dropdown.Trigger> <Button /> </Dropdown.Trigger> <Dropdown.Popover> <Dropdown.Menu> <Dropdown.Item> <Label /> <Description /> <Kbd slot="keyboard" /> <Dropdown.ItemIndicator /> </Dropdown.Item> <Separator /> <Dropdown.Section> <Header /> <Dropdown.Item /> </Dropdown.Section> <Dropdown.SubmenuTrigger> <Dropdown.Item> <Label /> <Dropdown.SubmenuIndicator /> </Dropdown.Item> <Dropdown.Popover> <Dropdown.Menu> <Dropdown.Item /> </Dropdown.Menu> </Dropdown.Popover> </Dropdown.SubmenuTrigger> </Dropdown.Menu> </Dropdown.Popover> </Dropdown>)示例
带图标
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-icons, Apache-2.0.import { FloppyDisk, FolderOpen, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithIcons() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Open file" icon={<FolderOpen {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="O" />} onClick={() => console.log("Selected: open-file")} /> <ActionItem label="Save file" icon={<FloppyDisk {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="S" />} onClick={() => console.log("Selected: save-file")} /> <ActionItem label="Delete file" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
带描述
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-descriptions, Apache-2.0.import { FloppyDisk, FolderOpen, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithDescriptions() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" description="Create a new file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Open file" description="Open an existing file" icon={<FolderOpen {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="O" />} onClick={() => console.log("Selected: open-file")} /> <ActionItem label="Save file" description="Save the current file" icon={<FloppyDisk {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="S" />} onClick={() => console.log("Selected: save-file")} /> <ActionItem label="Delete file" description="Move to trash" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
含禁用项
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-disabled-items, Apache-2.0.import { Bars, Pencil, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithDisabledItems() { return ( <Dropdown> <Dropdown.Trigger render={<Button isIconOnly aria-label="Menu" variant="secondary" />}> <Bars /> </Dropdown.Trigger> <Popup xstyle={styles.disabledWidth}> <Dropdown.Section> <Dropdown.Section.Label>Actions</Dropdown.Section.Label> <ActionItem label="New file" description="Create a new file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Edit file" description="Make changes" icon={<Pencil {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="E" />} onClick={() => console.log("Selected: edit-file")} /> </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Danger zone</Dropdown.Section.Label> <ActionItem disabled label="Delete file" description="Move to trash" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Dropdown.Section> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
分组选项
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-sections, Apache-2.0.import { EllipsisVertical, Pencil, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithSections() { return ( <Dropdown> <Dropdown.Trigger render={<Button isIconOnly aria-label="Menu" variant="secondary" />}> <EllipsisVertical /> </Dropdown.Trigger> <Popup> <Dropdown.Section> <Dropdown.Section.Label>Actions</Dropdown.Section.Label> <ActionItem label="New file" description="Create a new file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Edit file" description="Make changes" icon={<Pencil {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="E" />} onClick={() => console.log("Selected: edit-file")} /> </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Danger zone</Dropdown.Section.Label> <ActionItem label="Delete file" description="Move to trash" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Dropdown.Section> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
多选
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-multiple-selection, Apache-2.0.import { useState } from "react";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Checkmark, Popup, styles } from "./_shared";
export function WithMultipleSelection() { const [selected, setSelected] = useState(new Set(["apple"])); const [open, setOpen] = useState(false); const fruit = (value: string, label: string) => ( <Dropdown.CheckboxItem key={value} checked={selected.has(value)} closeOnClick={false} onKeyDown={(event) => { if (event.key === "Enter") setOpen(false); }} onCheckedChange={(checked) => setSelected((previous) => { const next = new Set(previous); if (checked) next.add(value); else next.delete(value); return next; }) } > <Dropdown.CheckboxItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.CheckboxItemIndicator> <MenuItem.Label>{label}</MenuItem.Label> </Dropdown.CheckboxItem> ); return ( <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Preferred Fruits </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.Section> <Dropdown.Section.Label>Select a fruit</Dropdown.Section.Label> {fruit("apple", "Apple")} {fruit("banana", "Banana")} {fruit("cherry", "Cherry")} </Dropdown.Section> {fruit("orange", "Orange")} {fruit("pear", "Pear")} </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
受控组件
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 controlled, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Checkmark, Popup, styles } from "./_shared";
export function Controlled() { const [selected, setSelected] = useState(new Set(["bold"])); const [open, setOpen] = useState(false); const selectedItems = Array.from(selected); return ( <div {...stylex.props(styles.controlled)}> <p {...stylex.props(styles.status)}> Selected: {selectedItems.length > 0 ? selectedItems.join(", ") : "None"} </p> <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> {(["bold", "italic", "underline"] as const).map((value) => ( <Dropdown.CheckboxItem key={value} checked={selected.has(value)} closeOnClick={false} onKeyDown={(event) => { if (event.key === "Enter") setOpen(false); }} onCheckedChange={(checked) => setSelected((previous) => { const next = new Set(previous); if (checked) next.add(value); else next.delete(value); return next; }) } > <MenuItem.Label>{value.charAt(0).toUpperCase() + value.slice(1)}</MenuItem.Label> <Dropdown.CheckboxItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.CheckboxItemIndicator> </Dropdown.CheckboxItem> ))} </Popup> </Dropdown> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
受控展开状态
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 controlled-open-state, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function ControlledOpenState() { const [open, setOpen] = useState(false); return ( <div {...stylex.props(styles.controlled)}> <p {...stylex.props(styles.status)}> Dropdown is: <strong>{open ? "open" : "closed"}</strong> </p> <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" /> <ActionItem label="Open file" /> <ActionItem label="Save file" /> <ActionItem label="Delete file" variant="danger" /> </Popup> </Dropdown> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
单选
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-single-selection, Apache-2.0.import { useState } from "react";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Checkmark, Popup, styles } from "./_shared";
export function WithSingleSelection() { const [selected, setSelected] = useState("apple"); const fruit = (value: string, label: string) => ( <Dropdown.RadioItem key={value} value={value} closeOnClick> <Dropdown.RadioItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.RadioItemIndicator> <MenuItem.Label>{label}</MenuItem.Label> </Dropdown.RadioItem> ); return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Fruit </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.RadioGroup value={selected} onValueChange={setSelected}> <Dropdown.Section> <Dropdown.Section.Label>Select a fruit</Dropdown.Section.Label> {fruit("apple", "Apple")} {fruit("banana", "Banana")} {fruit("cherry", "Cherry")} </Dropdown.Section> {fruit("orange", "Orange")} {fruit("pear", "Pear")} </Dropdown.RadioGroup> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
单选自定义指示器
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 single-with-custom-indicator, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Popup, styles } from "./_shared";
export function SingleWithCustomIndicator() { const [selected, setSelected] = useState("apple"); const customCheckmarkIcon = ( <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" > <path {...stylex.props(styles.accent)} clipRule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14m3.1-8.55a.75.75 0 1 0-1.2-.9L7.419 8.858L6.03 7.47a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.13-.08z" fill="currentColor" fillRule="evenodd" /> </svg> ); const fruit = (value: string, label: string) => ( <Dropdown.RadioItem key={value} value={value} closeOnClick> <Dropdown.RadioItemIndicator keepMounted xstyle={styles.selectionIndicator}> {customCheckmarkIcon} </Dropdown.RadioItemIndicator> <MenuItem.Label>{label}</MenuItem.Label> </Dropdown.RadioItem> ); return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Fruits </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.RadioGroup value={selected} onValueChange={setSelected}> <Dropdown.Section> <Dropdown.Section.Label>Select a fruit</Dropdown.Section.Label> {fruit("apple", "Apple")} {fruit("banana", "Banana")} {fruit("cherry", "Cherry")} </Dropdown.Section> {fruit("orange", "Orange")} {fruit("pear", "Pear")} </Dropdown.RadioGroup> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
分组级选择
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-section-level-selection, Apache-2.0.import { useState } from "react";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { ActionItem, Checkmark, Dotmark, Popup, Shortcut, styles } from "./_shared";
export function WithSectionLevelSelection() { const [textStyles, setTextStyles] = useState(new Set(["bold", "italic"])); const [textAlignment, setTextAlignment] = useState("left"); const [open, setOpen] = useState(false); return ( <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Styles </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.Section> <Dropdown.Section.Label>Actions</Dropdown.Section.Label> <ActionItem label="Cut" shortcut={<Shortcut letter="X" />} /> <ActionItem label="Copy" shortcut={<Shortcut letter="C" />} /> <ActionItem label="Paste" shortcut={<Shortcut letter="U" />} /> </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Text Style</Dropdown.Section.Label> {(["bold", "italic", "underline"] as const).map((value) => ( <Dropdown.CheckboxItem key={value} checked={textStyles.has(value)} aria-label={value.charAt(0).toUpperCase() + value.slice(1)} closeOnClick={false} onKeyDown={(event) => { if (event.key === "Enter") setOpen(false); }} onCheckedChange={(checked) => setTextStyles((previous) => { const next = new Set(previous); if (checked) next.add(value); else next.delete(value); return next; }) } > <Dropdown.CheckboxItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.CheckboxItemIndicator> <MenuItem.Label>{value.charAt(0).toUpperCase() + value.slice(1)}</MenuItem.Label> <Shortcut letter={value === "underline" ? "U" : value.charAt(0).toUpperCase()} /> </Dropdown.CheckboxItem> ))} </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Text Alignment</Dropdown.Section.Label> <Dropdown.RadioGroup value={textAlignment} onValueChange={setTextAlignment}> {(["left", "center", "right"] as const).map((value) => ( <Dropdown.RadioItem key={value} value={value} aria-label={value.charAt(0).toUpperCase() + value.slice(1)} closeOnClick > <Dropdown.RadioItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Dotmark /> </Dropdown.RadioItemIndicator> <MenuItem.Label>{value.charAt(0).toUpperCase() + value.slice(1)}</MenuItem.Label> <Shortcut modifier="alt" letter={value === "left" ? "A" : value === "center" ? "H" : "D"} /> </Dropdown.RadioItem> ))} </Dropdown.RadioGroup> </Dropdown.Section> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
带快捷键
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-keyboard-shortcuts, Apache-2.0.import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut } from "./_shared";
export function WithKeyboardShortcuts() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New" shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new")} /> <ActionItem label="Open" shortcut={<Shortcut letter="O" />} onClick={() => console.log("Selected: open")} /> <ActionItem label="Save" shortcut={<Shortcut letter="S" />} onClick={() => console.log("Selected: save")} /> <ActionItem label="Delete" variant="danger" shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
含子菜单
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-submenus, Apache-2.0.import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, IconChevronRight, MenuItem } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function WithSubmenus() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Share </Dropdown.Trigger> <Popup> <ActionItem label="Copy Link" onClick={() => console.log("Selected: copy-link")} /> <ActionItem label="Facebook" onClick={() => console.log("Selected: facebook")} /> <ActionItem label="X / Twitter" navigationLabel="Twitter" onClick={() => console.log("Selected: twitter")} /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger label="Share"> <MenuItem.Label>Other</MenuItem.Label> <MenuItem.SubmenuIndicator> <IconChevronRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="WhatsApp" /> <ActionItem label="Telegram" /> <ActionItem label="Discord" /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger> <MenuItem.Label>Email</MenuItem.Label> <MenuItem.SubmenuIndicator> <IconChevronRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="Work email" /> <ActionItem label="Personal email" /> </Popup> </Dropdown.SubmenuRoot> </Popup> </Dropdown.SubmenuRoot> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义子菜单指示器
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 with-custom-submenu-indicator, Apache-2.0.import { ArrowRight } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, IconChevronRight, MenuItem } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function WithCustomSubmenuIndicator() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Share </Dropdown.Trigger> <Popup> <ActionItem label="Copy Link" onClick={() => console.log("Selected: copy-link")} /> <ActionItem label="Facebook" onClick={() => console.log("Selected: facebook")} /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger label="Share"> <MenuItem.Label>More options</MenuItem.Label> <MenuItem.SubmenuIndicator> <ArrowRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="WhatsApp" /> <ActionItem label="Telegram" /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger> <MenuItem.Label>Email</MenuItem.Label> <MenuItem.SubmenuIndicator> <svg {...stylex.props(styles.smallIcon)} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" aria-hidden="true" > <path d="M9 18l6-6-6-6" /> </svg> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="Work email" /> <ActionItem label="Personal email" /> </Popup> </Dropdown.SubmenuRoot> <ActionItem label="Discord" /> </Popup> </Dropdown.SubmenuRoot> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger label="Other"> <MenuItem.Label>Other (default indicator)</MenuItem.Label> <MenuItem.SubmenuIndicator> <IconChevronRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="SMS" /> </Popup> </Dropdown.SubmenuRoot> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义触发器
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 custom-trigger, Apache-2.0; original avatar asset retained.import { ArrowRightFromSquare, Gear, Persons } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Avatar, Dropdown, MenuItem } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
const avatar = "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg";
export function CustomTrigger() { return ( <Dropdown> <Dropdown.Trigger xstyle={styles.round} aria-label="Account menu"> <Avatar> <Avatar.Image alt="Junior Garcia" src={avatar} /> <Avatar.Fallback delay={600}>JD</Avatar.Fallback> </Avatar> </Dropdown.Trigger> <Popup> <div {...stylex.props(styles.profile)}> <div {...stylex.props(styles.profileRow)}> <Avatar size="sm"> <Avatar.Image alt="Jane" src={avatar} /> <Avatar.Fallback delay={600}>JD</Avatar.Fallback> </Avatar> <div {...stylex.props(styles.text)}> <p {...stylex.props(styles.profileName)}>Jane Doe</p> <p {...stylex.props(styles.profileEmail)}>[email protected]</p> </div> </div> </div> <ActionItem label="Dashboard" /> <ActionItem label="Profile" /> <Dropdown.Item label="Settings"> <div {...stylex.props(styles.itemRow)}> <MenuItem.Label>Settings</MenuItem.Label> <Gear {...stylex.props(styles.smallIcon)} /> </div> </Dropdown.Item> <Dropdown.Item label="New project"> <div {...stylex.props(styles.itemRow)}> <MenuItem.Label>Create Team</MenuItem.Label> <Persons {...stylex.props(styles.smallIcon)} /> </div> </Dropdown.Item> <Dropdown.Item label="Logout" variant="danger"> <div {...stylex.props(styles.itemRow)}> <MenuItem.Label>Log Out</MenuItem.Label> <ArrowRightFromSquare {...stylex.props(styles.smallDangerIcon)} /> </div> </Dropdown.Item> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
长按触发
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 long-press-trigger, Apache-2.0.import { useEffect, useRef, useState } from "react";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup } from "./_shared";
export function LongPressTrigger() { const [open, setOpen] = useState(false); const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const pointerActive = useRef(false); const cancel = () => { clearTimeout(timer.current); timer.current = undefined; }; useEffect(() => () => clearTimeout(timer.current), []); return ( <Dropdown open={open} onOpenChange={(next, details) => { if (details.reason === "trigger-press" && pointerActive.current) return; setOpen(next); }} > <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />} onPointerDown={(event) => { if (event.button !== 0) return; pointerActive.current = true; cancel(); timer.current = setTimeout(() => setOpen(true), 500); }} onPointerUp={cancel} onPointerCancel={cancel} onPointerLeave={cancel} onKeyDown={() => { pointerActive.current = false; }} > Long Press </Dropdown.Trigger> <Popup> <ActionItem label="New file" /> <ActionItem label="Open file" /> <ActionItem label="Save file" /> <ActionItem label="Delete file" variant="danger" /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义样式
Tailwind CSS
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 custom-styles, Apache-2.0.import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function CustomStyles() { return ( <Dropdown> <Dropdown.Trigger render={<Button variant="secondary" />}>Actions</Dropdown.Trigger> <Popup xstyle={styles.customPopup}> <ActionItem label="Rename" xstyle={styles.customItem} /> <ActionItem label="Duplicate" xstyle={styles.customItem} /> <ActionItem label="Delete" variant="danger" xstyle={styles.customDangerItem} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
全局 CSS
可使用 @layer components 指令自定义 Dropdown 组件类。
了解更多。
@layer components { .dropdown { @apply flex flex-col gap-1; }
.dropdown__trigger { @apply outline-none; }
.dropdown__popover { @apply rounded-lg border border-border bg-overlay p-2; }
.dropdown__menu { @apply flex flex-col gap-1; }}样式参考
HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。
CSS 类
Dropdown 组件使用以下 CSS 类(查看源码样式):
基础类 [!toc]
.dropdown- Dropdown 根容器.dropdown__trigger- 触发 Dropdown 的按钮或元素.dropdown__popover- Popover 容器.dropdown__menu- Popover 内的菜单容器
状态类 [!toc]
.dropdown__trigger[data-focus-visible="true"]- 触发器聚焦状态.dropdown__trigger[data-disabled="true"]- 触发器禁用状态.dropdown__trigger[data-pressed="true"]- 触发器按下状态.dropdown__popover[data-entering]- 进入动画状态.dropdown__popover[data-exiting]- 退出动画状态.dropdown__menu[data-selection-mode="single"]- 单选模式.dropdown__menu[data-selection-mode="multiple"]- 多选模式
菜单组件类
Dropdown 使用 Menu、MenuItem 与 MenuSection 作为底层组件。以下类名也可用于自定义:
菜单类 [!toc]
.menu- 菜单容器(menu.css)[data-slot="separator"]- 菜单内的分隔线元素
菜单项类 [!toc]
.menu-item- 菜单项容器(menu-item.css).menu-item__indicator- 选中指示器(对勾或圆点)[data-slot="menu-item-indicator--checkmark"]- 对勾指示器 SVG[data-slot="menu-item-indicator--dot"]- 圆点指示器 SVG
.menu-item__indicator--submenu- 子菜单指示器(箭头).menu-item--default- 默认样式变体.menu-item--danger- 危险样式变体
菜单项状态类 [!toc]
.menu-item[data-focus-visible="true"]- 聚焦状态(键盘焦点).menu-item[data-focus="true"]- 聚焦状态.menu-item[data-pressed]- 按下状态.menu-item[data-hovered]- 悬停状态.menu-item[data-selected="true"]- 选中状态.menu-item[data-disabled]- 禁用状态.menu-item[data-has-submenu="true"]- 带子菜单的项.menu-item[data-selection-mode="single"]- 单选模式.menu-item[data-selection-mode="multiple"]- 多选模式.menu-item[aria-checked="true"]- 已勾选(ARIA).menu-item[aria-selected="true"]- 已选中(ARIA)
菜单分区类 [!toc]
.menu-section- 菜单分区容器(menu-section.css)
交互状态
组件同时支持 CSS 伪类与 data 属性:
- Hover:触发器与菜单项上
:hover或[data-hovered="true"] - Focus:触发器与菜单项上
:focus-visible或[data-focus-visible="true"] - Disabled:触发器与菜单项上
:disabled或[data-disabled="true"] - Pressed:触发器与菜单项上
:active或[data-pressed="true"] - Selected:菜单项上
[data-selected="true"]或[aria-selected="true"]
API 参考
Dropdown
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
isOpen | boolean | - | 设置菜单展开状态(受控)。 |
defaultOpen | boolean | - | 设置菜单默认展开状态(非受控)。 |
onOpenChange | (isOpen: boolean) => void | - | 展开状态变化时调用的事件处理函数。 |
trigger | "press" | "longPress" | "press" | 触发菜单的交互类型。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | - | Dropdown 内容。 |
Dropdown.Trigger
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | RenderFunction | - | 触发器内容或渲染函数。 |
使用 Button 作为触发器时,同样支持所有 Button props。
Dropdown.Popover
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
placement | "bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom" | "bottom" | 相对于触发器的 Popover 位置。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | - | 子内容。 |
同样支持所有 Popover props。
Dropdown.Menu
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
selectionMode | "single" | "multiple" | "none" | "none" | 是否启用单选、多选或不启用选择。 |
selectedKeys | Iterable<Key> | - | 当前选中的 key(受控)。 |
defaultSelectedKeys | Iterable<Key> | - | 初始选中的 key(非受控)。 |
onSelectionChange | (keys: Selection) => void | - | 选中变化时调用的事件处理函数。 |
disabledKeys | Iterable<Key> | - | 禁用项的 key。 |
onAction | (key: Key) => void | - | 激活菜单项时调用的事件处理函数。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | - | 菜单内容。 |
同样支持所有 Menu props。
Dropdown.Section
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
selectionMode | "single" | "multiple" | - | 该分组内菜单项的选择模式。 |
selectedKeys | Iterable<Key> | - | 当前选中的 key(受控)。 |
defaultSelectedKeys | Iterable<Key> | - | 初始选中的 key(非受控)。 |
onSelectionChange | (keys: Selection) => void | - | 选中变化时调用的事件处理函数。 |
disabledKeys | Iterable<Key> | - | 禁用项的 key。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | - | 分组内容。 |
同样支持所有 MenuSection props。
Dropdown.Item
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
id | Key | - | 菜单项唯一标识。 |
textValue | string | - | 用于首字母导航的文本内容。 |
variant | "default" | "danger" | "default" | 菜单项视觉变体。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | RenderFunction | - | 菜单项内容或渲染函数。 |
同样支持所有 MenuItem props。
Dropdown.ItemIndicator
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
type | "checkmark" | "dot" | "checkmark" | 指示器类型。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | RenderFunction | - | 自定义指示器内容或渲染函数。 |
使用渲染函数时,会传入以下值:
| Prop | 类型 | 描述 |
|---|---|---|
isSelected | boolean | 该项是否选中。 |
isIndeterminate | boolean | 该项是否处于不确定状态。 |
Dropdown.SubmenuIndicator
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | - | 自定义指示器内容。 |
Dropdown.SubmenuTrigger
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | - | 子菜单触发器内容。 |
同样支持所有 SubmenuTrigger props。
Render Props
在 Dropdown.Item 中使用渲染函数时,会传入以下值:
| Prop | 类型 | 描述 |
|---|---|---|
isSelected | boolean | 该项是否选中。 |
isFocused | boolean | 该项是否聚焦。 |
isDisabled | boolean | 该项是否禁用。 |
isPressed | boolean | 该项是否处于按下状态。 |
示例
基本用法
import { Dropdown, Button, Label } from '@lenso/ui';
<Dropdown> <Button aria-label="Menu" variant="secondary"> Actions </Button> <Dropdown.Popover> <Dropdown.Menu onAction={(key) => alert(`Selected: ${key}`)}> <Dropdown.Item id="new-file" textValue="New file"> <Label>New file</Label> </Dropdown.Item> <Dropdown.Item id="open-file" textValue="Open file"> <Label>Open file</Label> </Dropdown.Item> <Dropdown.Item id="delete-file" textValue="Delete file" variant="danger"> <Label>Delete file</Label> </Dropdown.Item> </Dropdown.Menu> </Dropdown.Popover></Dropdown>代码示例:分组选项
import { Dropdown, Button, Label, Header, Separator } from '@lenso/ui';
<Dropdown> <Button aria-label="Menu" variant="secondary"> Actions </Button> <Dropdown.Popover> <Dropdown.Menu onAction={(key) => alert(`Selected: ${key}`)}> <Dropdown.Section> <Header>Actions</Header> <Dropdown.Item id="new-file" textValue="New file"> <Label>New file</Label> </Dropdown.Item> <Dropdown.Item id="edit-file" textValue="Edit file"> <Label>Edit file</Label> </Dropdown.Item> </Dropdown.Section> <Separator /> <Dropdown.Section> <Header>Danger zone</Header> <Dropdown.Item id="delete-file" textValue="Delete file" variant="danger"> <Label>Delete file</Label> </Dropdown.Item> </Dropdown.Section> </Dropdown.Menu> </Dropdown.Popover></Dropdown>受控选择
import type { Selection } from '@lenso/ui';
import { Dropdown, Button, Label } from '@lenso/ui';import { useState } from 'react';
function ControlledDropdown() { const [selected, setSelected] = useState<Selection>(new Set(['bold']));
return ( <Dropdown> <Button aria-label="Menu" variant="secondary"> Actions </Button> <Dropdown.Popover> <Dropdown.Menu selectedKeys={selected} selectionMode="multiple" onSelectionChange={setSelected} > <Dropdown.Item id="bold" textValue="Bold"> <Label>Bold</Label> <Dropdown.ItemIndicator /> </Dropdown.Item> <Dropdown.Item id="italic" textValue="Italic"> <Label>Italic</Label> <Dropdown.ItemIndicator /> </Dropdown.Item> </Dropdown.Menu> </Dropdown.Popover> </Dropdown> );}代码示例:含子菜单
import { Dropdown, Button, Label } from '@lenso/ui';
<Dropdown> <Button aria-label="Menu" variant="secondary"> Share </Button> <Dropdown.Popover> <Dropdown.Menu onAction={(key) => alert(`Selected: ${key}`)}> <Dropdown.Item id="copy-link" textValue="Copy Link"> <Label>Copy Link</Label> </Dropdown.Item> <Dropdown.SubmenuTrigger> <Dropdown.Item id="share" textValue="Share"> <Label>Other</Label> <Dropdown.SubmenuIndicator /> </Dropdown.Item> <Dropdown.Popover> <Dropdown.Menu> <Dropdown.Item id="whatsapp" textValue="WhatsApp"> <Label>WhatsApp</Label> </Dropdown.Item> <Dropdown.Item id="telegram" textValue="Telegram"> <Label>Telegram</Label> </Dropdown.Item> </Dropdown.Menu> </Dropdown.Popover> </Dropdown.SubmenuTrigger> </Dropdown.Menu> </Dropdown.Popover></Dropdown>无障碍
Dropdown 组件实现 ARIA 菜单模式,并提供:
- 完整键盘导航(方向键、Home/End、首字母导航)
- 屏幕阅读器对操作与选中变化的播报
- 合理的焦点管理
- 禁用态支持
- 长按交互支持
- 子菜单导航
更多信息见 React Aria Menu 文档。