Coverage for src / lexigram / ui / atoms / inputs / selection / select.py: 26%

50 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-10 04:11 +0800

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.ui.atoms.inputs.base import AbstractInput 

6from lexigram.ui.core.base import el 

7 

8 

9class Select(AbstractInput): 

10 """Select dropdown with label and error support. 

11 

12 Args: 

13 name: Input name attribute 

14 choices: List of (value, label) tuples 

15 value: Currently selected value 

16 label: Optional label text 

17 error: Error message to display 

18 multiple: Whether multiple selection is allowed 

19 disabled: Whether input is disabled 

20 required: Whether input is required 

21 """ 

22 

23 def __init__( 

24 self, 

25 name: str, 

26 choices: list[tuple[str, str]] | None = None, 

27 multiple: bool = False, 

28 **kwargs, 

29 ) -> None: 

30 super().__init__(name=name, **kwargs) 

31 self.choices = choices or [] 

32 self.multiple = multiple 

33 

34 def _render_input(self) -> Any: 

35 options = [] 

36 for val, label in self.choices: 

37 is_selected = False 

38 if self.multiple and isinstance(self.value, (list, tuple)): 

39 is_selected = val in self.value 

40 else: 

41 is_selected = str(val) == str( 

42 self.value if self.value is not None else "", 

43 ) 

44 

45 options.append( 

46 el( 

47 "option", 

48 label, 

49 value=val, 

50 selected=is_selected, 

51 ), 

52 ) 

53 

54 # Merge HTMX triggers for cascading dropdowns 

55 extra_props = self._get_extra_props(exclude=["choices", "multiple"]) 

56 depends_on = self.props.get("depends_on") 

57 options_from = self.props.get("options_from") 

58 if depends_on and options_from: 

59 extra_props["hx-get"] = options_from 

60 extra_props["hx-trigger"] = f"change from:#{depends_on}" 

61 extra_props["hx-target"] = "this" 

62 extra_props["hx-swap"] = "innerHTML" 

63 extra_props["hx-include"] = f"#{depends_on}" 

64 

65 return el( 

66 "select", 

67 *options, 

68 name=self.name, 

69 id=self.input_id, 

70 multiple=self.multiple, 

71 disabled=self.disabled, 

72 required=self.required, 

73 class_=self._get_input_classes( 

74 "pl-3 pr-10 h-8 shadow-sm ring-1 ring-inset sm:text-sm leading-5", 

75 ), 

76 **extra_props, 

77 ) 

78 

79 

80class NativeMultiSelect(Select): 

81 """Native browser multiple select dropdown.""" 

82 

83 def __init__(self, *args, **kwargs) -> None: 

84 kwargs["multiple"] = True 

85 super().__init__(*args, **kwargs) 

86 

87 

88class LazySelect(Select): 

89 """Select dropdown that support infinite scroll / lazy loading of options.""" 

90 

91 def __init__( 

92 self, 

93 name: str, 

94 lazy_url: str, 

95 choices: list[tuple[str, str]] | None = None, 

96 page: int = 1, 

97 **kwargs, 

98 ) -> None: 

99 super().__init__(name, choices=choices, **kwargs) 

100 self.lazy_url = lazy_url 

101 self.page = page 

102 

103 def render_options(self) -> list[Any]: 

104 options = [] 

105 for val, label in self.choices: 

106 is_selected = str(val) == str(self.value if self.value is not None else "") 

107 options.append(el("option", label, value=val, selected=is_selected)) 

108 

109 next_page = self.page + 1 

110 sentinel = el( 

111 "option", 

112 "Loading more...", 

113 value="", 

114 disabled=True, 

115 **{ 

116 "hx-get": f"{self.lazy_url}?page={next_page}", 

117 "hx-trigger": "intersect once", 

118 "hx-swap": "afterend", 

119 "hx-indicator": "this", 

120 }, 

121 ) 

122 options.append(sentinel) 

123 return options 

124 

125 def _render_input(self) -> Any: 

126 if self.props.get("is_fragment"): 

127 return self.render_options() 

128 

129 options = self.render_options() 

130 

131 return el( 

132 "select", 

133 *options, 

134 name=self.name, 

135 id=self.input_id, 

136 multiple=self.multiple, 

137 disabled=self.disabled, 

138 required=self.required, 

139 class_=self._get_input_classes( 

140 "pl-3 pr-10 h-8 shadow-sm ring-1 ring-inset sm:text-sm leading-5", 

141 ), 

142 **self._get_extra_props(exclude=["lazy_url", "page", "is_fragment"]), 

143 )