From 75df8fc18bcc79ad947dfb17d5984d4bbec14d46 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 8 Sep 2025 13:58:20 +0200 Subject: [PATCH] chore: Migrate `FilterOptions` component to new primitives (#10127) * chore: Migrate FilterOptions component to new primitives * alignemnt --- app/components/FilterOptions.tsx | 127 ++++++++------------- app/components/Menu/ContextMenu.tsx | 2 +- app/components/Menu/DropdownMenu.tsx | 2 +- shared/i18n/locales/en_US/translation.json | 1 + 4 files changed, 52 insertions(+), 80 deletions(-) diff --git a/app/components/FilterOptions.tsx b/app/components/FilterOptions.tsx index d621c0710f..e907c66cfb 100644 --- a/app/components/FilterOptions.tsx +++ b/app/components/FilterOptions.tsx @@ -1,22 +1,19 @@ import deburr from "lodash/deburr"; import * as React from "react"; import { useTranslation } from "react-i18next"; -import { MenuButton } from "reakit/Menu"; import styled from "styled-components"; import { s } from "@shared/styles"; import type { FetchPageParams } from "~/stores/base/Store"; import Button, { Inner } from "~/components/Button"; -import ContextMenu from "~/components/ContextMenu"; -import MenuItem from "~/components/ContextMenu/MenuItem"; import Text from "~/components/Text"; -import { useMenuState } from "~/hooks/useMenuState"; import Input, { NativeInput, Outline } from "./Input"; import PaginatedList, { PaginatedItem } from "./PaginatedList"; +import { MenuProvider } from "./primitives/Menu/MenuContext"; +import { Menu, MenuContent, MenuTrigger, MenuButton } from "./primitives/Menu"; interface TFilterOption extends PaginatedItem { key: string; label: string; - note?: string; icon?: React.ReactNode; } @@ -34,19 +31,17 @@ type Props = { const FilterOptions = ({ options, selectedKeys = [], - defaultLabel = "Filter options", className, onSelect, showFilter, fetchQuery, fetchQueryOptions, + ...rest }: Props) => { const { t } = useTranslation(); const searchInputRef = React.useRef(null); const listRef = React.useRef(null); - const menu = useMenuState({ - modal: false, - }); + const [open, setOpen] = React.useState(false); const selectedItems = options.filter((option) => selectedKeys.includes(option.key) ); @@ -58,32 +53,26 @@ const FilterOptions = ({ const renderItem = React.useCallback( (option) => ( - { onSelect(option.key); - menu.hide(); + setOpen(false); }} selected={selectedKeys.includes(option.key)} - {...menu} - > - {option.icon} - {option.note ? ( - - {option.label} - {option.note} - - ) : ( - option.label - )} - + /> ), - [menu, onSelect, selectedKeys] + [onSelect, selectedKeys] ); - const handleFilter = (ev: React.ChangeEvent) => { - setQuery(ev.target.value); - }; + const handleFilter = React.useCallback( + (ev: React.ChangeEvent) => { + setQuery(ev.target.value); + }, + [] + ); const filteredOptions = React.useMemo(() => { const normalizedQuery = deburr(query.toLowerCase()); @@ -121,13 +110,13 @@ const FilterOptions = ({ switch (ev.key) { case "Escape": - menu.hide(); + setOpen(false); break; case "Enter": if (filteredOptions.length === 1) { ev.preventDefault(); onSelect(filteredOptions[0].key); - menu.hide(); + setOpen(false); } break; case "ArrowDown": @@ -138,7 +127,7 @@ const FilterOptions = ({ break; } }, - [filteredOptions, menu, onSelect] + [filteredOptions, onSelect] ); const handleEscapeFromList = React.useCallback((ev: React.KeyboardEvent) => { @@ -150,21 +139,21 @@ const FilterOptions = ({ }, []); React.useEffect(() => { - if (menu.visible) { + if (open) { searchInputRef.current?.focus(); } else { setQuery(""); } - }, [menu.visible]); + }, [open]); const showFilterInput = showFilter || options.length > 10; + const defaultLabel = rest.defaultLabel || t("Filter options"); return ( - <> - - {(props) => ( + + + {selectedItems.length ? selectedLabel : defaultLabel} - )} - - - - listRef={listRef} - options={{ query, ...fetchQueryOptions }} - items={filteredOptions} - fetch={fetchQuery} - renderItem={renderItem} - onEscape={handleEscapeFromList} - heading={showFilterInput ? : undefined} - empty={} - /> - {showFilterInput && ( - + + + listRef={listRef} + options={{ query, ...fetchQueryOptions }} + items={filteredOptions} + fetch={fetchQuery} + renderItem={renderItem} + onEscape={handleEscapeFromList} + heading={showFilterInput ? : undefined} + empty={} /> - )} - - + {showFilterInput && ( + + )} + + + ); }; @@ -242,24 +231,6 @@ const SearchInput = styled(Input)` } `; -const Note = styled(Text)` - display: block; - margin: 2px 0; - line-height: 1.2em; - font-size: 14px; - font-weight: 500; - color: ${s("textTertiary")}; -`; - -const LabelWithNote = styled.div` - font-weight: 500; - text-align: left; - - &:hover ${Note} { - color: ${(props) => props.theme.white50}; - } -`; - export const StyledButton = styled(Button)` box-shadow: none; text-transform: none; diff --git a/app/components/Menu/ContextMenu.tsx b/app/components/Menu/ContextMenu.tsx index 2571e1df47..6894a0e884 100644 --- a/app/components/Menu/ContextMenu.tsx +++ b/app/components/Menu/ContextMenu.tsx @@ -80,7 +80,7 @@ export const ContextMenu = observer( const content = toMenuItems(menuItems); return ( - + {children} + {children} diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index dd7bb4dcb4..216e36419a 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -286,6 +286,7 @@ "Including uploaded images and files in the exported data": "Including uploaded images and files in the exported data", "{{count}} more user": "{{count}} more user", "{{count}} more user_plural": "{{count}} more users", + "Filter options": "Filter options", "Filter": "Filter", "No results": "No results", "{{authorName}} created <3>": "{{authorName}} created <3>",