1- import React , { useState } from "react" ;
1+ import React , { useState , useCallback , useMemo , forwardRef , memo } from "react" ;
22import Dropdown from "react-bootstrap/Dropdown" ;
33import ButtonGroup from "react-bootstrap/ButtonGroup" ;
44import { ChevronIcon } from "../SvgIcons/index" ;
55
6- interface DropdownItemConfig {
6+ /**
7+ * Dropdown item descriptor for `V8CustomDropdownButton`.
8+ */
9+ export interface DropdownItemConfig {
10+ /** Text label or translation key for the item */
711 label : string ;
12+ /** Value associated with this item */
813 value ?: string ;
14+ /** Called when this item is clicked */
915 onClick ?: ( ) => void ;
16+ /** Test ID for automated testing */
1017 dataTestId ?: string ;
18+ /** Accessible label for screen readers */
1119 ariaLabel ?: string ;
1220}
1321
14- interface V8CustomDropdownButtonProps {
22+ /**
23+ * Props for `V8CustomDropdownButton` component.
24+ * Optimized, accessible dropdown button with separate label and dropdown actions.
25+ */
26+ export interface V8CustomDropdownButtonProps
27+ extends Omit < React . ComponentPropsWithoutRef < "div" > , "onClick" > {
28+ /** Button label text */
1529 label ?: string ;
30+ /** Array of dropdown menu items */
1631 dropdownItems : DropdownItemConfig [ ] ;
32+ /** Visual style variant */
1733 variant ?: "primary" | "secondary" ;
34+ /** Disables the entire dropdown button */
1835 disabled ?: boolean ;
36+ /** Additional CSS classes */
1937 className ?: string ;
38+ /** Test ID for automated testing */
2039 dataTestId ?: string ;
40+ /** Accessible label for screen readers */
2141 ariaLabel ?: string ;
22- menuPosition ?: "left" | "right" ; // controls dropdown menu alignment
42+ /** Dropdown menu alignment */
43+ menuPosition ?: "left" | "right" ;
44+ /** Called when the label is clicked (separate from dropdown) */
45+ onLabelClick ?: ( ) => void ;
2346}
2447
25- export const V8CustomDropdownButton : React . FC < V8CustomDropdownButtonProps > = ( {
48+ /**
49+ * Utility function to build className string
50+ */
51+ const buildClassNames = ( ...classes : ( string | boolean | undefined ) [ ] ) : string => {
52+ return classes . filter ( Boolean ) . join ( " " ) ;
53+ } ;
54+
55+ /**
56+ * V8CustomDropdownButton: Accessible, memoized dropdown button with separate label and dropdown actions.
57+ *
58+ * Usage:
59+ * <V8CustomDropdownButton
60+ * label="Actions"
61+ * dropdownItems={[
62+ * { label: 'Edit', value: 'edit', onClick: handleEdit },
63+ * { label: 'Delete', value: 'delete', onClick: handleDelete }
64+ * ]}
65+ * onLabelClick={handlePrimaryAction}
66+ * variant="primary"
67+ * />
68+ */
69+ const V8CustomDropdownButtonComponent = forwardRef < HTMLDivElement , V8CustomDropdownButtonProps > ( ( {
2670 label = "Edit" ,
2771 dropdownItems,
2872 variant = "primary" ,
@@ -31,65 +75,145 @@ export const V8CustomDropdownButton: React.FC<V8CustomDropdownButtonProps> = ({
3175 dataTestId = "v8-dropdown" ,
3276 ariaLabel = "Custom dropdown" ,
3377 menuPosition = "left" ,
34- } ) => {
78+ onLabelClick,
79+ ...restProps
80+ } , ref ) => {
81+ // State management
3582 const [ open , setOpen ] = useState ( false ) ;
3683 const [ selectedValue , setSelectedValue ] = useState < string | null > ( null ) ;
3784
38- const handleItemClick = ( item : DropdownItemConfig ) => {
85+ // Memoized dropdown items to prevent unnecessary re-renders
86+ const memoizedDropdownItems = useMemo ( ( ) => dropdownItems , [ dropdownItems ] ) ;
87+
88+ // Memoized click handlers for better performance
89+ const handleItemClick = useCallback ( ( item : DropdownItemConfig ) => {
3990 setSelectedValue ( item . value || item . label ) ;
4091 item . onClick ?.( ) ;
41- setOpen ( false ) ; // close after selecting
42- } ;
92+ setOpen ( false ) ; // Close dropdown after selection
93+ } , [ ] ) ;
94+
95+ const handleLabelClick = useCallback ( ( e : React . MouseEvent ) => {
96+ e . preventDefault ( ) ;
97+ e . stopPropagation ( ) ;
98+ if ( ! disabled && onLabelClick ) {
99+ onLabelClick ( ) ;
100+ }
101+ } , [ disabled , onLabelClick ] ) ;
102+
103+ const handleDropdownIconClick = useCallback ( ( e : React . MouseEvent ) => {
104+ e . preventDefault ( ) ;
105+ e . stopPropagation ( ) ;
106+ if ( ! disabled ) {
107+ setOpen ( ! open ) ;
108+ }
109+ } , [ disabled , open ] ) ;
110+
111+ // Memoized dropdown toggle handler
112+ const handleDropdownToggle = useCallback ( ( isOpen : boolean ) => {
113+ if ( ! disabled ) {
114+ setOpen ( isOpen ) ;
115+ }
116+ } , [ disabled ] ) ;
117+
118+ // Memoized container className
119+ const containerClassName = useMemo ( ( ) => buildClassNames (
120+ "v8-custom-dropdown" ,
121+ `menu-${ menuPosition } ` ,
122+ className
123+ ) , [ menuPosition , className ] ) ;
124+
125+ // Memoized toggle button className
126+ const toggleClassName = useMemo ( ( ) => buildClassNames (
127+ "v8-dropdown-toggle" ,
128+ open && "open"
129+ ) , [ open ] ) ;
43130
44131 return (
45132 < Dropdown
46133 as = { ButtonGroup }
47134 show = { open }
48- onToggle = { ( isOpen ) => setOpen ( isOpen ) }
49- className = { `v8-custom-dropdown menu-${ menuPosition } ${ className } ` }
135+ onToggle = { handleDropdownToggle }
136+ className = { containerClassName }
137+ ref = { ref }
50138 { ...( dataTestId ? { "data-testid" : dataTestId } : { } ) }
139+ { ...restProps }
51140 >
52141 < Dropdown . Toggle
53142 variant = { variant }
54143 disabled = { disabled }
55- className = { `v8-dropdown-toggle ${ open ? "open" : "" } ` }
144+ className = { toggleClassName }
56145 aria-haspopup = "listbox"
57146 aria-expanded = { open }
58147 { ...( ariaLabel ? { "aria-label" : ariaLabel } : { } ) }
59148 { ...( dataTestId ? { "data-testid" : `${ dataTestId } -toggle` } : { } ) }
60149 >
61- < div className = "label-div" >
150+ { /* Label section - triggers separate action */ }
151+ < div
152+ className = "label-div"
153+ onClick = { handleLabelClick }
154+ data-testid = { `${ dataTestId } -label` }
155+ role = "button"
156+ tabIndex = { disabled ? - 1 : 0 }
157+ aria-label = { `${ label } action` }
158+ >
62159 < span className = "dropdown-label" > { label } </ span >
63160 </ div >
161+
162+ { /* Visual divider */ }
64163 < span className = "v8-dropdown-divider" aria-hidden = "true" />
65- < div className = "dropdown-icon" >
164+
165+ { /* Dropdown icon section - toggles menu */ }
166+ < div
167+ className = "dropdown-icon"
168+ onClick = { handleDropdownIconClick }
169+ data-testid = { `${ dataTestId } -icon` }
170+ role = "button"
171+ tabIndex = { disabled ? - 1 : 0 }
172+ aria-label = "Toggle dropdown menu"
173+ >
66174 < span className = "chevron-icon" >
67175 < ChevronIcon />
68176 </ span >
69177 </ div >
70178 </ Dropdown . Toggle >
71179
180+ { /* Dropdown menu */ }
72181 < Dropdown . Menu
73182 className = "v8-dropdown-menu"
74183 role = "listbox"
75184 { ...( dataTestId ? { "data-testid" : `${ dataTestId } -menu` } : { } ) }
76185 >
77- { dropdownItems . map ( ( item ) => (
78- < Dropdown . Item
79- key = { item . value || item . label }
80- onClick = { ( ) => handleItemClick ( item ) }
81- className = { `v8-dropdown-item ${
82- selectedValue === ( item . value || item . label ) ? "selected" : ""
83- } `}
84- role = "option"
85- aria-selected = { selectedValue === ( item . value || item . label ) }
86- { ...( item . ariaLabel ? { "aria-label" : item . ariaLabel } : { } ) }
87- { ...( item . dataTestId ? { "data-testid" : item . dataTestId } : { } ) }
88- >
89- { item . label }
90- </ Dropdown . Item >
91- ) ) }
186+ { memoizedDropdownItems . map ( ( item , index ) => {
187+ const itemKey = item . value || item . label || index ;
188+ const isSelected = selectedValue === ( item . value || item . label ) ;
189+
190+ return (
191+ < Dropdown . Item
192+ key = { itemKey }
193+ onClick = { ( ) => handleItemClick ( item ) }
194+ className = { buildClassNames (
195+ "v8-dropdown-item" ,
196+ isSelected && "selected"
197+ ) }
198+ role = "option"
199+ aria-selected = { isSelected }
200+ { ...( item . ariaLabel ? { "aria-label" : item . ariaLabel } : { } ) }
201+ { ...( item . dataTestId ? { "data-testid" : item . dataTestId } : { } ) }
202+ >
203+ { item . label }
204+ </ Dropdown . Item >
205+ ) ;
206+ } ) }
92207 </ Dropdown . Menu >
93208 </ Dropdown >
94209 ) ;
95- } ;
210+ } ) ;
211+
212+ // Set display name for better debugging
213+ V8CustomDropdownButtonComponent . displayName = "V8CustomDropdownButton" ;
214+
215+ // Export memoized component for performance optimization
216+ export const V8CustomDropdownButton = memo ( V8CustomDropdownButtonComponent ) ;
217+
218+ // Export types for consumers
219+ export type { V8CustomDropdownButtonProps } ;
0 commit comments