Skip to content

chore(site): refactor filter component to be more extendable #13688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add select menu component
  • Loading branch information
BrunoQuaresma committed Jun 26, 2024
commit 050f2c9fb9d9d84aa3a04811d515672c6ed039ea
9 changes: 6 additions & 3 deletions site/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ const sizeStyles = {
xs: {
width: 16,
height: 16,
fontSize: 8,
// Should never be overrided
fontSize: "8px !important",
fontWeight: 700,
},
sm: {
width: 24,
height: 24,
fontSize: 12,
// Should never be overrided
fontSize: "12px !important",
fontWeight: 600,
},
md: {},
xl: {
width: 48,
height: 48,
fontSize: 24,
// Should never be overrided
fontSize: "24px !important",
},
} satisfies Record<string, Interpolation<Theme>>;

Expand Down
2 changes: 1 addition & 1 deletion site/src/components/SearchField/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const SearchField: FC<SearchFieldProps> = ({
<InputAdornment position="start">
<SearchIcon
css={{
fontSize: 14,
fontSize: 16,
color: theme.palette.text.secondary,
}}
/>
Expand Down
66 changes: 66 additions & 0 deletions site/src/components/SelectMenu/SelectMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { withDesktopViewport } from "testHelpers/storybook";
import {
SelectMenu,
SelectMenuButton,
SelectMenuContent,
SelectMenuIcon,
SelectMenuItem,
SelectMenuList,
SelectMenuSearch,
SelectMenuTrigger,
} from "./SelectMenu";

const meta: Meta<typeof SelectMenu> = {
title: "components/SelectMenu",
component: SelectMenu,
render: function SelectMenuRender() {
const opts = options(50);
const selectedOpt = opts[20];

return (
<SelectMenu>
<SelectMenuTrigger>
<SelectMenuButton
startIcon={<UserAvatar size="xs" username={selectedOpt} />}
>
{selectedOpt}
</SelectMenuButton>
</SelectMenuTrigger>
<SelectMenuContent>
<SelectMenuSearch onChange={() => {}} />
<SelectMenuList>
{opts.map((o) => (
<SelectMenuItem key={o} selected={o === selectedOpt}>
<SelectMenuIcon>
<UserAvatar size="xs" username={o} />
</SelectMenuIcon>
{o}
</SelectMenuItem>
))}
</SelectMenuList>
</SelectMenuContent>
</SelectMenu>
);
},
decorators: [withDesktopViewport],
};

function options(n: number): string[] {
return Array.from({ length: n }, (_, i) => `Item ${i + 1}`);
}

export default meta;
type Story = StoryObj<typeof SelectMenu>;

export const Closed: Story = {};

export const Open: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
},
};
118 changes: 118 additions & 0 deletions site/src/components/SelectMenu/SelectMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import CheckOutlined from "@mui/icons-material/CheckOutlined";
import Button, { type ButtonProps } from "@mui/material/Button";
import MenuItem, { type MenuItemProps } from "@mui/material/MenuItem";
import MenuList, { type MenuListProps } from "@mui/material/MenuList";
import {
type FC,
forwardRef,
Children,
isValidElement,
type HTMLProps,
} from "react";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import {
SearchField,
type SearchFieldProps,
} from "components/SearchField/SearchField";

const SIDE_PADDING = 16;

export const SelectMenu = Popover;

export const SelectMenuTrigger = PopoverTrigger;

export const SelectMenuContent = PopoverContent;

export const SelectMenuButton = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
return (
<Button
css={{
"& .MuiButton-startIcon": {
marginLeft: 0,
marginRight: SIDE_PADDING,
},
}}
endIcon={<DropdownArrow />}
ref={ref}
{...props}
/>
);
},
);

export const SelectMenuSearch: FC<SearchFieldProps> = (props) => {
return (
<SearchField
fullWidth
size="medium"
css={(theme) => ({
"& fieldset": {
border: 0,
borderRadius: 0,
borderBottom: `1px solid ${theme.palette.divider} !important`,
},
"& .MuiInputBase-root": {
padding: `12px ${SIDE_PADDING}px`,
},
"& .MuiInputAdornment-positionStart": {
marginRight: SIDE_PADDING,
},
})}
{...props}
/>
);
};

export const SelectMenuList: FC<MenuListProps> = (props) => {
const items = Children.toArray(props.children);
type ItemType = (typeof items)[number];
const selectedAsFirst = (a: ItemType, b: ItemType) => {
if (
!isValidElement<MenuItemProps>(a) ||
!isValidElement<MenuItemProps>(b)
) {
throw new Error(
"SelectMenuList children must be SelectMenuItem components",
);
}
return a.props.selected ? -1 : 0;
};
items.sort(selectedAsFirst);
return (
<MenuList css={{ maxHeight: 480 }} {...props}>
{items}
</MenuList>
);
};

export const SelectMenuIcon: FC<HTMLProps<HTMLDivElement>> = (props) => {
return <div css={{ marginRight: 16 }} {...props} />;
};

export const SelectMenuItem: FC<MenuItemProps> = (props) => {
return (
<MenuItem
css={{
fontSize: 14,
gap: 0,
lineHeight: 1,
padding: `12px ${SIDE_PADDING}px`,
}}
{...props}
>
{props.children}
{props.selected && (
<CheckOutlined
// TODO: Don't set the menu icon font size on default theme
css={{ marginLeft: "auto", fontSize: "inherit !important" }}
/>
)}
</MenuItem>
);
};