From e32fcbf6545aa95206687d102788c1fafaf0ee90 Mon Sep 17 00:00:00 2001 From: Donkoko <nbonev@duck.com> Date: Tue, 9 Apr 2024 18:09:25 +0300 Subject: [PATCH] reworking DynamicDropdown to instead use Popover and have updated mobile styling --- .../dynamic-dropdown/dynamic-dropdown.tsx | 318 ++++++++++-------- .../dynamic-select/dynamic-select.tsx | 7 +- 2 files changed, 176 insertions(+), 149 deletions(-) diff --git a/app/components/dynamic-dropdown/dynamic-dropdown.tsx b/app/components/dynamic-dropdown/dynamic-dropdown.tsx index ca3544a3..1828ebfc 100644 --- a/app/components/dynamic-dropdown/dynamic-dropdown.tsx +++ b/app/components/dynamic-dropdown/dynamic-dropdown.tsx @@ -1,5 +1,11 @@ -import { cloneElement } from "react"; +import { cloneElement, useState } from "react"; import { ChevronDownIcon } from "@radix-ui/react-icons"; +import { + Popover, + PopoverContent, + PopoverPortal, + PopoverTrigger, +} from "@radix-ui/react-popover"; import { useNavigation } from "@remix-run/react"; import { useModelFilters } from "~/hooks/use-model-filters"; import type { @@ -8,14 +14,11 @@ import type { } from "~/hooks/use-model-filters"; import { isFormProcessing, tw } from "~/utils"; import { EmptyState } from "./empty-state"; +import { MobileStyles } from "../dynamic-select/dynamic-select"; import Input from "../forms/input"; import { CheckIcon } from "../icons"; import { Button } from "../shared"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from "../shared/dropdown"; + import type { Icon } from "../shared/icons-map"; import { Spinner } from "../shared/spinner"; import When from "../when/when"; @@ -44,6 +47,7 @@ export default function DynamicDropdown({ }: Props) { const navigation = useNavigation(); const isSearching = isFormProcessing(navigation.state); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const { selectedItems, @@ -63,155 +67,173 @@ export default function DynamicDropdown({ }); return ( - <DropdownMenu modal={false}> - <DropdownMenuTrigger - className="inline-flex items-center gap-2 text-gray-500" - asChild - > - <div> - {cloneElement(trigger)} - <When truthy={selectedItems.length > 0}> - <div className="flex size-6 items-center justify-center rounded-full bg-primary-50 px-2 py-[2px] text-xs font-medium text-primary-700"> - {selectedItems.length} - </div> - </When> - </div> - </DropdownMenuTrigger> + <div className="relative w-full text-center"> + <MobileStyles open={isPopoverOpen} /> - <DropdownMenuContent - align="end" - className={tw( - "w-[290px] overflow-y-hidden p-0 md:w-[350px]", - className - )} - style={style} - > - <div className="flex items-center justify-between p-3"> - <div className="text-xs font-semibold text-gray-700">{label}</div> - <When truthy={selectedItems.length > 0 && showSearch}> - <Button - as="button" - variant="link" - className="whitespace-nowrap text-xs font-normal text-gray-500 hover:text-gray-600" - onClick={clearFilters} - > - Clear filter - </Button> - </When> - </div> - - <When truthy={showSearch}> - <div className="filters-form relative border-y border-y-gray-200 p-3"> - <Input - type="text" - label={`Search ${label}`} - placeholder={`Search ${label}`} - hideLabel - className="text-gray-500" - icon={searchIcon} - autoFocus - value={searchQuery} - onChange={handleSearchQueryChange} - /> - <When truthy={Boolean(searchQuery)}> - <Button - icon="x" - variant="tertiary" - disabled={Boolean(searchQuery)} - onClick={() => { - resetModelFiltersFetcher(); - setSearchQuery(""); - }} - className="z-100 pointer-events-auto absolute right-[14px] top-0 mr-2 h-full border-0 p-0 text-center text-gray-400 hover:text-gray-900" - /> + <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> + <PopoverTrigger + className="inline-flex items-center gap-2 text-gray-500" + asChild + > + <div> + {cloneElement(trigger)} + <When truthy={selectedItems.length > 0}> + <div className="flex size-6 items-center justify-center rounded-full bg-primary-50 px-2 py-[2px] text-xs font-medium text-primary-700"> + {selectedItems.length} + </div> </When> </div> - </When> - - <div className="max-h-[320px] divide-y overflow-y-auto"> - {searchQuery !== "" && items.length === 0 && ( - <EmptyState searchQuery={searchQuery} modelName={model.name} /> - )} - {items.map((item) => { - const checked = selectedItems.includes(item.id); - if (typeof renderItem === "function") { - return ( - <label - key={item.id} - htmlFor={item.id} - className={tw( - "flex cursor-pointer select-none items-center justify-between px-6 py-4 text-sm font-medium outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-gray-100 focus:bg-gray-100", - checked && "bg-gray-50" - )} + </PopoverTrigger> + <PopoverPortal> + <PopoverContent + align="end" + className={tw( + "z-[100] overflow-y-auto rounded-md border border-gray-300 bg-white p-0", + className + )} + style={style} + > + <div className="flex items-center justify-between p-3"> + <div className="text-xs font-semibold text-gray-700">{label}</div> + <When truthy={selectedItems.length > 0 && showSearch}> + <Button + as="button" + variant="link" + className="whitespace-nowrap text-xs font-normal text-gray-500 hover:text-gray-600" + onClick={clearFilters} > - {renderItem({ ...item, metadata: item })} - <input - id={item.id} - type="checkbox" - value={item.id} - className="hidden" - checked={checked} - onChange={(e) => { - handleSelectItemChange(e.currentTarget.value); - }} - /> - <When truthy={checked}> - <CheckIcon className="text-primary" /> - </When> - </label> - ); - } + Clear filter + </Button> + </When> + </div> - return ( - <label - key={item.id} - htmlFor={item.id} - className={tw( - "flex cursor-pointer select-none items-center justify-between px-6 py-4 text-sm font-medium outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-gray-100 focus:bg-gray-100", - checked && "bg-gray-50" - )} - > - {item.name} - <input - id={item.id} - type="checkbox" - value={item.id} - className="hidden" - checked={checked} - onChange={(e) => { - handleSelectItemChange(e.currentTarget.value); - }} + <When truthy={showSearch}> + <div className="filters-form relative border-y border-y-gray-200 p-3"> + <Input + type="text" + label={`Search ${label}`} + placeholder={`Search ${label}`} + hideLabel + className="text-gray-500" + icon={searchIcon} + autoFocus + value={searchQuery} + onChange={handleSearchQueryChange} /> - <When truthy={checked}> - <CheckIcon className="text-primary" /> + <When truthy={Boolean(searchQuery)}> + <Button + icon="x" + variant="tertiary" + disabled={Boolean(searchQuery)} + onClick={() => { + resetModelFiltersFetcher(); + setSearchQuery(""); + }} + className="z-100 pointer-events-auto absolute right-[14px] top-0 mr-2 h-full border-0 p-0 text-center text-gray-400 hover:text-gray-900" + /> </When> - </label> - ); - })} + </div> + </When> - {items.length < totalItems && searchQuery === "" && ( - <button - disabled={isSearching} - onClick={getAllEntries} - className="flex w-full cursor-pointer select-none items-center justify-between px-6 py-3 text-sm font-medium text-gray-600 outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-gray-100 focus:bg-gray-100" - > - Show all - <span> - {isSearching ? ( - <Spinner className="size-4" /> - ) : ( - <ChevronDownIcon className="size-4" /> - )} - </span> - </button> - )} - </div> - <When truthy={totalItems > 6}> - <div className="border-t p-3 text-gray-500"> - Showing {items.length} out of {totalItems}, type to search for more - </div> - </When> - </DropdownMenuContent> - </DropdownMenu> + <div className="max-h-[320px] divide-y overflow-y-auto"> + {searchQuery !== "" && items.length === 0 && ( + <EmptyState searchQuery={searchQuery} modelName={model.name} /> + )} + {items.map((item) => { + const checked = selectedItems.includes(item.id); + if (typeof renderItem === "function") { + return ( + <label + key={item.id} + htmlFor={item.id} + className={tw( + "flex cursor-pointer select-none items-center justify-between px-6 py-4 text-sm font-medium outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-gray-100 focus:bg-gray-100", + checked && "bg-gray-50" + )} + > + {renderItem({ ...item, metadata: item })} + <input + id={item.id} + type="checkbox" + value={item.id} + className="hidden" + checked={checked} + onChange={(e) => { + handleSelectItemChange(e.currentTarget.value); + }} + /> + <When truthy={checked}> + <CheckIcon className="text-primary" /> + </When> + </label> + ); + } + + return ( + <label + key={item.id} + htmlFor={item.id} + className={tw( + "flex cursor-pointer select-none items-center justify-between px-6 py-4 text-sm font-medium outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-gray-100 focus:bg-gray-100", + checked && "bg-gray-50" + )} + > + {item.name} + <input + id={item.id} + type="checkbox" + value={item.id} + className="hidden" + checked={checked} + onChange={(e) => { + handleSelectItemChange(e.currentTarget.value); + }} + /> + <When truthy={checked}> + <CheckIcon className="text-primary" /> + </When> + </label> + ); + })} + + {items.length < totalItems && searchQuery === "" && ( + <button + disabled={isSearching} + onClick={getAllEntries} + className="flex w-full cursor-pointer select-none items-center justify-between px-6 py-3 text-sm font-medium text-gray-600 outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-gray-100 focus:bg-gray-100" + > + Show all + <span> + {isSearching ? ( + <Spinner className="size-4" /> + ) : ( + <ChevronDownIcon className="size-4" /> + )} + </span> + </button> + )} + </div> + <When truthy={totalItems > 6}> + <div className="border-t p-3 text-gray-500"> + Showing {items.length} out of {totalItems}, type to search for + more + </div> + </When> + + <div className="flex justify-between gap-3 border-t p-3 md:hidden"> + <Button + onClick={() => { + setIsPopoverOpen(false); + }} + variant="secondary" + width="full" + > + Done + </Button> + </div> + </PopoverContent> + </PopoverPortal> + </Popover> + </div> ); } diff --git a/app/components/dynamic-select/dynamic-select.tsx b/app/components/dynamic-select/dynamic-select.tsx index e7167810..44f82ac8 100644 --- a/app/components/dynamic-select/dynamic-select.tsx +++ b/app/components/dynamic-select/dynamic-select.tsx @@ -234,7 +234,7 @@ export default function DynamicSelect({ ); } -const MobileStyles = ({ open }: { open: boolean }) => +export const MobileStyles = ({ open }: { open: boolean }) => open && ( <> <div @@ -256,7 +256,12 @@ const MobileStyles = ({ open }: { open: boolean }) => top: 20px !important; left: 50% !important; transform: translate(-50%, 0) !important; + width: calc(100% - 40px) !important; } + [data-radix-popper-content-wrapper] > div { + width: 100% !important; + } + }`, }} // is a hack to fix the dropdown menu not being in the right place on mobile // can not target [data-radix-popper-content-wrapper] for this file only with css