Skip to content

Commit f668150

Browse files
DanailHoliviertassinari
authored andcommitted
[DataGrid] Add touch support on column resize (mui#537)
* Add support for touch column resize * Change the touch start event listner from the separator to the document * Allow resizing on mobile only when column separator is touched * fix hover * Attach touchStart event to the columns header element, not the document * Add support for touch column resize * Change the touch start event listner from the separator to the document * Allow resizing on mobile only when column separator is touched * Attach touchStart event to the columns header element, not the document * fix hover * minimize git diff * Use findParentElementFromClassName dom util * Add new findHeaderElementFromField dom util Co-authored-by: Olivier Tassinari <[email protected]>
1 parent a92a543 commit f668150

File tree

7 files changed

+178
-12
lines changed

7 files changed

+178
-12
lines changed

packages/grid/_modules_/grid/components/styled-wrappers/GridRootStyles.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,15 @@ export const useStyles = makeStyles(
124124
},
125125
'& .MuiDataGrid-columnSeparatorResizable': {
126126
cursor: 'col-resize',
127-
'&:hover, &.Mui-resizing': {
127+
touchAction: 'none',
128+
'&:hover': {
129+
color: theme.palette.text.primary,
130+
// Reset on touch devices, it doesn't add specificity
131+
'@media (hover: none)': {
132+
color: borderColor,
133+
},
134+
},
135+
'&.Mui-resizing': {
128136
color: theme.palette.text.primary,
129137
},
130138
},

packages/grid/_modules_/grid/constants/cssClassesConstants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
export const CELL_CSS_CLASS = 'MuiDataGrid-cell';
33
export const ROW_CSS_CLASS = 'MuiDataGrid-row';
44
export const HEADER_CELL_CSS_CLASS = 'MuiDataGrid-colCell';
5+
export const HEADER_CELL_SEPARATOR_RESIZABLE_CSS_CLASS = 'MuiDataGrid-columnSeparatorResizable';
56
export const DATA_CONTAINER_CSS_CLASS = 'data-container';
67
export const HEADER_CELL_DROP_ZONE_CSS_CLASS = 'MuiDataGrid-colCell-dropZone';
78
export const HEADER_CELL_DRAGGING_CSS_CLASS = 'MuiDataGrid-colCell-dragging';

packages/grid/_modules_/grid/hooks/features/useColumnResize.tsx

Lines changed: 161 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,59 @@ import { ColDef } from '../../models/colDef';
44
import { useLogger } from '../utils';
55
import { useEventCallback } from '../../utils/material-ui-utils';
66
import { 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';
917
import { ApiRef } from '../../models';
18+
import { CursorCoordinates } from '../../models/api/columnReorderApi';
1019

1120
const 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
1462
export 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
};

packages/grid/_modules_/grid/hooks/features/virtualization/useVirtualRows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const useVirtualRows = (
3737
const paginationState = useGridSelector<PaginationState>(apiRef, paginationSelector);
3838
const totalRowCount = useGridSelector<number>(apiRef, rowCountSelector);
3939

40-
const [scrollTo] = useScrollFn(apiRef, renderingZoneRef, colRef);
40+
const [scrollTo] = useScrollFn(renderingZoneRef, colRef);
4141
const [renderedColRef, updateRenderedCols] = useVirtualColumns(options, apiRef);
4242

4343
const setRenderingState = React.useCallback(

packages/grid/_modules_/grid/hooks/utils/useScrollFn.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { ScrollFn, ScrollParams } from '../../models/params/scrollParams';
44
import { useLogger } from './useLogger';
55

66
export function useScrollFn(
7-
apiRef: any,
87
renderingZoneElementRef: React.RefObject<HTMLDivElement>,
98
columnHeadersElementRef: React.RefObject<HTMLDivElement>,
109
): [ScrollFn] {

packages/grid/_modules_/grid/utils/domUtils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ export function getIdFromRowElem(rowEl: Element): string {
2828
export function getFieldFromHeaderElem(colCellEl: Element): string {
2929
return colCellEl.getAttribute('data-field')!;
3030
}
31+
32+
export function findHeaderElementFromField(elem: Element, field: string): Element | null {
33+
return elem.querySelector(`[data-field="${field}"]`);
34+
}
35+
3136
export function findCellElementsFromCol(col: HTMLElement): NodeListOf<Element> | null {
3237
const field = col.getAttribute('data-field');
3338
const root = findParentElementFromClassName(col, 'MuiDataGrid-root');

packages/storybook/src/stories/grid-resize.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const ResizeSmallDataset = () => {
3030
Switch sizes
3131
</button>
3232
</div>
33-
<div style={{ width: size.width, height: size.height, display: 'flex' }}>
33+
<div style={{ width: size.width, height: size.height }}>
3434
<XGrid rows={data.rows} columns={data.columns} />
3535
</div>
3636
</React.Fragment>

0 commit comments

Comments
 (0)