Skip to content

Commit

Permalink
Table column layout (#1395)
Browse files Browse the repository at this point in the history
* enable column fit layout

* fix bug with Tab editing, enable Tabstrip tests
  • Loading branch information
heswell authored Jun 21, 2024
1 parent 1fa46ac commit 70621d6
Show file tree
Hide file tree
Showing 22 changed files with 929 additions and 953 deletions.
1 change: 0 additions & 1 deletion vuu-ui/packages/vuu-popups/src/popup-menu/usePopupMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export const usePopupMenu = ({

const handleMenuClose = useCallback<PopupCloseCallback>(
(reason) => {
console.log("onClose");
setMenuOpen(false);
// If user has clicked the MenuButton whilst menu is open, we want to close it.
// The PopupService will close it for us as a 'click-away' event. We don't want
Expand Down
10 changes: 10 additions & 0 deletions vuu-ui/packages/vuu-table-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,19 @@ export interface TableCellRendererProps
onCommit?: DataItemCommitHandler;
}

/**
* static layout simply respects widths specified on column descriptors.
* fit layout attempts to fit columns to available space, either stretching
* or squeezing. manual indicates that user has resized one or more columns,
* on what was originally a fit layout. Once this happens, no further auto
* fitting will take place. Fit layout always respects max and min widths,
*/
export type ColumnLayout = "static" | "fit" | "manual";

export interface TableAttributes {
columnDefaultWidth?: number;
columnFormatHeader?: "capitalize" | "uppercase";
columnLayout?: ColumnLayout;
columnSeparators?: boolean;
// showHighlightedRow?: boolean;
rowSeparators?: boolean;
Expand Down
3 changes: 1 addition & 2 deletions vuu-ui/packages/vuu-table/src/Table.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
}

.vuuTable-scrollbarContainer::-webkit-scrollbar {
border: none;
width: 10px;
}

Expand All @@ -63,7 +62,7 @@
}

.vuuTable-scrollbarContainer::-webkit-scrollbar-track {
background-color: white;
background-color: transparent;
}
.vuuTable-scrollbarContainer::-webkit-scrollbar-thumb {
background-clip: padding-box;
Expand Down
3 changes: 0 additions & 3 deletions vuu-ui/packages/vuu-table/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,6 @@ const TableCore = ({
[`${classBase}-zebra`]: tableAttributes.zebraStripes,
});

//TODO move TableBody into separate component
// we only render TableBody when we have measured TableHeader

const cssVariables = {
"--content-height": `${viewportMeasurements.contentHeight}px`,
"--content-width": `${viewportMeasurements.contentWidth}px`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe("Table scrolling and keyboard navigation", () => {
it("THEN only those columns within the viewport are rendered", () => {
// this width allows for exactly 6 visible columns, we allow a buffer of 200px
// so 2 out-of-viewport colums are rendered
cy.mount(<TwoHundredColumns width={915} />);
cy.mount(<TwoHundredColumns width={914} />);
assertRenderedColumns({
rendered: { from: 1, to: 8 },
visible: { from: 1, to: 6 },
Expand All @@ -202,7 +202,7 @@ describe("Table scrolling and keyboard navigation", () => {

describe("WHEN table is scrolled horizontally no more than 100px", () => {
it("THEN rendering is unchanged", () => {
cy.mount(<TwoHundredColumns width={915} />);
cy.mount(<TwoHundredColumns width={914} />);
cy.get(".vuuTable-scrollbarContainer").scrollTo(100, 0);
assertRenderedColumns({
rendered: { from: 1, to: 8 },
Expand Down
35 changes: 28 additions & 7 deletions vuu-ui/packages/vuu-table/src/table-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ColumnDescriptor, TableConfig } from "@finos/vuu-table-types";
import {
ColumnDescriptor,
RuntimeColumnDescriptor,
TableConfig,
} from "@finos/vuu-table-types";
import { getRuntimeColumnWidth } from "@finos/vuu-utils";

export type MoveColumnTableConfigAction = {
type: "col-move";
Expand All @@ -10,6 +15,7 @@ export type MoveColumnTableConfigAction = {
export type ResizeColumnTableConfigAction = {
type: "col-size";
column: ColumnDescriptor;
columns: RuntimeColumnDescriptor[];
width: number;
};

Expand All @@ -36,15 +42,30 @@ export const updateTableConfig = (
action: TableConfigAction
): TableConfig => {
switch (action.type) {
case "col-size":
case "col-size": {
const { columns: runtimeColumns, width } = action;
const isFit = config.columnLayout === "fit";
return {
...config,
columns: config.columns.map((col) =>
col.name === action.column.name
? { ...col, width: action.width }
: col
),
columnLayout: isFit ? "manual" : config.columnLayout,
columns: config.columns.map((col) => {
if (isFit) {
// When user resizes a column and 'fit' column layout is in effect,
// column layout becomes 'manual' and all columns are set to
// their current widths (unless subsequently resized by user).
return col.name === action.column.name
? { ...col, width }
: col.width
? col
: { ...col, width: getRuntimeColumnWidth(col, runtimeColumns) };
} else {
return col.name === action.column.name
? { ...col, width: action.width }
: col;
}
}),
};
}
case "column-prop":
return {
...config,
Expand Down
37 changes: 30 additions & 7 deletions vuu-ui/packages/vuu-table/src/useTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ export const useTable = ({
throw Error("no data source provided to Vuu Table");
}

const virtualContentHeight = rowHeight * rowCount;
const viewportBodyHeight = size.height - headerHeight;
const verticalScrollbarWidth =
virtualContentHeight > viewportBodyHeight ? 10 : 0;
const availableWidth = size.width - (verticalScrollbarWidth + 8);

const rowClassNameGenerator = useRowClassNameGenerators(config);

const useRowDragDrop = allowDragDrop ? useDragDrop : useNullDragDrop;
Expand All @@ -168,26 +174,34 @@ export const useTable = ({
headings,
tableAttributes,
tableConfig,
} = useTableModel(config, dataSource, selectionModel);
} = useTableModel(config, dataSource, selectionModel, availableWidth);

useLayoutEffectSkipFirst(() => {
dispatchTableModelAction({
availableWidth,
type: "init",
tableConfig: config,
dataSource,
});
}, [config, dataSource, dispatchTableModelAction]);
}, [
availableWidth,
config,
dataSource,
dispatchTableModelAction,
verticalScrollbarWidth,
]);

const applyTableConfigChange = useCallback(
(config: TableConfig) => {
dispatchTableModelAction({
availableWidth,
type: "init",
tableConfig: config,
dataSource,
});
onConfigChange?.(stripInternalProperties(config));
},
[dataSource, dispatchTableModelAction, onConfigChange]
[availableWidth, dataSource, dispatchTableModelAction, onConfigChange]
);

const columnMap = useMemo(
Expand Down Expand Up @@ -253,13 +267,14 @@ export const useTable = ({
const handleConfigEditedInSettingsPanel = useCallback(
(tableConfig: TableConfig) => {
dispatchTableModelAction({
type: "init",
tableConfig,
availableWidth,
dataSource,
tableConfig,
type: "init",
});
onConfigChange?.(stripInternalProperties(tableConfig));
},
[dataSource, dispatchTableModelAction, onConfigChange]
[availableWidth, dataSource, dispatchTableModelAction, onConfigChange]
);

const handleDataSourceConfigChanged = useCallback(
Expand Down Expand Up @@ -410,6 +425,7 @@ export const useTable = ({
updateTableConfig(tableConfig, {
type: "col-size",
column,
columns,
width,
})
)
Expand Down Expand Up @@ -605,13 +621,20 @@ export const useTable = ({
};

dispatchTableModelAction({
availableWidth,
type: "init",
tableConfig: newTableConfig,
dataSource,
});
onConfigChange?.(stripInternalProperties(newTableConfig));
},
[dataSource, dispatchTableModelAction, onConfigChange, tableConfig]
[
availableWidth,
dataSource,
dispatchTableModelAction,
onConfigChange,
tableConfig,
]
);

const handleDropRow = useCallback(
Expand Down
75 changes: 64 additions & 11 deletions vuu-ui/packages/vuu-table/src/useTableModel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ColumnDescriptor,
ColumnLayout,
PinLocation,
ResizePhase,
RuntimeColumnDescriptor,
Expand All @@ -11,7 +12,9 @@ import {
import {
applyFilterToColumns,
applyGroupByToColumns,
applyRuntimeColumnWidthsToConfig,
applySortToColumns,
applyWidthToColumns,
existingSort,
getCellRenderer,
getColumnHeaderContentRenderer,
Expand Down Expand Up @@ -91,6 +94,7 @@ export interface TableModel extends TableAttributes {
* readonly copy of the original TableConfig.
*/
interface InternalTableModel extends TableModel {
availableWidth: number;
tableConfig: Readonly<TableConfig>;
}

Expand All @@ -103,6 +107,7 @@ const getDefaultAlignment = (serverDataType?: VuuColumnDataType) =>
: "left";

export interface ColumnActionInit {
availableWidth: number;
type: "init";
tableConfig: TableConfig;
dataSource: DataSource;
Expand Down Expand Up @@ -208,8 +213,26 @@ export type ColumnActionDispatch = (action: GridModelAction) => void;
const columnReducer: GridModelReducer = (state, action) => {
info?.(`TableModelReducer ${action.type}`);
switch (action.type) {
case "init":
return init(action);
case "init": {
if (
state.tableConfig.columnLayout === "manual" &&
action.tableConfig.columnLayout === "fit"
) {
//TODO we're jumping through hoops here when we should just make config a controlled prop

// Manual columnLayout has been assigned because user has resized one or more columns.
// It happened during current session so tableConfig still reflects original value.
return init({
...action,
tableConfig: applyRuntimeColumnWidthsToConfig(
action.tableConfig,
state.columns
),
});
} else {
return init(action);
}
}
case "moveColumn":
return moveColumn(state, action);
case "resizeColumn":
Expand All @@ -235,14 +258,20 @@ const columnReducer: GridModelReducer = (state, action) => {
export const useTableModel = (
tableConfigProp: TableConfig,
dataSource: DataSource,
selectionModel: TableSelectionModel
selectionModel: TableSelectionModel,
availableWidth: number
) => {
const [state, dispatchTableModelAction] = useReducer<
GridModelReducer,
InitialConfig
>(
columnReducer,
{ tableConfig: tableConfigProp, dataSource, selectionModel },
{
availableWidth,
tableConfig: tableConfigProp,
dataSource,
selectionModel,
},
init
);

Expand All @@ -258,17 +287,22 @@ export const useTableModel = (
};

type InitialConfig = {
availableWidth: number;
columnLayout?: ColumnLayout;
dataSource: DataSource;
// TODO are we at risk of losing selectionModel on updates ?
selectionModel?: TableSelectionModel;
tableConfig: TableConfig;
};

function init({
availableWidth,
dataSource,
selectionModel,
tableConfig,
}: InitialConfig): InternalTableModel {
console.log(
`init model ${tableConfig?.columnLayout} columns, availableWidth=${availableWidth}`
);
const { columns, ...tableAttributes } = tableConfig;
const { config: dataSourceConfig, tableSchema } = dataSource;
const toRuntimeColumnDescriptor = columnDescriptorToRuntimeColumDescriptor(
Expand All @@ -279,9 +313,15 @@ function init({
.filter(subscribedOnly(dataSourceConfig?.columns))
.map(toRuntimeColumnDescriptor);

const columnsInRenderOrder = runtimeColumns.some(isPinned)
? sortPinnedColumns(runtimeColumns)
: runtimeColumns;
const { columnLayout = "static" } = tableConfig;
const runtimeColumnsWithLayout = applyWidthToColumns(runtimeColumns, {
availableWidth,
columnLayout,
});

const columnsInRenderOrder = runtimeColumnsWithLayout.some(isPinned)
? sortPinnedColumns(runtimeColumnsWithLayout)
: runtimeColumnsWithLayout;

if (selectionModel === "checkbox") {
columnsInRenderOrder.splice(
Expand All @@ -292,6 +332,7 @@ function init({
}

let state: InternalTableModel = {
availableWidth,
columns: columnsInRenderOrder,
headings: getTableHeadings(columnsInRenderOrder),
tableConfig,
Expand Down Expand Up @@ -348,7 +389,7 @@ const columnDescriptorToRuntimeColumDescriptor =
originalIdx: index,
serverDataType,
valueFormatter: getValueFormatter(column, serverDataType),
width: width,
width,
};

if (isGroupColumn(runtimeColumnWithDefaults)) {
Expand Down Expand Up @@ -424,8 +465,20 @@ function resizeColumn(
switch (phase) {
case "begin":
return updateColumnProp(state, { type, column, resizing });
case "end":
return updateColumnProp(state, { type, column, resizing, width });
case "end": {
const { tableConfig } = state;
const isFit = tableConfig.columnLayout === "fit";
const newState: InternalTableModel = isFit
? {
...state,
tableConfig: applyRuntimeColumnWidthsToConfig(
tableConfig,
state.columns
),
}
: state;
return updateColumnProp(newState, { type, column, resizing, width });
}
case "resize":
return updateColumnProp(state, { type, column, width });
default:
Expand Down
Loading

0 comments on commit 70621d6

Please sign in to comment.