1- import React , { useEffect , useState } from 'react' ;
1+ import React , { useRef , useState } from 'react' ;
22
33import {
4+ Divider ,
45 FormGroup ,
56 HelperText ,
67 HelperTextItem ,
78 Label ,
9+ Menu ,
10+ MenuContainer ,
11+ MenuContent ,
12+ MenuList ,
13+ MenuSearch ,
14+ MenuSearchInput ,
815 MenuToggle ,
9- MenuToggleElement ,
10- Select ,
11- SelectList ,
16+ SearchInput ,
1217 SelectOption ,
13- TextInputGroup ,
14- TextInputGroupMain ,
1518} from '@patternfly/react-core' ;
1619
1720import { changeTimezone , selectTimezone } from '@/store/slices/wizard' ;
@@ -29,127 +32,121 @@ const TimezoneDropDown = () => {
2932
3033 const [ errorText , setErrorText ] = useState ( stepValidation . errors [ 'timezone' ] ) ;
3134 const [ isOpen , setIsOpen ] = useState ( false ) ;
32- const [ inputValue , setInputValue ] = useState < string > ( '' ) ;
33- const [ filterValue , setFilterValue ] = useState < string > ( '' ) ;
34- const [ selectOptions , setSelectOptions ] = useState < string [ ] > ( timezones ) ;
35-
36- useEffect ( ( ) => {
37- let filteredTimezones = timezones ;
38-
39- if ( filterValue ) {
40- const normalizedFilter = filterValue
41- . toLowerCase ( )
42- . replace ( / [ _ / ] / g, ' ' )
43- . replace ( / \s + / g, ' ' )
44- . trim ( ) ;
45-
46- filteredTimezones = timezones . filter ( ( timezone : string ) => {
47- const normalizedTimezone = timezone
48- . toLowerCase ( )
49- . replace ( / [ _ / ] / g, ' ' )
50- . replace ( / \s + / g, ' ' ) ;
51- return normalizedTimezone . includes ( normalizedFilter ) ;
52- } ) ;
53-
54- if ( ! isOpen ) {
55- setIsOpen ( true ) ;
56- }
57- }
58-
59- const sortedTimezones = [ ...filteredTimezones ] . sort ( ( a , b ) => {
60- if ( a === DEFAULT_TIMEZONE ) return - 1 ;
61- if ( b === DEFAULT_TIMEZONE ) return 1 ;
62- return 0 ;
63- } ) ;
35+ const [ searchValue , setSearchValue ] = useState ( '' ) ;
6436
65- setSelectOptions ( sortedTimezones ) ;
37+ const toggleRef = useRef < HTMLButtonElement > ( null ) ;
38+ const menuRef = useRef < HTMLDivElement > ( null ) ;
6639
67- // This useEffect hook should run *only* on when the filter value changes.
68- // eslint's exhaustive-deps rule does not support this use.
69- // eslint-disable-next-line react-hooks/exhaustive-deps
70- } , [ filterValue ] ) ;
71-
72- const onInputClick = ( ) => {
40+ const handleSearchChange = ( value : string ) => {
7341 if ( ! isOpen ) {
7442 setIsOpen ( true ) ;
75- } else if ( ! inputValue ) {
76- setIsOpen ( false ) ;
7743 }
44+ setSearchValue ( value ) ;
7845 } ;
7946
80- const onSelect = ( _event ?: React . MouseEvent , value ?: string | number ) => {
81- if ( value && typeof value === 'string' ) {
82- setInputValue ( value ) ;
83- setFilterValue ( '' ) ;
47+ const onSelect = (
48+ _event : React . MouseEvent | undefined ,
49+ itemId : string | number | undefined ,
50+ ) => {
51+ if ( itemId && typeof itemId === 'string' ) {
52+ dispatch ( changeTimezone ( itemId ) ) ;
8453 setErrorText ( '' ) ;
85- dispatch ( changeTimezone ( value ) ) ;
54+ setSearchValue ( '' ) ;
8655 setIsOpen ( false ) ;
8756 }
8857 } ;
8958
90- const onTextInputChange = ( _event : React . FormEvent , value : string ) => {
91- setInputValue ( value ) ;
92- setFilterValue ( value ) ;
93-
94- if ( value !== timezone ) {
95- dispatch ( changeTimezone ( '' ) ) ;
96- }
97- } ;
98-
99- const onToggleClick = ( ) => {
100- setIsOpen ( ! isOpen ) ;
101- } ;
102-
103- const toggle = ( toggleRef : React . Ref < MenuToggleElement > ) => (
59+ const normalizeTimezoneString = ( value : string ) : string =>
60+ value . toLowerCase ( ) . replace ( / [ _ / ] / g, ' ' ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
61+
62+ const normalizedFilter = normalizeTimezoneString ( searchValue ) ;
63+
64+ const filteredTimezones = normalizedFilter
65+ ? timezones . filter ( ( tz ) =>
66+ normalizeTimezoneString ( tz ) . includes ( normalizedFilter ) ,
67+ )
68+ : timezones ;
69+
70+ const sortedTimezones = [ ...filteredTimezones ] . sort ( ( a , b ) => {
71+ if ( a === DEFAULT_TIMEZONE ) return - 1 ;
72+ if ( b === DEFAULT_TIMEZONE ) return 1 ;
73+ return 0 ;
74+ } ) ;
75+
76+ const menuItems = sortedTimezones . map ( ( option ) => (
77+ < SelectOption key = { option } itemId = { option } >
78+ { option } { ' ' }
79+ { option === DEFAULT_TIMEZONE && (
80+ < Label color = 'blue' isCompact >
81+ Default
82+ </ Label >
83+ ) }
84+ </ SelectOption >
85+ ) ) ;
86+
87+ if ( searchValue && menuItems . length === 0 ) {
88+ menuItems . push (
89+ < SelectOption isDisabled key = 'no-results' >
90+ { `No results found for "${ searchValue } "` }
91+ </ SelectOption > ,
92+ ) ;
93+ }
94+
95+ const toggle = (
10496 < MenuToggle
10597 ref = { toggleRef }
106- variant = 'typeahead'
107- onClick = { onToggleClick }
98+ onClick = { ( ) => setIsOpen ( ! isOpen ) }
10899 isExpanded = { isOpen }
100+ isFullWidth
101+ data-testid = 'timezone-toggle'
109102 >
110- < TextInputGroup isPlain >
111- < TextInputGroupMain
112- value = { timezone ? timezone : inputValue }
113- onClick = { onInputClick }
114- onChange = { onTextInputChange }
115- autoComplete = 'off'
116- placeholder = 'Select a timezone'
117- isExpanded = { isOpen }
118- />
119- </ TextInputGroup >
103+ { timezone || 'Select a timezone' }
120104 </ MenuToggle >
121105 ) ;
122106
107+ const menu = (
108+ < Menu
109+ ref = { menuRef }
110+ onSelect = { onSelect }
111+ activeItemId = { timezone || '' }
112+ isScrollable
113+ >
114+ < MenuSearch >
115+ < MenuSearchInput >
116+ < SearchInput
117+ value = { searchValue }
118+ aria-label = 'Filter timezone'
119+ onChange = { ( _event , value ) => handleSearchChange ( value ) }
120+ onClear = { ( event ) => {
121+ event . stopPropagation ( ) ;
122+ handleSearchChange ( '' ) ;
123+ } }
124+ />
125+ </ MenuSearchInput >
126+ </ MenuSearch >
127+ < Divider />
128+ < MenuContent maxMenuHeight = '300px' >
129+ < MenuList > { menuItems } </ MenuList >
130+ </ MenuContent >
131+ </ Menu >
132+ ) ;
133+
123134 return (
124135 < FormGroup isRequired = { false } label = 'Timezone' >
125- < Select
126- isScrollable
136+ < MenuContainer
137+ menu = { menu }
138+ menuRef = { menuRef }
139+ toggle = { toggle }
140+ toggleRef = { toggleRef }
127141 isOpen = { isOpen }
128142 onOpenChange = { ( isOpen ) => setIsOpen ( isOpen ) }
129- selected = { timezone }
130- onSelect = { onSelect }
131- toggle = { toggle }
132- shouldFocusFirstItemOnOpen = { false }
133- >
134- < SelectList >
135- { selectOptions . length > 0 ? (
136- selectOptions . map ( ( option ) => (
137- < SelectOption key = { option } value = { option } >
138- { option } { ' ' }
139- { option === DEFAULT_TIMEZONE && (
140- < Label color = 'blue' isCompact >
141- Default
142- </ Label >
143- ) }
144- </ SelectOption >
145- ) )
146- ) : (
147- < SelectOption isDisabled >
148- { `No results found for "${ filterValue } "` }
149- </ SelectOption >
150- ) }
151- </ SelectList >
152- </ Select >
143+ onOpenChangeKeys = { [ 'Escape' ] }
144+ />
145+ < HelperText className = 'pf-v6-u-pt-sm' >
146+ < HelperTextItem >
147+ Network time servers for system clock synchronization
148+ </ HelperTextItem >
149+ </ HelperText >
153150 { errorText && (
154151 < HelperText >
155152 < HelperTextItem variant = { 'error' } > { errorText } </ HelperTextItem >
0 commit comments