Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ $root: ".widget-datagrid";
align-self: center;
}

/* Drag handle */
.drag-handle {
cursor: grab;
pointer-events: auto;
position: relative;
width: 14px;
padding: 0;
flex-grow: 0;
display: flex;
justify-content: center;
z-index: 1;

&:hover {
background-color: var(--brand-primary-50, $brand-light);
color: var(--brand-primary, $brand-primary);
}
}

.drag-handle + .column-caption {
padding-inline-start: 4px;
}

&:focus:not(:focus-visible) {
outline: none;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import classNames from "classnames";
import { ReactElement } from "react";
import { ColumnHeader } from "./ColumnHeader";
import { useColumn, useColumnsStore, useDatagridConfig, useColumnHeaderVM } from "../model/hooks/injection-hooks";
import { ColumnResizerProps } from "./ColumnResizer";
import { observer } from "mobx-react-lite";

export interface ColumnContainerProps {
isLast?: boolean;
resizer: ReactElement<ColumnResizerProps>;
}

export const ColumnContainer = observer(function ColumnContainer(props: ColumnContainerProps): ReactElement {
const { columnsFilterable, id: gridId } = useDatagridConfig();
const { columnFilters } = useColumnsStore();
const { canSort, columnId, setHeaderElementRef, columnIndex, canResize, sortDir, header } = useColumn();
const vm = useColumnHeaderVM();
const caption = header.trim();

return (
<div
aria-sort={getAriaSort(canSort, sortDir)}
className={classNames("th", {
[`drop-${vm.dropTarget?.[1]}`]: columnId === vm.dropTarget?.[0],
dragging: columnId === vm.dragging?.[1],
"dragging-over-self": columnId === vm.dragging?.[1] && !vm.dropTarget
})}
role="columnheader"
style={!canSort ? { cursor: "unset" } : undefined}
title={caption}
ref={ref => setHeaderElementRef(ref)}
data-column-id={columnId}
onDrop={vm.handleOnDrop}
onDragEnter={vm.handleDragEnter}
onDragOver={vm.handleDragOver}
>
<div className={classNames("column-container")} id={`${gridId}-column${columnId}`}>
<ColumnHeader />
{columnsFilterable && (
<div className="filter" style={{ pointerEvents: vm.dragging ? "none" : undefined }}>
{columnFilters[columnIndex]?.renderFilterWidgets()}
</div>
)}
</div>
{canResize ? props.resizer : null}
</div>
);
});

function getAriaSort(canSort: boolean, sortDir: string | undefined): "ascending" | "descending" | "none" | undefined {
if (!canSort) {
return undefined;
}

switch (sortDir) {
case "asc":
return "ascending";
case "desc":
return "descending";
default:
return "none";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import classNames from "classnames";
import { DragEventHandler, DragEvent, HTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode } from "react";
import { FaArrowsAltV } from "./icons/FaArrowsAltV";
import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown";
import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp";
import { useColumn, useColumnHeaderVM } from "../model/hooks/injection-hooks";
import { observer } from "mobx-react-lite";

interface DragHandleProps {
draggable: boolean;
onDragStart?: DragEventHandler<HTMLSpanElement>;
onDragEnd?: DragEventHandler<HTMLSpanElement>;
}

export const ColumnHeader = observer(function ColumnHeader(): ReactElement {
const { header, canSort, alignment, toggleSort } = useColumn();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleSort will break.
replace with this:

const { header, canSort, alignement } = column
...
getSortProps(() => column.toggleSort)

const caption = header.trim();
const sortProps = canSort ? getSortProps(toggleSort) : null;
const vm = useColumnHeaderVM();

return (
<div
className={classNames("column-header", { clickable: canSort }, `align-column-${alignment}`)}
style={{ pointerEvents: vm.dragging ? "none" : undefined }}
{...sortProps}
aria-label={canSort ? "sort " + caption : caption}
>
{vm.isDraggable && (
<DragHandle draggable={vm.isDraggable} onDragStart={vm.handleDragStart} onDragEnd={vm.handleDragEnd} />
)}
<span className="column-caption">{caption.length > 0 ? caption : "\u00a0"}</span>
{canSort ? <SortIcon /> : null}
</div>
);
});

function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement {
const handleMouseDown = (e: MouseEvent<HTMLSpanElement>): void => {
// Only stop propagation, don't prevent default - we need default for drag to work
e.stopPropagation();
};

const handleClick = (e: MouseEvent<HTMLSpanElement>): void => {
// Stop click events from bubbling to prevent sorting
e.stopPropagation();
e.preventDefault();
};

const handleDragStart = (e: DragEvent<HTMLSpanElement>): void => {
// Don't stop propagation here - let the drag start properly
if (onDragStart) {
onDragStart(e);
}
};

const handleDragEnd = (e: DragEvent<HTMLSpanElement>): void => {
if (onDragEnd) {
onDragEnd(e);
}
};

return (
<span
className="drag-handle"
draggable={draggable}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onMouseDown={handleMouseDown}
onClick={handleClick}
>
</span>
);
}

function SortIcon(): ReactNode {
const column = useColumn();
switch (column.sortDir) {
case "asc":
return <FaLongArrowAltUp />;
case "desc":
return <FaLongArrowAltDown />;
default:
return <FaArrowsAltV />;
}
}

function getSortProps(toggleSort: () => void): HTMLAttributes<HTMLDivElement> {
return {
onClick: () => {
toggleSort();
},
onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggleSort();
}
},
role: "button",
tabIndex: 0
};
}
Original file line number Diff line number Diff line change
@@ -1,47 +1,41 @@
import { useEventCallback } from "@mendix/widget-plugin-hooks/useEventCallback";
import { MouseEvent, ReactElement, TouchEvent, useCallback, useEffect, useRef, useState } from "react";
import { useColumn, useColumnsStore } from "../model/hooks/injection-hooks";

export interface ColumnResizerProps {
minWidth?: number;
setColumnWidth: (width: number) => void;
onResizeEnds?: () => void;
onResizeStart?: () => void;
}

export function ColumnResizer({
minWidth = 50,
setColumnWidth,
onResizeEnds,
onResizeStart
}: ColumnResizerProps): ReactElement {
export function ColumnResizer({ minWidth = 50 }: ColumnResizerProps): ReactElement {
const column = useColumn();
const columnsStore = useColumnsStore();
const [isResizing, setIsResizing] = useState(false);
const [startPosition, setStartPosition] = useState(0);
const [currentWidth, setCurrentWidth] = useState(0);
const resizerReference = useRef<HTMLDivElement>(null);
const onStart = useEventCallback(onResizeStart);

const onStartDrag = useCallback(
(e: TouchEvent<HTMLDivElement> & MouseEvent<HTMLDivElement>): void => {
const mouseX = e.touches ? e.touches[0].screenX : e.screenX;
setStartPosition(mouseX);
setIsResizing(true);
if (resizerReference.current) {
const column = resizerReference.current.parentElement!;
setCurrentWidth(column.offsetWidth);
const columnElement = resizerReference.current.parentElement!;
setCurrentWidth(columnElement.offsetWidth);
}
onStart();
columnsStore.setIsResizing(true);
},
[onStart]
[columnsStore]
);
const onEndDrag = useCallback((): void => {
if (!isResizing) {
return;
}
setIsResizing(false);
setCurrentWidth(0);
onResizeEnds?.();
}, [onResizeEnds, isResizing]);
const setColumnWidthStable = useEventCallback(setColumnWidth);
columnsStore.setIsResizing(false);
}, [columnsStore, isResizing]);
const setColumnWidthStable = useEventCallback((width: number) => column.setSize(width));
const onMouseMove = useCallback(
(e: TouchEvent & MouseEvent & Event): void => {
if (!isResizing) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { ReactElement, useState } from "react";
import { ReactElement } from "react";
import { useColumnsStore, useDatagridConfig } from "../model/hooks/injection-hooks";
import { ColumnId } from "../typings/GridColumn";
import { CheckboxColumnHeader } from "./CheckboxColumnHeader";
import { ColumnProvider } from "./ColumnProvider";
import { ColumnResizer } from "./ColumnResizer";
import { ColumnSelector } from "./ColumnSelector";
import { Header } from "./Header";
import { ColumnContainer } from "./ColumnContainer";
import { HeaderSkeletonLoader } from "./loader/HeaderSkeletonLoader";

export function GridHeader(): ReactElement {
const { columnsHidable, id: gridId } = useDatagridConfig();
const columnsStore = useColumnsStore();
const columns = columnsStore.visibleColumns;
const [dragOver, setDragOver] = useState<[ColumnId, "before" | "after"] | undefined>(undefined);
const [isDragging, setIsDragging] = useState<[ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined>();

if (!columnsStore.loaded) {
return <HeaderSkeletonLoader size={columns.length} />;
Expand All @@ -25,19 +22,7 @@ export function GridHeader(): ReactElement {
<CheckboxColumnHeader key="headers_column_select_all" />
{columns.map(column => (
<ColumnProvider column={column} key={`${column.columnId}`}>
<Header
dropTarget={dragOver}
isDragging={isDragging}
resizer={
<ColumnResizer
onResizeStart={() => columnsStore.setIsResizing(true)}
onResizeEnds={() => columnsStore.setIsResizing(false)}
setColumnWidth={(width: number) => column.setSize(width)}
/>
}
setDropTarget={setDragOver}
setIsDragging={setIsDragging}
/>
<ColumnContainer resizer={<ColumnResizer />} />
</ColumnProvider>
))}
{columnsHidable && (
Expand Down
Loading