11// biome-ignore lint/style/noNamespaceImport: from Radix
22import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' ;
33import type * as React from 'react' ;
4+ import { createContext , useContext , useState } from 'react' ;
45import type { Control , FieldPath , FieldValues } from 'react-hook-form' ;
56import { Button } from './button' ;
67import { DropdownMenuContent } from './dropdown-menu' ;
8+ import {
9+ DropdownMenuCheckboxItem as BaseDropdownMenuCheckboxItem ,
10+ DropdownMenuItem as BaseDropdownMenuItem ,
11+ DropdownMenuRadioItem as BaseDropdownMenuRadioItem ,
12+ } from './dropdown-menu' ;
713import {
814 type FieldComponents ,
915 FormControl ,
@@ -44,25 +50,35 @@ export function DropdownMenuSelectField<
4450 components,
4551 ...props
4652} : DropdownMenuSelectProps < TFieldValues , TName > ) {
53+ const [ open , setOpen ] = useState ( false ) ;
54+
4755 return (
4856 < FormField
4957 control = { control }
5058 name = { name }
51- render = { ( { field, fieldState } ) => (
59+ render = { ( { field, fieldState, formState } ) => (
5260 < FormItem className = { className } >
5361 { label && (
5462 < FormLabel Component = { components ?. FormLabel } className = { labelClassName } >
5563 { label }
5664 </ FormLabel >
5765 ) }
5866 < FormControl >
59- < DropdownMenuPrimitive . Root { ...field } { ...props } data-slot = "dropdown-menu-select-root" >
60- < DropdownMenuPrimitive . Trigger asChild >
61- < Button className = { dropdownClassName } data-slot = "dropdown-select-trigger" >
62- { field . value ? field . value : 'Select an option' }
63- </ Button >
64- </ DropdownMenuPrimitive . Trigger >
65- < DropdownMenuContent data-slot = "dropdown-select-content" > { children } </ DropdownMenuContent >
67+ < DropdownMenuPrimitive . Root
68+ { ...field }
69+ open = { open }
70+ onOpenChange = { setOpen }
71+ { ...props }
72+ data-slot = "dropdown-menu-select-root"
73+ >
74+ < DropdownMenuSelectContext . Provider value = { { onValueChange : field . onChange , value : field . value } } >
75+ < DropdownMenuPrimitive . Trigger asChild >
76+ < Button className = { dropdownClassName } data-slot = "dropdown-select-trigger" >
77+ { field . value ? field . value : 'Select an option' }
78+ </ Button >
79+ </ DropdownMenuPrimitive . Trigger >
80+ < DropdownMenuContent data-slot = "dropdown-select-content" > { children } </ DropdownMenuContent >
81+ </ DropdownMenuSelectContext . Provider >
6682 </ DropdownMenuPrimitive . Root >
6783 </ FormControl >
6884 { description && < FormDescription Component = { components ?. FormDescription } > { description } </ FormDescription > }
@@ -74,3 +90,66 @@ export function DropdownMenuSelectField<
7490}
7591
7692DropdownMenuSelectField . displayName = 'DropdownMenuSelect' ;
93+
94+ // Context to wire menu items to form field
95+ interface DropdownMenuSelectContextValue < T > {
96+ onValueChange : ( value : T ) => void ;
97+ value : T ;
98+ }
99+ const DropdownMenuSelectContext = createContext < DropdownMenuSelectContextValue < unknown > | null > ( null ) ;
100+
101+ /** Hook to access select context in item wrappers */
102+ export function useDropdownMenuSelectContext < T = unknown > ( ) {
103+ const ctx = useContext ( DropdownMenuSelectContext ) ;
104+ if ( ! ctx ) {
105+ throw new Error ( 'useDropdownMenuSelectContext must be used within DropdownMenuSelectField' ) ;
106+ }
107+ return ctx as { onValueChange : ( value : T ) => void ; value : T } ;
108+ }
109+
110+ /** Single-select menu item */
111+ export function DropdownMenuSelectItem ( {
112+ value,
113+ children,
114+ ...props
115+ } : { value : string ; children : React . ReactNode } & React . ComponentProps < typeof BaseDropdownMenuItem > ) {
116+ const { onValueChange } = useDropdownMenuSelectContext < string > ( ) ;
117+ return (
118+ < BaseDropdownMenuItem { ...props } onSelect = { ( ) => onValueChange ( value ) } >
119+ { children }
120+ </ BaseDropdownMenuItem >
121+ ) ;
122+ }
123+
124+ /** Multi-select checkbox menu item */
125+ export function DropdownMenuSelectCheckboxItem ( {
126+ value,
127+ children,
128+ ...props
129+ } : { value : string ; children : React . ReactNode } & React . ComponentProps < typeof BaseDropdownMenuCheckboxItem > ) {
130+ const { onValueChange, value : selected } = useDropdownMenuSelectContext < string [ ] > ( ) ;
131+ const isChecked = Array . isArray ( selected ) && selected . includes ( value ) ;
132+ const handleChange = ( ) => {
133+ const newValue = isChecked ? selected . filter ( ( v ) => v !== value ) : [ ...( selected || [ ] ) , value ] ;
134+ onValueChange ( newValue ) ;
135+ } ;
136+ return (
137+ < BaseDropdownMenuCheckboxItem { ...props } checked = { isChecked } onCheckedChange = { handleChange } >
138+ { children }
139+ </ BaseDropdownMenuCheckboxItem >
140+ ) ;
141+ }
142+
143+ /** Radio-select menu item */
144+ export function DropdownMenuSelectRadioItem ( {
145+ value : itemValue ,
146+ children,
147+ ...props
148+ } : { value : string ; children : React . ReactNode } & React . ComponentProps < typeof BaseDropdownMenuRadioItem > ) {
149+ const { onValueChange } = useDropdownMenuSelectContext < string > ( ) ;
150+ return (
151+ < BaseDropdownMenuRadioItem value = { itemValue } { ...props } onSelect = { ( ) => onValueChange ( itemValue ) } >
152+ { children }
153+ </ BaseDropdownMenuRadioItem >
154+ ) ;
155+ }
0 commit comments