@@ -7,14 +7,15 @@ import React, {
77 Ref ,
88 ReactNode ,
99 useId ,
10- useState ,
11- useEffect ,
12- useCallback ,
1310} from 'react' ;
1411import classnames from 'classnames' ;
1512import { FormElement , FormElementProps } from './FormElement' ;
1613import { Icon } from './Icon' ;
17- import { DropdownMenuProps } from './DropdownMenu' ;
14+ import {
15+ DropdownMenu ,
16+ DropdownMenuItem ,
17+ DropdownMenuProps ,
18+ } from './DropdownMenu' ;
1819import { isElInChildren } from './util' ;
1920import { ComponentSettingsContext } from './ComponentSettings' ;
2021import { useControlledValue , useEventCallback , useMergeRefs } from './hooks' ;
@@ -155,124 +156,8 @@ export const Picklist: (<MultiSelect extends boolean | undefined>(
155156 opened_ ,
156157 defaultOpened ?? false
157158 ) ;
158- const [ focusedValue , setFocusedValue ] = useState <
159- PicklistValue | undefined
160- > ( ) ;
161-
162159 const { getActiveElement } = useContext ( ComponentSettingsContext ) ;
163160
164- // Get option values from children
165- const getOptionValues = useCallback ( ( ) => {
166- const optionValues : PicklistValue [ ] = [ ] ;
167- React . Children . forEach ( children , ( child ) => {
168- if ( ! React . isValidElement ( child ) ) {
169- return ;
170- }
171-
172- const props : unknown = child . props ;
173- const isPropsObject = typeof props === 'object' && props !== null ;
174-
175- if (
176- isPropsObject &&
177- 'value' in props &&
178- ( typeof props . value === 'string' || typeof props . value === 'number' )
179- ) {
180- optionValues . push ( props . value ) ;
181- }
182- } ) ;
183- return optionValues ;
184- } , [ children ] ) ;
185-
186- // Get next option value for keyboard navigation
187- const getNextValue = useCallback (
188- ( currentValue ?: PicklistValue ) => {
189- const optionValues = getOptionValues ( ) ;
190- if ( optionValues . length === 0 ) return undefined ;
191-
192- if ( ! currentValue ) return optionValues [ 0 ] ;
193-
194- const currentIndex = optionValues . indexOf ( currentValue ) ;
195- return optionValues [
196- Math . min ( currentIndex + 1 , optionValues . length - 1 )
197- ] ; // not wrap around
198- } ,
199- [ getOptionValues ]
200- ) ;
201-
202- // Get previous option value for keyboard navigation
203- const getPrevValue = useCallback (
204- ( currentValue ?: PicklistValue ) => {
205- const optionValues = getOptionValues ( ) ;
206- if ( optionValues . length === 0 ) return undefined ;
207-
208- if ( ! currentValue ) return optionValues [ optionValues . length - 1 ] ;
209-
210- const currentIndex = optionValues . indexOf ( currentValue ) ;
211- return optionValues [ Math . max ( currentIndex - 1 , 0 ) ] ; // not wrap around
212- } ,
213- [ getOptionValues ]
214- ) ;
215-
216- // Scroll focused element into view
217- const scrollFocusedElementIntoView = useEventCallback (
218- ( nextFocusedValue : PicklistValue | undefined ) => {
219- if ( ! nextFocusedValue || ! dropdownElRef . current ) {
220- return ;
221- }
222-
223- const dropdownContainer = dropdownElRef . current ;
224- const targetElement = dropdownContainer . querySelector (
225- `#${ CSS . escape ( optionIdPrefix ) } -${ nextFocusedValue } `
226- ) ;
227-
228- if ( ! ( targetElement instanceof HTMLElement ) ) {
229- return ;
230- }
231-
232- // Calculate element position within container
233- const elementTopPosition = targetElement . offsetTop ;
234- const elementBottomPosition =
235- elementTopPosition + targetElement . offsetHeight ;
236-
237- // Calculate currently visible area
238- const currentScrollPosition = dropdownContainer . scrollTop ;
239- const visibleAreaHeight = dropdownContainer . clientHeight ;
240- const visibleAreaTop = currentScrollPosition ;
241- const visibleAreaBottom = currentScrollPosition + visibleAreaHeight ;
242-
243- // Check if element is outside the visible area
244- const isAbove = elementTopPosition < visibleAreaTop ;
245- const isBelow = elementBottomPosition > visibleAreaBottom ;
246-
247- // Scroll only if element is not currently visible
248- if ( isAbove || isBelow ) {
249- targetElement . scrollIntoView ( {
250- block : 'center' ,
251- } ) ;
252- }
253- }
254- ) ;
255-
256- // Set initial focus when dropdown opens
257- useEffect ( ( ) => {
258- if ( opened && ! focusedValue ) {
259- // Focus on first selected value or first option
260- const initialFocus =
261- values . length > 0 ? values [ 0 ] : getOptionValues ( ) [ 0 ] ;
262- setFocusedValue ( initialFocus ) ;
263- scrollFocusedElementIntoView ( initialFocus ) ;
264- } else if ( ! opened ) {
265- // Reset focus when dropdown closes
266- setFocusedValue ( undefined ) ;
267- }
268- } , [
269- opened ,
270- values ,
271- getOptionValues ,
272- focusedValue ,
273- scrollFocusedElementIntoView ,
274- ] ) ;
275-
276161 const elRef = useRef < HTMLDivElement | null > ( null ) ;
277162 const elementRef = useMergeRefs ( [ elRef , elementRef_ ] ) ;
278163 const comboboxElRef = useRef < HTMLDivElement | null > ( null ) ;
@@ -325,6 +210,20 @@ export const Picklist: (<MultiSelect extends boolean | undefined>(
325210 ) ;
326211 } ) ;
327212
213+ const focusToTargetItemEl = useEventCallback ( ( ) => {
214+ const dropdownEl = dropdownElRef . current ;
215+ if ( ! dropdownEl ) {
216+ return ;
217+ }
218+ const firstItemEl : HTMLAnchorElement | null =
219+ dropdownEl . querySelector (
220+ '.slds-is-selected > .react-slds-menuitem[tabIndex]'
221+ ) || dropdownEl . querySelector ( '.react-slds-menuitem[tabIndex]' ) ;
222+ if ( firstItemEl ) {
223+ firstItemEl . focus ( ) ;
224+ }
225+ } ) ;
226+
328227 const onClick = useEventCallback ( ( ) => {
329228 if ( ! disabled ) {
330229 setOpened ( ( opened ) => ! opened ) ;
@@ -346,81 +245,25 @@ export const Picklist: (<MultiSelect extends boolean | undefined>(
346245 } , 10 ) ;
347246 } ) ;
348247
349- const onKeyDown = useEventCallback ( ( e : React . KeyboardEvent ) => {
248+ const onKeydown = useEventCallback ( ( e : React . KeyboardEvent ) => {
350249 if ( e . keyCode === 40 ) {
351250 // down
352251 e . preventDefault ( ) ;
353252 e . stopPropagation ( ) ;
354253 if ( ! opened ) {
355254 setOpened ( true ) ;
255+ setTimeout ( ( ) => {
256+ focusToTargetItemEl ( ) ;
257+ } , 10 ) ;
356258 } else {
357- // Navigate to next option
358- const nextValue = getNextValue ( focusedValue ) ;
359- setFocusedValue ( nextValue ) ;
360- scrollFocusedElementIntoView ( nextValue ) ;
361- }
362- } else if ( e . keyCode === 38 ) {
363- // up
364- e . preventDefault ( ) ;
365- e . stopPropagation ( ) ;
366- if ( ! opened ) {
367- setOpened ( true ) ;
368- } else {
369- // Navigate to previous option
370- const prevValue = getPrevValue ( focusedValue ) ;
371- setFocusedValue ( prevValue ) ;
372- scrollFocusedElementIntoView ( prevValue ) ;
373- }
374- } else if ( e . keyCode === 9 ) {
375- // Tab or Shift+Tab
376- if ( opened ) {
377- e . preventDefault ( ) ;
378- e . stopPropagation ( ) ;
379- const optionValues = getOptionValues ( ) ;
380- const currentIndex = focusedValue
381- ? optionValues . indexOf ( focusedValue )
382- : - 1 ;
383-
384- if ( e . shiftKey ) {
385- // Shift+Tab - Navigate to previous option or close if at first
386- if ( currentIndex <= 0 ) {
387- // At first option or no focus, close the picklist
388- setOpened ( false ) ;
389- onComplete ?.( ) ;
390- } else {
391- const prevValue = getPrevValue ( focusedValue ) ;
392- setFocusedValue ( prevValue ) ;
393- scrollFocusedElementIntoView ( prevValue ) ;
394- }
395- } else {
396- // Tab - Navigate to next option or close if at last
397- if ( currentIndex >= optionValues . length - 1 ) {
398- // At last option, close the picklist
399- setOpened ( false ) ;
400- onComplete ?.( ) ;
401- } else {
402- const nextValue = getNextValue ( focusedValue ) ;
403- setFocusedValue ( nextValue ) ;
404- scrollFocusedElementIntoView ( nextValue ) ;
405- }
406- }
259+ focusToTargetItemEl ( ) ;
407260 }
408261 } else if ( e . keyCode === 27 ) {
409262 // ESC
410263 e . preventDefault ( ) ;
411264 e . stopPropagation ( ) ;
412265 setOpened ( false ) ;
413266 onComplete ?.( ) ;
414- } else if ( e . keyCode === 13 || e . keyCode === 32 ) {
415- // Enter or Space
416- e . preventDefault ( ) ;
417- e . stopPropagation ( ) ;
418- if ( opened && focusedValue != null ) {
419- // Select focused option
420- onPicklistItemSelect ( focusedValue ) ;
421- } else {
422- setOpened ( ( opened ) => ! opened ) ;
423- }
424267 }
425268 onKeyDown_ ?.( e ) ;
426269 } ) ;
@@ -473,11 +316,6 @@ export const Picklist: (<MultiSelect extends boolean | undefined>(
473316 'slds-is-disabled' : disabled ,
474317 }
475318 ) ;
476- const dropdownClassNames = classnames (
477- 'slds-dropdown' ,
478- 'slds-dropdown_length-5' ,
479- menuSize ? `slds-dropdown_${ menuSize } ` : 'slds-dropdown_fluid'
480- ) ;
481319
482320 const formElemProps = {
483321 id,
@@ -494,7 +332,6 @@ export const Picklist: (<MultiSelect extends boolean | undefined>(
494332 values,
495333 multiSelect,
496334 onSelect : onPicklistItemSelect ,
497- focusedValue,
498335 optionIdPrefix,
499336 } ;
500337
@@ -516,11 +353,8 @@ export const Picklist: (<MultiSelect extends boolean | undefined>(
516353 aria-expanded = { opened }
517354 aria-haspopup = 'listbox'
518355 aria-disabled = { disabled }
519- aria-activedescendant = {
520- focusedValue ? `${ optionIdPrefix } -${ focusedValue } ` : undefined
521- }
522356 onClick = { onClick }
523- onKeyDown = { onKeyDown }
357+ onKeyDown = { onKeydown }
524358 onBlur = { onBlur }
525359 { ...rprops }
526360 >
@@ -534,25 +368,22 @@ export const Picklist: (<MultiSelect extends boolean | undefined>(
534368 </ span >
535369 </ div >
536370 { opened && (
537- < div
538- id = { listboxId }
539- className = { dropdownClassNames }
540- role = 'listbox'
541- aria-label = 'Options'
542- tabIndex = { 0 }
543- aria-busy = { false }
544- ref = { dropdownRef }
545- style = { { ...menuStyle , left : 0 , transform : 'translate(0)' } }
371+ < DropdownMenu
372+ portalClassName = { classnames ( className , 'slds-picklist' ) }
373+ elementRef = { dropdownRef }
374+ size = { menuSize }
375+ style = { menuStyle }
376+ onMenuSelect = { onPicklistItemSelect }
377+ onMenuClose = { ( ) => {
378+ setOpened ( false ) ;
379+ onComplete ?.( ) ;
380+ } }
381+ onBlur = { onBlur }
546382 >
547- < ul
548- className = 'slds-listbox slds-listbox_vertical'
549- role = 'presentation'
550- >
551- < PicklistContext . Provider value = { contextValue } >
552- { children }
553- </ PicklistContext . Provider >
554- </ ul >
555- </ div >
383+ < PicklistContext . Provider value = { contextValue } >
384+ { children }
385+ </ PicklistContext . Provider >
386+ </ DropdownMenu >
556387 ) }
557388 </ div >
558389 </ div >
@@ -582,54 +413,22 @@ export const PicklistItem: FC<PicklistItemProps> = ({
582413 value,
583414 disabled,
584415 children,
416+ ...props
585417} ) => {
586- const { values, multiSelect, onSelect, focusedValue, optionIdPrefix } =
587- useContext ( PicklistContext ) ;
418+ const { values } = useContext ( PicklistContext ) ;
588419 const selected =
589420 selected_ ?? ( value != null ? values . indexOf ( value ) >= 0 : false ) ;
590- const isFocused = focusedValue === value ;
591-
592- const onClick = useEventCallback ( ( ) => {
593- if ( ! disabled && value != null ) {
594- onSelect ( value ) ;
595- }
596- } ) ;
597-
598- const itemClassNames = classnames (
599- 'slds-media' ,
600- 'slds-listbox__option' ,
601- 'slds-listbox__option_plain' ,
602- 'slds-media_small' ,
603- {
604- 'slds-is-selected' : selected ,
605- 'slds-has-focus' : isFocused ,
606- }
607- ) ;
608421
609422 return (
610- < li role = 'presentation' className = 'slds-listbox__item' >
611- < div
612- id = { value ? `${ optionIdPrefix } -${ value } ` : undefined }
613- className = { itemClassNames }
614- role = 'option'
615- aria-selected = { selected }
616- aria-checked = { multiSelect ? selected : undefined }
617- aria-disabled = { disabled }
618- onClick = { onClick }
619- >
620- < span className = 'slds-media__figure slds-listbox__option-icon' >
621- { selected && (
622- < span className = 'slds-icon_container slds-icon-utility-check slds-current-color' >
623- < Icon icon = 'check' className = 'slds-icon slds-icon_x-small' />
624- </ span >
625- ) }
626- </ span >
627- < span className = 'slds-media__body' >
628- < span className = 'slds-truncate' title = { String ( label || children ) } >
629- { label || children }
630- </ span >
631- </ span >
632- </ div >
633- </ li >
423+ < DropdownMenuItem
424+ icon = { selected ? 'check' : 'none' }
425+ role = 'option'
426+ selected = { selected }
427+ disabled = { disabled }
428+ eventKey = { value }
429+ { ...props }
430+ >
431+ { label || children }
432+ </ DropdownMenuItem >
634433 ) ;
635434} ;
0 commit comments