@@ -4,11 +4,59 @@ import { ColDef } from '../../models/colDef';
44import { useLogger } from '../utils' ;
55import { useEventCallback } from '../../utils/material-ui-utils' ;
66import { COL_RESIZE_START , COL_RESIZE_STOP } from '../../constants/eventsConstants' ;
7- import { HEADER_CELL_CSS_CLASS } from '../../constants/cssClassesConstants' ;
8- import { findCellElementsFromCol } from '../../utils' ;
7+ import {
8+ HEADER_CELL_CSS_CLASS ,
9+ HEADER_CELL_SEPARATOR_RESIZABLE_CSS_CLASS ,
10+ } from '../../constants/cssClassesConstants' ;
11+ import {
12+ findCellElementsFromCol ,
13+ findParentElementFromClassName ,
14+ getFieldFromHeaderElem ,
15+ findHeaderElementFromField ,
16+ } from '../../utils/domUtils' ;
917import { ApiRef } from '../../models' ;
18+ import { CursorCoordinates } from '../../models/api/columnReorderApi' ;
1019
1120const MIN_COL_WIDTH = 50 ;
21+ let cachedSupportsTouchActionNone = false ;
22+
23+ // TODO: remove support for Safari < 13.
24+ // https://caniuse.com/#search=touch-action
25+ //
26+ // Safari, on iOS, supports touch action since v13.
27+ // Over 80% of the iOS phones are compatible
28+ // in August 2020.
29+ function doesSupportTouchActionNone ( ) : boolean {
30+ if ( ! cachedSupportsTouchActionNone ) {
31+ const element = document . createElement ( 'div' ) ;
32+ element . style . touchAction = 'none' ;
33+ document . body . appendChild ( element ) ;
34+ cachedSupportsTouchActionNone = window . getComputedStyle ( element ) . touchAction === 'none' ;
35+ element . parentElement ! . removeChild ( element ) ;
36+ }
37+ return cachedSupportsTouchActionNone ;
38+ }
39+
40+ function trackFinger ( event , currentTouchId ) : CursorCoordinates | boolean {
41+ if ( currentTouchId !== undefined && event . changedTouches ) {
42+ for ( let i = 0 ; i < event . changedTouches . length ; i += 1 ) {
43+ const touch = event . changedTouches [ i ] ;
44+ if ( touch . identifier === currentTouchId ) {
45+ return {
46+ x : touch . clientX ,
47+ y : touch . clientY ,
48+ } ;
49+ }
50+ }
51+
52+ return false ;
53+ }
54+
55+ return {
56+ x : event . clientX ,
57+ y : event . clientY ,
58+ } ;
59+ }
1260
1361// TODO improve experience for last column
1462export const useColumnResize = ( columnsRef : React . RefObject < HTMLDivElement > , apiRef : ApiRef ) => {
@@ -18,6 +66,8 @@ export const useColumnResize = (columnsRef: React.RefObject<HTMLDivElement>, api
1866 const colCellElementsRef = React . useRef < NodeListOf < Element > > ( ) ;
1967 const initialOffset = React . useRef < number > ( ) ;
2068 const stopResizeEventTimeout = React . useRef < number > ( ) ;
69+ const touchId = React . useRef < number > ( ) ;
70+ const columnsHeaderElement = columnsRef . current ;
2171
2272 const updateWidth = ( newWidth : number ) => {
2373 logger . debug ( `Updating width to ${ newWidth } for col ${ colDefRef . current ! . field } ` ) ;
@@ -81,8 +131,9 @@ export const useColumnResize = (columnsRef: React.RefObject<HTMLDivElement>, api
81131 // Avoid text selection
82132 event . preventDefault ( ) ;
83133
84- colElementRef . current = event . currentTarget . closest (
85- `.${ HEADER_CELL_CSS_CLASS } ` ,
134+ colElementRef . current = findParentElementFromClassName (
135+ event . currentTarget ,
136+ HEADER_CELL_CSS_CLASS ,
86137 ) as HTMLDivElement ;
87138 const field = colElementRef . current . getAttribute ( 'data-field' ) as string ;
88139 const colDef = apiRef . current . getColumnFromField ( field ) ;
@@ -91,7 +142,7 @@ export const useColumnResize = (columnsRef: React.RefObject<HTMLDivElement>, api
91142 apiRef . current . publishEvent ( COL_RESIZE_START , { field } ) ;
92143
93144 colDefRef . current = colDef ;
94- colElementRef . current = columnsRef . current ! . querySelector (
145+ colElementRef . current = columnsHeaderElement ! . querySelector (
95146 `[data-field="${ colDef . field } "]` ,
96147 ) as HTMLDivElement ;
97148
@@ -110,19 +161,121 @@ export const useColumnResize = (columnsRef: React.RefObject<HTMLDivElement>, api
110161 doc . addEventListener ( 'mouseup' , handleResizeMouseUp ) ;
111162 } ) ;
112163
164+ const handleTouchEnd = useEventCallback ( ( nativeEvent ) => {
165+ const finger = trackFinger ( nativeEvent , touchId . current ) ;
166+
167+ if ( ! finger ) {
168+ return ;
169+ }
170+
171+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
172+ stopListening ( ) ;
173+
174+ apiRef . current ! . updateColumn ( colDefRef . current as ColDef ) ;
175+
176+ clearTimeout ( stopResizeEventTimeout . current ) ;
177+ stopResizeEventTimeout . current = setTimeout ( ( ) => {
178+ apiRef . current . publishEvent ( COL_RESIZE_STOP ) ;
179+ } ) ;
180+
181+ logger . debug (
182+ `Updating col ${ colDefRef . current ! . field } with new width: ${ colDefRef . current ! . width } ` ,
183+ ) ;
184+ } ) ;
185+
186+ const handleTouchMove = useEventCallback ( ( nativeEvent ) => {
187+ const finger = trackFinger ( nativeEvent , touchId . current ) ;
188+ if ( ! finger ) {
189+ return ;
190+ }
191+
192+ // Cancel move in case some other element consumed a touchmove event and it was not fired.
193+ if ( nativeEvent . type === 'mousemove' && nativeEvent . buttons === 0 ) {
194+ handleTouchEnd ( nativeEvent ) ;
195+ return ;
196+ }
197+
198+ let newWidth =
199+ initialOffset . current ! +
200+ ( finger as CursorCoordinates ) . x -
201+ colElementRef . current ! . getBoundingClientRect ( ) . left ;
202+ newWidth = Math . max ( MIN_COL_WIDTH , newWidth ) ;
203+
204+ updateWidth ( newWidth ) ;
205+ } ) ;
206+
207+ const handleTouchStart = useEventCallback ( ( event ) => {
208+ const cellSeparator = findParentElementFromClassName (
209+ event . target ,
210+ HEADER_CELL_SEPARATOR_RESIZABLE_CSS_CLASS ,
211+ ) ;
212+ // Let the event bubble if the target is not a col separator
213+ if ( ! cellSeparator ) return ;
214+ // If touch-action: none; is not supported we need to prevent the scroll manually.
215+ if ( ! doesSupportTouchActionNone ( ) ) {
216+ event . preventDefault ( ) ;
217+ }
218+
219+ const touch = event . changedTouches [ 0 ] ;
220+ if ( touch != null ) {
221+ // A number that uniquely identifies the current finger in the touch session.
222+ touchId . current = touch . identifier ;
223+ }
224+
225+ colElementRef . current = findParentElementFromClassName (
226+ event . target ,
227+ HEADER_CELL_CSS_CLASS ,
228+ ) as HTMLDivElement ;
229+ const field = getFieldFromHeaderElem ( colElementRef . current ! ) ;
230+ const colDef = apiRef . current . getColumnFromField ( field ) ;
231+
232+ logger . debug ( `Start Resize on col ${ colDef . field } ` ) ;
233+ apiRef . current . publishEvent ( COL_RESIZE_START , { field } ) ;
234+
235+ colDefRef . current = colDef ;
236+ colElementRef . current = findHeaderElementFromField (
237+ columnsHeaderElement ! ,
238+ colDef . field ,
239+ ) as HTMLDivElement ;
240+ colCellElementsRef . current = findCellElementsFromCol ( colElementRef . current ) as NodeListOf <
241+ Element
242+ > ;
243+
244+ initialOffset . current =
245+ ( colDefRef . current . width as number ) -
246+ ( touch . clientX - colElementRef . current ! . getBoundingClientRect ( ) . left ) ;
247+
248+ const doc = ownerDocument ( event . currentTarget as HTMLElement ) ;
249+ doc . addEventListener ( 'touchmove' , handleTouchMove ) ;
250+ doc . addEventListener ( 'touchend' , handleTouchEnd ) ;
251+ } ) ;
252+
113253 const stopListening = React . useCallback ( ( ) => {
114254 const doc = ownerDocument ( apiRef . current . rootElementRef ! . current as HTMLElement ) ;
115255 doc . body . style . removeProperty ( 'cursor' ) ;
116256 doc . removeEventListener ( 'mousemove' , handleResizeMouseMove ) ;
117257 doc . removeEventListener ( 'mouseup' , handleResizeMouseUp ) ;
118- } , [ apiRef , handleResizeMouseMove , handleResizeMouseUp ] ) ;
258+ doc . removeEventListener ( 'touchmove' , handleTouchMove ) ;
259+ doc . removeEventListener ( 'touchend' , handleTouchEnd ) ;
260+ } , [ apiRef , handleResizeMouseMove , handleResizeMouseUp , handleTouchMove , handleTouchEnd ] ) ;
119261
120262 React . useEffect ( ( ) => {
263+ columnsHeaderElement ?. addEventListener ( 'touchstart' , handleTouchStart , {
264+ passive : doesSupportTouchActionNone ( ) ,
265+ } ) ;
266+
121267 return ( ) => {
268+ columnsHeaderElement ?. removeEventListener ( 'touchstart' , handleTouchStart ) ;
269+
122270 clearTimeout ( stopResizeEventTimeout . current ) ;
123271 stopListening ( ) ;
124272 } ;
125- } , [ stopListening ] ) ;
273+ } , [ columnsHeaderElement , handleTouchStart , stopListening ] ) ;
126274
127- return React . useMemo ( ( ) => ( { onMouseDown : handleMouseDown } ) , [ handleMouseDown ] ) ;
275+ return React . useMemo (
276+ ( ) => ( {
277+ onMouseDown : handleMouseDown ,
278+ } ) ,
279+ [ handleMouseDown ] ,
280+ ) ;
128281} ;
0 commit comments