From 51b35bbfaf5e3888c230db798f81b55a0ba0598c Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 21 Oct 2020 15:51:26 +0100 Subject: [PATCH] Fixed the default header css style Fixed span styling when we specify a tooltip to use header class Added border color for highlight selection via props Added material ui/icons as devDependency for storybook and as peer for production Added a validation for given width % and throws error if overflows the limit Removed theme from props and added inside apiRef Added a new rowsApi method "getRowAt" in order to allow index based search Refactored the existing getRows with array index and replaced with getRowAt Removed rows as editorManager dependecy Added additionalProps on editors that receive dynamically editorProps from column and editorClass from theme Removed the calendar default styles and moved as external configuration for Spreadsheet Fixed the rows not updating in the grid due to createData not using the latest rows and always going on the API Fixed the spreadsheet storybook example where the state was staling --- package.json | 8 +- src/ApolloSpreadsheet.tsx | 474 +++++++++--------- src/api/types/coreApi.ts | 5 + src/api/types/rowsApi.ts | 10 +- src/api/useApiFactory.ts | 9 +- src/columnGrid/ColumnGrid.tsx | 37 +- src/columnGrid/column-grid-props.ts | 6 +- src/columnGrid/types/header.type.ts | 13 +- src/columnGrid/useHeaders.tsx | 14 + src/data/createData.tsx | 4 +- src/data/useData.tsx | 5 +- .../components/CalendarEditor.tsx | 34 +- .../components/NumericEditor.tsx | 7 +- src/editorManager/components/TextEditor.tsx | 7 +- src/editorManager/editorProps.ts | 5 +- src/editorManager/useEditorManager.tsx | 19 +- src/gridWrapper/GridWrapper.tsx | 27 +- src/gridWrapper/gridWrapperProps.ts | 6 +- src/navigation/useNavigation.tsx | 2 +- src/types/grid-theme.ts | 8 +- .../components/Spreadsheet/Spreadsheet.tsx | 65 ++- .../components/Spreadsheet/dataUseCases.tsx | 8 +- yarn.lock | 6 +- 23 files changed, 426 insertions(+), 353 deletions(-) diff --git a/package.json b/package.json index 5c209a3..05cda33 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,11 @@ }, "peerDependencies": { "react": ">= 16.13.1", - "react-dom": ">= 16.13.1" + "react-dom": ">= 16.13.1", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1" }, "dependencies": { - "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.9.1", "@xobotyi/scrollbar-width": "^1.9.5", "babel-loader": "^8.1.0", "babel-preset-react-app": "^9.1.2", @@ -49,6 +49,8 @@ "resize-detector": "^0.2.2" }, "devDependencies": { + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", "@babel/preset-typescript": "^7.10.4", "@storybook/addon-options": "~5.1.11", "@storybook/addon-storysource": "~5.1.11", diff --git a/src/ApolloSpreadsheet.tsx b/src/ApolloSpreadsheet.tsx index caeab73..292db01 100644 --- a/src/ApolloSpreadsheet.tsx +++ b/src/ApolloSpreadsheet.tsx @@ -1,10 +1,4 @@ -import React, { - forwardRef, - useCallback, - useMemo, - useRef, - useState, -} from 'react' +import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react' import GridWrapper from './gridWrapper/GridWrapper' import ColumnGrid from './columnGrid/ColumnGrid' import { KeyDownEventParams, useNavigation } from './navigation/useNavigation' @@ -30,265 +24,261 @@ import { makeStyles } from '@material-ui/core/styles' import { useEvents } from './events/useEvents' import { useApiEventHandler } from './api/useApiEventHandler' import { CELL_CLICK, CELL_DOUBLE_CLICK } from './api/eventConstants' +import { GridTheme } from './types' const useStyles = makeStyles(() => ({ - root: { - height: '100%', - width: '100%', - }, + root: { + height: '100%', + width: '100%', + }, })) interface Props extends GridWrapperCommonProps, GridContainerCommonProps { - className?: string - rows: TRow[] - /** @default 50 **/ - minRowHeight?: number - /** @default 50 **/ - minColumnHeight?: number - /** @default 50 **/ - minColumnWidth?: number - /** @default StretchMode.None */ - stretchMode?: StretchMode - onKeyDown?: (params: KeyDownEventParams) => void - selection?: SelectionProps - onCreateRow?: (coords: NavigationCoords) => void - /** - * Indicates if the sort is disabled globally or on a specific column - * @default true **/ - disableSort?: boolean | DisableSortFilterParam - /** - * Providing a custom ApiRef will override internal ref by allowing the exposure of grid methods - */ - apiRef?: ApiRef + theme?: GridTheme + /** + * Main grid body (rows and cells) class name + */ + className?: string + rows: TRow[] + /** @default 50 **/ + minRowHeight?: number + /** @default 50 **/ + minColumnHeight?: number + /** @default 50 **/ + minColumnWidth?: number + /** @default StretchMode.None */ + stretchMode?: StretchMode + onKeyDown?: (params: KeyDownEventParams) => void + selection?: SelectionProps + onCreateRow?: (coords: NavigationCoords) => void + /** + * Indicates if the sort is disabled globally or on a specific column + * @default true **/ + disableSort?: boolean | DisableSortFilterParam + /** + * Providing a custom ApiRef will override internal ref by allowing the exposure of grid methods + */ + apiRef?: ApiRef } export const ApolloSpreadSheet = forwardRef( - (props: Props, componentRef: React.Ref) => { - const classes = useStyles() - const minColumnWidth = props.minColumnWidth ?? 60 - const [gridFocused, setGridFocused] = useState(true) - const defaultApiRef = useApiRef() - const apiRef = React.useMemo(() => (!props.apiRef ? defaultApiRef : props.apiRef), [ - props.apiRef, - defaultApiRef, - ]) - const rootContainerRef = useRef(null) - const forkedRef = useForkRef(rootContainerRef, componentRef) - const initialised = useApiFactory(rootContainerRef, apiRef) - // console.log({ - // initialised, - // forkedRef, - // - // apiRef: apiRef.current, - // }) + (props: Props, componentRef: React.Ref) => { + const classes = useStyles() + const minColumnWidth = props.minColumnWidth ?? 60 + const [gridFocused, setGridFocused] = useState(true) + const defaultApiRef = useApiRef() + const apiRef = React.useMemo(() => (!props.apiRef ? defaultApiRef : props.apiRef), [ + props.apiRef, + defaultApiRef, + ]) + const rootContainerRef = useRef(null) + const forkedRef = useForkRef(rootContainerRef, componentRef) + const initialised = useApiFactory(rootContainerRef, apiRef, props.theme) - const [sort, setSort] = useState<{ - field: string - order: 'asc' | 'desc' - } | null>(null) + const [sort, setSort] = useState<{ + field: string + order: 'asc' | 'desc' + } | null>(null) - const rows = useMemo(() => { - if (sort) { - return orderBy(props.rows, [sort.field], [sort.order]) - } - return props.rows - }, [sort, props.rows]) + const rows = useMemo(() => { + if (sort) { + return orderBy(props.rows, [sort.field], [sort.order]) + } + return props.rows + }, [sort, props.rows]) - const headers = useMemo(() => { - if (!props.selection) { - return props.headers - } + const headers = useMemo(() => { + if (!props.selection) { + return props.headers + } - //Bind our selection header - const newHeaders = [...props.headers] - newHeaders.push({ - colSpan: 1, - id: ROW_SELECTION_HEADER_ID, - title: '', - renderer: () => { - return ( - - - - - - ) - }, - accessor: ROW_SELECTION_HEADER_ID, - width: '2%', - }) - return newHeaders - }, [props.headers, props.selection]) + //Bind our selection header + const newHeaders = [...props.headers] + newHeaders.push({ + colSpan: 1, + id: ROW_SELECTION_HEADER_ID, + title: '', + renderer: () => { + return ( + + + + + + ) + }, + accessor: ROW_SELECTION_HEADER_ID, + width: '2%', + }) + return newHeaders + }, [props.headers, props.selection]) - useEvents(rootContainerRef, apiRef) + useEvents(rootContainerRef, apiRef) - useMergeCells({ - data: props.mergeCells, - rowCount: rows.length, - columnCount: headers.length, - apiRef, - initialised, - }) + useMergeCells({ + data: props.mergeCells, + rowCount: rows.length, + columnCount: headers.length, + apiRef, + initialised, + }) - const data = useData({ - rows, - headers, - selection: props.selection, - apiRef, - initialised, - }) + const data = useData({ + rows, + headers, + selection: props.selection, + apiRef, + initialised, + }) - useRowSelection({ - selection: props.selection, - apiRef, - initialised, - }) + useRowSelection({ + selection: props.selection, + apiRef, + initialised, + }) + const { headersData, getColumnAt, dynamicColumnCount } = useHeaders({ + headers, + nestedHeaders: props.nestedHeaders, + minColumnWidth, + }) - const { headersData, getColumnAt, dynamicColumnCount } = useHeaders({ - headers, - nestedHeaders: props.nestedHeaders, - minColumnWidth, - }) + const editorNode = useEditorManager({ + getColumnAt, + onCellChange: props.onCellChange, + apiRef, + initialised, + }) + const [coords, selectCell] = useNavigation({ + defaultCoords: props.defaultCoords ?? { + rowIndex: 0, + colIndex: 0, + }, + data, + columnCount: headers.length, + suppressControls: props.suppressNavigation || !gridFocused, + getColumnAt, + onCellChange: props.onCellChange, + onCreateRow: props.onCreateRow, + apiRef, + initialised, + }) - const editorNode = useEditorManager({ - rows, - getColumnAt, - onCellChange: props.onCellChange, - apiRef, - initialised, - }) + /** @todo Extract to useSort hook **/ + function onSortClick(field: string) { + if (field === sort?.field) { + const nextSort = sort?.order === 'asc' ? 'desc' : 'asc' + if (nextSort === 'asc') { + setSort(null) + } else { + setSort({ + field, + order: nextSort, + }) + } + } else { + setSort({ + field, + order: 'asc', + }) + } + } - const [coords, selectCell] = useNavigation({ - defaultCoords: props.defaultCoords ?? { - rowIndex: 0, - colIndex: 0, - }, - data, - columnCount: headers.length, - suppressControls: props.suppressNavigation || !gridFocused, - getColumnAt, - onCellChange: props.onCellChange, - onCreateRow: props.onCreateRow, - apiRef, - initialised, - }) + const onClickAway = useCallback(() => { + if (!gridFocused) { + return + } + if (props.outsideClickDeselects) { + setGridFocused(false) + selectCell({ + rowIndex: -1, + colIndex: -1, + }) + } + }, [props.outsideClickDeselects, selectCell, gridFocused]) - /** @todo Extract to useSort hook **/ - function onSortClick(field: string) { - if (field === sort?.field) { - const nextSort = sort?.order === 'asc' ? 'desc' : 'asc' - if (nextSort === 'asc') { - setSort(null) - } else { - setSort({ - field, - order: nextSort, - }) - } - } else { - setSort({ - field, - order: 'asc', - }) - } - } + //Detect if any element is clicked again to enable focus + const onCellMouseHandler = useCallback(() => { + if (!gridFocused) { + setGridFocused(true) + } + }, [gridFocused]) - const onClickAway = useCallback(() => { - if (!gridFocused) { - return - } - if (props.outsideClickDeselects) { - setGridFocused(false) - selectCell({ - rowIndex: -1, - colIndex: -1, - }) - } - }, [props.outsideClickDeselects, selectCell, gridFocused]) + useApiEventHandler(apiRef, CELL_CLICK, onCellMouseHandler) + useApiEventHandler(apiRef, CELL_DOUBLE_CLICK, onCellMouseHandler) - //Detect if any element is clicked again to enable focus - const onCellMouseHandler = useCallback(() => { - if (!gridFocused) { - setGridFocused(true) - } - }, [gridFocused]) - - useApiEventHandler(apiRef, CELL_CLICK, onCellMouseHandler) - useApiEventHandler(apiRef, CELL_DOUBLE_CLICK, onCellMouseHandler) - - return ( - -
- - {({ getColumnWidth, width, columnGridRef, height, mainGridRef, registerChild }) => ( - - {({ scrollLeft, onScroll }) => ( -
- - -
- )} -
- )} -
- {editorNode && createPortal(editorNode, document.body)} -
-
- ) - }, + return ( + +
+ + {({ getColumnWidth, width, columnGridRef, height, mainGridRef, registerChild }) => ( + + {({ scrollLeft, onScroll }) => ( +
+ + +
+ )} +
+ )} +
+ {editorNode && createPortal(editorNode, document.body)} +
+
+ ) + }, ) export default ApolloSpreadSheet diff --git a/src/api/types/coreApi.ts b/src/api/types/coreApi.ts index d577df5..d92840e 100644 --- a/src/api/types/coreApi.ts +++ b/src/api/types/coreApi.ts @@ -1,10 +1,15 @@ import { EventEmitter } from 'events' import React from 'react' +import { GridTheme } from "../../types"; /** * The core API interface that is available in the grid [[apiRef]]. */ export interface CoreApi extends EventEmitter { + /** + * The grid theme that is used to extend the default styling + */ + theme?: GridTheme /** * The react ref of the grid root container div element. */ diff --git a/src/api/types/rowsApi.ts b/src/api/types/rowsApi.ts index 74a6279..7a25bc9 100644 --- a/src/api/types/rowsApi.ts +++ b/src/api/types/rowsApi.ts @@ -3,10 +3,16 @@ */ export interface RowApi { /** - * Get the full set of rows as [[Rows]]. - * @returns [[Rows]] + * Get the full set of rows as `TRow`. + * @returns `TRow`[] */ getRows: () => TRow[] + + /** + * Fetches the row at the given index + */ + getRowAt: (index: number) => TRow | undefined + /** * Get the total number of rows in the grid. */ diff --git a/src/api/useApiFactory.ts b/src/api/useApiFactory.ts index 3ebcd6b..ad469e5 100644 --- a/src/api/useApiFactory.ts +++ b/src/api/useApiFactory.ts @@ -1,6 +1,7 @@ -import React, { useCallback, useState } from "react" +import React, { useCallback, useEffect, useState } from "react" import { useApiExtends } from './useApiExtends' import { ApiRef } from './types/apiRef' +import { GridTheme } from "../types" /** * Initializes a new api instance @@ -10,6 +11,7 @@ import { ApiRef } from './types/apiRef' export function useApiFactory( gridRootRef: React.RefObject, apiRef: ApiRef, + theme?: GridTheme ): boolean { const [initialised, setInit] = useState(false) @@ -33,10 +35,11 @@ export function useApiFactory( [apiRef], ) - React.useEffect(() => { + useEffect(() => { console.debug('Initializing grid api.') apiRef.current.isInitialised = true apiRef.current.rootElementRef = gridRootRef + apiRef.current.theme = theme setInit(true) const api = apiRef.current @@ -45,7 +48,7 @@ export function useApiFactory( console.debug('Clearing all events listeners') api.removeAllListeners() } - }, [gridRootRef, apiRef]) + }, [gridRootRef, apiRef, theme]) useApiExtends(apiRef, { subscribeEvent, dispatchEvent: publishEvent }, 'CoreApi') diff --git a/src/columnGrid/ColumnGrid.tsx b/src/columnGrid/ColumnGrid.tsx index eb89387..eec3051 100644 --- a/src/columnGrid/ColumnGrid.tsx +++ b/src/columnGrid/ColumnGrid.tsx @@ -25,6 +25,9 @@ const useStyles = makeStyles(() => ({ defaultHeader: { display: 'flex', justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', boxSizing: 'border-box', background: '#efefef', cursor: 'default', @@ -138,17 +141,7 @@ export const ColumnGrid = React.memo( const headerRendererWrapper = ({ style, cell, ref, columnIndex, rowIndex }) => { const { title, renderer } = cell as GridHeader - /** @todo Cache cell renderer result because if may have not changed so no need to invoke again **/ - const children = renderer ? ( - (renderer(cell) as any) - ) : cell.tooltip ? ( - - {title} - - ) : ( - title - ) - + const theme = props.apiRef.current.theme const isSortDisabled = headersSortDisabledMap[cell.id] ?? true //in case its not found, we set to true const sortComponent = @@ -158,9 +151,10 @@ export const ColumnGrid = React.memo( let headerClassName = !cell.dummy ? cell.isNested - ? clsx(classes.defaultHeader, props.theme?.headerClass, props.theme?.nestedHeaderClass, cell.className) - : clsx(classes.defaultHeader, props.theme?.headerClass, cell.className) + ? clsx(classes.defaultHeader, theme?.headerClass, theme?.nestedHeaderClass, cell.className) + : clsx(classes.defaultHeader, theme?.headerClass, cell.className) : undefined + //If the cell is selected we set the column as selected too if ( !cell.dummy && @@ -168,14 +162,25 @@ export const ColumnGrid = React.memo( !cell['isNested'] && rowIndex === 0 ) { - headerClassName = clsx(headerClassName, props.theme?.currentColumnClass) + headerClassName = clsx(headerClassName, theme?.currentColumnClass) } + /** @todo Cache cell renderer result because if may have not changed so no need to invoke again **/ + const children = renderer ? ( + (renderer(cell) as any) + ) : cell.tooltip ? ( + + {title} + + ) : ( + title + ) + return (
headers: Header[] nestedHeaders?: Array @@ -21,7 +24,6 @@ export interface ColumnGridProps { width: number scrollLeft: number // isScrolling: boolean - theme?: GridTheme coords: NavigationCoords /** @default StretchMode.None */ stretchMode?: StretchMode diff --git a/src/columnGrid/types/header.type.ts b/src/columnGrid/types/header.type.ts index 1e30a45..2fcef6a 100644 --- a/src/columnGrid/types/header.type.ts +++ b/src/columnGrid/types/header.type.ts @@ -1,8 +1,9 @@ -import React from 'react' -import { TooltipProps } from '@material-ui/core' +import React, { CSSProperties } from "react"; +import { InputBaseProps, TooltipProps } from "@material-ui/core"; import { NavigationCoords } from '../../navigation/types/navigation-coords.type' import { EditorRef } from '../../editorManager/useEditorManager' import { PopperProps } from '@material-ui/core/Popper/Popper' +import { ReactDatePickerProps } from "react-datepicker"; export interface CellRendererProps { row: TRow @@ -80,6 +81,14 @@ export interface Header { * @param event */ editorKeyboardHook?: (event: KeyboardEvent) => boolean + /** + * Provides additional props to the active editor of this column + */ + editorProps?: { + className?: string + style?: CSSProperties + componentProps?: Partial> | Partial + } cellRenderer?: ICellRenderer renderer?: IHeaderRenderer colSpan?: number diff --git a/src/columnGrid/useHeaders.tsx b/src/columnGrid/useHeaders.tsx index 432f17d..03026f4 100644 --- a/src/columnGrid/useHeaders.tsx +++ b/src/columnGrid/useHeaders.tsx @@ -55,6 +55,20 @@ export function useHeaders({ ) } + //Validate % width to prevent overflow + const totalWidth = headers.reduce((acc, e) => { + if (e.width && typeof e.width === 'string' && e.width.includes('%')) { + return acc + parseFloat(e.width) + } + return acc + }, 0) + + if (totalWidth > 100) { + throw new Error( + `Column widths cannot pass 100%, please review your configuration. Received ${totalWidth}% out of 100%`, + ) + } + //Check and maybe validate if needed const transformedHeaders = headers.map( e => diff --git a/src/data/createData.tsx b/src/data/createData.tsx index af24f6e..f00892d 100644 --- a/src/data/createData.tsx +++ b/src/data/createData.tsx @@ -8,6 +8,7 @@ import React from 'react' import { ApiRef } from '../api/types/apiRef' interface CreateDataParams { + rows: TRow[] apiRef: ApiRef headers: Header[] selection?: SelectionProps @@ -17,8 +18,9 @@ export function createData({ headers, selection, apiRef, + rows }: CreateDataParams) { - const cellsList = apiRef.current.getRows().reduce((list: any[], row: any, rowIndex) => { + const cellsList = rows.reduce((list: any[], row: any, rowIndex) => { const cells = headers.reduce((_cells, header, colIndex) => { const isDummy = apiRef.current.isMerged({ rowIndex, colIndex }) if (isDummy) { diff --git a/src/data/useData.tsx b/src/data/useData.tsx index 1d112e5..3362560 100644 --- a/src/data/useData.tsx +++ b/src/data/useData.tsx @@ -33,7 +33,6 @@ export function useData({ apiRef, }: Props) { const rowsRef = useRef(rows) - const [data, setData] = useState([]) useEffect(() => { @@ -42,6 +41,7 @@ export function useData({ return } const updatedData = createData({ + rows, headers, apiRef, selection, @@ -59,6 +59,7 @@ export function useData({ return } const updatedData = createData({ + rows: apiRef.current.getRows(), headers, apiRef, selection, @@ -74,6 +75,7 @@ export function useData({ rowsRef.current = rows }, [rows]) + const getRowAt = useCallback((index: number) => rowsRef.current[index], []) const getRows = useCallback(() => rowsRef.current, []) const getRowsCount = useCallback(() => rowsRef.current.length, [rows]) @@ -82,6 +84,7 @@ export function useData({ { getRows, getRowsCount, + getRowAt }, 'Rows/Data API', ) diff --git a/src/editorManager/components/CalendarEditor.tsx b/src/editorManager/components/CalendarEditor.tsx index 6bf4e11..de524ed 100644 --- a/src/editorManager/components/CalendarEditor.tsx +++ b/src/editorManager/components/CalendarEditor.tsx @@ -1,9 +1,10 @@ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react' import { EditorProps } from '../editorProps' import { Popper } from '@material-ui/core' -import ReactDatePicker from 'react-datepicker' +import ReactDatePicker, { ReactDatePickerProps } from "react-datepicker"; import dayjs from 'dayjs' import { makeStyles } from '@material-ui/core/styles' +import clsx from "clsx"; const useStyles = makeStyles(theme => ({ root: { @@ -11,35 +12,11 @@ const useStyles = makeStyles(theme => ({ }, calendarContainer: { border: 'none', - color: theme.palette.type === 'dark' ? '#fff' : '#4d4d4d', - backgroundColor: theme.palette.type === 'dark' ? '#121212' : '#fff', - '& .react-datepicker__header': { - backgroundColor: theme.palette.type === 'dark' ? '#121212' : '#fff', - }, - '& .react-datepicker__current-month, .react-datepicker-time__header, .react-datepicker-year-header': { - color: theme.palette.type === 'dark' ? '#fff' : '#47956A', - }, - '& .react-datepicker__day-name, .react-datepicker__day, .react-datepicker__time-name': { - color: theme.palette.type === 'dark' ? '#fff' : '#808080', - borderRadius: '20px', - '&:hover': { - transform: 'scale(1.07)', - }, - }, - '& .react-datepicker__day--disabled, .react-datepicker__month-text--disabled, .react-datepicker__quarter-text--disabled': { - color: theme.palette.type === 'dark' ? 'black' : '#ccc', - cursor: 'default', - }, - '& .react-datepicker__day--selected ': { - backgroundColor: '#77C698', - color: '#fff', - borderRadius: '20px', - }, }, })) export const CalendarEditor = forwardRef( - ({ stopEditing, anchorRef, value }: EditorProps, componentRef) => { + ({ stopEditing, anchorRef, value, additionalProps }: EditorProps, componentRef) => { const classes = useStyles() const [state, setState] = useState<{ value: dayjs.Dayjs; close: boolean }>({ value: dayjs(value), @@ -120,8 +97,9 @@ export const CalendarEditor = forwardRef( }, [state]) return ( - + diff --git a/src/editorManager/components/NumericEditor.tsx b/src/editorManager/components/NumericEditor.tsx index f8754ce..cf36262 100644 --- a/src/editorManager/components/NumericEditor.tsx +++ b/src/editorManager/components/NumericEditor.tsx @@ -11,6 +11,7 @@ import { Popover, TextareaAutosize, TextField } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import { addListener, removeListener } from 'resize-detector' import { handleEditorKeydown } from "../utils/handleEditorKeydown" +import clsx from "clsx"; const useStyles = makeStyles(() => ({ input: { @@ -28,7 +29,7 @@ const useStyles = makeStyles(() => ({ })) export const NumericEditor = forwardRef( - ({ value, stopEditing, anchorRef, maxLength, validatorHook }: EditorProps, componentRef) => { + ({ value, stopEditing, anchorRef, maxLength, validatorHook, additionalProps }: EditorProps, componentRef) => { const classes = useStyles() const [editingValue, setEditingValue] = useState( isNaN(Number(value)) ? '0' : String(value), @@ -121,6 +122,7 @@ export const NumericEditor = forwardRef( }} > } id={'apollo-textarea'} value={editingValue} ref={onTextAreaResizeMount} @@ -134,7 +136,8 @@ export const NumericEditor = forwardRef( aria-label="numeric apollo editor" rowsMin={1} maxLength={maxLength} - className={classes.input} + className={clsx(classes.input, additionalProps?.className)} + style={additionalProps?.style} />
diff --git a/src/editorManager/components/TextEditor.tsx b/src/editorManager/components/TextEditor.tsx index 6fa5da3..079ba6a 100644 --- a/src/editorManager/components/TextEditor.tsx +++ b/src/editorManager/components/TextEditor.tsx @@ -14,6 +14,7 @@ import { EditorProps } from '../editorProps' import { makeStyles } from '@material-ui/core/styles' import { addListener, removeListener } from 'resize-detector' import { handleEditorKeydown } from "../utils/handleEditorKeydown" +import clsx from "clsx"; const useStyles = makeStyles(() => ({ input: { @@ -30,7 +31,7 @@ const useStyles = makeStyles(() => ({ }, })) export const TextEditor = forwardRef( - ({ value, stopEditing, anchorRef, maxLength, validatorHook }: EditorProps, componentRef) => { + ({ value, stopEditing, anchorRef, maxLength, validatorHook, additionalProps }: EditorProps, componentRef) => { const classes = useStyles() const [editingValue, setEditingValue] = useState(String(value)) @@ -114,6 +115,7 @@ export const TextEditor = forwardRef( }} > } id={'apollo-textarea'} value={editingValue} ref={onTextAreaResizeMount} @@ -123,7 +125,8 @@ export const TextEditor = forwardRef( aria-label="text apollo editor" rowsMin={1} maxLength={maxLength} - className={classes.input} + className={clsx(classes.input, additionalProps?.className)} + style={additionalProps?.style} /> diff --git a/src/editorManager/editorProps.ts b/src/editorManager/editorProps.ts index f94238e..225d3ac 100644 --- a/src/editorManager/editorProps.ts +++ b/src/editorManager/editorProps.ts @@ -1,12 +1,13 @@ import { NavigationKey } from './enums/navigation-key.enum' import React, { CSSProperties } from 'react' import { StopEditingParams } from './useEditorManager' +import { Header } from "../columnGrid/types" export interface EditorProps { value: string stopEditing: (params?: StopEditingParams) => void anchorRef: Element - cellStyle?: CSSProperties + additionalProps: Header['editorProps'] maxLength: number - validatorHook?: (value: unknown) => boolean + validatorHook?: Header['validatorHook'] } diff --git a/src/editorManager/useEditorManager.tsx b/src/editorManager/useEditorManager.tsx index c35369e..68c47c7 100644 --- a/src/editorManager/useEditorManager.tsx +++ b/src/editorManager/useEditorManager.tsx @@ -12,6 +12,7 @@ import { NavigationKey } from './enums/navigation-key.enum' import { useApiExtends } from '../api/useApiExtends' import { ApiRef } from '../api/types/apiRef' import { CELL_BEGIN_EDITING, CELL_STOP_EDITING } from "../api/eventConstants" +import clsx from "clsx"; export interface StopEditingParams { /** @default true **/ @@ -42,7 +43,6 @@ export interface CellChangeParams { } export interface EditorManagerProps { - rows: TRow[] getColumnAt: GetColumnAt onCellChange?: (params: CellChangeParams) => void apiRef: ApiRef @@ -66,7 +66,6 @@ export interface EditorRef { */ export function useEditorManager({ getColumnAt, - rows, onCellChange, apiRef, initialised, @@ -78,7 +77,7 @@ export function useEditorManager({ //Detect if row/column has changed or has been deleted (compares with the active editing info) useEffect(() => { if (editorNode && state.current) { - const target = apiRef.current.getRows()[state.current.rowIndex] as TRow + const target = apiRef.current.getRowAt(state.current.rowIndex) as TRow const column = getColumnAt(state.current.colIndex) if (target && column) { const value = target[column.accessor] @@ -102,6 +101,7 @@ export function useEditorManager({ if (!editorState) { return } + if ((params === undefined || params.save) && editorState) { const newValue = editorRef.current?.getValue() ?? undefined if (newValue === undefined) { @@ -110,6 +110,7 @@ export function useEditorManager({ apiRef.current.dispatchEvent(CELL_STOP_EDITING, { colIndex: editorState.colIndex, rowIndex: editorState.rowIndex }) return setEditorNode(null) } + const isValid = editorState.validatorHook?.(newValue) ?? true if (!isValid) { editorRef.current = null @@ -118,7 +119,7 @@ export function useEditorManager({ return setEditorNode(null) } - if (newValue != editorState.initialValue) { + if (newValue != editorState.initialValue){ onCellChange?.({ coords: { rowIndex: editorState.rowIndex, @@ -135,7 +136,7 @@ export function useEditorManager({ apiRef.current.dispatchEvent(CELL_STOP_EDITING, { colIndex: editorState.colIndex, rowIndex: editorState.rowIndex }) setEditorNode(null) }, - [editorNode], + [editorNode, onCellChange, apiRef], ) //Invoked when the editor mounts on DOM @@ -218,7 +219,7 @@ export function useEditorManager({ return } - const row = apiRef.current.getRows()[coords.rowIndex] + const row = apiRef.current.getRowAt(coords.rowIndex) if (!row) { return console.warn( `Row not found at ${coords.rowIndex}, therefore we can't start editing at column: ${column.id}`, @@ -239,6 +240,10 @@ export function useEditorManager({ anchorRef: targetElement, value, maxLength: column.maxLength ?? 500, + additionalProps: { + ...column.editorProps, + className: clsx(apiRef.current.theme?.editorClass, column.editorProps?.className), + }, stopEditing, validatorHook: column.validatorHook, } @@ -256,7 +261,7 @@ export function useEditorManager({ setEditorNode(editor) apiRef.current.dispatchEvent(CELL_BEGIN_EDITING, coords) }, - [getColumnAt, editorNode, stopEditing], + [getColumnAt, editorNode, stopEditing, apiRef], ) function getEditorState() { diff --git a/src/gridWrapper/GridWrapper.tsx b/src/gridWrapper/GridWrapper.tsx index ebe76e8..b7c2a3c 100644 --- a/src/gridWrapper/GridWrapper.tsx +++ b/src/gridWrapper/GridWrapper.tsx @@ -28,6 +28,10 @@ const useStyles = makeStyles(() => ({ border: 0, }, }, + disabledCell: { + cursor:'default', //no clickable action for this cell + pointerEvents: 'none' //no events for this cell + }, suppressHorizontalOverflow: { overflowX: 'hidden', }, @@ -161,18 +165,19 @@ const GridWrapper = forwardRef((props: GridWrapperProps, componentRef: React.Ref const column = props.headers[columnIndex] //Dummy zIndex is 0 and a spanned cell has 5 but a normal cell has 1 const zIndex = (cell.rowSpan || cell.colSpan) && !cell.dummy ? 5 : cell.dummy ? 0 : 1 - const isRowSelected = isActiveRow({ rowIndex, colIndex: columnIndex }) + const theme = props.apiRef.current.theme if (isSelected) { + //Ensure there are no other borders style.borderLeft = '0px' style.borderRight = '0px' style.borderTop = '0px' style.borderBottom = '0px' - style.border = '1px solid blue' + style.border = props.highlightBorderColor ? `1px solid ${props.highlightBorderColor}` :'1px solid blue' } else { - //Bind default border - if (!props.theme || (!props.theme.cellClass && !cell.dummy)) { + //Bind default border and clear other borders + if (!theme || (!theme.cellClass && !cell.dummy)) { style.borderLeft = '0px' style.borderRight = '0px' style.borderTop = '0px' @@ -187,15 +192,13 @@ const GridWrapper = forwardRef((props: GridWrapperProps, componentRef: React.Ref * dummy 1 has a rowspan of total 3 but none of its parent are visible, so dummy 3 assume the children value and highlight * of the parent because there is none visible * */ - let cellClassName = clsx(classes.cellDefaultStyle, props.theme?.cellClass, column.cellClassName) - if (isRowSelected && !cell.dummy && props.theme?.currentRowClass) { - cellClassName = clsx(cellClassName, props.theme?.currentRowClass) + let cellClassName = clsx(classes.cellDefaultStyle, theme?.cellClass, column.cellClassName) + if (isRowSelected && !cell.dummy && theme?.currentRowClass) { + cellClassName = clsx(cellClassName, theme?.currentRowClass) } - if (navigationDisabled && !cell.dummy && props.theme?.disabledCellClass) { - style.cursor = 'default' //no clickable action for this cell - style.pointerEvents = 'none' //no events for this cell - cellClassName = clsx(cellClassName, props.theme?.disabledCellClass) + if (navigationDisabled && !cell.dummy && theme?.disabledCellClass) { + cellClassName = clsx(cellClassName, classes.disabledCell, theme?.disabledCellClass) } return ( @@ -211,7 +214,6 @@ const GridWrapper = forwardRef((props: GridWrapperProps, componentRef: React.Ref justifyContent: cell?.dummy ? 'top' : 'center', zIndex, }} - // tabIndex={1} ref={ref} > {cell.value} @@ -256,7 +258,6 @@ const GridWrapper = forwardRef((props: GridWrapperProps, componentRef: React.Ref }, [ props.coords, - props.theme, props.width, props.data, props.apiRef, diff --git a/src/gridWrapper/gridWrapperProps.ts b/src/gridWrapper/gridWrapperProps.ts index 41fadd4..75f16a5 100644 --- a/src/gridWrapper/gridWrapperProps.ts +++ b/src/gridWrapper/gridWrapperProps.ts @@ -26,7 +26,6 @@ export interface GridWrapperCommonProps { suppressNavigation?: boolean /** @default false **/ outsideClickDeselects?: boolean - theme?: GridTheme mergeCells?: MergeCell[] mergedPositions?: MergePosition[] /** @@ -48,6 +47,11 @@ export interface GridWrapperCommonProps { * Use "start" to align cells to the top/left of the Grid and "end" to align bottom/right. */ scrollToAlignment?: Alignment; + + /** + * Border for highlighted cell + */ + highlightBorderColor?: string } export interface GridWrapperProps extends GridWrapperCommonProps { diff --git a/src/navigation/useNavigation.tsx b/src/navigation/useNavigation.tsx index 0803809..e631d5b 100644 --- a/src/navigation/useNavigation.tsx +++ b/src/navigation/useNavigation.tsx @@ -427,7 +427,7 @@ export function useNavigation({ return handleArrowNavigationControls(event) } - const row = apiRef.current.getRows()[coords.rowIndex] + const row = apiRef.current.getRowAt(coords.rowIndex) if (!row) { return console.warn('Row index') } diff --git a/src/types/grid-theme.ts b/src/types/grid-theme.ts index 2d6a847..255ff2a 100644 --- a/src/types/grid-theme.ts +++ b/src/types/grid-theme.ts @@ -1,6 +1,6 @@ export interface GridTheme { /** - * Styles the whole row where the cell is selected and also applies styling to the highligted cell + * Styles the whole row where the cell is selected and also applies styling to the highlighted cell */ currentRowClass?: string /** @@ -11,4 +11,10 @@ export interface GridTheme { nestedHeaderClass?: string cellClass?: string disabledCellClass?: string + /** + * Styles to be applied in the root container of the active editor + * Not every editor behaves the same and this will only work for default editor, if you wish + * a more advanced configuration, use column/header `editorProps` + */ + editorClass?: string } diff --git a/stories/components/Spreadsheet/Spreadsheet.tsx b/stories/components/Spreadsheet/Spreadsheet.tsx index 076d029..003748a 100644 --- a/stories/components/Spreadsheet/Spreadsheet.tsx +++ b/stories/components/Spreadsheet/Spreadsheet.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alignment } from 'react-virtualized' import { createMergeCellsData } from './createMergedCells' import { GridTheme } from '../../../src/types/grid-theme' @@ -8,12 +8,12 @@ import { orderBy } from 'lodash' import { Box, Button, Checkbox, FormControl, InputLabel, MenuItem, Select } from '@material-ui/core' import { ApolloSpreadSheet } from '../../../src' import { StretchMode } from '../../../src/types/stretch-mode.enum' -import { getTopUseCase } from './dataUseCases' +import { useTopCase } from './dataUseCases' import { makeStyles } from '@material-ui/core/styles' -import { useApiRef } from "../../../src/api/useApiRef" +import { useApiRef } from '../../../src/api/useApiRef' import 'react-datepicker/dist/react-datepicker.css' -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ root: { margin: 10, }, @@ -30,8 +30,8 @@ const useStyles = makeStyles(() => ({ color: 'blue', }, headerClass: { - background: 'white !important' as any, - border: 'none !important' as any, + background: 'white', + border: 'none', fontWeight: 700, fontSize: '11px', }, @@ -58,13 +58,38 @@ const useStyles = makeStyles(() => ({ height: '10px', width: '10px', }, + calendarClass: { + color: theme.palette.type === 'dark' ? '#fff' : '#4d4d4d', + backgroundColor: theme.palette.type === 'dark' ? '#121212' : '#fff', + '& .react-datepicker__header': { + backgroundColor: theme.palette.type === 'dark' ? '#121212' : '#fff', + }, + '& .react-datepicker__current-month, .react-datepicker-time__header, .react-datepicker-year-header': { + color: theme.palette.type === 'dark' ? '#fff' : '#47956A', + }, + '& .react-datepicker__day-name, .react-datepicker__day, .react-datepicker__time-name': { + color: theme.palette.type === 'dark' ? '#fff' : '#808080', + borderRadius: '20px', + '&:hover': { + transform: 'scale(1.07)', + }, + }, + '& .react-datepicker__day--disabled, .react-datepicker__month-text--disabled, .react-datepicker__quarter-text--disabled': { + color: theme.palette.type === 'dark' ? 'black' : '#ccc', + cursor: 'default', + }, + '& .react-datepicker__day--selected ': { + backgroundColor: '#77C698', + color: '#fff', + borderRadius: '20px', + }, + }, })) -const { headerData: topHeaders, data: topDefaultData } = getTopUseCase() export function Spreadsheet() { const classes = useStyles() - const [headers, setHeaders] = useState(topHeaders) - const [data, setData] = useState(topDefaultData) + const { headerData: headers, data: defaultData } = useTopCase(classes.calendarClass) + const [data, setData] = useState(defaultData) const [outsideClickDeselects, setOutsideClickDeselect] = useState(true) const [darkTheme, setDarkTheme] = useState(false) const [selectionEnabled, setSelectionEnabled] = useState(true) @@ -91,15 +116,17 @@ export function Spreadsheet() { cellClass: classes.rowClass, } - function onCellChange(changes: CellChangeParams) { - const newData = [...data] - const column = headers[changes.coords.colIndex] - newData[changes.coords.rowIndex] = { - ...newData[changes.coords.rowIndex], - [column.accessor]: changes.newValue, - } - setData(newData) - } + const onCellChange = useCallback((changes: CellChangeParams) => { + setData(prev => { + const newData = [...prev] + const column = headers[changes.coords.colIndex] + newData[changes.coords.rowIndex] = { + ...newData[changes.coords.rowIndex], + [column.accessor]: changes.newValue, + } + return newData + }) + }, [data,headers]) const [delayedPosition, setDelayedPosition] = useState(null) @@ -151,7 +178,7 @@ export function Spreadsheet() { sortOrder++ } setData(sortedRows) - if (!parentRow){ + if (!parentRow) { setDelayedPosition({ rowIndex: newOrder - 1, colIndex: coords.colIndex }) } } diff --git a/stories/components/Spreadsheet/dataUseCases.tsx b/stories/components/Spreadsheet/dataUseCases.tsx index 8a3ef2e..bb70684 100644 --- a/stories/components/Spreadsheet/dataUseCases.tsx +++ b/stories/components/Spreadsheet/dataUseCases.tsx @@ -3,7 +3,7 @@ import React from 'react' import PeopleIcon from '@material-ui/icons/People' import { ColumnCellType, Header } from '../../../src/columnGrid/types/header.type' -export const getTopUseCase = () => { +export function useTopCase (calendarClass?: string) { const headerData: Header[] = [ { id: 'deliverable', @@ -89,6 +89,9 @@ export const getTopUseCase = () => { width: '5%', type: ColumnCellType.Calendar, disableBackspace: true, + editorProps: { + className: calendarClass + } }, { id: 'endDate', @@ -98,6 +101,9 @@ export const getTopUseCase = () => { type: ColumnCellType.Calendar, disableBackspace: true, delayEditorOpen: 1000, + editorProps: { + className: calendarClass + } }, { id: 'taskControl', diff --git a/yarn.lock b/yarn.lock index 95dec5d..65df54a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1196,9 +1196,9 @@ regenerator-runtime "^0.13.4" "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.3": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.0.tgz#98bd7666186969c04be893d747cf4a6c6c8fa6b0" - integrity sha512-lS4QLXQ2Vbw2ubfQjeQcn+BZgZ5+ROHW9f+DWjEp5Y+NHYmkRGKqHSJ1tuhbUauKu2nhZNTBIvsIQ8dXfY5Gjw== + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740" + integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA== dependencies: regenerator-runtime "^0.13.4"