Skip to content

Commit ea8b8d1

Browse files
committed
fixes and tests for filterclause
1 parent 0722bd5 commit ea8b8d1

File tree

8 files changed

+192
-146
lines changed

8 files changed

+192
-146
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
NewFilterClause,
3+
PartialFilterClauseColumnAndOperator,
4+
} from "../../../../../showcase/src/examples/Filters/FilterBar/FilterClause.examples";
5+
6+
describe("FilterClause", () => {
7+
describe("WHEN new filter clause is rendered", () => {
8+
it("THEN expected classname is present", () => {
9+
cy.mount(<NewFilterClause />);
10+
const container = cy.findByTestId("filterclause");
11+
container.should("have.class", "vuuFilterClause");
12+
});
13+
14+
it("THEN component is rendered with column input, is focused and shows suggestions", () => {
15+
cy.mount(<NewFilterClause />);
16+
const container = cy.findByTestId("filterclause");
17+
container.find(".vuuFilterClauseField").should("have.length", 1);
18+
container.find("input").should("be.focused");
19+
cy.findAllByRole("option", { name: "currency" }).should("be.visible");
20+
});
21+
});
22+
23+
describe("WHEN partial filter clause with Column and Operator is rendered", () => {
24+
it("THEN component is rendered with controls for column, operator and value", () => {
25+
cy.mount(<PartialFilterClauseColumnAndOperator />);
26+
const container = cy.findByTestId("filterclause");
27+
container.find(".vuuFilterClauseField").should("have.length", 3);
28+
cy.findByTestId("filterclause")
29+
.find(".vuuFilterClauseValue input")
30+
.should("be.focused");
31+
cy.findAllByRole("option", { name: "GBP" }).should("be.visible");
32+
});
33+
});
34+
});

vuu-ui/packages/vuu-filters/src/filter-clause/FilterClause.css

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,77 +21,70 @@
2121
padding: 0 var(--salt-spacing-100);
2222
width: fit-content;
2323

24+
.vuuFilterClauseField {
25+
flex: 0 0 auto;
26+
}
27+
.vuuFilterClauseField:last-child {
28+
flex: 1 0 auto;
29+
}
30+
31+
.vuuExpandoCombobox {
32+
flex-basis: auto;
33+
flex-shrink: 0;
34+
flex-grow: 0;
35+
}
36+
37+
&:focus-within {
38+
border-color: var(--vuu-color-purple-10);
39+
}
2440

2541
.saltComboBox-focused {
2642
outline: none;
2743
}
28-
}
2944

30-
.vuuFilterClause:focus-within {
31-
border-color: var(--vuu-color-purple-10);
45+
.saltTokenizedInput {
46+
height: 16px;
47+
min-height: 16px;
48+
}
49+
50+
.saltTokenizedInput .saltInputPill {
51+
--pill-fontSize: 12px;
52+
--saltButton-borderStyle: none;
53+
--pill-background: none;
54+
height: 16px;
55+
margin: 0;
56+
}
57+
58+
.saltTokenizedInput-pillGroup {
59+
padding: 0;
60+
height: 16px;
61+
}
3262
}
3363

34-
.vuu-density-high .vuuFilterClause {
64+
.salt-density-high .vuuFilterClause {
3565
padding: 4px 8px;
3666
gap: 4px;
3767
--salt-text-lineHeight: 12px;
3868
--saltInputLegacy-fontSize: 12px;
3969
--saltInputLegacy-minWidth: 12px;
4070
}
4171

42-
.vuu-density-high .vuuFilterClause .saltInput {
72+
.salt-density-high .vuuFilterClause .saltInput {
4373
padding: 0;
4474
min-height: 16px;
4575
height: 16px;
4676
}
4777

48-
.vuuFilterClause .vuuExpandoCombobox {
49-
flex-basis: auto;
50-
flex-shrink: 0;
51-
flex-grow: 0;
52-
}
53-
5478
.vuuFilterClauseOperator-hidden {
5579
display: none;
5680
}
5781

58-
5982
.vuuFilterClause .saltInput-focused,
6083
.vuuFilterClause .saltTokenizedInput-focused {
6184
outline: none;
6285
color: var(--salt-content-primary-foreground);
6386
}
6487

65-
.vuu-theme .saltList {
66-
--list-borderWidth: 1px;
67-
--list-borderStyle: solid;
68-
border-radius: 4px;
69-
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.4);
70-
}
71-
72-
.saltListItem[aria-selected="true"]:not(.saltListItem-checkbox) {
73-
--list-item-background: var(--list-item-background-active);
74-
color: var(--list-item-text-color-active);
75-
}
76-
77-
.saltTokenizedInput {
78-
height: 16px;
79-
min-height: 16px;
80-
}
81-
82-
.saltTokenizedInput .saltInputPill {
83-
--pill-fontSize: 12px;
84-
--saltButton-borderStyle: none;
85-
--pill-background: none;
86-
height: 16px;
87-
margin: 0;
88-
}
89-
90-
.saltTokenizedInput-pillGroup {
91-
padding: 0;
92-
height: 16px;
93-
}
94-
9588
.vuuFilterClause-DatePicker {
9689
border: none;
9790
}

vuu-ui/packages/vuu-filters/src/filter-clause/FilterClause.tsx

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ export const FilterClause = ({
5757
// onChangeColumn,
5858
onSelectColumn,
5959
onSelectOperator,
60-
onFocus,
6160
onDeselectValue,
6261
operatorRef,
6362
selectedColumn,
63+
valueRef,
6464
} = useFilterClause({
6565
filterClauseModel,
6666
onCancel,
@@ -78,20 +78,15 @@ export const FilterClause = ({
7878
const columns = useMemo(() => Object.values(columnsByName), [columnsByName]);
7979

8080
return (
81-
<div
82-
className={cx(classBase, className)}
83-
{...htmlAttributes}
84-
onFocus={onFocus}
85-
tabIndex={0}
86-
>
81+
<div className={cx(classBase, className)} {...htmlAttributes} tabIndex={0}>
8782
<ColumnPicker
8883
inputProps={inputProps}
8984
className={cx(`${classBase}Field`, `${classBase}Column`)}
9085
columns={columns}
9186
key="column-field"
9287
onSelect={onSelectColumn}
9388
ref={columnRef}
94-
value={filterClause.column ?? ""}
89+
value={filterClauseModel.column ?? ""}
9590
/>
9691
{selectedColumn?.name ? (
9792
<OperatorPicker
@@ -103,23 +98,26 @@ export const FilterClause = ({
10398
key="operator-field"
10499
onSelect={onSelectOperator}
105100
ref={operatorRef}
106-
value={filterClause.op ?? ""}
101+
value={filterClauseModel.op ?? ""}
102+
/>
103+
) : null}
104+
{filterClauseModel.op ? (
105+
<FilterClauseValueEditor
106+
inputProps={inputProps}
107+
key="value-field"
108+
onChangeValue={onChangeValue}
109+
onDeselectValue={onDeselectValue}
110+
operator={filterClauseModel.op}
111+
ref={valueRef}
112+
selectedColumn={selectedColumn}
113+
suggestionProvider={suggestionProvider}
114+
table={tableSchema.table}
115+
value={
116+
(filterClause as MultiValueFilterClause)?.values ??
117+
(filterClause as SingleValueFilterClause)?.value
118+
}
107119
/>
108120
) : null}
109-
<FilterClauseValueEditor
110-
inputProps={inputProps}
111-
key="value-field"
112-
onChangeValue={onChangeValue}
113-
onDeselectValue={onDeselectValue}
114-
operator={filterClause.op}
115-
selectedColumn={selectedColumn}
116-
suggestionProvider={suggestionProvider}
117-
table={tableSchema.table}
118-
value={
119-
(filterClause as MultiValueFilterClause)?.values ??
120-
(filterClause as SingleValueFilterClause)?.value
121-
}
122-
/>
123121
</div>
124122
);
125123
};

vuu-ui/packages/vuu-filters/src/filter-clause/filterClauseFocusManagement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const getFocusedFieldDetails = (): [number, string] | [] => {
4141

4242
// Focus the input control within field. If clause passed, will
4343
// focus first field within clause
44-
export const focusField = (fieldOrClause: HTMLElement | null) => {
44+
const focusField = (fieldOrClause: HTMLElement | null) => {
4545
const input = fieldOrClause?.querySelector("input");
4646
if (input) {
4747
input.focus();

vuu-ui/packages/vuu-filters/src/filter-clause/useFilterClause.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { FilterClause, FilterClauseOp } from "@finos/vuu-filter-types";
22
import { hasOpenOptionList } from "@finos/vuu-utils";
33
import {
4-
FocusEventHandler,
54
KeyboardEvent,
5+
RefCallback,
66
SyntheticEvent,
77
useCallback,
88
useEffect,
@@ -13,8 +13,6 @@ import {
1313
import { FilterClauseProps } from "./FilterClause";
1414
import {
1515
clauseIsNotFirst,
16-
elementIsFilterClause,
17-
focusField,
1816
focusNextElement,
1917
focusNextFocusableElement,
2018
navigateToNextItemIfAtBoundary,
@@ -48,6 +46,17 @@ export const useFilterClause = ({
4846

4947
const columnRef = useRef<HTMLDivElement>(null);
5048
const operatorRef = useRef<HTMLDivElement>(null);
49+
const valueRef = useRef<HTMLDivElement | null>(null);
50+
51+
const setValueRef = useCallback<RefCallback<HTMLDivElement>>(
52+
(el) => {
53+
valueRef.current = el;
54+
if (!filterClauseModel.isValid) {
55+
el?.querySelector("input")?.focus();
56+
}
57+
},
58+
[filterClauseModel.isValid]
59+
);
5160

5261
const removeAndNavigateToNextInputIfAtBoundary = useCallback(
5362
(evt: KeyboardEvent) => {
@@ -152,12 +161,6 @@ export const useFilterClause = ({
152161
]
153162
);
154163

155-
const handleFocus = useCallback<FocusEventHandler>((evt) => {
156-
if (elementIsFilterClause(evt.target)) {
157-
focusField(evt.target);
158-
}
159-
}, []);
160-
161164
const inputProps = useMemo(
162165
() => ({
163166
onKeyDownCapture: handleKeyDownCaptureNavigation,
@@ -168,10 +171,18 @@ export const useFilterClause = ({
168171

169172
// Do we need this or can we leave it to the filterEditor
170173
useEffect(() => {
171-
if (filterClauseModel.column === undefined) {
174+
// leave the valueInput to callbackRef handler above, may
175+
// fire after the requestAnimationFrame
176+
if (!filterClauseModel.isValid) {
177+
const inputRef =
178+
filterClauseModel.column === undefined
179+
? columnRef
180+
: filterClauseModel.op === undefined
181+
? operatorRef
182+
: null;
183+
172184
requestAnimationFrame(() => {
173-
const columnInput = columnRef?.current?.querySelector("input");
174-
columnInput?.focus();
185+
inputRef?.current?.querySelector("input")?.focus();
175186
});
176187
}
177188
}, [filterClauseModel]);
@@ -183,9 +194,9 @@ export const useFilterClause = ({
183194
onChangeValue: handleChangeValue,
184195
onDeselectValue: handleDeselectValue,
185196
onSelectColumn,
186-
onFocus: handleFocus,
187197
onSelectOperator,
188198
operatorRef,
189-
selectedColumn: columnsByName[filterClause.column ?? ""],
199+
selectedColumn: columnsByName[filterClauseModel.column ?? ""],
200+
valueRef: setValueRef,
190201
};
191202
};

0 commit comments

Comments
 (0)