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 = {}; pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy