From d222f7231a9f7a9cc5c396dc619d991093e62e61 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jun 2024 19:29:11 +0000 Subject: [PATCH 01/15] Replace search by menu search --- site/src/components/Menu/MenuSearch.tsx | 23 +++++++++ .../pages/WorkspacesPage/WorkspacesButton.tsx | 9 ++-- .../WorkspacesPage/WorkspacesSearchBox.tsx | 50 ------------------- 3 files changed, 27 insertions(+), 55 deletions(-) create mode 100644 site/src/components/Menu/MenuSearch.tsx delete mode 100644 site/src/pages/WorkspacesPage/WorkspacesSearchBox.tsx diff --git a/site/src/components/Menu/MenuSearch.tsx b/site/src/components/Menu/MenuSearch.tsx new file mode 100644 index 0000000000000..32f8cab9f4a8f --- /dev/null +++ b/site/src/components/Menu/MenuSearch.tsx @@ -0,0 +1,23 @@ +import type { FC } from "react"; +import { + SearchField, + type SearchFieldProps, +} from "components/SearchField/SearchField"; + +export const MenuSearch: FC = (props) => { + return ( + ({ + "& fieldset": { + border: 0, + borderRadius: 0, + // MUI has so many nested selectors that it's easier to just + // override the border directly using the `!important` hack + borderBottom: `1px solid ${theme.palette.divider} !important`, + }, + })} + {...props} + /> + ); +}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index aef8f7518331a..95df46d32316f 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -11,6 +11,7 @@ import { import type { Template } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Loader } from "components/Loader/Loader"; +import { MenuSearch } from "components/Menu/MenuSearch"; import { OverflowY } from "components/OverflowY/OverflowY"; import { Popover, @@ -18,7 +19,6 @@ import { PopoverTrigger, } from "components/Popover/Popover"; import { SearchEmpty, searchStyles } from "components/Search/Search"; -import { SearchBox } from "./WorkspacesSearchBox"; const ICON_SIZE = 18; @@ -67,12 +67,11 @@ export const WorkspacesButton: FC = ({ ".MuiPaper-root": searchStyles.content, }} > - setSearchTerm(newValue)} + onChange={setSearchTerm} placeholder="Type/select a workspace template" - label="Template select for workspace" - css={{ flexShrink: 0, columnGap: 12 }} + aria-label="Template select for workspace" /> { - label?: string; - value: string; - onKeyDown?: (event: KeyboardEvent) => void; - onValueChange: (newValue: string) => void; - $$ref?: Ref; -} - -export const SearchBox: FC = ({ - onValueChange, - onKeyDown, - label = "Search", - placeholder = "Search...", - $$ref, - ...attrs -}) => { - const hookId = useId(); - const inputId = `${hookId}-${SearchBox.name}-input`; - - return ( - - onValueChange(e.target.value)} - /> - - ); -}; From 050f2c9fb9d9d84aa3a04811d515672c6ed039ea Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 26 Jun 2024 16:06:07 +0000 Subject: [PATCH 02/15] Add select menu component --- site/src/components/Avatar/Avatar.tsx | 9 +- .../components/SearchField/SearchField.tsx | 2 +- .../SelectMenu/SelectMenu.stories.tsx | 66 ++++++++++ site/src/components/SelectMenu/SelectMenu.tsx | 118 ++++++++++++++++++ 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 site/src/components/SelectMenu/SelectMenu.stories.tsx create mode 100644 site/src/components/SelectMenu/SelectMenu.tsx diff --git a/site/src/components/Avatar/Avatar.tsx b/site/src/components/Avatar/Avatar.tsx index 5c4e46f6d863d..ad9f1e6d7f4bb 100644 --- a/site/src/components/Avatar/Avatar.tsx +++ b/site/src/components/Avatar/Avatar.tsx @@ -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>; diff --git a/site/src/components/SearchField/SearchField.tsx b/site/src/components/SearchField/SearchField.tsx index 9e81b74e972ac..2f2c65001df22 100644 --- a/site/src/components/SearchField/SearchField.tsx +++ b/site/src/components/SearchField/SearchField.tsx @@ -29,7 +29,7 @@ export const SearchField: FC = ({ diff --git a/site/src/components/SelectMenu/SelectMenu.stories.tsx b/site/src/components/SelectMenu/SelectMenu.stories.tsx new file mode 100644 index 0000000000000..26a3c4c434b52 --- /dev/null +++ b/site/src/components/SelectMenu/SelectMenu.stories.tsx @@ -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 = { + title: "components/SelectMenu", + component: SelectMenu, + render: function SelectMenuRender() { + const opts = options(50); + const selectedOpt = opts[20]; + + return ( + + + } + > + {selectedOpt} + + + + {}} /> + + {opts.map((o) => ( + + + + + {o} + + ))} + + + + ); + }, + decorators: [withDesktopViewport], +}; + +function options(n: number): string[] { + return Array.from({ length: n }, (_, i) => `Item ${i + 1}`); +} + +export default meta; +type Story = StoryObj; + +export const Closed: Story = {}; + +export const Open: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByRole("button"); + await userEvent.click(button); + }, +}; diff --git a/site/src/components/SelectMenu/SelectMenu.tsx b/site/src/components/SelectMenu/SelectMenu.tsx new file mode 100644 index 0000000000000..6e9f44b14dc27 --- /dev/null +++ b/site/src/components/SelectMenu/SelectMenu.tsx @@ -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( + (props, ref) => { + return ( + - ); -}); - -interface SearchMenuProps - extends Pick { - options?: TOption[]; - renderOption: (option: TOption) => ReactNode; - query: string; - onQueryChange: (query: string) => void; -} - -function SearchMenu({ - options, - renderOption, - query, - onQueryChange, - ...menuProps -}: SearchMenuProps) { - const menuListRef = useRef(null); - const searchInputRef = useRef(null); - - return ( - { - menuProps.onClose && menuProps.onClose(event, reason); - onQueryChange(""); - }} - css={{ - "& .MuiPaper-root": searchStyles.content, - }} - // Disabled this so when we clear the filter and do some sorting in the - // search items it does not look strange. Github removes exit transitions - // on their filters as well. - transitionDuration={{ - enter: 250, - exit: 0, - }} - onKeyDown={(e) => { - e.stopPropagation(); - if (e.key === "ArrowDown" && menuListRef.current) { - const firstItem = menuListRef.current.firstChild as HTMLElement; - firstItem.focus(); - } - }} - > - - { - onQueryChange(e.target.value); - }} - /> - - -
  • - { - if (e.shiftKey && e.code === "Tab") { - e.preventDefault(); - e.stopPropagation(); - searchInputRef.current?.focus(); - } - }} - > - {options ? ( - options.length > 0 ? ( - options.map(renderOption) - ) : ( - - ) - ) : ( - - )} - -
  • -
    - ); -} diff --git a/site/src/components/Filter/menu.ts b/site/src/components/Filter/menu.ts index 21cfec33ad3cc..d011ada4f0d3e 100644 --- a/site/src/components/Filter/menu.ts +++ b/site/src/components/Filter/menu.ts @@ -1,8 +1,8 @@ import { useMemo, useRef, useState } from "react"; import { useQuery } from "react-query"; -import type { BaseOption } from "./options"; +import type { SelectFilterOption } from "components/SelectFilter/SelectFilter"; -export type UseFilterMenuOptions = { +export type UseFilterMenuOptions = { id: string; value: string | undefined; // Using null because of react-query @@ -13,7 +13,9 @@ export type UseFilterMenuOptions = { enabled?: boolean; }; -export const useFilterMenu = ({ +export const useFilterMenu = < + TOption extends SelectFilterOption = SelectFilterOption, +>({ id, value, getSelectedOption, @@ -78,16 +80,13 @@ export const useFilterMenu = ({ selectedOption, ]); - const selectOption = (option: TOption) => { - let newSelectedOptionValue: TOption | undefined = option; - selectedOptionsCacheRef.current[option.value] = option; - setQuery(""); - - if (option.value === selectedOption?.value) { - newSelectedOptionValue = undefined; + const selectOption = (option: TOption | undefined) => { + if (option) { + selectedOptionsCacheRef.current[option.value] = option; } - onChange(newSelectedOptionValue); + setQuery(""); + onChange(option); }; return { diff --git a/site/src/components/Filter/options.ts b/site/src/components/Filter/options.ts deleted file mode 100644 index 08b71deb88a3a..0000000000000 --- a/site/src/components/Filter/options.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type BaseOption = { - label: string; - value: string; -}; diff --git a/site/src/components/SelectFilter/SelectFilter.tsx b/site/src/components/SelectFilter/SelectFilter.tsx index 2014509164d53..64171650f8db3 100644 --- a/site/src/components/SelectFilter/SelectFilter.tsx +++ b/site/src/components/SelectFilter/SelectFilter.tsx @@ -13,12 +13,12 @@ import { export type SelectFilterOption = { startIcon?: ReactNode; - label: ReactNode; + label: string; value: string; }; export type SelectFilterProps = { - options: SelectFilterOption[]; + options: SelectFilterOption[] | undefined; onSelect: (option: SelectFilterOption | undefined) => void; selectedOption?: SelectFilterOption; placeholder: string; diff --git a/site/src/components/SelectMenu/SelectMenu.tsx b/site/src/components/SelectMenu/SelectMenu.tsx index f68e829fdb4c6..9dd3c3b4b9c84 100644 --- a/site/src/components/SelectMenu/SelectMenu.tsx +++ b/site/src/components/SelectMenu/SelectMenu.tsx @@ -33,6 +33,7 @@ export const SelectMenuButton = forwardRef( return ( ); }, ); diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 41b1eecb9baca..0ffd5a204b311 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -13,7 +13,7 @@ import { type UseFilterMenuOptions, } from "components/Filter/menu"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; -import type { SelectFilterOption } from "components/SelectFilter/SelectFilter"; +import type { SelectFilterOption } from "components/Filter/SelectFilter"; import { docs } from "utils/docs"; const PRESET_FILTERS = [ diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index d8a933d2378a2..b9f9490d16b7b 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -10,7 +10,7 @@ import { type UseFilterMenuOptions, useFilterMenu, } from "components/Filter/menu"; -import type { SelectFilterOption } from "components/SelectFilter/SelectFilter"; +import type { SelectFilterOption } from "components/Filter/SelectFilter"; import { StatusIndicator } from "components/StatusIndicator/StatusIndicator"; import { docs } from "utils/docs"; diff --git a/site/src/pages/WorkspacesPage/filter/menus.tsx b/site/src/pages/WorkspacesPage/filter/menus.tsx index 3ece64c7c6bba..3c97c3559c6ef 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.tsx +++ b/site/src/pages/WorkspacesPage/filter/menus.tsx @@ -5,7 +5,7 @@ import { useFilterMenu, type UseFilterMenuOptions, } from "components/Filter/menu"; -import type { SelectFilterOption } from "components/SelectFilter/SelectFilter"; +import type { SelectFilterOption } from "components/Filter/SelectFilter"; import { StatusIndicator } from "components/StatusIndicator/StatusIndicator"; import { TemplateAvatar } from "components/TemplateAvatar/TemplateAvatar"; import { getDisplayWorkspaceStatus } from "utils/workspace"; From c7e15ec024068368f3c2fe265739a21b064f201b Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 27 Jun 2024 13:31:09 +0000 Subject: [PATCH 07/15] Improve accessibility and searching --- .../Filter/SelectFilter.stories.tsx | 2 +- site/src/components/Filter/SelectFilter.tsx | 21 +++++++++++++------ site/src/components/Filter/UserFilter.tsx | 6 +++++- site/src/pages/AuditPage/AuditFilter.tsx | 8 ++++--- site/src/pages/UsersPage/UsersFilter.tsx | 1 + .../src/pages/WorkspacesPage/filter/menus.tsx | 7 +++++-- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/site/src/components/Filter/SelectFilter.stories.tsx b/site/src/components/Filter/SelectFilter.stories.tsx index 6d8fd5ed0e873..e755a1efe04be 100644 --- a/site/src/components/Filter/SelectFilter.stories.tsx +++ b/site/src/components/Filter/SelectFilter.stories.tsx @@ -104,7 +104,7 @@ export const UnselectingOption: Story = { export const SearchingOption: Story = { args: { searchPlaceholder: "Search options...", - searchAriaLabel: "Search options", + searchLabel: "Search options", }, render: function SelectFilterWithSearch(args) { const [selectedOption, setSelectedOption] = useState< diff --git a/site/src/components/Filter/SelectFilter.tsx b/site/src/components/Filter/SelectFilter.tsx index b0f5ac2097ba8..9abf96632e250 100644 --- a/site/src/components/Filter/SelectFilter.tsx +++ b/site/src/components/Filter/SelectFilter.tsx @@ -1,3 +1,4 @@ +import visuallyHidden from "@mui/utils/visuallyHidden"; import { useState, type FC, type ReactNode } from "react"; import { Loader } from "components/Loader/Loader"; import { @@ -21,24 +22,31 @@ export type SelectFilterOption = { export type SelectFilterProps = { options: SelectFilterOption[] | undefined; - onSelect: (option: SelectFilterOption | undefined) => void; selectedOption?: SelectFilterOption; + // Used to add a accessibility label to the select + label: string; + // Used when there is no option selected placeholder: string; + // Used to customize the empty state message emptyText?: string; - // Search props + onSelect: (option: SelectFilterOption | undefined) => void; + // Value of the search input search?: string; - onSearchChange?: (search: string) => void; + // Used to customize the search input placeholder searchPlaceholder?: string; - searchAriaLabel?: string; + // Used to add a accessibility label to the search input + searchLabel?: string; + onSearchChange?: (search: string) => void; }; export const SelectFilter: FC = ({ + label, options, selectedOption, onSelect, onSearchChange, placeholder, - searchAriaLabel, + searchLabel, searchPlaceholder, emptyText, search, @@ -53,6 +61,7 @@ export const SelectFilter: FC = ({ css={{ width: BASE_WIDTH }} > {selectedOption?.label ?? placeholder} + {label} = ({ value={search} onChange={onSearchChange} placeholder={searchPlaceholder} - inputProps={{ "aria-label": searchAriaLabel }} + inputProps={{ "aria-label": searchLabel }} /> )} {options ? ( diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index d4f3dc5b5725e..467d33db1a6fe 100644 --- a/site/src/components/Filter/UserFilter.tsx +++ b/site/src/components/Filter/UserFilter.tsx @@ -99,9 +99,13 @@ interface UserMenuProps { export const UserMenu: FC = ({ menu }) => { return ( ; const ActionMenu = (menu: ActionFilterMenu) => { return ( ); @@ -146,9 +147,10 @@ export type ResourceTypeFilterMenu = ReturnType< const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => { return ( ); diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index b9f9490d16b7b..d02473749f155 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -89,6 +89,7 @@ export const UsersFilter: FC = ({ filter, error, menus }) => { const StatusMenu = (menu: StatusFilterMenu) => { return ( ; export const TemplateMenu = (menu: TemplateFilterMenu) => { return ( { return ( Date: Thu, 27 Jun 2024 13:55:46 +0000 Subject: [PATCH 08/15] Fix width of popover and move search to has its own component --- .../Filter/SelectFilter.stories.tsx | 30 ++++++++++++------- site/src/components/Filter/SelectFilter.tsx | 28 +++++++---------- site/src/components/Filter/UserFilter.tsx | 21 ++++++++----- site/src/components/Filter/filter.tsx | 8 ----- site/src/pages/AuditPage/AuditFilter.tsx | 10 ++++--- site/src/pages/UsersPage/UsersFilter.tsx | 8 +++-- .../src/pages/WorkspacesPage/filter/menus.tsx | 23 +++++++++----- 7 files changed, 70 insertions(+), 58 deletions(-) diff --git a/site/src/components/Filter/SelectFilter.stories.tsx b/site/src/components/Filter/SelectFilter.stories.tsx index e755a1efe04be..617de0eb8ea81 100644 --- a/site/src/components/Filter/SelectFilter.stories.tsx +++ b/site/src/components/Filter/SelectFilter.stories.tsx @@ -4,7 +4,11 @@ import { userEvent, within, expect } from "@storybook/test"; import { useState } from "react"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { withDesktopViewport } from "testHelpers/storybook"; -import { SelectFilter, type SelectFilterOption } from "./SelectFilter"; +import { + SelectFilter, + SelectFilterSearch, + type SelectFilterOption, +} from "./SelectFilter"; const options: SelectFilterOption[] = Array.from({ length: 50 }, (_, i) => ({ startIcon: , @@ -57,9 +61,13 @@ export const Selected: Story = { export const WithSearch: Story = { args: { selectedOption: options[25], - search: "", - onSearchChange: action("onSearch"), - searchPlaceholder: "Search options...", + search: ( + + ), }, }; @@ -102,10 +110,6 @@ export const UnselectingOption: Story = { }; export const SearchingOption: Story = { - args: { - searchPlaceholder: "Search options...", - searchLabel: "Search options", - }, render: function SelectFilterWithSearch(args) { const [selectedOption, setSelectedOption] = useState< SelectFilterOption | undefined @@ -118,11 +122,17 @@ export const SearchingOption: Story = { return ( + } /> ); }, diff --git a/site/src/components/Filter/SelectFilter.tsx b/site/src/components/Filter/SelectFilter.tsx index 9abf96632e250..61f99b5af8810 100644 --- a/site/src/components/Filter/SelectFilter.tsx +++ b/site/src/components/Filter/SelectFilter.tsx @@ -13,6 +13,7 @@ import { } from "components/SelectMenu/SelectMenu"; const BASE_WIDTH = 200; +const POPOVER_WIDTH = 320; export type SelectFilterOption = { startIcon?: ReactNode; @@ -30,13 +31,8 @@ export type SelectFilterProps = { // Used to customize the empty state message emptyText?: string; onSelect: (option: SelectFilterOption | undefined) => void; - // Value of the search input - search?: string; - // Used to customize the search input placeholder - searchPlaceholder?: string; - // Used to add a accessibility label to the search input - searchLabel?: string; - onSearchChange?: (search: string) => void; + // SelectFilterSearch element + search?: ReactNode; }; export const SelectFilter: FC = ({ @@ -44,10 +40,7 @@ export const SelectFilter: FC = ({ options, selectedOption, onSelect, - onSearchChange, placeholder, - searchLabel, - searchPlaceholder, emptyText, search, }) => { @@ -68,18 +61,15 @@ export const SelectFilter: FC = ({ horizontal="right" css={{ "& .MuiPaper-root": { + // When including search, we aim for the width to be as wide as + // possible. + width: search ? "100%" : undefined, + maxWidth: POPOVER_WIDTH, minWidth: BASE_WIDTH, }, }} > - {onSearchChange && ( - - )} + {search} {options ? ( options.length > 0 ? ( @@ -123,3 +113,5 @@ export const SelectFilter: FC = ({ ); }; + +export const SelectFilterSearch = SelectMenuSearch; diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index 467d33db1a6fe..6677a83062bcd 100644 --- a/site/src/components/Filter/UserFilter.tsx +++ b/site/src/components/Filter/UserFilter.tsx @@ -1,9 +1,12 @@ import type { FC } from "react"; import { API } from "api/api"; -import type { SelectFilterOption } from "components/Filter/SelectFilter"; +import { + SelectFilter, + SelectFilterSearch, + type SelectFilterOption, +} from "components/Filter/SelectFilter"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { FilterMenu } from "./filter"; import { type UseFilterMenuOptions, useFilterMenu } from "./menu"; export const useUserFilterMenu = ({ @@ -98,17 +101,21 @@ interface UserMenuProps { export const UserMenu: FC = ({ menu }) => { return ( - + } /> ); }; diff --git a/site/src/components/Filter/filter.tsx b/site/src/components/Filter/filter.tsx index b8ab5e23da241..b26ce444a805f 100644 --- a/site/src/components/Filter/filter.tsx +++ b/site/src/components/Filter/filter.tsx @@ -13,10 +13,6 @@ import { hasError, isApiValidationError, } from "api/errors"; -import { - SelectFilter, - type SelectFilterProps, -} from "components/Filter/SelectFilter"; import { InputGroup } from "components/InputGroup/InputGroup"; import { SearchField } from "components/SearchField/SearchField"; import { useDebouncedFunction } from "hooks/debounce"; @@ -325,7 +321,3 @@ const PresetMenu: FC = ({ ); }; - -export const FilterMenu: FC = (props) => { - return ; -}; diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index c5288cde87eb7..0127637a4b69d 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -3,7 +3,6 @@ import type { FC } from "react"; import { AuditActions, ResourceTypes } from "api/typesGenerated"; import { Filter, - FilterMenu, MenuSkeleton, SearchFieldSkeleton, type useFilter, @@ -12,7 +11,10 @@ import { useFilterMenu, type UseFilterMenuOptions, } from "components/Filter/menu"; -import type { SelectFilterOption } from "components/Filter/SelectFilter"; +import { + SelectFilter, + type SelectFilterOption, +} from "components/Filter/SelectFilter"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; import { docs } from "utils/docs"; @@ -92,7 +94,7 @@ export type ActionFilterMenu = ReturnType; const ActionMenu = (menu: ActionFilterMenu) => { return ( - { return ( - = ({ filter, error, menus }) => { const StatusMenu = (menu: StatusFilterMenu) => { return ( - ; export const TemplateMenu = (menu: TemplateFilterMenu) => { return ( - + } /> ); }; @@ -109,7 +116,7 @@ export type StatusFilterMenu = ReturnType; export const StatusMenu = (menu: StatusFilterMenu) => { return ( - Date: Fri, 28 Jun 2024 10:44:10 -0300 Subject: [PATCH 09/15] Update site/src/components/Filter/UserFilter.tsx Co-authored-by: Michael Smith --- site/src/components/Filter/UserFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index 6677a83062bcd..5b2af028d6520 100644 --- a/site/src/components/Filter/UserFilter.tsx +++ b/site/src/components/Filter/UserFilter.tsx @@ -76,7 +76,7 @@ export const useUserFilterMenu = ({ }, getOptions: async (query) => { const usersRes = await API.getUsers({ q: query, limit: 25 }); - let options: SelectFilterOption[] = usersRes.users.map((user) => ({ + let options = usersRes.users.map((user) => ({ label: user.username, value: user.username, startIcon: ( From 06ae934adb8f581fce56ab01cad7f2ef7ea0bd06 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 28 Jun 2024 18:13:55 +0000 Subject: [PATCH 10/15] Remove important style --- site/src/components/Avatar/Avatar.tsx | 6 ++---- site/src/components/SelectMenu/SelectMenu.tsx | 5 +++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/site/src/components/Avatar/Avatar.tsx b/site/src/components/Avatar/Avatar.tsx index ad9f1e6d7f4bb..b27d1a64798cc 100644 --- a/site/src/components/Avatar/Avatar.tsx +++ b/site/src/components/Avatar/Avatar.tsx @@ -18,15 +18,13 @@ const sizeStyles = { xs: { width: 16, height: 16, - // Should never be overrided - fontSize: "8px !important", + fontSize: 8, fontWeight: 700, }, sm: { width: 24, height: 24, - // Should never be overrided - fontSize: "12px !important", + fontSize: 12, fontWeight: 600, }, md: {}, diff --git a/site/src/components/SelectMenu/SelectMenu.tsx b/site/src/components/SelectMenu/SelectMenu.tsx index 707ab381e9451..039966a44a9fc 100644 --- a/site/src/components/SelectMenu/SelectMenu.tsx +++ b/site/src/components/SelectMenu/SelectMenu.tsx @@ -48,6 +48,11 @@ export const SelectMenuButton = forwardRef( endIcon={} ref={ref} {...props} + // MUI applies a style that affects the sizes of start icons. + // .MuiButton-startIcon > *:nth-of-type(1) { font-size: 20px }. To + // prevent this from breaking the inner components of startIcon, we wrap + // it in a div. + startIcon={props.startIcon &&
    {props.startIcon}
    } > Date: Fri, 28 Jun 2024 18:45:33 +0000 Subject: [PATCH 11/15] Apply review comments --- site/src/components/Avatar/Avatar.tsx | 3 +- site/src/components/Filter/SelectFilter.tsx | 17 ++++---- .../SelectMenu/SelectMenu.stories.tsx | 32 +++++++++++++++ site/src/components/SelectMenu/SelectMenu.tsx | 39 ++++++++++++------- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/site/src/components/Avatar/Avatar.tsx b/site/src/components/Avatar/Avatar.tsx index b27d1a64798cc..5c4e46f6d863d 100644 --- a/site/src/components/Avatar/Avatar.tsx +++ b/site/src/components/Avatar/Avatar.tsx @@ -31,8 +31,7 @@ const sizeStyles = { xl: { width: 48, height: 48, - // Should never be overrided - fontSize: "24px !important", + fontSize: 24, }, } satisfies Record>; diff --git a/site/src/components/Filter/SelectFilter.tsx b/site/src/components/Filter/SelectFilter.tsx index 61f99b5af8810..7521affc7efb6 100644 --- a/site/src/components/Filter/SelectFilter.tsx +++ b/site/src/components/Filter/SelectFilter.tsx @@ -1,4 +1,3 @@ -import visuallyHidden from "@mui/utils/visuallyHidden"; import { useState, type FC, type ReactNode } from "react"; import { Loader } from "components/Loader/Loader"; import { @@ -32,7 +31,7 @@ export type SelectFilterProps = { emptyText?: string; onSelect: (option: SelectFilterOption | undefined) => void; // SelectFilterSearch element - search?: ReactNode; + selectFilterSearch?: ReactNode; }; export const SelectFilter: FC = ({ @@ -42,7 +41,7 @@ export const SelectFilter: FC = ({ onSelect, placeholder, emptyText, - search, + selectFilterSearch, }) => { const [open, setOpen] = useState(false); @@ -52,24 +51,24 @@ export const SelectFilter: FC = ({ {selectedOption?.label ?? placeholder} - {label} - {search} + {selectFilterSearch} {options ? ( options.length > 0 ? ( @@ -103,7 +102,7 @@ export const SelectFilter: FC = ({ lineHeight: 1, })} > - {emptyText ?? "No options found"} + {emptyText || "No options found"} ) ) : ( diff --git a/site/src/components/SelectMenu/SelectMenu.stories.tsx b/site/src/components/SelectMenu/SelectMenu.stories.tsx index 3edcbc764d551..f47244504bb49 100644 --- a/site/src/components/SelectMenu/SelectMenu.stories.tsx +++ b/site/src/components/SelectMenu/SelectMenu.stories.tsx @@ -98,3 +98,35 @@ export const LongButtonText: Story = { ); }, }; + +export const NoSelectedOption: Story = { + render: function SelectMenuRender() { + const opts = options(50); + + return ( + + + All users + + + {}} /> + + {opts.map((o) => ( + + + + + {o} + + ))} + + + + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByRole("button"); + await userEvent.click(button); + }, +}; diff --git a/site/src/components/SelectMenu/SelectMenu.tsx b/site/src/components/SelectMenu/SelectMenu.tsx index 039966a44a9fc..06c176c192b10 100644 --- a/site/src/components/SelectMenu/SelectMenu.tsx +++ b/site/src/components/SelectMenu/SelectMenu.tsx @@ -8,6 +8,8 @@ import { Children, isValidElement, type HTMLProps, + type ReactElement, + useMemo, } from "react"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { @@ -75,14 +77,13 @@ export const SelectMenuSearch: FC = (props) => { fullWidth size="medium" css={(theme) => ({ + borderBottom: `1px solid ${theme.palette.divider}`, "& input": { fontSize: 14, }, - "& fieldset": { border: 0, borderRadius: 0, - borderBottom: `1px solid ${theme.palette.divider} !important`, }, "& .MuiInputBase-root": { padding: `12px ${SIDE_PADDING}px`, @@ -97,20 +98,16 @@ export const SelectMenuSearch: FC = (props) => { }; export const SelectMenuList: FC = (props) => { - const items = Children.toArray(props.children); - type ItemType = (typeof items)[number]; - const selectedAsFirst = (a: ItemType, b: ItemType) => { - if ( - !isValidElement(a) || - !isValidElement(b) - ) { - throw new Error( - "SelectMenuList children must be SelectMenuItem components", - ); + const items = useMemo(() => { + let children = Children.toArray(props.children); + if (!children.every(isValidElement)) { + throw new Error("SelectMenuList only accepts MenuItem children"); } - return a.props.selected ? -1 : 0; - }; - items.sort(selectedAsFirst); + children = moveSelectedElementToFirst( + children as ReactElement[], + ); + return children; + }, [props.children]); return ( {items} @@ -118,6 +115,18 @@ export const SelectMenuList: FC = (props) => { ); }; +function moveSelectedElementToFirst(items: ReactElement[]) { + const selectedElement = items.find((i) => i.props.selected); + if (!selectedElement) { + return items; + } + const selectedElementIndex = items.indexOf(selectedElement); + const newItems = items.slice(); + newItems.splice(selectedElementIndex, 1); + newItems.unshift(selectedElement); + return newItems; +} + export const SelectMenuIcon: FC> = (props) => { return
    ; }; From 928209cf96f7147615765dbb7e4340bea9901df5 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 28 Jun 2024 18:48:01 +0000 Subject: [PATCH 12/15] Add auto focus to search --- site/src/components/SelectMenu/SelectMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/components/SelectMenu/SelectMenu.tsx b/site/src/components/SelectMenu/SelectMenu.tsx index 06c176c192b10..39837720d0023 100644 --- a/site/src/components/SelectMenu/SelectMenu.tsx +++ b/site/src/components/SelectMenu/SelectMenu.tsx @@ -93,6 +93,7 @@ export const SelectMenuSearch: FC = (props) => { }, })} {...props} + inputProps={{ autoFocus: true, ...props.inputProps }} /> ); }; From d344d9c21b6818ef99a3aea633abbbaa872dc6c2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 1 Jul 2024 13:26:53 +0000 Subject: [PATCH 13/15] Fix types --- site/src/components/Filter/SelectFilter.stories.tsx | 4 ++-- site/src/components/Filter/UserFilter.tsx | 2 +- site/src/pages/WorkspacesPage/filter/menus.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/Filter/SelectFilter.stories.tsx b/site/src/components/Filter/SelectFilter.stories.tsx index 617de0eb8ea81..167983e691482 100644 --- a/site/src/components/Filter/SelectFilter.stories.tsx +++ b/site/src/components/Filter/SelectFilter.stories.tsx @@ -61,7 +61,7 @@ export const Selected: Story = { export const WithSearch: Story = { args: { selectedOption: options[25], - search: ( + selectFilterSearch: ( = ({ menu }) => { options={menu.searchOptions} onSelect={menu.selectOption} selectedOption={menu.selectedOption ?? undefined} - search={ + selectFilterSearch={ { options={menu.searchOptions} onSelect={menu.selectOption} selectedOption={menu.selectedOption ?? undefined} - search={ + selectFilterSearch={ Date: Tue, 2 Jul 2024 16:00:09 +0000 Subject: [PATCH 14/15] Apply PR suggestions --- site/src/components/Filter/SelectFilter.stories.tsx | 2 +- site/src/components/SelectMenu/SelectMenu.stories.tsx | 3 ++- site/src/pages/WorkspacesPage/filter/menus.tsx | 5 +---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/site/src/components/Filter/SelectFilter.stories.tsx b/site/src/components/Filter/SelectFilter.stories.tsx index 167983e691482..d545625a23974 100644 --- a/site/src/components/Filter/SelectFilter.stories.tsx +++ b/site/src/components/Filter/SelectFilter.stories.tsx @@ -11,7 +11,7 @@ import { } from "./SelectFilter"; const options: SelectFilterOption[] = Array.from({ length: 50 }, (_, i) => ({ - startIcon: , + startIcon: , label: `Option ${i + 1}`, value: `option-${i + 1}`, })); diff --git a/site/src/components/SelectMenu/SelectMenu.stories.tsx b/site/src/components/SelectMenu/SelectMenu.stories.tsx index f47244504bb49..86b33ae817969 100644 --- a/site/src/components/SelectMenu/SelectMenu.stories.tsx +++ b/site/src/components/SelectMenu/SelectMenu.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; @@ -109,7 +110,7 @@ export const NoSelectedOption: Story = { All users - {}} /> + {opts.map((o) => ( diff --git a/site/src/pages/WorkspacesPage/filter/menus.tsx b/site/src/pages/WorkspacesPage/filter/menus.tsx index 699b31f6c0f81..0316f158e87c9 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.tsx +++ b/site/src/pages/WorkspacesPage/filter/menus.tsx @@ -31,10 +31,7 @@ export const useTemplateFilterMenu = ({ const template = templates.find((template) => template.name === value); if (template) { return { - label: - template.display_name !== "" - ? template.display_name - : template.name, + label: template.display_name || template.name, value: template.name, startIcon: , }; From 50a6ce478d2bf925a340ee6fc6f1110155796081 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 2 Jul 2024 16:02:47 +0000 Subject: [PATCH 15/15] Fix closed story --- site/src/components/Filter/SelectFilter.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Filter/SelectFilter.stories.tsx b/site/src/components/Filter/SelectFilter.stories.tsx index d545625a23974..21d2afe288146 100644 --- a/site/src/components/Filter/SelectFilter.stories.tsx +++ b/site/src/components/Filter/SelectFilter.stories.tsx @@ -47,7 +47,7 @@ export default meta; type Story = StoryObj; export const Closed: Story = { - play: undefined, + play: () => {}, }; export const Open: Story = {};