Skip to content

Commit 28cf7a6

Browse files
committed
add FilterProvider, FilterPillNext etc
1 parent cc7e6dd commit 28cf7a6

16 files changed

Lines changed: 714 additions & 48 deletions

File tree

vuu-ui/packages/vuu-filter-types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,5 @@ export declare type FilterState = {
8989
filters: Filter[];
9090
activeIndices: number[];
9191
};
92+
93+
export declare type FilterChangeHandler = (filter: Filter | undefined) => void;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Menu, MenuItem, MenuPanel, MenuProps } from "@salt-ds/core";
2+
import { ReactElement, useMemo } from "react";
3+
4+
export type FilterPermissions = {
5+
/** Closing a filter removes it from current display, without deleting*/
6+
allowClose?: boolean;
7+
/** Edit the details of the filter itself */
8+
allowEdit?: boolean;
9+
/** Rename the filter */
10+
allowRename?: boolean;
11+
/** Removing a filter deletes it entirely */
12+
allowRemove?: boolean;
13+
};
14+
15+
const defaultPermissions: FilterPermissions = {
16+
allowClose: true,
17+
allowEdit: true,
18+
allowRename: true,
19+
allowRemove: true,
20+
};
21+
22+
export interface FilterMenuProps
23+
extends Pick<MenuProps, "getVirtualElement" | "onOpenChange" | "open"> {
24+
filterId: string;
25+
onMenuAction: FilterMenuActionHandler;
26+
permissions?: FilterPermissions;
27+
}
28+
29+
export type FilterAction = "close" | "remove" | "edit" | "rename";
30+
31+
export type FilterMenuActionHandler = <T extends FilterAction = FilterAction>(
32+
filterId: string,
33+
filterAction: T,
34+
) => void;
35+
36+
export const FilterMenu = ({
37+
filterId,
38+
getVirtualElement,
39+
onMenuAction,
40+
onOpenChange,
41+
open,
42+
permissions = defaultPermissions,
43+
}: FilterMenuProps) => {
44+
const {
45+
allowClose = defaultPermissions.allowClose,
46+
allowEdit = defaultPermissions.allowEdit,
47+
allowRename = defaultPermissions.allowRename,
48+
allowRemove = defaultPermissions.allowRemove,
49+
} = permissions;
50+
51+
const menuItems = useMemo<ReactElement[]>(() => {
52+
const items: ReactElement[] = [];
53+
if (allowClose) {
54+
items.push(
55+
<MenuItem key="close" onClick={() => onMenuAction(filterId, "close")}>
56+
Close
57+
</MenuItem>,
58+
);
59+
}
60+
if (allowEdit) {
61+
items.push(
62+
<MenuItem key="edit" onClick={() => onMenuAction(filterId, "edit")}>
63+
Edit Filter
64+
</MenuItem>,
65+
);
66+
}
67+
if (allowRename) {
68+
items.push(
69+
<MenuItem key="rename" onClick={() => onMenuAction(filterId, "rename")}>
70+
Rename
71+
</MenuItem>,
72+
);
73+
}
74+
if (allowRemove) {
75+
items.push(
76+
<MenuItem key="delete" onClick={() => onMenuAction(filterId, "remove")}>
77+
Delete
78+
</MenuItem>,
79+
);
80+
}
81+
82+
return items;
83+
}, [allowClose, allowEdit, allowRemove, allowRename, filterId, onMenuAction]);
84+
return (
85+
<Menu
86+
getVirtualElement={getVirtualElement}
87+
open={open}
88+
onOpenChange={onOpenChange}
89+
>
90+
<MenuPanel>{menuItems}</MenuPanel>
91+
</Menu>
92+
);
93+
};

vuu-ui/packages/vuu-filters/src/filter-pill/FilterPill.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
renameCommand,
2727
} from "../filter-pill-menu/FilterPillMenuOptions";
2828
import { getFilterLabel } from "./getFilterLabel";
29-
import { getFilterTooltipText } from "./getFilterTooltipText";
29+
import { getFilterAsFormattedText } from "./getFilterTooltipText";
3030

3131
import filterPillCss from "./FilterPill.css";
3232
import { ButtonProps } from "@salt-ds/core";
@@ -92,7 +92,7 @@ export const FilterPill = ({
9292
[getLabel, filter],
9393
);
9494

95-
const getTooltipTextl = getFilterTooltipText(columnsByName);
95+
const getTooltipText = getFilterAsFormattedText(columnsByName);
9696

9797
const id = useId(idProp);
9898

@@ -164,7 +164,7 @@ export const FilterPill = ({
164164
anchorQuery: ".vuuFilterPill",
165165
id,
166166
placement: ["above", "below"],
167-
tooltipContent: filterAsReactNode(filter, getTooltipTextl),
167+
tooltipContent: filterAsReactNode(filter, getTooltipText),
168168
});
169169

170170
const buttonProps: Partial<ButtonProps> = {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.vuuFilterPillNext {
2+
--saltButton-background: black;
3+
}
4+
5+
.vuuFilterPillNext-active {
6+
--saltButton-background: red;
7+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Button, Tooltip } from "@salt-ds/core";
2+
import { useComponentCssInjection } from "@salt-ds/styles";
3+
import { useWindow } from "@salt-ds/window";
4+
import cx from "clsx";
5+
import { HTMLAttributes } from "react";
6+
import { FilterMenu, type FilterPermissions } from "./FilterMenu";
7+
import {
8+
type FilterPillNextHookProps,
9+
useFilterPillNext,
10+
} from "./useFilterPillNext";
11+
12+
import filterPillNextCss from "./FilterPillNext.css";
13+
14+
export interface FilterPillNextProps
15+
extends FilterPillNextHookProps,
16+
Omit<HTMLAttributes<HTMLButtonElement>, "id"> {
17+
id: string;
18+
permissions?: FilterPermissions;
19+
showTooltip?: boolean;
20+
}
21+
22+
const classBase = "vuuFilterPillNext";
23+
24+
export const FilterPillNext = ({
25+
active,
26+
appearence,
27+
className,
28+
defaultActive,
29+
filter,
30+
id,
31+
onClick,
32+
onMenuAction: onMenuActionProp,
33+
permissions,
34+
showTooltip = true,
35+
...htmlAttributes
36+
}: FilterPillNextProps) => {
37+
const targetWindow = useWindow();
38+
useComponentCssInjection({
39+
testId: "vuu-filter-pill",
40+
css: filterPillNextCss,
41+
window: targetWindow,
42+
});
43+
44+
const {
45+
contextMenuOpen,
46+
onContextMenu,
47+
onContextMenuOpenChange,
48+
onMenuAction,
49+
tooltipText,
50+
virtualElement,
51+
} = useFilterPillNext({
52+
active,
53+
appearence,
54+
defaultActive,
55+
filter,
56+
onMenuAction: onMenuActionProp,
57+
});
58+
59+
const filterPill = (
60+
<Button
61+
{...htmlAttributes}
62+
appearance="solid"
63+
className={cx(classBase, className, {
64+
[`${classBase}-active`]: active,
65+
})}
66+
id={id}
67+
onClick={onClick}
68+
onContextMenu={onContextMenu}
69+
sentiment="accented"
70+
>
71+
{filter.name}
72+
</Button>
73+
);
74+
75+
const filterMenu = (
76+
<FilterMenu
77+
filterId={id}
78+
getVirtualElement={() => virtualElement}
79+
onMenuAction={onMenuAction}
80+
onOpenChange={onContextMenuOpenChange}
81+
open={contextMenuOpen}
82+
/>
83+
);
84+
85+
return showTooltip ? (
86+
<>
87+
<Tooltip className={`${classBase}-tooltip`} content={tooltipText}>
88+
{filterPill}
89+
</Tooltip>
90+
{filterMenu}
91+
</>
92+
) : (
93+
<>
94+
{filterPill}
95+
{filterMenu}
96+
</>
97+
);
98+
};

vuu-ui/packages/vuu-filters/src/filter-pill/getFilterTooltipText.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function formatFilterValue(
4848
return filter;
4949
}
5050

51-
export const getFilterTooltipText =
51+
export const getFilterAsFormattedText =
5252
(columnsByName?: ColumnDescriptorsByName) => (filter: Filter) => {
5353
if (isMultiClauseFilter(filter)) {
5454
const [firstClause] = filterClauses(filter);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { VirtualElement } from "@floating-ui/react";
2+
import { useControlled } from "@salt-ds/core";
3+
import { MouseEventHandler, useCallback, useMemo, useState } from "react";
4+
import { FilterMenuActionHandler } from "./FilterMenu";
5+
import { getFilterAsFormattedText } from "./getFilterTooltipText";
6+
import { Filter } from "@vuu-ui/vuu-filter-types";
7+
8+
export interface FilterPillNextHookProps {
9+
active?: boolean;
10+
/**
11+
* Determines how menu items will be presented
12+
* 'button' - menu items will be context menu
13+
* 'split-button' - menu items will be dropdowns
14+
*/
15+
appearence?: "split-button" | "button";
16+
defaultActive?: boolean;
17+
filter: Filter;
18+
onMenuAction?: FilterMenuActionHandler;
19+
}
20+
21+
export const useFilterPillNext = ({
22+
active: activeProp,
23+
appearence = "button",
24+
defaultActive,
25+
filter,
26+
onMenuAction,
27+
}: FilterPillNextHookProps) => {
28+
const [active] = useControlled({
29+
controlled: activeProp,
30+
default: defaultActive ?? false,
31+
name: "FilterPillNext",
32+
state: "active",
33+
});
34+
35+
const [contextMenuOpen, setContextMenuOpen] = useState(false);
36+
const [virtualElement, setVirtualElement] = useState<VirtualElement | null>(
37+
null,
38+
);
39+
40+
const tooltipText = useMemo(() => {
41+
const getTooltipText = getFilterAsFormattedText();
42+
return getTooltipText(filter);
43+
}, [filter]);
44+
45+
const onContextMenu: MouseEventHandler<HTMLButtonElement> = (e) => {
46+
e.preventDefault();
47+
if (appearence === "button") {
48+
setVirtualElement({
49+
getBoundingClientRect: () => ({
50+
width: 0,
51+
height: 0,
52+
x: e.clientX,
53+
y: e.clientY,
54+
top: e.clientY,
55+
right: e.clientX,
56+
bottom: e.clientY,
57+
left: e.clientX,
58+
}),
59+
});
60+
setContextMenuOpen(true);
61+
}
62+
};
63+
64+
const handleMenuAction = useCallback<FilterMenuActionHandler>(
65+
(filterId, actionType) => {
66+
setContextMenuOpen(false);
67+
onMenuAction?.(filterId, actionType);
68+
},
69+
[onMenuAction],
70+
);
71+
72+
return {
73+
active,
74+
contextMenuOpen,
75+
onContextMenu,
76+
onContextMenuOpenChange: setContextMenuOpen,
77+
onMenuAction: handleMenuAction,
78+
tooltipText,
79+
virtualElement,
80+
};
81+
};

0 commit comments

Comments
 (0)