Coverage for src / lexigram / ui / molecules / dropdown.py: 100%
24 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1from __future__ import annotations
3from typing import Any
5from lexigram.ui.core.base import Component, el
8class Dropdown(Component):
9 """
10 A refined dropdown menu component with positioning control.
11 """
13 def __init__(
14 self,
15 trigger: str | Any,
16 items: list[Any],
17 position: str = "right",
18 direction: str = "down",
19 **props,
20 ) -> None:
21 super().__init__(
22 trigger=trigger,
23 items=items,
24 position=position,
25 direction=direction,
26 **props,
27 )
28 self.trigger = trigger
29 self.items = items
30 self.position = position
31 self.direction = direction
33 def render(self) -> Any:
34 # Determine alignment classes
35 origin_cls = "origin-top-right right-0"
36 if self.position == "left":
37 origin_cls = "origin-top-left left-0"
39 # Determine direction classes
40 mt_cls = "mt-2"
41 origin_dir = "origin-top"
42 if self.direction == "up":
43 mt_cls = "mb-2 bottom-full"
44 origin_dir = "origin-bottom"
45 if self.position == "right":
46 origin_cls = "origin-bottom-right right-0"
47 else:
48 origin_cls = "origin-bottom-left left-0"
50 menu_items = [
51 el("div", item, {"role": "menuitem", "tabindex": "-1", "class": "cursor-pointer"})
52 for item in self.items
53 ]
55 return el(
56 "div",
57 {
58 "x-data": "{ open: false, focusedIndex: -1 }",
59 "class": "relative inline-block text-left w-full",
60 "x-on:keydown.escape.prevent": "open = false",
61 "x-on:keydown.down.prevent": "focusedIndex = Math.min(focusedIndex + 1, " + str(len(self.items) - 1) + "); $nextTick(() => $el.querySelector('[role=menuitem]:nth-child(' + (focusedIndex + 1) + ')')?.focus())",
62 "x-on:keydown.up.prevent": "focusedIndex = Math.max(focusedIndex - 1, 0); $nextTick(() => $el.querySelector('[role=menuitem]:nth-child(' + (focusedIndex + 1) + ')')?.focus())",
63 },
64 # Trigger
65 el(
66 "div",
67 {
68 "x-ref": "trigger",
69 "x-on:click": "open = !open",
70 "x-on:click.outside": "open = false",
71 ":aria-expanded": "open",
72 "class": "w-full",
73 "tabindex": "0",
74 "role": "button",
75 "x-on:keydown.enter.prevent": "open = !open",
76 "x-on:keydown.space.prevent": "open = !open",
77 },
78 self.trigger,
79 ),
80 # Menu
81 el(
82 "div",
83 {
84 "x-show": "open",
85 "x-transition:enter": "transition ease-out duration-100",
86 "x-transition:enter-start": "transform opacity-0 scale-95",
87 "x-transition:enter-end": "transform opacity-100 scale-100",
88 "x-transition:leave": "transition ease-in duration-75",
89 "x-transition:leave-start": "transform opacity-100 scale-100",
90 "x-transition:leave-end": "transform opacity-0 scale-95",
91 "class": f"absolute {origin_cls} {mt_cls} {origin_dir} z-50 w-full min-w-[14rem] rounded-md bg-popover text-popover-foreground shadow-lg ring-1 ring-border focus:outline-none",
92 "role": "menu",
93 "x-cloak": True,
94 },
95 el("div", {"class": "py-1", "role": "none"}, *menu_items),
96 ),
97 )