10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
- import { ChangeEvent , RefObject , useCallback , useRef } from 'react' ;
13
+ import { ChangeEvent , Key , RefObject , useCallback , useRef } from 'react' ;
14
+ import { ColumnSize } from '@react-types/table' ;
14
15
import { DOMAttributes , MoveEndEvent , MoveMoveEvent } from '@react-types/shared' ;
15
16
import { focusSafely } from '@react-aria/focus' ;
16
17
import { focusWithoutScrolling , mergeProps , useId } from '@react-aria/utils' ;
17
18
import { getColumnHeaderId } from './utils' ;
18
19
import { GridNode } from '@react-types/grid' ;
19
20
// @ts -ignore
20
21
import intlMessages from '../intl/*.json' ;
21
- import { TableColumnResizeState , TableState } from '@react-stately/table' ;
22
+ import { TableState } from '@react-stately/table' ;
22
23
import { useKeyboard , useMove , usePress } from '@react-aria/interactions' ;
23
24
import { useLocale , useLocalizedStringFormatter } from '@react-aria/i18n' ;
24
25
@@ -28,37 +29,102 @@ export interface TableColumnResizeAria {
28
29
}
29
30
30
31
export interface AriaTableColumnResizeProps < T > {
32
+ /** An object representing the [column header](https://www.w3.org/TR/wai-aria-1.1/#columnheader). Contains all the relevant information that makes up the column header. */
31
33
column : GridNode < T > ,
34
+ /** Aria label for the hidden input. Gets read when resizing. */
32
35
label : string ,
33
- triggerRef : RefObject < HTMLDivElement > ,
36
+ /**
37
+ * Ref to the trigger if resizing was started from a column header menu. If it's provided,
38
+ * focus will be returned there when resizing is done.
39
+ * */
40
+ triggerRef ?: RefObject < HTMLDivElement > ,
41
+ /** If resizing is disabled. */
34
42
isDisabled ?: boolean ,
35
- onMove : ( e : MoveMoveEvent ) => void ,
36
- onMoveEnd : ( e : MoveEndEvent ) => void
43
+ /** If the resizer was moved. Different from onResize because it is always called. */
44
+ onMove ?: ( e : MoveMoveEvent ) => void ,
45
+ /**
46
+ * If the resizer was moved. Different from onResizeEnd because it is always called.
47
+ * It also carries the interaction details in the object.
48
+ * */
49
+ onMoveEnd ?: ( e : MoveEndEvent ) => void ,
50
+ /** Called when resizing starts. */
51
+ onResizeStart : ( key : Key ) => void ,
52
+ /** Called for every resize event that results in new column sizes. */
53
+ onResize : ( widths : Map < Key , number | string > ) => void ,
54
+ /** Called when resizing ends. */
55
+ onResizeEnd : ( key : Key ) => void
37
56
}
38
57
39
- export function useTableColumnResize < T > ( props : AriaTableColumnResizeProps < T > , state : TableState < T > , columnState : TableColumnResizeState < T > , ref : RefObject < HTMLInputElement > ) : TableColumnResizeAria {
40
- let { column : item , triggerRef, isDisabled} = props ;
41
- const stateRef = useRef < TableColumnResizeState < T > > ( null ) ;
42
- stateRef . current = columnState ;
58
+
59
+ export interface TableLayoutState {
60
+ /** Get the current width of the specified column. */
61
+ getColumnWidth : ( key : Key ) => number ,
62
+ /** Get the current min width of the specified column. */
63
+ getColumnMinWidth : ( key : Key ) => number ,
64
+ /** Get the current max width of the specified column. */
65
+ getColumnMaxWidth : ( key : Key ) => number ,
66
+ /** Get the currently resizing column. */
67
+ resizingColumn : Key ,
68
+ /** Called to update the state that resizing has started. */
69
+ onColumnResizeStart : ( key : Key ) => void ,
70
+ /**
71
+ * Called to update the state that a resize event has occurred.
72
+ * Returns the new widths for all columns based on the resized column.
73
+ **/
74
+ onColumnResize : ( column : Key , width : number ) => Map < Key , ColumnSize > ,
75
+ /** Called to update the state that resizing has ended. */
76
+ onColumnResizeEnd : ( key : Key ) => void
77
+ }
78
+
79
+ export function useTableColumnResize < T > ( props : AriaTableColumnResizeProps < T > , state : TableState < T > , layoutState : TableLayoutState , ref : RefObject < HTMLInputElement > ) : TableColumnResizeAria {
80
+ let { column : item , triggerRef, isDisabled, onResizeStart, onResize, onResizeEnd} = props ;
43
81
const stringFormatter = useLocalizedStringFormatter ( intlMessages ) ;
44
82
let id = useId ( ) ;
83
+ let isResizing = useRef ( false ) ;
84
+ let lastSize = useRef ( null ) ;
45
85
46
86
let { direction} = useLocale ( ) ;
47
87
let { keyboardProps} = useKeyboard ( {
48
88
onKeyDown : ( e ) => {
49
- if ( e . key === 'Escape' || e . key === 'Enter' || e . key === ' ' || e . key === 'Tab' ) {
89
+ if ( triggerRef ?. current && ( e . key === 'Escape' || e . key === 'Enter' || e . key === ' ' || e . key === 'Tab' ) ) {
50
90
e . preventDefault ( ) ;
51
91
// switch focus back to the column header on anything that ends edit mode
52
92
focusSafely ( triggerRef . current ) ;
53
93
}
54
94
}
55
95
} ) ;
56
96
97
+ let startResize = useCallback ( ( item ) => {
98
+ if ( ! isResizing . current ) {
99
+ layoutState . onColumnResizeStart ( item . key ) ;
100
+ onResizeStart ?.( item . key ) ;
101
+ }
102
+ isResizing . current = true ;
103
+ } , [ isResizing , onResizeStart , layoutState ] ) ;
104
+
105
+ let resize = useCallback ( ( item , newWidth ) => {
106
+ let sizes = layoutState . onColumnResize ( item . key , newWidth ) ;
107
+ onResize ?.( sizes ) ;
108
+ lastSize . current = sizes ;
109
+ } , [ onResize , layoutState ] ) ;
110
+
111
+ let endResize = useCallback ( ( item ) => {
112
+ if ( lastSize . current == null ) {
113
+ lastSize . current = layoutState . onColumnResize ( item . key , layoutState . getColumnWidth ( item . key ) ) ;
114
+ }
115
+ if ( isResizing . current ) {
116
+ layoutState . onColumnResizeEnd ( item . key ) ;
117
+ onResizeEnd ?.( lastSize . current ) ;
118
+ }
119
+ isResizing . current = false ;
120
+ lastSize . current = null ;
121
+ } , [ isResizing , onResizeEnd , layoutState ] ) ;
122
+
57
123
const columnResizeWidthRef = useRef < number > ( 0 ) ;
58
124
const { moveProps} = useMove ( {
59
125
onMoveStart ( ) {
60
- columnResizeWidthRef . current = stateRef . current . getColumnWidth ( item . key ) ;
61
- stateRef . current . onColumnResizeStart ( item ) ;
126
+ columnResizeWidthRef . current = layoutState . getColumnWidth ( item . key ) ;
127
+ startResize ( item ) ;
62
128
} ,
63
129
onMove ( e ) {
64
130
let { deltaX, deltaY, pointerType} = e ;
@@ -71,29 +137,29 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
71
137
}
72
138
deltaX *= 10 ;
73
139
}
140
+ props . onMove ?.( e ) ;
74
141
// if moving up/down only, no need to resize
75
142
if ( deltaX !== 0 ) {
76
143
columnResizeWidthRef . current += deltaX ;
77
- stateRef . current . onColumnResize ( item , columnResizeWidthRef . current ) ;
78
- props . onMove ( e ) ;
144
+ resize ( item , columnResizeWidthRef . current ) ;
79
145
}
80
146
} ,
81
147
onMoveEnd ( e ) {
82
148
let { pointerType} = e ;
83
149
columnResizeWidthRef . current = 0 ;
84
- props . onMoveEnd ( e ) ;
150
+ props . onMoveEnd ?. ( e ) ;
85
151
if ( pointerType === 'mouse' ) {
86
- stateRef . current . onColumnResizeEnd ( item ) ;
152
+ endResize ( item ) ;
87
153
}
88
154
}
89
155
} ) ;
90
- let min = Math . floor ( stateRef . current . getColumnMinWidth ( item . key ) ) ;
91
- let max = Math . floor ( stateRef . current . getColumnMaxWidth ( item . key ) ) ;
156
+
157
+ let min = Math . floor ( layoutState . getColumnMinWidth ( item . key ) ) ;
158
+ let max = Math . floor ( layoutState . getColumnMaxWidth ( item . key ) ) ;
92
159
if ( max === Infinity ) {
93
160
max = Number . MAX_SAFE_INTEGER ;
94
161
}
95
- let value = Math . floor ( stateRef . current . getColumnWidth ( item . key ) ) ;
96
-
162
+ let value = Math . floor ( layoutState . getColumnWidth ( item . key ) ) ;
97
163
let ariaProps = {
98
164
'aria-label' : props . label ,
99
165
'aria-orientation' : 'horizontal' as 'horizontal' ,
@@ -111,25 +177,29 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
111
177
} , [ ref ] ) ;
112
178
113
179
let onChange = ( e : ChangeEvent < HTMLInputElement > ) => {
114
- let currentWidth = stateRef . current . getColumnWidth ( item . key ) ;
180
+ let currentWidth = layoutState . getColumnWidth ( item . key ) ;
115
181
let nextValue = parseFloat ( e . target . value ) ;
116
182
117
183
if ( nextValue > currentWidth ) {
118
184
nextValue = currentWidth + 10 ;
119
185
} else {
120
186
nextValue = currentWidth - 10 ;
121
187
}
122
- stateRef . current . onColumnResize ( item , nextValue ) ;
188
+ props . onMove ( { pointerType : 'virtual' } as MoveMoveEvent ) ;
189
+ props . onMoveEnd ( { pointerType : 'virtual' } as MoveEndEvent ) ;
190
+ resize ( item , nextValue ) ;
123
191
} ;
124
192
125
193
let { pressProps} = usePress ( {
126
194
onPressStart : ( e ) => {
127
195
if ( e . ctrlKey || e . altKey || e . metaKey || e . shiftKey || e . pointerType === 'keyboard' ) {
128
196
return ;
129
197
}
130
- if ( e . pointerType === 'virtual' && columnState . currentlyResizingColumn != null ) {
131
- stateRef . current . onColumnResizeEnd ( item ) ;
132
- focusSafely ( triggerRef . current ) ;
198
+ if ( e . pointerType === 'virtual' && layoutState . resizingColumn != null ) {
199
+ endResize ( item ) ;
200
+ if ( triggerRef ?. current ) {
201
+ focusSafely ( triggerRef . current ) ;
202
+ }
133
203
return ;
134
204
}
135
205
focusInput ( ) ;
@@ -138,7 +208,9 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
138
208
if ( e . pointerType === 'touch' ) {
139
209
focusInput ( ) ;
140
210
} else if ( e . pointerType !== 'virtual' ) {
141
- focusSafely ( triggerRef . current ) ;
211
+ if ( triggerRef ?. current ) {
212
+ focusSafely ( triggerRef . current ) ;
213
+ }
142
214
}
143
215
}
144
216
} ) ;
@@ -155,11 +227,11 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
155
227
onFocus : ( ) => {
156
228
// useMove calls onMoveStart for every keypress, but we want resize start to only be called when we start resize mode
157
229
// call instead during focus and blur
158
- stateRef . current . onColumnResizeStart ( item ) ;
230
+ startResize ( item ) ;
159
231
state . setKeyboardNavigationDisabled ( true ) ;
160
232
} ,
161
233
onBlur : ( ) => {
162
- stateRef . current . onColumnResizeEnd ( item ) ;
234
+ endResize ( item ) ;
163
235
state . setKeyboardNavigationDisabled ( false ) ;
164
236
} ,
165
237
onChange,
0 commit comments