@@ -21,6 +21,13 @@ interface SelectProps {
2121 disabled ?: boolean
2222 /** Swap the plain select for a searchable combobox. */
2323 searchable ?: boolean
24+ /**
25+ * Renders a real `<select>` instead of `FormSelect`/`FormCombobox`. The browser draws its
26+ * option list outside the page's layout, so no ancestor can clip or reposition it — the
27+ * dependable choice for a `Select` embedded in a host layout this component doesn't
28+ * control. Takes priority over `searchable`, which has no native equivalent.
29+ */
30+ native ?: boolean
2431}
2532
2633function normalize ( option : string | SelectOption ) : { value : string , label ?: string } {
@@ -39,6 +46,7 @@ const SelectImpl = defineComponent({
3946 label : { type : String , default : undefined } ,
4047 disabled : { type : Boolean , default : undefined } ,
4148 searchable : { type : Boolean , default : undefined } ,
49+ native : { type : Boolean , default : undefined } ,
4250 bindingPath : { type : String , default : undefined } ,
4351 onChange : { type : Function as PropType < ( ) => void > , default : undefined } ,
4452 } ,
@@ -56,7 +64,30 @@ const SelectImpl = defineComponent({
5664 props . onChange ?.( )
5765 }
5866 const options = computed ( ( ) => props . options . map ( normalize ) )
67+ const withLabel = ( control : ReturnType < typeof h > ) => {
68+ if ( ! props . label )
69+ return control
70+ return h ( 'div' , { class : 'flex flex-col gap-1' } , [
71+ h ( 'label' , { class : 'text-sm font-medium' } , props . label ) ,
72+ control ,
73+ ] )
74+ }
5975 return ( ) => {
76+ if ( props . native ) {
77+ return withLabel ( h ( 'select' , {
78+ 'value' : model . value ?? '' ,
79+ 'disabled' : props . disabled ,
80+ 'aria-label' : props . label ,
81+ 'class' : 'text-sm px2.5 h-9 min-w-40 border border-base rounded bg-base color-base outline-none transition disabled:op50 disabled:pointer-events-none focus-visible:ring-2 focus-visible:ring-primary-500/40' ,
82+ 'onChange' : ( e : Event ) => setModel ( ( e . target as HTMLSelectElement ) . value ) ,
83+ } , [
84+ // Only while unset, so the placeholder can't be re-selected afterwards.
85+ props . placeholder && model . value === undefined
86+ ? h ( 'option' , { value : '' , disabled : true } , props . placeholder )
87+ : null ,
88+ ...options . value . map ( option => h ( 'option' , { value : option . value } , option . label ?? option . value ) ) ,
89+ ] ) )
90+ }
6091 const Comp = ( props . searchable ? FormCombobox : FormSelect ) as unknown as Parameters < typeof h > [ 0 ]
6192 const control = h ( Comp , {
6293 'options' : options . value ,
@@ -65,13 +96,7 @@ const SelectImpl = defineComponent({
6596 'modelValue' : model . value ,
6697 'onUpdate:modelValue' : ( next : string ) => setModel ( next ) ,
6798 } )
68- if ( props . label ) {
69- return h ( 'div' , { class : 'flex flex-col gap-1' } , [
70- h ( 'label' , { class : 'text-sm font-medium' } , props . label ) ,
71- control ,
72- ] )
73- }
74- return control
99+ return withLabel ( control )
75100 }
76101 } ,
77102} )
@@ -84,6 +109,7 @@ export const Select: JrComponent<SelectProps> = ({ props, on, bindings }) =>
84109 label : props . label ,
85110 disabled : props . disabled ,
86111 searchable : props . searchable ,
112+ native : props . native ,
87113 bindingPath : bindings ?. value ,
88114 onChange : ( ) => on ( 'change' ) . emit ( ) ,
89115 } )
0 commit comments