@@ -60,59 +60,187 @@ export const ColumnsSelector = ({ children }: ColumnsSelectorProps) => {
6060
6161 const [ columnFilter , setColumnFilter ] = React . useState < string > ( '' ) ;
6262
63- if ( ! container ) return null ;
64-
6563 const childrenArray = Children . toArray ( children ) ;
6664 const paddedColumnRanks = padRanks ( columnRanks ?? [ ] , childrenArray . length ) ;
6765 const shouldDisplaySearchInput = childrenArray . length > 5 ;
6866
67+ const handleMove = ( index1 , index2 ) => {
68+ const colRanks = ! columnRanks
69+ ? padRanks ( [ ] , Math . max ( index1 , index2 ) + 1 )
70+ : Math . max ( index1 , index2 ) > columnRanks . length - 1
71+ ? padRanks ( columnRanks , Math . max ( index1 , index2 ) + 1 )
72+ : columnRanks ;
73+ const index1Pos = colRanks . findIndex (
74+ // eslint-disable-next-line eqeqeq
75+ index => index == index1
76+ ) ;
77+ const index2Pos = colRanks . findIndex (
78+ // eslint-disable-next-line eqeqeq
79+ index => index == index2
80+ ) ;
81+ if ( index1Pos === - 1 || index2Pos === - 1 ) {
82+ return ;
83+ }
84+ let newColumnRanks ;
85+ if ( index1Pos > index2Pos ) {
86+ newColumnRanks = [
87+ ...colRanks . slice ( 0 , index2Pos ) ,
88+ colRanks [ index1Pos ] ,
89+ ...colRanks . slice ( index2Pos , index1Pos ) ,
90+ ...colRanks . slice ( index1Pos + 1 ) ,
91+ ] ;
92+ } else {
93+ newColumnRanks = [
94+ ...colRanks . slice ( 0 , index1Pos ) ,
95+ ...colRanks . slice ( index1Pos + 1 , index2Pos + 1 ) ,
96+ colRanks [ index1Pos ] ,
97+ ...colRanks . slice ( index2Pos + 1 ) ,
98+ ] ;
99+ }
100+ setColumnRanks ( newColumnRanks ) ;
101+ return index2Pos ;
102+ } ;
103+
104+ const list = React . useRef < HTMLUListElement | null > ( null ) ;
105+ const draggedItem = React . useRef < HTMLLIElement | null > ( null ) ;
106+ const dropItem = React . useRef < HTMLLIElement | null > ( null ) ;
107+
108+ const handleKeyDown = ( event : React . KeyboardEvent ) => {
109+ // Use setTimeout to let MenuList handle the focus management
110+ setTimeout ( ( ) => {
111+ if ( document . activeElement ?. tagName !== 'LI' ) {
112+ return ;
113+ }
114+
115+ if ( event . key === ' ' ) {
116+ if ( ! draggedItem . current ) {
117+ // Start dragging the currently focused item
118+ draggedItem . current =
119+ document . activeElement as HTMLLIElement ;
120+ draggedItem . current . classList . add ( 'drag-active-keyboard' ) ;
121+ } else {
122+ if ( ! dropItem . current ) {
123+ return ;
124+ }
125+ // Drop the dragged item
126+ draggedItem . current . classList . remove (
127+ 'drag-active-keyboard'
128+ ) ;
129+ const itemToFocusIndex = handleMove (
130+ draggedItem . current . dataset . index ,
131+ dropItem . current ?. dataset . index
132+ ) ;
133+ setTimeout ( ( ) => {
134+ // We wait for the DOM to update before focusing
135+ // the item that was moved.
136+ // We use the actual position it was moved to and not the data-index which may not be updated yet
137+ if ( itemToFocusIndex && list . current ) {
138+ const itemToFocus =
139+ list . current . querySelectorAll ( 'li' ) [
140+ itemToFocusIndex
141+ ] ;
142+ if ( itemToFocus ) {
143+ ( itemToFocus as HTMLLIElement ) . focus ( ) ;
144+ }
145+ }
146+ draggedItem . current = null ;
147+ } ) ;
148+ }
149+ }
150+ if ( ! draggedItem . current ) {
151+ return ;
152+ }
153+ if ( event . key === 'ArrowDown' ) {
154+ // Swap the dragged item with the next one
155+ const nextItem = draggedItem . current . nextElementSibling ;
156+ if ( nextItem ) {
157+ draggedItem . current . parentNode ?. insertBefore (
158+ draggedItem . current ,
159+ nextItem . nextSibling
160+ ) ;
161+ dropItem . current = nextItem as HTMLLIElement ;
162+ draggedItem . current . focus ( ) ;
163+ } else {
164+ // Start of the list, move the dragged item as the first item
165+ draggedItem . current . parentNode ?. insertBefore (
166+ draggedItem . current ,
167+ draggedItem . current ?. parentNode ?. firstChild
168+ ) ;
169+ dropItem . current = draggedItem . current ?. parentNode
170+ ?. firstChild as HTMLLIElement ;
171+ draggedItem . current . focus ( ) ;
172+ }
173+ } else if ( event . key === 'ArrowUp' ) {
174+ // Swap the dragged item with the previous one
175+ const prevItem = draggedItem . current . previousElementSibling ;
176+ if ( prevItem ) {
177+ draggedItem . current ?. parentNode ?. insertBefore (
178+ draggedItem . current ,
179+ prevItem
180+ ) ;
181+ dropItem . current = prevItem as HTMLLIElement ;
182+ draggedItem . current . focus ( ) ;
183+ } else {
184+ // End of the list, move the dragged item as the last item
185+ draggedItem . current ?. parentNode ?. appendChild (
186+ draggedItem . current
187+ ) ;
188+ dropItem . current = draggedItem . current ?. parentNode
189+ ?. lastChild as HTMLLIElement ;
190+ draggedItem . current . focus ( ) ;
191+ }
192+ }
193+ } ) ;
194+ } ;
195+
196+ if ( ! container ) return null ;
197+
69198 return createPortal (
70- < MenuList >
199+ < >
71200 { shouldDisplaySearchInput ? (
72- < Box component = "li" tabIndex = { - 1 } >
73- < ResettableTextField
74- hiddenLabel
75- label = ""
76- value = { columnFilter }
77- onChange = { e => {
78- if ( typeof e === 'string' ) {
79- setColumnFilter ( e ) ;
80- return ;
81- }
82- setColumnFilter ( e . target . value ) ;
83- } }
84- placeholder = { translate ( 'ra.action.search_columns' , {
85- _ : 'Search columns' ,
86- } ) }
87- InputProps = { {
88- endAdornment : (
89- < InputAdornment position = "end" >
90- < SearchIcon color = "disabled" />
91- </ InputAdornment >
92- ) ,
93- } }
94- resettable
95- autoFocus
96- size = "small"
97- sx = { { mb : 1 } }
98- />
99- </ Box >
201+ < ResettableTextField
202+ hiddenLabel
203+ label = ""
204+ value = { columnFilter }
205+ onChange = { e => {
206+ if ( typeof e === 'string' ) {
207+ setColumnFilter ( e ) ;
208+ return ;
209+ }
210+ setColumnFilter ( e . target . value ) ;
211+ } }
212+ placeholder = { translate ( 'ra.action.search_columns' , {
213+ _ : 'Search columns' ,
214+ } ) }
215+ InputProps = { {
216+ endAdornment : (
217+ < InputAdornment position = "end" >
218+ < SearchIcon color = "disabled" />
219+ </ InputAdornment >
220+ ) ,
221+ } }
222+ resettable
223+ autoFocus
224+ size = "small"
225+ sx = { { my : 1 } }
226+ />
100227 ) : null }
101- { paddedColumnRanks . map ( ( position , index ) => (
102- < DataTableColumnRankContext . Provider
103- value = { position }
104- key = { index }
105- >
106- < DataTableColumnFilterContext . Provider
107- value = { columnFilter }
228+ < MenuList onKeyDown = { handleKeyDown } ref = { list } >
229+ { paddedColumnRanks . map ( ( position , index ) => (
230+ < DataTableColumnRankContext . Provider
231+ value = { position }
108232 key = { index }
109233 >
110- { childrenArray [ position ] }
111- </ DataTableColumnFilterContext . Provider >
112- </ DataTableColumnRankContext . Provider >
113- ) ) }
234+ < DataTableColumnFilterContext . Provider
235+ value = { columnFilter }
236+ key = { index }
237+ >
238+ { childrenArray [ position ] }
239+ </ DataTableColumnFilterContext . Provider >
240+ </ DataTableColumnRankContext . Provider >
241+ ) ) }
242+ </ MenuList >
114243 < Box
115- component = "li"
116244 className = "columns-selector-actions"
117245 sx = { { textAlign : 'center' , mt : 1 } }
118246 >
@@ -125,7 +253,7 @@ export const ColumnsSelector = ({ children }: ColumnsSelectorProps) => {
125253 Reset
126254 </ Button >
127255 </ Box >
128- </ MenuList > ,
256+ </ > ,
129257 container
130258 ) ;
131259} ;
0 commit comments