Skip to content

Commit

Permalink
Inline filters (#1524)
Browse files Browse the repository at this point in the history
* connect inline filter to dataSource in example

* assign aria-rowindex correctly

* table cell navigation ny aria index

* type fixes

* remove ubused type

* fix cypress tests
  • Loading branch information
heswell authored Oct 24, 2024
1 parent 61f108e commit 4b6700a
Show file tree
Hide file tree
Showing 23 changed files with 489 additions and 232 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { InputProps } from "@salt-ds/core";
export interface DataItemEditControlProps {
InputProps?: Partial<InputProps>;
TypeaheadProps?: Pick<VuuTypeaheadInputProps, "highlightFirstSuggestion">;
commitWhenCleared?: boolean;
/**
* A table column or form field Descriptor.
*/
Expand All @@ -28,6 +29,7 @@ export type ValidationStatus = "initial" | true | string;
export const getDataItemEditControl = ({
InputProps,
TypeaheadProps,
commitWhenCleared,
dataDescriptor,
errorMessage,
onCommit,
Expand Down Expand Up @@ -67,6 +69,7 @@ export const getDataItemEditControl = ({
<VuuInput
variant="secondary"
{...InputProps}
commitWhenCleared={commitWhenCleared}
onCommit={onCommit}
errorMessage={errorMessage}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
} from "@finos/vuu-protocol-types";
import {
buildColumnMap,
getTypedValue,
isActionMessage,
isErrorResponse,
isValidNumber,
queryClosest,
shallowEquals,
vuuEditCellRequest,
Expand Down Expand Up @@ -106,49 +106,6 @@ const Status = {
invalid: 3,
};

function getTypedValue(
value: string,
type: VuuColumnDataType,
throwIfUndefined?: false,
): VuuRowDataItemType | undefined;
function getTypedValue(
value: string,
type: VuuColumnDataType,
throwIfUndefined: true,
): VuuRowDataItemType;
function getTypedValue(
value: string,
type: VuuColumnDataType,
throwIfUndefined = false,
): VuuRowDataItemType | undefined {
switch (type) {
case "int":
case "long": {
const typedValue = parseInt(value, 10);
if (isValidNumber(typedValue)) {
return typedValue;
} else if (throwIfUndefined) {
throw Error("SessionEditingForm getTypedValue");
} else {
return undefined;
}
}

case "double": {
const typedValue = parseFloat(value);
if (isValidNumber(typedValue)) {
return typedValue;
}
return undefined;
}

case "boolean":
return value === "true" ? true : false;
default:
return value;
}
}

const getDataSource = (
dataSource?: DataSource,
schema?: TableSchema,
Expand Down
70 changes: 60 additions & 10 deletions vuu-ui/packages/vuu-filters/src/inline-filter/InlineFilter.css
Original file line number Diff line number Diff line change
@@ -1,31 +1,76 @@
.vuuInlineFilter {
--filter-borderColor: var(--salt-separable-secondary-borderColor);
--filter-color: var(--salt-content-primary-foreground-disabled);
--filter-padding: 1px 1px 1px 0;
--filter-content-height: 22px;
height: var(--salt-size-base);

.vuuInlineFilter-filter:has(input:focus) {
--filter-borderColor: transparent;
--filter-content-height: 20px;

outline-width: 2px;
outline-offset: -2px;
outline-style: dotted;
outline-color: var(--salt-focused-outlineColor);

/* the padding ensures the outline is not clipped */
--filter-padding: 0 2px;
/* prevents shift when we apply the padding */
--saltInput-paddingLeft: 2px;

.saltInput-input {
outline: none;
}
.saltInput-focused {
border: none;
outline: none;
}

.saltComboBox-focused {
outline: none;
}
}

.vuuInlineFilter-filter:focus {
--filter-borderColor: transparent;
--filter-content-height: 20px;
/* the padding ensures the outline is not clipped */
--filter-padding: 0 2px;
/* prevents shift when we apply the padding */
--saltInput-paddingLeft: 2px;
}

.vuuInlineFilter-filter {
--saltInput-minHeight: 22px;
display: inline-block;
--saltInput-minHeight: var(--filter-content-height);
align-items: center;
display: inline-flex;
height: 100%;
padding: 1px 1px 1px 0;
padding: var(--filter-padding);

.vuuTypeaheadInput {
border: solid 1px var(--salt-separable-secondary-borderColor);
border-style: solid;
border-color: var(--filter-borderColor);
border-width: 1px;
border-radius: 0;
}

.vuuTypeaheadInput {
height: 22px;
height: var(--filter-content-height);
input::placeholder {
color: var(--salt-content-primary-foreground-disabled);
color: var(--filter-color);
}
}

.saltInput-primary.vuuInput {
border: solid 1px var(--salt-separable-secondary-borderColor);
border-style: solid;
border-color: var(--filter-borderColor);
border-width: 1px;
border-radius: 0;
height: 22px;
min-height: 22px;
height: var(--filter-content-height);
min-height: var(--filter-content-height);
input::placeholder {
color: var(--salt-content-primary-foreground-disabled);
color: var(--filter-color);
}
}
}
Expand All @@ -34,3 +79,8 @@
display: inline-block;
}
}

.vuuInlineFilter:focus-within {
--filter-borderColor: var(--salt-separable-primary-borderColor);
--filter-color: var(--salt-content-primary-foreground);
}
54 changes: 40 additions & 14 deletions vuu-ui/packages/vuu-filters/src/inline-filter/InlineFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,26 @@ import {
} from "@finos/vuu-utils";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { HTMLAttributes, useCallback, useMemo } from "react";
import { ColumnDescriptor } from "@finos/vuu-table-types";
import {
HTMLAttributes,
KeyboardEventHandler,
useCallback,
useMemo,
} from "react";
import cx from "clsx";

import inlineFilteCss from "./InlineFilter.css";
import { InputProps } from "@salt-ds/core";
import { TableSchemaTable } from "@finos/vuu-data-types";
import { VuuFilter } from "@finos/vuu-protocol-types";
import { BaseRowProps } from "@finos/vuu-table-types";

const classBase = "vuuInlineFilter";

export type FilterValueChangeHandler = (
column: ColumnDescriptor,
value: string,
) => void;
export type FilterValueChangeHandler = (filter: VuuFilter) => void;
export interface InlineFilterProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
extends Partial<BaseRowProps>,
Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
onChange: FilterValueChangeHandler;
table: TableSchemaTable;
}
Expand All @@ -38,6 +43,7 @@ const TypeaheadProps = {
};

export const InlineFilter = ({
ariaRole,
onChange,
table,
...htmlAttributes
Expand All @@ -50,7 +56,7 @@ export const InlineFilter = ({
});

const filterAggregator = useMemo(() => new FilterAggregator(), []);
const { columns, virtualColSpan = 0 } = useHeaderProps();
const { columns = [], virtualColSpan = 0 } = useHeaderProps();

const onCommit = useCallback<
CommitHandler<HTMLElement, string | number | undefined>
Expand All @@ -59,26 +65,46 @@ export const InlineFilter = ({
const fieldName = getFieldName(evt.target);
const column = columns.find((c) => c.name === fieldName);
if (column) {
filterAggregator.addFilter(fieldName, value.toString());
// onChange(column, value.toString());
if (value === "") {
if (filterAggregator.removeFilter(column)) {
onChange(filterAggregator.filter);
}
} else {
filterAggregator.addFilter(column, value);
onChange(filterAggregator.filter);
}
}
},
[columns, filterAggregator, onChange],
);

const handleKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>(
(evt) => {
if (evt.key === "Enter") {
const el = evt.target as HTMLElement;
const inputElement = el.querySelector("input");
inputElement?.focus();
}
},
[columns, filterAggregator],
[],
);

return (
<div {...htmlAttributes} className={classBase} role="row">
<div {...htmlAttributes} className={classBase} role={ariaRole}>
<VirtualColSpan width={virtualColSpan} />
{columns.map((column) => (
{columns.map((column, i) => (
<div
className={`${classBase}-filter`}
aria-colindex={i + 1}
className={cx(`${classBase}-filter`, "vuuTableCell")}
data-field={column.name}
onKeyDown={handleKeyDown}
key={column.name}
style={{ width: column.width }}
>
{getDataItemEditControl({
InputProps,
TypeaheadProps,
commitWhenCleared: true,
dataDescriptor: column,
onCommit,
table,
Expand Down
2 changes: 2 additions & 0 deletions vuu-ui/packages/vuu-table-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ export declare type TableColumnResizeHandler = (
) => void;

export interface BaseRowProps {
ariaRole?: string;
ariaRowIndex?: number;
className?: string;
columns: RuntimeColumnDescriptor[];
style?: CSSProperties;
Expand Down
4 changes: 2 additions & 2 deletions vuu-ui/packages/vuu-table/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ const TableCore = ({
focusCellPlaceholderRef,
getRowOffset,
handleContextMenuAction,
headerHeight,
headerState: { height: headerHeight, count: headerCount },
headings,
highlightedIndex,
menuBuilder,
Expand Down Expand Up @@ -365,7 +365,7 @@ const TableCore = ({
<div className={`${classBase}-body`} ref={tableBodyRef}>
{data.map((data) => (
<Row
aria-rowindex={data[0] + 1}
aria-rowindex={data[0] + headerCount + 1}
classNameGenerator={rowClassNameGenerator}
columnMap={columnMap}
columns={scrollProps.columnsWithinViewport}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ describe("WHEN it initially renders", () => {
height={625}
tableName="instruments"
width={800}
/>
/>,
);
const container = cy.findByTestId("table");
container.should("have.class", "vuuTable");
});

it("THEN expected number of rows are present, with buffered rows, all with correct aria index", () => {
cy.mount(<TestTable {...tableConfig} />);
assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT);
assertRenderedRows({ from: 1, to: 30 }, RENDER_BUFFER, ROW_COUNT);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@ describe("Table scrolling and keyboard navigation", () => {
cy.findByRole("cell", { name: "row 1" }).should("be.focused");
cy.realPress("PageDown");

cy.findByRole("row", withAriaRowIndex(25)).should("not.exist");
cy.findByRole("row", withAriaRowIndex(26)).should("exist");
cy.findByRole("row", withAriaRowIndex(26)).should("not.exist");
cy.findByRole("row", withAriaRowIndex(27)).should("exist");

cy.get(".vuuTable-contentContainer")
.then((el) => el[0].scrollTop)
.should("equal", 600);

// row 31 should be top row in viewport
cy.findByRole("row", withAriaRowIndex(31)).should(
cy.findByRole("row", withAriaRowIndex(32)).should(
"have.css",
"top",
"600px",
);
// cy.findByRole("row", withAriaRowIndex(31)).should(
// cy.findByRole("row", withAriaRowIndex(32)).should(
// "have.css",
// "transform",
// "matrix(1, 0, 0, 1, 0, 600)"
Expand Down Expand Up @@ -78,7 +78,7 @@ describe("Table scrolling and keyboard navigation", () => {
);
cy.findByRole("cell", { name: "row 1" }).should("be.focused");

assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT);
assertRenderedRows({ from: 1, to: 30 }, RENDER_BUFFER, ROW_COUNT);
});
});
});
Expand All @@ -97,7 +97,7 @@ describe("Table scrolling and keyboard navigation", () => {
"0",
);
cy.findByRole("cell", { name: "row 1" }).should("be.focused");
assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT);
assertRenderedRows({ from: 1, to: 30 }, RENDER_BUFFER, ROW_COUNT);
});
});
describe("WHEN topmost rows are in viewport, cell in middle of viewport is focussed and Home key pressed ", () => {
Expand All @@ -112,7 +112,7 @@ describe("Table scrolling and keyboard navigation", () => {
"0",
);
cy.findByRole("cell", { name: "row 1" }).should("be.focused");
assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT);
assertRenderedRows({ from: 1, to: 30 }, RENDER_BUFFER, ROW_COUNT);
});
});

Expand Down
Loading

0 comments on commit 4b6700a

Please sign in to comment.