Skip to content

Commit

Permalink
feat: Add single click boolean checkbox (#484)
Browse files Browse the repository at this point in the history
* Update libs, add boolean checkbox
  • Loading branch information
matttdawson authored Sep 23, 2024
1 parent d701db5 commit 3b527ff
Show file tree
Hide file tree
Showing 24 changed files with 2,705 additions and 4,984 deletions.
7,325 changes: 2,399 additions & 4,926 deletions package-lock.json

Large diffs are not rendered by default.

60 changes: 30 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
},
"peerDependencies": {
"@linzjs/lui": ">=21",
"ag-grid-community": "^32.1.0",
"ag-grid-react": "^32.1.0",
"ag-grid-community": "^32.2.0",
"ag-grid-react": "^32.2.0",
"lodash-es": ">=4",
"react": ">=18",
"react-dom": ">=18"
Expand Down Expand Up @@ -77,51 +77,51 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@chromatic-com/storybook": "^1.8.0",
"@linzjs/lui": "^21.44.4",
"@chromatic-com/storybook": "^2.0.2",
"@linzjs/lui": "^21.46.0",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-node-resolve": "^15.2.4",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@storybook/addon-essentials": "^8.2.9",
"@storybook/addon-interactions": "^8.2.9",
"@storybook/addon-links": "^8.2.9",
"@storybook/react": "^8.2.9",
"@storybook/react-vite": "^8.2.9",
"@storybook/test": "^8.2.9",
"@storybook/addon-essentials": "^8.3.2",
"@storybook/addon-interactions": "^8.3.2",
"@storybook/addon-links": "^8.3.2",
"@storybook/react": "^8.3.2",
"@storybook/react-vite": "^8.3.2",
"@storybook/test": "^8.3.2",
"@storybook/test-runner": "^0.19.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^15.0.7",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/debounce-promise": "^3.1.9",
"@types/jest": "^29.5.12",
"@types/jest": "^29.5.13",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.39",
"@types/react": "^18.3.5",
"@types/node": "^22.5.5",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"ag-grid-community": "^32.1.0",
"ag-grid-react": "^32.1.0",
"ag-grid-community": "^32.2.0",
"ag-grid-react": "^32.2.0",
"babel-jest": "^29.7.0",
"babel-preset-react-app": "^10.0.1",
"chromatic": "^11.7.1",
"chromatic": "^11.10.2",
"css-loader": "^7.1.2",
"esbuild": "^0.23.1",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^28.8.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react": "^7.36.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^6.3.0",
Expand All @@ -131,33 +131,33 @@
"jest-expect-message": "^1.1.3",
"mkdirp": "^3.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.43",
"postcss": "^8.4.47",
"postcss-loader": "^7.3.4",
"postcss-scss": "^4.0.9",
"prettier": "^3.3.3",
"react": ">=18",
"react-app-polyfill": "^3.0.0",
"react-dom": "^18.3.1",
"rollup": "^4.21.2",
"rollup": "^4.22.4",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-typescript2": "^0.36.0",
"sass": "^1.77.8",
"sass": "^1.79.3",
"sass-loader": "^14.2.1",
"semantic-release": "^22.0.12",
"storybook": "^8.2.9",
"storybook": "^8.3.2",
"storybook-css-modules-preset": "^1.1.1",
"style-loader": "^4.0.0",
"stylelint": "^15.11.0",
"stylelint-config-recommended-scss": "^13.1.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-prettier": "^4.1.0",
"stylelint-scss": "^5.3.2",
"typescript": "^5.5.4",
"vite": "^5.4.2",
"typescript": "^5.6.2",
"vite": "^5.4.7",
"vite-plugin-html": "^3.2.2",
"vite-tsconfig-paths": "^4.3.2"
"vite-tsconfig-paths": "^5.0.1"
},
"babel": {
"presets": [
Expand Down
41 changes: 18 additions & 23 deletions src/components/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import {
AgGridEvent,
CellClassParams,
CellClickedEvent,
CellDoubleClickedEvent,
CellEditingStartedEvent,
CellKeyDownEvent,
ColDef,
ColGroupDef,
ColumnResizedEvent,
EditableCallback,
EditableCallbackParams,
GridOptions,
GridReadyEvent,
IClientSideRowModel,
ModelUpdatedEvent,
RowDragEndEvent,
RowDragLeaveEvent,
RowDragMoveEvent,
RowHighlightPosition,
RowNode,
} from "ag-grid-community";
import { CellClassParams, EditableCallback, EditableCallbackParams } from "ag-grid-community";
import { GridOptions } from "ag-grid-community";
import {
AgGridEvent,
CellKeyDownEvent,
GridReadyEvent,
RowDragEndEvent,
RowDragMoveEvent,
SelectionChangedEvent,
} from "ag-grid-community";
import { AgGridReact } from "ag-grid-react";
Expand All @@ -34,6 +34,7 @@ import { GridNoRowsOverlay } from "./GridNoRowsOverlay";
import { usePostSortRowsHook } from "./PostSortRowsHook";
import { GridHeaderSelect } from "./gridHeader";
import { GridContextMenuComponent, useGridContextMenu } from "./gridHook";
import { clickInputWhenContainingCellClicked } from "./clickInputWhenContainingCellClicked";

export interface GridBaseRow {
id: string | number;
Expand Down Expand Up @@ -266,16 +267,6 @@ export const Grid = ({
getFirstRowId,
]);

/**
* AgGrid checkbox select does not pass clicks within cell but not on the checkbox to checkbox.
* This passes the event to the checkbox when you click anywhere in the cell.
*/
const clickSelectorCheckboxWhenContainingCellClicked = useCallback(({ event }: CellClickedEvent) => {
if (!event) return;
const input = (event.target as Element).querySelector("input");
input?.dispatchEvent(event);
}, []);

/**
* Ensure external selected items list is in sync with panel.
*/
Expand Down Expand Up @@ -388,7 +379,7 @@ export const Grid = ({
}
return false;
},
onCellClicked: clickSelectorCheckboxWhenContainingCellClicked,
onCellClicked: clickInputWhenContainingCellClicked,
},
...adjustColDefs,
]
Expand All @@ -402,7 +393,6 @@ export const Grid = ({
params.defaultColDef?.editable,
selectColumnPinned,
rowSelection,
clickSelectorCheckboxWhenContainingCellClicked,
]);

/**
Expand Down Expand Up @@ -528,11 +518,16 @@ export const Grid = ({
*/
const onCellKeyPress = useCallback(
(e: CellKeyDownEvent) => {
if ((e.event as KeyboardEvent).key === "Enter") {
const kbe = e.event as KeyboardEvent;
if (kbe.key === "Enter") {
if (!invokeEditAction(e)) startCellEditing(e);
}
if (kbe.key === "Tab") {
// eslint-disable-next-line
prePopupOps();
}
},
[startCellEditing],
[prePopupOps, startCellEditing],
);

/**
Expand Down
8 changes: 5 additions & 3 deletions src/components/GridCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ export interface ColDefT<TData extends GridBaseRow, ValueType = any> extends Col
editable?: boolean | SAEditableCallback<TData, ValueType>;
valueGetter?: string | SAValueGetterFunc<TData, ValueType>;
valueFormatter?: string | SAValueFormatterFunc<TData, ValueType>;
cellRenderer?: (props: SAICellRendererParams<TData, ValueType>) => ReactElement | string | false | null | undefined;
cellRenderer?:
| ((props: SAICellRendererParams<TData, ValueType>) => ReactElement | string | false | null | undefined)
| string;
cellRendererParams?: {
singleClickEdit?: boolean;
rightHoverElement?: ReactElement;
Expand All @@ -116,7 +118,7 @@ export const suppressCellKeyboardEvents = (e: SuppressKeyboardEventParams) => {
const exec = shortcutKeys[e.event.key];
if (exec && !e.editing && !e.event.repeat && e.event.type === "keydown") {
const editable = fnOrVar(e.colDef?.editable, e);
return editable ? exec(e) ?? true : true;
return editable ? (exec(e) ?? true) : true;
}
// It's important that aggrid doesn't trigger edit on enter
// as the incorrect selected rows will be returned
Expand Down Expand Up @@ -200,7 +202,7 @@ export const GridCell = <TData extends GridBaseRow, TValue = any, Props extends
else return JSON.stringify(params.value);
},
...props,
cellRenderer: GridCellRenderer,
cellRenderer: typeof props.cellRenderer === "string" ? props.cellRenderer : GridCellRenderer,
cellRendererParams: {
originalCellRenderer: props.cellRenderer,
...props.cellRendererParams,
Expand Down
40 changes: 40 additions & 0 deletions src/components/clickInputWhenContainingCellClicked.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CellClickedEvent } from "ag-grid-community";

/**
* AgGrid checkbox select does not pass clicks within cell but not on the checkbox to checkbox.
* This passes the event to the checkbox when you click anywhere in the cell.
*/
export const clickInputWhenContainingCellClicked = ({ data, event, colDef }: CellClickedEvent) => {
if (!data || !event) return;

const element = event.target as Element;
// Already handled
if (["BUTTON", "INPUT"].includes(element?.tagName) && element.closest(".ag-cell-inline-editing")) return;

const row = element.closest("[row-id]");
if (!row) return;

const colId = colDef.colId;
if (!colId) return;

const clickInput = (cnt: number) => {
const cell = row.querySelector(`[col-id='${colId}']`);
if (!cell) return;

const input = cell.querySelector("input, button");
if (!input) {
return;
}
// When clicking on a cell that is not editing, the cell changes to editing and the input/button ref becomes invalid
// So wait until the cell is in edit mode before sending the click
if (!input.ownerDocument.contains(input)) {
if (cnt !== 0) {
setTimeout(() => clickInput(cnt - 1));
}
return;
}
input?.dispatchEvent(event);
};

setTimeout(() => clickInput(20), 10);
};
1 change: 1 addition & 0 deletions src/components/gridForm/GridFormDropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface GridFormDropDownProps<TData extends GridBaseRow> extends CellEd
| "GridPopoverEditDropDown-containerMedium"
| "GridPopoverEditDropDown-containerLarge"
| "GridPopoverEditDropDown-containerUnlimited"
| "GridPopoverEditDropDown-containerAutoWidth"
| string
| undefined;
// local means the use the local filter, otherwise it's expected options will be passed a function that takes a filter
Expand Down
2 changes: 1 addition & 1 deletion src/components/gridForm/GridFormMultiSelectGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const GridFormMultiSelectGrid = <TData extends GridBaseRow>(
}}
>
<LuiCheckboxInput
isChecked={!!o.checked ?? false}
isChecked={!!o.checked}
isIndeterminate={o.checked === "partial"}
value={`${o.value}`}
label={
Expand Down
79 changes: 79 additions & 0 deletions src/components/gridPopoverEdit/GridEditBoolean.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { CellFocusedEvent } from "ag-grid-community";
import { CellEditorCommon, ColDefT, GridCell } from "../GridCell";
import { useEffect, useRef } from "react";
import { fnOrVar } from "../../utils/util";
import clsx from "clsx";
import { GenericCellColDef } from "../gridRender";
import { GridBaseRow } from "../Grid";
import { CustomCellEditorProps } from "ag-grid-react";
import { clickInputWhenContainingCellClicked } from "../clickInputWhenContainingCellClicked";

const BooleanCellRenderer = (props: CustomCellEditorProps) => {
const { onValueChange, value, api, node, column, colDef, data } = props;
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
const checkFocus = (event: CellFocusedEvent) => {
if (event.rowIndex === node.rowIndex && event.column === column) {
inputRef.current?.focus();
}
};
api.addEventListener("cellFocused", checkFocus);
return () => {
api.removeEventListener("cellFocused", checkFocus);
};
}, [api, column, node.rowIndex]);

return (
<div className={clsx("ag-wrapper ag-input-wrapper ag-checkbox-input-wrapper", { "ag-checked": props.value })}>
<input
type="checkbox"
className="ag-input-field-input ag-checkbox-input"
disabled={!fnOrVar(colDef?.editable, props)}
ref={inputRef}
checked={value}
onChange={() => {}}
onClick={(e) => {
e.stopPropagation();
// cell has to be in edit mode
// if in non-edit mode clickInputWhenContainingCellClicked will click to put it in edit mode
if (!onValueChange) return;
const params = props?.colDef?.cellEditorParams as GridEditBooleanEditorProps<any> | undefined;
if (!params) return;
const selectedRows = [data];
const checked = !value;
onValueChange(checked);
params.onClick({ selectedRows, selectedRowIds: selectedRows.map((r) => r.id), checked }).then();
}}
/>
</div>
);
};

export interface GridEditBooleanEditorProps<TData> extends CellEditorCommon {
onClick: (props: {
selectedRows: TData[];
selectedRowIds: (string | number)[];
checked: boolean;
}) => Promise<boolean>;
}

export const GridEditBoolean = <TData extends GridBaseRow>(
colDef: GenericCellColDef<TData, boolean>,
editorProps: GridEditBooleanEditorProps<TData>,
): ColDefT<TData> => {
return GridCell({
minWidth: 64,
maxWidth: 64,
cellRenderer: BooleanCellRenderer as any,
cellEditor: BooleanCellRenderer,
cellEditorParams: editorProps,
onCellClicked: clickInputWhenContainingCellClicked,
singleClickEdit: true,
resizable: false,
editable: true,
cellClass: "GridCellAlignCenter",
headerClass: "GridHeaderAlignCenter",
...colDef,
});
};
Loading

0 comments on commit 3b527ff

Please sign in to comment.