Skip to content

Commit

Permalink
Add Grid component (#251)
Browse files Browse the repository at this point in the history
* Update tokens

* Update tokens

* Add grid changes

* Update tokens

* Add Grid Changes

* Feature: Add grid changes

* Optimise: Remove unwanted code

* Fix types

* Add grid changes

* Fix Grid optimization

* Add Grid changes

* Fix header resize indicator

* Add grid changes

* Fix select all

* remove unwanted content

* Fix selction border not showing fix

* Fix resize pointer issue

* Fix grid selection border styling

* Add Grid Context Changes

* Add grid changes

* Update story

* Update tests

* Fix lint error

* Clear selection on page change

* Fix type error

* Remove console log

* fix typo

* Fix tests

* Fix Grid changes

* Add grid scroll changes

* Add scroll fix

* Add column resize changes

* Update grid resize logic

* Update token values

* Update token values

* Fix tests
  • Loading branch information
vineethasok authored Jan 16, 2024
1 parent 9ab8c42 commit b7b9c06
Show file tree
Hide file tree
Showing 26 changed files with 2,745 additions and 89 deletions.
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default {
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
setupFiles: ["./setupTests.ts"],
};
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"lodash": "^4.17.21",
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.5.0",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
"sortablejs": "^1.15.0"
},
"devDependencies": {
Expand All @@ -85,6 +87,7 @@
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-syntax-highlighter": "^15.5.7",
"@types/react-window": "^1.8.8",
"@types/sortablejs": "^1.15.2",
"@types/styled-components": "^5.1.26",
"@typescript-eslint/eslint-plugin": "^5.57.1",
Expand Down
3 changes: 3 additions & 0 deletions setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TextEncoder } from "util";

global.TextEncoder = TextEncoder;
5 changes: 4 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ import {
import { Dialog } from "@/components/Dialog/Dialog";
import { ConfirmationDialog } from "@/components/ConfirmationDialog/ConfirmationDialog";
import { ProgressBar } from "./components/ProgressBar/ProgressBar";
import GridExample from "./examples/GridExample";

const headers: Array<TableHeaderType> = [
{ label: "Company", isSortable: true, sortDir: "asc" },
{ label: "Contact", isSortable: true, sortDir: "desc", sortPosition: "start" },
{ label: "Country" },
];

const App = () => {
const [currentTheme, setCurrentTheme] = useState<ThemeName>("dark");
const [selectedButton, setSelectedButton] = useState("center1");
Expand Down Expand Up @@ -92,7 +94,6 @@ const App = () => {
return [...rows];
});
};

return (
<div style={{ padding: "6rem" }}>
<ClickUIProvider
Expand Down Expand Up @@ -603,6 +604,8 @@ const App = () => {
<div style={{ border: "1px solid blue" }}>Child 5</div>
<div style={{ border: "1px solid green" }}>Child 6</div>
</GridContainer>
<Spacer />
<GridExample />
</ClickUIProvider>
</div>
);
Expand Down
31 changes: 17 additions & 14 deletions src/components/ContextMenu/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@ import { HorizontalDirection, Icon, IconName } from "@/components";
import { Arrow, GenericMenuItem, GenericMenuPanel } from "../GenericMenu";
import PopoverArrow from "../icons/PopoverArrow";
import IconWrapper from "../IconWrapper/IconWrapper";
import { forwardRef } from "react";

export const ContextMenu = (props: RightMenu.ContextMenuProps) => (
<RightMenu.Root {...props} />
);

const ContextMenuTrigger = ({
disabled,
...props
}: RightMenu.ContextMenuTriggerProps) => {
return (
<RightMenu.Trigger
asChild
disabled={disabled}
>
<div {...props} />
</RightMenu.Trigger>
);
};
const ContextMenuTrigger = forwardRef<HTMLDivElement, RightMenu.ContextMenuTriggerProps>(
({ disabled, ...props }, ref) => {
return (
<RightMenu.Trigger
asChild
disabled={disabled}
>
<div
ref={ref}
{...props}
/>
</RightMenu.Trigger>
);
}
);

ContextMenuTrigger.displayName = "ContextMenuTrigger";
ContextMenu.Trigger = ContextMenuTrigger;
Expand Down Expand Up @@ -156,7 +159,7 @@ const ContextMenuSub = ({ ...props }: RightMenu.ContextMenuGroupProps) => {

ContextMenuSub.displayName = "ContextMenuSub";
ContextMenu.Sub = ContextMenuSub;
interface ContextMenuItemProps extends RightMenu.ContextMenuItemProps {
export interface ContextMenuItemProps extends RightMenu.ContextMenuItemProps {
icon?: IconName;
iconDir?: HorizontalDirection;
}
Expand Down
86 changes: 86 additions & 0 deletions src/components/Grid/Cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { memo } from "react";
import { GridChildComponentProps, areEqual } from "react-window";
import { ItemDataType } from "./types";
import { StyledCell } from "./StyledCell";

export const Cell = memo(
({
data,
rowIndex,
columnIndex,
style,
...props
}: GridChildComponentProps<ItemDataType>) => {
const {
cell: CellData,
getSelectionType,
focus,
columnCount,
rowCount,
showRowNumber,
showHeader,
rowHeight,
rounded,
} = data;

const { row: focusedRow, column: focusedColumn } = focus;
const isFocused = columnIndex === focusedColumn && rowIndex === focusedRow;
const rightOfFocus = columnIndex - 1 === focusedColumn && rowIndex === focusedRow;
const belowFocus = columnIndex === focusedColumn && rowIndex - 1 === focusedRow;

const selectionType = getSelectionType({
row: rowIndex,
column: columnIndex,
type: "cell",
});
const rightSelection = getSelectionType({
row: rowIndex,
column: columnIndex - 1,
type: "cell",
});
const belowSelection = getSelectionType({
row: rowIndex - 1,
column: columnIndex,
type: "cell",
});
const rightOfSelectionBorder =
(selectionType === "selectDirect" || rightSelection === "selectDirect") &&
selectionType !== rightSelection;
const belowSelectionBorder =
(selectionType === "selectDirect" || belowSelection === "selectDirect") &&
selectionType !== belowSelection;

const selectionBorderLeft = rightOfSelectionBorder || rightOfFocus || isFocused;
const selectionBorderTop = belowSelectionBorder || belowFocus || isFocused;
return (
<div
style={style}
data-row={rowIndex}
data-column={columnIndex}
>
<StyledCell
as={CellData}
rowIndex={rowIndex}
columnIndex={columnIndex}
type="row-cell"
data-selected={isFocused || selectionType === "selectDirect"}
data-focused={isFocused}
$isSelectedTop={selectionBorderTop}
$isSelectedLeft={selectionBorderLeft}
$isFocused={isFocused}
$isLastRow={rowCount - 1 === rowIndex}
$isLastColumn={columnCount - 1 === columnIndex}
$isFirstColumn={columnIndex === 0 && !showRowNumber}
$isFirstRow={rowIndex === 0 && !showHeader}
$selectionType={selectionType}
$height={rowHeight}
$rounded={rounded}
data-grid-row={rowIndex}
data-grid-column={columnIndex}
{...props}
/>
</div>
);
},
areEqual
);
127 changes: 127 additions & 0 deletions src/components/Grid/ColumnResizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { MouseEventHandler, PointerEventHandler, useCallback, useRef } from "react";
import styled from "styled-components";
import { ColumnResizeFn, SetResizeCursorPositionFn } from "./types";
import throttle from "lodash/throttle";

const ResizeSpan = styled.div<{ $height: number }>`
top: 0;
left: calc(100% - 4px);
z-index: 1;
position: absolute;
height: ${({ $height }) => $height}px;
cursor: col-resize;
width: 4px;
overflow: auto;
&:hover,
&:active,
&:hover {
background: ${({ theme }) => theme.click.grid.header.cell.color.stroke.selectDirect};
}
&:active {
height: 100%;
position: fixed;
}
`;
type PointerRefType = {
width: number;
pointerId: number;
initialClientX: number;
};

interface Props {
height: number;
onColumnResize: ColumnResizeFn;
columnIndex: number;
setResizeCursorPosition: SetResizeCursorPositionFn;
}
const ColumnResizer = ({
height,
onColumnResize: onColumnResizeProp,
columnIndex,
setResizeCursorPosition,
}: Props) => {
const resizeRef = useRef<HTMLDivElement>(null);
const pointerRef = useRef<PointerRefType | null>(null);
const onColumnResize = throttle(onColumnResizeProp, 1000);

const onMouseDown: MouseEventHandler<HTMLDivElement> = useCallback(
e => {
e.preventDefault();
e.stopPropagation();
if (e.detail > 1) {
onColumnResize(columnIndex, 0, "auto");
}
},
[columnIndex, onColumnResize]
);
const onPointerDown: PointerEventHandler<HTMLDivElement> = useCallback(
e => {
e.stopPropagation();
if (resizeRef.current) {
resizeRef.current.setPointerCapture(e.pointerId);
const header = resizeRef.current.closest(`[data-header="${columnIndex}"]`);
if (header) {
pointerRef.current = {
pointerId: e.pointerId,
initialClientX: e.clientX,
width: header.clientWidth,
};

setResizeCursorPosition(
resizeRef.current,
e.clientX,
header.clientWidth,
columnIndex
);
}
}
},
[columnIndex, setResizeCursorPosition]
);

const onMouseMove: MouseEventHandler<HTMLDivElement> = useCallback(
e => {
e.stopPropagation();
if (resizeRef.current && pointerRef.current) {
const header = resizeRef.current.closest(`[data-header="${columnIndex}"]`);
if (header) {
resizeRef.current.setPointerCapture(pointerRef.current.pointerId);
const width =
header.scrollWidth + (e.clientX - pointerRef.current.initialClientX);
setResizeCursorPosition(resizeRef.current, e.clientX, width, columnIndex);
pointerRef.current.width = Math.max(width, 50);
}
}
},
[columnIndex, setResizeCursorPosition]
);

return (
<ResizeSpan
ref={resizeRef}
$height={height}
onPointerDown={onPointerDown}
onPointerUp={e => {
e.preventDefault();
e.stopPropagation();
if (resizeRef.current && pointerRef.current?.pointerId) {
resizeRef.current.releasePointerCapture(pointerRef.current.pointerId);
const shouldCallResize = e.clientX !== pointerRef.current.initialClientX;
if (shouldCallResize) {
onColumnResize(columnIndex, pointerRef.current.width, "manual");
}
resizeRef.current.style.top = "0";
resizeRef.current.style.left = "calc(100% - 4px)";
pointerRef.current = null;
}
}}
onMouseMove={onMouseMove}
onMouseDown={onMouseDown}
onClick={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()}
data-resize
/>
);
};

export default ColumnResizer;
Loading

1 comment on commit b7b9c06

@vercel
Copy link

@vercel vercel bot commented on b7b9c06 Jan 16, 2024

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

click-ui – ./

click-ui.vercel.app
click-ui-git-main-clickhouse.vercel.app
click-ui-clickhouse.vercel.app

Please sign in to comment.