diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index ac374cbe0..298b330ca 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -32,8 +32,9 @@ import { useGridTableApi, } from "src/components/index"; import { Css, Palette } from "src/Css"; +import { jan1, jan2, jan29 } from "src/forms/formStateDomain"; import { useComputed } from "src/hooks"; -import { SelectField } from "src/inputs"; +import { DateField, SelectField } from "src/inputs"; import { NumberField } from "src/inputs/NumberField"; import { noop } from "src/utils"; import { newStory, withRouter, zeroTo } from "src/utils/sb"; @@ -2144,3 +2145,87 @@ export function MinColumnWidths() { ); } + +enum EditableRowStatus { + Active = "Active", + Inactive = "Inactive", +} + +type EditableRowData = { + kind: "data"; + id: string; + data: { id: string; name: string; status: EditableRowStatus; value: number; date?: Date }; +}; +type EditableRow = EditableRowData | HeaderRow; + +export function EditableRows() { + const [rows, setRows] = useState[]>([ + simpleHeader, + { + kind: "data" as const, + id: "1", + data: { id: "1", name: "Tony Stark", status: EditableRowStatus.Active, value: 1, date: jan1 }, + }, + { + kind: "data" as const, + id: "2", + data: { id: "2", name: "Natasha Romanova", status: EditableRowStatus.Active, value: 2, date: jan2 }, + }, + { + kind: "data" as const, + id: "3", + data: { id: "3", name: "Thor Odinson", status: EditableRowStatus.Active, value: 3, date: jan29 }, + }, + ]); + + const nameColumn: GridColumn = { + header: "Name", + data: ({ name }) => name, + }; + + const selectColumn: GridColumn = { + header: "Status", + data: (row) => ({ + content: ( + ({ label: status, code: status }))} + value={row.status} + onSelect={noop} + /> + ), + editableOnHover: true, + }), + w: "100px", + }; + + const date1Column: GridColumn = { + header: "Date", + data: (row, { editable }) => ({ + content: ( + + ), + editableOnHover: true, + }), + w: "120px", + }; + + const date2Column: GridColumn = { + header: "Date", + data: (row, { editable }) => ({ + content: ( + + ), + editableOnHover: true, + }), + w: "120px", + }; + + return ( + + ); +} diff --git a/src/components/Table/GridTableApi.ts b/src/components/Table/GridTableApi.ts index d4b3360bd..96ac3c769 100644 --- a/src/components/Table/GridTableApi.ts +++ b/src/components/Table/GridTableApi.ts @@ -227,7 +227,7 @@ export class GridTableApiImpl implements GridTableApi { .filter((c) => !c.isAction) .map((c) => { // Just guessing for level=1 - const maybeContent = applyRowFn(c, rs.row, this as any as GridRowApi, 1, true, undefined); + const maybeContent = applyRowFn(c, rs.row, this as any as GridRowApi, 1, true, false, undefined); if (isGridCellContent(maybeContent)) { const cell = maybeContent; const content = maybeApply(cell.content); diff --git a/src/components/Table/TableStyles.tsx b/src/components/Table/TableStyles.tsx index c36b15c52..62e2151af 100644 --- a/src/components/Table/TableStyles.tsx +++ b/src/components/Table/TableStyles.tsx @@ -42,6 +42,8 @@ export interface GridStyle { firstRowMessageCss?: Properties; /** Applied on hover if a row has a rowLink/onClick set. */ rowHoverColor?: Palette | "none"; + /** Applied on hover to a cell TextFieldBase */ + rowEditableCellBorderColor?: Palette; /** Applied on hover of a row */ nonHeaderRowHoverCss?: Properties; /** Default content to put into an empty cell */ diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index 1d1759aa2..e06c4e92a 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -88,6 +88,7 @@ function RowImpl(props: RowProps): ReactElement { const sortOn = tableState.sortConfig?.on; const revealOnRowHoverClass = "revealOnRowHover"; + const editableOnRowHoverClass = "editableOnRowHover"; const showRowHoverColor = !reservedRowKinds.includes(row.kind) && !omitRowHover && style.rowHoverColor !== "none"; @@ -123,6 +124,11 @@ function RowImpl(props: RowProps): ReactElement { [` > .${revealOnRowHoverClass} > *`]: Css.vh.$, [`:hover > .${revealOnRowHoverClass} > *`]: Css.vv.$, }, + ...{ + [`:hover > .${editableOnRowHoverClass} .textFieldBaseWrapper`]: Css.px1.br4.ba.bc( + style.rowEditableCellBorderColor ?? Palette.Blue300, + ).$, + }, ...(isLastKeptRow && Css.addIn("&>*", style.keptLastRowCss).$), }; @@ -210,7 +216,19 @@ function RowImpl(props: RowProps): ReactElement { onDragOver: onDragOverDebounced, }; - const maybeContent = applyRowFn(column as GridColumnWithId, row, rowApi, level, isExpanded, dragData); + const cellId = `${row.kind}_${row.id}_${column.id}`; + const applyCellHighlight = cellHighlight && !!column.id && !isHeader && !isTotals; + const isCellActive = tableState.activeCellId === cellId; + + const maybeContent = applyRowFn( + column as GridColumnWithId, + row, + rowApi, + level, + isExpanded, + isCellActive, + dragData, + ); // Only use the `numExpandedColumns` as the `colspan` when rendering the "Expandable Header" currentColspan = @@ -220,6 +238,7 @@ function RowImpl(props: RowProps): ReactElement { ? numExpandedColumns + 1 : 1; const revealOnRowHover = isGridCellContent(maybeContent) ? maybeContent.revealOnRowHover : false; + const editableOnRowHover = isGridCellContent(maybeContent) ? maybeContent.editableOnHover : false; const canSortColumn = (sortOn === "client" && column.clientSideSort !== false) || @@ -274,11 +293,6 @@ function RowImpl(props: RowProps): ReactElement { // This relies on our column sizes being defined in pixel values, which is currently true as we calculate to pixel values in the `useSetupColumnSizes` hook minStickyLeftOffset += maybeSticky === "left" ? parseInt(columnSizes[columnIndex].replace("px", ""), 10) : 0; - - const cellId = `${row.kind}_${row.id}_${column.id}`; - const applyCellHighlight = cellHighlight && !!column.id && !isHeader && !isTotals; - const isCellActive = tableState.activeCellId === cellId; - // Note that it seems expensive to calc a per-cell class name/CSS-in-JS output, // vs. setting global/table-wide CSS like `style.cellCss` on the root grid div with // a few descendent selectors. However, that approach means the root grid-applied @@ -334,8 +348,15 @@ function RowImpl(props: RowProps): ReactElement { })`, }; - const cellClassNames = revealOnRowHover ? revealOnRowHoverClass : undefined; + const cellClassNames = [ + ...(revealOnRowHover ? [revealOnRowHoverClass] : []), + ...(editableOnRowHover && (isCellActive || !tableState.activeCellId) ? [editableOnRowHoverClass] : []), + ].join(" "); + const cellOnHover = + isGridCellContent(maybeContent) && maybeContent.editableOnHover + ? (enter: boolean) => (enter ? api.setActiveCellId(cellId) : api.setActiveCellId(undefined)) + : undefined; const cellOnClick = applyCellHighlight ? () => api.setActiveCellId(cellId) : undefined; const tooltip = isGridCellContent(maybeContent) ? maybeContent.tooltip : undefined; @@ -348,7 +369,17 @@ function RowImpl(props: RowProps): ReactElement { ? rowClickRenderFn(as, api, currentColspan) : defaultRenderFn(as, currentColspan); - return renderFn(columnIndex, cellCss, content, row, rowStyle, cellClassNames, cellOnClick, tooltip); + return renderFn( + columnIndex, + cellCss, + content, + row, + rowStyle, + cellClassNames, + cellOnClick, + cellOnHover, + tooltip, + ); }) )} diff --git a/src/components/Table/components/cell.tsx b/src/components/Table/components/cell.tsx index 4cc0f29f2..01ffa48f7 100644 --- a/src/components/Table/components/cell.tsx +++ b/src/components/Table/components/cell.tsx @@ -34,6 +34,8 @@ export type GridCellContent = { revealOnRowHover?: true; /** Tooltip to add to a cell */ tooltip?: ReactNode; + /** Allows cell to be editable when hovering, and also highlights the field on hover */ + editableOnHover?: true; }; /** Allows rendering a specific cell. */ @@ -45,12 +47,14 @@ export type RenderCellFn = ( rowStyle: RowStyle | undefined, classNames: string | undefined, onClick: VoidFunction | undefined, + onHover: ((enter: boolean) => void) | undefined, tooltip: ReactNode | undefined, ) => ReactNode; /** Renders our default cell element, i.e. if no row links and no custom renderCell are used. */ export const defaultRenderFn: (as: RenderAs, colSpan: number) => RenderCellFn = - (as: RenderAs, colSpan) => (key, css, content, row, rowStyle, classNames: string | undefined, onClick, tooltip) => { + (as: RenderAs, colSpan) => + (key, css, content, row, rowStyle, classNames: string | undefined, onClick, onHover, tooltip) => { const Cell = as === "table" ? "td" : "div"; return ( RenderCellFn onHover?.(true)} + onMouseLeave={() => onHover?.(false)} {...(as === "table" && { colSpan })} > {content} diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 4c727b8e4..6efd7eb89 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -39,11 +39,11 @@ export type GridColumn = { | (DiscriminateUnion extends { data: infer D } ? ( data: D, - opts: { row: GridRowKind; api: GridRowApi; level: number; expanded: boolean }, + opts: { row: GridRowKind; api: GridRowApi; level: number; expanded: boolean; editable: boolean }, ) => ReactNode | GridCellContent : ( data: undefined, - opts: { row: GridRowKind; api: GridRowApi; level: number; expanded: boolean }, + opts: { row: GridRowKind; api: GridRowApi; level: number; expanded: boolean; editable: boolean }, ) => ReactNode | GridCellContent); } & { /** @@ -85,6 +85,8 @@ export type GridColumn = { initExpanded?: boolean; /** Determines whether this column should be hidden when expanded (only the 'expandColumns' would show) */ hideOnExpand?: boolean; + /** Flag that changes the field behavior to be editable on hover */ + editableOnHover?: boolean; }; /** diff --git a/src/components/Table/utils/utils.tsx b/src/components/Table/utils/utils.tsx index afd30de1c..cad2b3c3d 100644 --- a/src/components/Table/utils/utils.tsx +++ b/src/components/Table/utils/utils.tsx @@ -146,13 +146,21 @@ export function applyRowFn( api: GridRowApi, level: number, expanded: boolean, + editable: boolean, dragData?: DragData, ): ReactNode | GridCellContent { // Usually this is a function to apply against the row, but sometimes it's a hard-coded value, i.e. for headers const maybeContent = column[row.kind]; if (typeof maybeContent === "function") { // Auto-destructure data - return (maybeContent as Function)((row as any)["data"], { row: row as any, api, level, expanded, dragData }); + return (maybeContent as Function)((row as any)["data"], { + row: row as any, + api, + level, + expanded, + editable, + dragData, + }); } else { return maybeContent; } diff --git a/src/inputs/TextFieldBase.tsx b/src/inputs/TextFieldBase.tsx index 77434e97b..cea445799 100644 --- a/src/inputs/TextFieldBase.tsx +++ b/src/inputs/TextFieldBase.tsx @@ -229,6 +229,7 @@ export function TextFieldBase>(props: TextFieldB ...(multiline ? Css.fdc.aifs.gap2.$ : Css.if(wrap === false).truncate.$), ...xss, }} + className="textFieldBaseWrapper" data-readonly="true" {...tid} > @@ -259,6 +260,7 @@ export function TextFieldBase>(props: TextFieldB ...(errorMsg && !inputProps.disabled ? fieldStyles.error : {}), ...Css.if(multiline).aifs.oh.mhPx(textAreaMinHeight).$, }} + className="textFieldBaseWrapper" {...hoverProps} ref={inputWrapRef as any} onClick={unfocusedPlaceholder ? handleUnfocusedPlaceholderClick : undefined}