From eec7c66ab6254bc0f29932205f87705853ab79f2 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Sat, 27 Jul 2024 11:09:43 +0200 Subject: [PATCH 01/13] initial lang picker migration and refactor to shadcn --- package.json | 2 + src/components/LanguagePicker/MenuItem.tsx | 115 +++----- .../LanguagePicker/MobileCloseBar.tsx | 16 +- .../LanguagePicker/NoResultsCallout.tsx | 43 ++- src/components/LanguagePicker/ProgressBar.tsx | 3 + src/components/LanguagePicker/index.tsx | 271 ++++++------------ .../LanguagePicker/useLanguagePicker.tsx | 38 +-- src/components/Nav/Desktop/index.tsx | 59 +--- src/components/Nav/Mobile/MenuFooter.tsx | 28 +- src/components/ui/command.tsx | 157 ++++++++++ src/components/ui/dialog.tsx | 120 ++++++++ src/components/ui/popover.tsx | 2 +- src/hooks/useDisclosure.ts | 4 +- tailwind.config.ts | 15 + yarn.lock | 30 +- 15 files changed, 515 insertions(+), 388 deletions(-) create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/dialog.tsx diff --git a/package.json b/package.json index 2d3be73046e..7b52e5db518 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@emotion/styled": "^11.11.0", "@hookform/resolvers": "^3.8.0", "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", @@ -43,6 +44,7 @@ "chartjs-plugin-datalabels": "^2.2.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "embla-carousel-react": "^7.0.0", "ethereum-blockies-base64": "^1.0.2", "framer-motion": "^10.13.0", diff --git a/src/components/LanguagePicker/MenuItem.tsx b/src/components/LanguagePicker/MenuItem.tsx index c9f7a99e455..3e2116eec28 100644 --- a/src/components/LanguagePicker/MenuItem.tsx +++ b/src/components/LanguagePicker/MenuItem.tsx @@ -1,28 +1,22 @@ +import { ComponentPropsWithoutRef } from "react" import { useRouter } from "next/router" import { useTranslation } from "next-i18next" import { BsCheck } from "react-icons/bs" -import { - Badge, - Box, - Flex, - forwardRef, - Icon, - MenuItem as ChakraMenuItem, - type MenuItemProps as ChakraMenuItemProps, - Text, -} from "@chakra-ui/react" +import { Badge } from "@chakra-ui/react" import type { LocaleDisplayInfo } from "@/lib/types" -import { BaseLink } from "@/components/Link" +import { cn } from "@/lib/utils/cn" + +import { CommandItem } from "../ui/command" import ProgressBar from "./ProgressBar" -type ItemProps = ChakraMenuItemProps & { +type ItemProps = ComponentPropsWithoutRef & { displayInfo: LocaleDisplayInfo } -const MenuItem = forwardRef(({ displayInfo, ...props }: ItemProps, ref) => { +const MenuItem = ({ displayInfo, ...props }: ItemProps) => { const { localeOption, sourceName, @@ -32,7 +26,7 @@ const MenuItem = forwardRef(({ displayInfo, ...props }: ItemProps, ref) => { isBrowserDefault, } = displayInfo const { t } = useTranslation("common") - const { asPath, locale } = useRouter() + const { locale } = useRouter() const isCurrent = localeOption === locale const getProgressInfo = (approvalProgress: number, wordsApproved: number) => { @@ -48,51 +42,37 @@ const MenuItem = forwardRef(({ displayInfo, ...props }: ItemProps, ref) => { const { progress, words } = getProgressInfo(approvalProgress, wordsApproved) return ( - { - e.target.scrollIntoView({ block: "nearest" }) - }} - scrollMarginY="8" - _hover={{ - bg: "primary.lowContrast", - textDecoration: "none", - "p.language-name": { color: "primary.base" }, - }} - _focus={{ bg: "primary.lowContrast" }} - sx={{ - p: { - textDecoration: "none", - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - }, - }} - href={asPath} - locale={localeOption} + - - - - +
+
+

{targetName} - +

+ {/* TODO */} {isBrowserDefault && ( { {t("page-languages-browser-default")} )} - - - {sourceName} - - +
+

{sourceName}

+
{isCurrent && ( - + )} -
- + +

{progress} {t("page-languages-translated")} • {words}{" "} {t("page-languages-words")} - +

-
+ ) -}) +} export default MenuItem diff --git a/src/components/LanguagePicker/MobileCloseBar.tsx b/src/components/LanguagePicker/MobileCloseBar.tsx index c54bba831ef..6238aadfed2 100644 --- a/src/components/LanguagePicker/MobileCloseBar.tsx +++ b/src/components/LanguagePicker/MobileCloseBar.tsx @@ -1,8 +1,7 @@ import { MouseEventHandler } from "react" import { useTranslation } from "next-i18next" -import { Flex } from "@chakra-ui/react" -import { Button } from "@/components/Buttons" +import { Button } from "../../../tailwind/ui/buttons/Button" type MobileCloseBarProps = { handleClick: MouseEventHandler @@ -12,17 +11,10 @@ export const MobileCloseBar = ({ handleClick }: MobileCloseBarProps) => { const { t } = useTranslation() return ( - - - + ) } diff --git a/src/components/LanguagePicker/NoResultsCallout.tsx b/src/components/LanguagePicker/NoResultsCallout.tsx index 2478d55c38f..69b3dc767f2 100644 --- a/src/components/LanguagePicker/NoResultsCallout.tsx +++ b/src/components/LanguagePicker/NoResultsCallout.tsx @@ -1,5 +1,4 @@ import { useTranslation } from "next-i18next" -import { FormHelperText, forwardRef, Text } from "@chakra-ui/react" import { BaseLink } from "@/components/Link" @@ -7,27 +6,25 @@ import MenuItem from "./MenuItem" type NoResultsCalloutProps = { onClose: () => void } -const NoResultsCallout = forwardRef( - ({ onClose }: NoResultsCalloutProps, ref) => { - const { t } = useTranslation("common") - return ( - - - {t("page-languages-want-more-header")} - - {t("page-languages-want-more-paragraph")}{" "} - - {t("page-languages-want-more-link")} - - - ) - } -) +const NoResultsCallout = ({ onClose }: NoResultsCalloutProps) => { + const { t } = useTranslation("common") + return ( +
+

{t("page-languages-want-more-header")}

+

+ {t("page-languages-want-more-paragraph")} +

+ {/* TODO: use migrated Link component */} + + {t("page-languages-want-more-link")} + +
+ ) +} export default NoResultsCallout diff --git a/src/components/LanguagePicker/ProgressBar.tsx b/src/components/LanguagePicker/ProgressBar.tsx index 06bb008923f..1ea42523757 100644 --- a/src/components/LanguagePicker/ProgressBar.tsx +++ b/src/components/LanguagePicker/ProgressBar.tsx @@ -2,6 +2,9 @@ import { Progress, ProgressProps } from "@chakra-ui/react" type ProgressBarProps = Pick +{ + /* TODO migrate */ +} const ProgressBar = ({ value }: ProgressBarProps) => ( & { +type LanguagePickerProps = { children: React.ReactNode - placement?: MenuProps["placement"] + className?: string handleClose?: () => void - menuState?: UseDisclosureReturn } const LanguagePicker = ({ children, - placement, handleClose, - menuState, - ...props + className, }: LanguagePickerProps) => { + const { asPath, push } = useRouter() const { t, - refs, disclosure, - filterValue, - setFilterValue, filteredNames, - handleInputFocus, - } = useLanguagePicker(handleClose, menuState) - const { inputRef, firstItemRef, noResultsRef, footerRef } = refs - const { onClose } = disclosure + // TODO: Implement this + // handleInputFocus, + } = useLanguagePicker(handleClose) + const { isOpen, setValue, onClose } = disclosure /** * Adds a keydown event listener to focus filter input (\). * @param {string} event - The keydown event. */ - useEventListener("keydown", (e) => { - if (e.key !== "\\") return - e.preventDefault() - inputRef.current?.focus() - }) + // useEventListener("keydown", (e) => { + // if (e.key !== "\\") return + // e.preventDefault() + // inputRef.current?.focus() + // }) // onClick handlers const handleMobileCloseBarClick = () => onClose() - const handleMenuItemClose = (displayInfo: LocaleDisplayInfo) => + const handleMenuItemSelect = (currentValue: string) => { + push(asPath, asPath, { + locale: currentValue, + }) onClose({ eventAction: "Locale chosen", - eventName: displayInfo.localeOption, + eventName: currentValue, }) + } const handleBaseLinkClose = () => onClose({ eventAction: "Translation program link (menu footer)", eventName: "/contributing/translation-program", }) - const { locale } = useRouter() - const isRtl = isLangRightToLeft(locale! as Lang) - return ( - - {children} - { - if (e.key === "Tab" || e.key === "\\") { - e.preventDefault() - ;(e.shiftKey ? inputRef : footerRef).current?.focus() - } - }} - {...props} + + {children} + {/* Mobile Close bar */} {/* avoid rendering mobile only feature on desktop */} @@ -104,113 +83,44 @@ const LanguagePicker = ({ )} - {/* Main Language selection menu */} - { + const item = filteredNames.find( + (name) => name.localeOption === value + ) + + if (!item) return 0 + + const { localeOption, sourceName, targetName, englishName } = item + + if ( + (localeOption + sourceName + targetName + englishName) + .toLowerCase() + .includes(search.toLowerCase()) + ) { + return 1 + } + + return 0 + }} > - - - {t("page-languages-filter-label")}{" "} - - ({filteredNames.length} {t("common:languages")}) - - - - setFilterValue(e.target.value)} - onBlur={(e) => { - if (e.relatedTarget?.tagName.toLowerCase() === "div") { - e.currentTarget.focus() - } - }} - ref={inputRef} - h="8" - mt="1" - mb="2" - bg="background.base" - color="body.base" - sx={isRtl ? { pl: 10, pr: 2 } : {}} - onKeyDown={(e) => { - // Navigate to first result on enter - if (e.key === "Enter") { - e.preventDefault() - firstItemRef.current?.click() - } - // If Tab/ArrowDown, focus on first item if available, NoResults link otherwise - if (e.key === "Tab" || e.key === "ArrowDown") { - e.preventDefault() - ;(filteredNames.length === 0 - ? noResultsRef - : firstItemRef - ).current?.focus() - e.stopPropagation() - } - }} - onFocus={handleInputFocus} - /> - {isRtl ? ( - - - / - - - ) : ( - - - \ - - - )} - - - {filteredNames.map((displayInfo, index) => ( - { - if (e.key !== "\\") return - e.preventDefault() - inputRef.current?.focus() - }} - onClick={() => handleMenuItemClose(displayInfo)} - /> - ))} +
+ {t("page-languages-filter-label")}{" "} + + ({filteredNames.length} {t("common:languages")}) + +
+ + - {filteredNames.length === 0 && ( + + onClose({ eventAction: "Translation program link (no results)", @@ -218,33 +128,34 @@ const LanguagePicker = ({ }) } /> - )} -
-
+ + + {filteredNames.map((displayInfo) => ( + + ))} + + + {/* Footer callout */} - - +
+

{t("page-languages-recruit-community")}{" "} + {/* TODO migrate once #13411 is merged */} {t("common:learn-more")} - - - -

+

+ + + ) } diff --git a/src/components/LanguagePicker/useLanguagePicker.tsx b/src/components/LanguagePicker/useLanguagePicker.tsx index b0cf925d43b..d1215b20321 100644 --- a/src/components/LanguagePicker/useLanguagePicker.tsx +++ b/src/components/LanguagePicker/useLanguagePicker.tsx @@ -1,7 +1,6 @@ -import { useEffect, useRef, useState } from "react" +import { useEffect, useState } from "react" import { useRouter } from "next/router" import { useTranslation } from "next-i18next" -import { useDisclosure, type UseDisclosureReturn } from "@chakra-ui/react" import type { I18nLocale, @@ -17,21 +16,13 @@ import progressDataJson from "@/data/translationProgress.json" import { DEFAULT_LOCALE } from "@/lib/constants" +import useDisclosure from "@/hooks/useDisclosure" + const progressData = progressDataJson satisfies ProjectProgressData[] -export const useLanguagePicker = ( - handleClose?: () => void, - menuState?: UseDisclosureReturn -) => { +export const useLanguagePicker = (handleClose?: () => void) => { const { t } = useTranslation("common") const { locale, locales: rawLocales } = useRouter() - const refs = { - inputRef: useRef(null), - firstItemRef: useRef(null), - noResultsRef: useRef(null), - footerRef: useRef(null), - } - const [filterValue, setFilterValue] = useState("") const [filteredNames, setFilteredNames] = useState([]) @@ -161,17 +152,10 @@ export const useLanguagePicker = ( return b.approvalProgress - a.approvalProgress }) || [] - setFilteredNames( - displayNames.filter( - ({ localeOption, sourceName, targetName, englishName }) => - (localeOption + sourceName + targetName + englishName) - .toLowerCase() - .includes(filterValue.toLowerCase()) - ) - ) - }, [filterValue, locale, rawLocales, t]) + setFilteredNames(displayNames) + }, [locale, rawLocales, t]) - const { isOpen, ...menu } = useDisclosure() + const { isOpen, setValue, ...menu } = useDisclosure() const eventBase: Pick = { eventCategory: `Language picker`, @@ -180,7 +164,6 @@ export const useLanguagePicker = ( const onOpen = () => { menu.onOpen() - menuState?.onOpen() trackCustomEvent({ ...eventBase, eventName: "Opened", @@ -195,10 +178,8 @@ export const useLanguagePicker = ( customMatomoEvent?: Required> & Partial ): void => { - setFilterValue("") handleClose && handleClose() menu.onClose() - menuState?.onClose() trackCustomEvent( (customMatomoEvent ? { ...eventBase, ...customMatomoEvent } @@ -223,10 +204,7 @@ export const useLanguagePicker = ( return { t, - refs, - disclosure: { isOpen, onOpen, onClose }, - filterValue, - setFilterValue, + disclosure: { isOpen, setValue, onOpen, onClose }, filteredNames, handleInputFocus, } diff --git a/src/components/Nav/Desktop/index.tsx b/src/components/Nav/Desktop/index.tsx index d4cb0a59587..911b1d3c659 100644 --- a/src/components/Nav/Desktop/index.tsx +++ b/src/components/Nav/Desktop/index.tsx @@ -3,22 +3,15 @@ import { useRouter } from "next/router" import { useTranslation } from "next-i18next" import { BsTranslate } from "react-icons/bs" import { MdBrightness2, MdWbSunny } from "react-icons/md" -import { - Button, - HStack, - Icon, - MenuButton, - Text, - useColorModeValue, - useDisclosure, - useEventListener, -} from "@chakra-ui/react" +import { HStack, useColorModeValue, useEventListener } from "@chakra-ui/react" import { IconButton } from "@/components/Buttons" import LanguagePicker from "@/components/LanguagePicker" import { DESKTOP_LANGUAGE_BUTTON_NAME } from "@/lib/constants" +import { Button } from "../../../../tailwind/ui/buttons/Button" + type DesktopNavMenuProps = { toggleColorMode: () => void } @@ -26,7 +19,6 @@ type DesktopNavMenuProps = { const DesktopNavMenu = ({ toggleColorMode }: DesktopNavMenuProps) => { const { t } = useTranslation("common") const { locale } = useRouter() - const languagePickerState = useDisclosure() const languagePickerRef = useRef(null) const ThemeIcon = useColorModeValue(, ) @@ -54,8 +46,9 @@ const DesktopNavMenu = ({ toggleColorMode }: DesktopNavMenuProps) => { if (e.metaKey || e.ctrlKey) { toggleColorMode() } else { - if (languagePickerState.isOpen) return - languagePickerRef.current?.click() + // TODO add this to the language picker + // if (languagePickerState.isOpen) return + // languagePickerRef.current?.click() } }) @@ -74,46 +67,20 @@ const DesktopNavMenu = ({ toggleColorMode }: DesktopNavMenuProps) => { {/* Locale-picker menu */} - - - + + {t("common:languages")}  - + {locale!.toUpperCase()} - + ) diff --git a/src/components/Nav/Mobile/MenuFooter.tsx b/src/components/Nav/Mobile/MenuFooter.tsx index da537b82d81..acbe645064f 100644 --- a/src/components/Nav/Mobile/MenuFooter.tsx +++ b/src/components/Nav/Mobile/MenuFooter.tsx @@ -1,12 +1,7 @@ import { useTranslation } from "next-i18next" import { BsTranslate } from "react-icons/bs" import { MdBrightness2, MdSearch, MdWbSunny } from "react-icons/md" -import { - DrawerFooter, - Grid, - MenuButton, - useColorModeValue, -} from "@chakra-ui/react" +import { DrawerFooter, Grid, useColorModeValue } from "@chakra-ui/react" import LanguagePicker from "@/components/LanguagePicker" @@ -57,26 +52,13 @@ const MenuFooter = ({ {t(themeLabelKey)} - + {/* TODO migrate once #13449 is merged */} + {t("languages")} - + diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 00000000000..5e165866111 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,157 @@ +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" +import { type DialogProps } from "@radix-ui/react-dialog" + +import { Dialog, DialogContent } from "@/components/ui/dialog" + +import { cn } from "@/lib/utils/cn" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 00000000000..947ea462c0b --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import { X } from "lucide-react" +import * as DialogPrimitive from "@radix-ui/react-dialog" + +import { cn } from "@/lib/utils/cn" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index ee8322f3345..e83b7de3107 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -17,7 +17,7 @@ const PopoverContent = React.forwardRef< align={align} sideOffset={sideOffset} className={cn( - "bg-popover text-popover-foreground z-50 w-72 rounded-md border p-4 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + "text-popover-foreground z-popover w-72 rounded-md border bg-background p-4 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className )} {...props} diff --git a/src/hooks/useDisclosure.ts b/src/hooks/useDisclosure.ts index 2bb5612be23..f5a8ce11d1b 100644 --- a/src/hooks/useDisclosure.ts +++ b/src/hooks/useDisclosure.ts @@ -5,13 +5,15 @@ import { useBoolean } from "usehooks-ts" * modal, dropdown, or any other component that can be opened and closed. */ function useDisclosure(defaultValue = false) { - const { value, setTrue, setFalse, toggle } = useBoolean(defaultValue) + const { value, setTrue, setFalse, toggle, setValue } = + useBoolean(defaultValue) return { isOpen: value, onOpen: setTrue, onClose: setFalse, onToggle: toggle, + setValue, } } diff --git a/tailwind.config.ts b/tailwind.config.ts index 5e6cb29630a..5e8ffb5ddd9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -37,6 +37,21 @@ const config = { sm: "1.5", base: "1.6", }, + zIndex: { + hide: "-1", + auto: "auto", + base: "0", + docked: "10", + dropdown: "1000", + sticky: "1100", + banner: "1200", + overlay: "1300", + modal: "1400", + popover: "1500", + skipLink: "1600", + toast: "1700", + tooltip: "1800", + }, colors: { primary: { DEFAULT: "var(--primary)", diff --git a/yarn.lock b/yarn.lock index 4790756573b..93346cef5a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3837,7 +3837,7 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== -"@radix-ui/react-dialog@^1.0.5": +"@radix-ui/react-dialog@1.0.5", "@radix-ui/react-dialog@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== @@ -3858,6 +3858,26 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.5" +"@radix-ui/react-dialog@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz#4906507f7b4ad31e22d7dad69d9330c87c431d44" + integrity sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.7" + "@radix-ui/react-direction@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" @@ -7037,6 +7057,14 @@ clsx@^2.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== +cmdk@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.0.0.tgz#0a095fdafca3dfabed82d1db78a6262fb163ded9" + integrity sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q== + dependencies: + "@radix-ui/react-dialog" "1.0.5" + "@radix-ui/react-primitive" "1.0.3" + collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" From 895203521c02ebb2050a6057a93e67d97a970fc6 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Sun, 28 Jul 2024 11:47:10 +0200 Subject: [PATCH 02/13] migrate progress bar --- package.json | 1 + src/components/LanguagePicker/ProgressBar.tsx | 21 ++++----------- src/components/ui/progress.tsx | 26 +++++++++++++++++++ yarn.lock | 8 ++++++ 4 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 src/components/ui/progress.tsx diff --git a/package.json b/package.json index 7b52e5db518..d2c3fab9209 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@socialgouv/matomo-next": "^1.8.0", "chart.js": "^4.4.2", diff --git a/src/components/LanguagePicker/ProgressBar.tsx b/src/components/LanguagePicker/ProgressBar.tsx index 1ea42523757..327ae272ba8 100644 --- a/src/components/LanguagePicker/ProgressBar.tsx +++ b/src/components/LanguagePicker/ProgressBar.tsx @@ -1,22 +1,11 @@ -import { Progress, ProgressProps } from "@chakra-ui/react" +import { ComponentPropsWithoutRef } from "react" -type ProgressBarProps = Pick +import { Progress } from "../ui/progress" + +type ProgressBarProps = Pick, "value"> -{ - /* TODO migrate */ -} const ProgressBar = ({ value }: ProgressBarProps) => ( - + ) export default ProgressBar diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 00000000000..46c7aeca193 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils/cn" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/yarn.lock b/yarn.lock index 93346cef5a5..c80989d52e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4057,6 +4057,14 @@ dependencies: "@radix-ui/react-slot" "1.1.0" +"@radix-ui/react-progress@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2" + integrity sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg== + dependencies: + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" From 87ddb2fc73ccd7c4ba3b1e1a09f7c23f01456767 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Sun, 28 Jul 2024 12:20:10 +0200 Subject: [PATCH 03/13] migrate badge used in language picker --- src/components/LanguagePicker/MenuItem.tsx | 12 ++----- src/components/ui/badge.tsx | 40 ++++++++++++++++++++++ tailwind.config.ts | 1 + 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 src/components/ui/badge.tsx diff --git a/src/components/LanguagePicker/MenuItem.tsx b/src/components/LanguagePicker/MenuItem.tsx index 3e2116eec28..fc4873ca98e 100644 --- a/src/components/LanguagePicker/MenuItem.tsx +++ b/src/components/LanguagePicker/MenuItem.tsx @@ -2,12 +2,12 @@ import { ComponentPropsWithoutRef } from "react" import { useRouter } from "next/router" import { useTranslation } from "next-i18next" import { BsCheck } from "react-icons/bs" -import { Badge } from "@chakra-ui/react" import type { LocaleDisplayInfo } from "@/lib/types" import { cn } from "@/lib/utils/cn" +import { Badge } from "../ui/badge" import { CommandItem } from "../ui/command" import ProgressBar from "./ProgressBar" @@ -75,14 +75,8 @@ const MenuItem = ({ displayInfo, ...props }: ItemProps) => { {/* TODO */} {isBrowserDefault && ( {t("page-languages-browser-default")} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 00000000000..5d453ef21ca --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils/cn" + +/** + * TODO: finish migration. Needs to implement the DS badge styles. Currently is + * only used in the LanguagePicker component. + */ +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/tailwind.config.ts b/tailwind.config.ts index 5e8ffb5ddd9..f53ccbc9de7 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -26,6 +26,7 @@ const config = { md: ["1rem", "1.6"], // [md, base] sm: ["0.875rem", "1.6"], // [sm, base] xs: ["0.75rem", "1.6"], // [xs, base] + "2xs": ["0.625rem", "1.6"], // [2xs, base] }, lineHeight: { "6xs": "1.1", From 9d11e3ad9e7544924900bd790ef3cf1da5ea5bc8 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Sun, 28 Jul 2024 12:22:53 +0200 Subject: [PATCH 04/13] add event listener for shortcut to open the language picker --- src/components/LanguagePicker/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/LanguagePicker/index.tsx b/src/components/LanguagePicker/index.tsx index c3053f9f51c..373a85117b0 100644 --- a/src/components/LanguagePicker/index.tsx +++ b/src/components/LanguagePicker/index.tsx @@ -19,6 +19,8 @@ import { MobileCloseBar } from "./MobileCloseBar" import NoResultsCallout from "./NoResultsCallout" import { useLanguagePicker } from "./useLanguagePicker" +import { useEventListener } from "@/hooks/useEventListener" + type LanguagePickerProps = { children: React.ReactNode className?: string @@ -38,17 +40,17 @@ const LanguagePicker = ({ // TODO: Implement this // handleInputFocus, } = useLanguagePicker(handleClose) - const { isOpen, setValue, onClose } = disclosure + const { isOpen, setValue, onClose, onOpen } = disclosure /** * Adds a keydown event listener to focus filter input (\). * @param {string} event - The keydown event. */ - // useEventListener("keydown", (e) => { - // if (e.key !== "\\") return - // e.preventDefault() - // inputRef.current?.focus() - // }) + useEventListener("keydown", (e) => { + if (e.key !== "\\") return + e.preventDefault() + onOpen() + }) // onClick handlers const handleMobileCloseBarClick = () => onClose() From d2a6906d3ab1ac9b41beb4ba4f722d4412e8df3e Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Sun, 28 Jul 2024 12:31:04 +0200 Subject: [PATCH 05/13] cleanup --- src/components/LanguagePicker/MenuItem.tsx | 1 - src/components/LanguagePicker/index.tsx | 8 +------ .../LanguagePicker/useLanguagePicker.tsx | 24 ------------------- 3 files changed, 1 insertion(+), 32 deletions(-) diff --git a/src/components/LanguagePicker/MenuItem.tsx b/src/components/LanguagePicker/MenuItem.tsx index fc4873ca98e..a5acc4ef5a4 100644 --- a/src/components/LanguagePicker/MenuItem.tsx +++ b/src/components/LanguagePicker/MenuItem.tsx @@ -72,7 +72,6 @@ const MenuItem = ({ displayInfo, ...props }: ItemProps) => { > {targetName}

- {/* TODO */} {isBrowserDefault && ( { const { asPath, push } = useRouter() - const { - t, - disclosure, - filteredNames, - // TODO: Implement this - // handleInputFocus, - } = useLanguagePicker(handleClose) + const { t, disclosure, filteredNames } = useLanguagePicker(handleClose) const { isOpen, setValue, onClose, onOpen } = disclosure /** diff --git a/src/components/LanguagePicker/useLanguagePicker.tsx b/src/components/LanguagePicker/useLanguagePicker.tsx index d1215b20321..3750fa23fbd 100644 --- a/src/components/LanguagePicker/useLanguagePicker.tsx +++ b/src/components/LanguagePicker/useLanguagePicker.tsx @@ -26,14 +26,6 @@ export const useLanguagePicker = (handleClose?: () => void) => { const [filteredNames, setFilteredNames] = useState([]) - // Used to only send one matomo event for users who focus the filter input - const [hasFocusedInput, setHasFocusedInput] = useState(false) - - // Reset if user switches languages - useEffect(() => { - setHasFocusedInput(false) - }, [locale]) - // perform all the filtering and mapping when the filter value change useEffect(() => { const locales = filterRealLocales(rawLocales) @@ -187,25 +179,9 @@ export const useLanguagePicker = (handleClose?: () => void) => { ) } - /** - * Send Matomo event when user focuses in the filter input. - * Only send once per user per session per language - * @returns void - */ - const handleInputFocus = (): void => { - if (hasFocusedInput) return - trackCustomEvent({ - ...eventBase, - eventAction: "Filter input", - eventName: "Focused inside filter input", - }) - setHasFocusedInput(true) - } - return { t, disclosure: { isOpen, setValue, onOpen, onClose }, filteredNames, - handleInputFocus, } } From 9aa5d3f747fe858ffd3cd4ad47ed9b5df612b4b9 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Sun, 28 Jul 2024 19:01:48 +0200 Subject: [PATCH 06/13] refactor: move localeToDisplayInfo to its own function + remove useEffect for filtering --- src/components/LanguagePicker/index.tsx | 10 +- .../LanguagePicker/localeToDisplayInfo.ts | 95 ++++++++++++ .../LanguagePicker/useLanguagePicker.tsx | 135 ++++-------------- 3 files changed, 124 insertions(+), 116 deletions(-) create mode 100644 src/components/LanguagePicker/localeToDisplayInfo.ts diff --git a/src/components/LanguagePicker/index.tsx b/src/components/LanguagePicker/index.tsx index 91b482931ff..4fc9424f72b 100644 --- a/src/components/LanguagePicker/index.tsx +++ b/src/components/LanguagePicker/index.tsx @@ -33,7 +33,7 @@ const LanguagePicker = ({ className, }: LanguagePickerProps) => { const { asPath, push } = useRouter() - const { t, disclosure, filteredNames } = useLanguagePicker(handleClose) + const { t, disclosure, languages } = useLanguagePicker(handleClose) const { isOpen, setValue, onClose, onOpen } = disclosure /** @@ -82,9 +82,7 @@ const LanguagePicker = ({ { - const item = filteredNames.find( - (name) => name.localeOption === value - ) + const item = languages.find((name) => name.localeOption === value) if (!item) return 0 @@ -104,7 +102,7 @@ const LanguagePicker = ({
{t("page-languages-filter-label")}{" "} - ({filteredNames.length} {t("common:languages")}) + ({languages.length} {t("common:languages")})
@@ -126,7 +124,7 @@ const LanguagePicker = ({ /> - {filteredNames.map((displayInfo) => ( + {languages.map((displayInfo) => ( string +): LocaleDisplayInfo => { + const i18nItem: I18nLocale = languages[localeOption] + const englishName = i18nItem.name + + // Get "source" display name (Language choice displayed in language of current locale) + const intlSource = new Intl.DisplayNames([sourceLocale], { + type: "language", + }).of(localeOption) + // For languages that do not have an Intl display name, use English name as fallback + const fallbackSource = intlSource !== localeOption ? intlSource : englishName + const i18nKey = "language-" + localeOption.toLowerCase() + const i18nSource = t(i18nKey) // Falls back to English namespace if not found + + // If i18nSource (fetched from `language-{locale}` in current namespace) + // is not translated (output === englishName), or not available + // (output === i18nKey), use the Intl.DisplayNames result as fallback + const sourceName = [i18nKey, englishName].includes(i18nSource) + ? fallbackSource + : i18nSource + + // Get "target" display name (Language choice displayed in that language) + const fallbackTarget = new Intl.DisplayNames([localeOption], { + type: "language", + }).of(localeOption) + const i18nConfigTarget = i18nItem.localName + const targetName = i18nConfigTarget || fallbackTarget + + if (!sourceName || !targetName) { + console.warn("Missing language display name:", { + localeOption, + sourceName, + targetName, + }) + } + + // English will not have a dataItem + const dataItem = progressData.find( + ({ languageId }) => + i18nItem.crowdinCode.toLowerCase() === languageId.toLowerCase() + ) + + const approvalProgress = + localeOption === DEFAULT_LOCALE + ? 100 + : Math.floor((dataItem!.words.approved / dataItem!.words.total) * 100) || + 0 + + const returnData: Partial = { + localeOption, + sourceName: sourceName ?? localeOption, + targetName: targetName ?? localeOption, + englishName, + } + + if (progressData.length < 1) { + console.warn(`Missing translation progress data; check GitHub action`) + return { + ...returnData, + approvalProgress: 0, + wordsApproved: 0, + } as LocaleDisplayInfo + } + + const totalWords = progressData[0].words.total + + const wordsApproved = + localeOption === DEFAULT_LOCALE + ? totalWords || 0 + : dataItem?.words.approved || 0 + + return { + ...returnData, + approvalProgress, + wordsApproved, + } as LocaleDisplayInfo +} diff --git a/src/components/LanguagePicker/useLanguagePicker.tsx b/src/components/LanguagePicker/useLanguagePicker.tsx index 3750fa23fbd..b88942b2a3e 100644 --- a/src/components/LanguagePicker/useLanguagePicker.tsx +++ b/src/components/LanguagePicker/useLanguagePicker.tsx @@ -1,33 +1,21 @@ -import { useEffect, useState } from "react" +import { useMemo } from "react" import { useRouter } from "next/router" import { useTranslation } from "next-i18next" -import type { - I18nLocale, - Lang, - LocaleDisplayInfo, - ProjectProgressData, -} from "@/lib/types" +import type { Lang, LocaleDisplayInfo } from "@/lib/types" import { MatomoEventOptions, trackCustomEvent } from "@/lib/utils/matomo" -import { filterRealLocales, languages } from "@/lib/utils/translations" +import { filterRealLocales } from "@/lib/utils/translations" -import progressDataJson from "@/data/translationProgress.json" - -import { DEFAULT_LOCALE } from "@/lib/constants" +import { localeToDisplayInfo } from "./localeToDisplayInfo" import useDisclosure from "@/hooks/useDisclosure" -const progressData = progressDataJson satisfies ProjectProgressData[] - export const useLanguagePicker = (handleClose?: () => void) => { const { t } = useTranslation("common") const { locale, locales: rawLocales } = useRouter() - const [filteredNames, setFilteredNames] = useState([]) - - // perform all the filtering and mapping when the filter value change - useEffect(() => { + const languages = useMemo(() => { const locales = filterRealLocales(rawLocales) // Get the preferred languages for the users browser @@ -52,99 +40,26 @@ export const useLanguagePicker = (handleClose?: () => void) => { // Remove duplicate matches const browserLocales = Array.from(new Set(allBrowserLocales)) - const localeToDisplayInfo = (localeOption: Lang): LocaleDisplayInfo => { - const i18nItem: I18nLocale = languages[localeOption] - const englishName = i18nItem.name - - // Get "source" display name (Language choice displayed in language of current locale) - const intlSource = new Intl.DisplayNames([locale!], { - type: "language", - }).of(localeOption) - // For languages that do not have an Intl display name, use English name as fallback - const fallbackSource = - intlSource !== localeOption ? intlSource : englishName - const i18nKey = "language-" + localeOption.toLowerCase() - const i18nSource = t(i18nKey) // Falls back to English namespace if not found - - // If i18nSource (fetched from `language-{locale}` in current namespace) - // is not translated (output === englishName), or not available - // (output === i18nKey), use the Intl.DisplayNames result as fallback - const sourceName = [i18nKey, englishName].includes(i18nSource) - ? fallbackSource - : i18nSource - - // Get "target" display name (Language choice displayed in that language) - const fallbackTarget = new Intl.DisplayNames([localeOption], { - type: "language", - }).of(localeOption) - const i18nConfigTarget = i18nItem.localName - const targetName = i18nConfigTarget || fallbackTarget - - if (!sourceName || !targetName) { - console.warn("Missing language display name:", { - localeOption, - sourceName, - targetName, + return ( + (locales as Lang[]) + ?.map((localeOption) => { + const displayInfo = localeToDisplayInfo( + localeOption, + locale as Lang, + t + ) + const isBrowserDefault = browserLocales.includes(localeOption) + return { ...displayInfo, isBrowserDefault } }) - } - - // English will not have a dataItem - const dataItem = progressData.find( - ({ languageId }) => - i18nItem.crowdinCode.toLowerCase() === languageId.toLowerCase() - ) - - const approvalProgress = - localeOption === DEFAULT_LOCALE - ? 100 - : Math.floor( - (dataItem!.words.approved / dataItem!.words.total) * 100 - ) || 0 - - const isBrowserDefault = browserLocales.includes(localeOption) - - const returnData: Partial = { - localeOption, - sourceName: sourceName ?? localeOption, - targetName: targetName ?? localeOption, - englishName, - isBrowserDefault, - } - - if (progressData.length < 1) { - console.warn(`Missing translation progress data; check GitHub action`) - return { - ...returnData, - approvalProgress: 0, - wordsApproved: 0, - } as LocaleDisplayInfo - } - - const totalWords = progressData[0].words.total - - const wordsApproved = - localeOption === DEFAULT_LOCALE - ? totalWords || 0 - : dataItem?.words.approved || 0 - - return { - ...returnData, - approvalProgress, - wordsApproved, - } as LocaleDisplayInfo - } - - const displayNames: LocaleDisplayInfo[] = - (locales as Lang[])?.map(localeToDisplayInfo).sort((a, b) => { - const indexA = browserLocales.indexOf(a.localeOption as Lang) - const indexB = browserLocales.indexOf(b.localeOption as Lang) - if (indexA >= 0 && indexB >= 0) return indexA - indexB - if (indexA >= 0) return -1 - if (indexB >= 0) return 1 - return b.approvalProgress - a.approvalProgress - }) || [] - - setFilteredNames(displayNames) + .sort((a, b) => { + const indexA = browserLocales.indexOf(a.localeOption as Lang) + const indexB = browserLocales.indexOf(b.localeOption as Lang) + if (indexA >= 0 && indexB >= 0) return indexA - indexB + if (indexA >= 0) return -1 + if (indexB >= 0) return 1 + return b.approvalProgress - a.approvalProgress + }) || [] + ) }, [locale, rawLocales, t]) const { isOpen, setValue, ...menu } = useDisclosure() @@ -182,6 +97,6 @@ export const useLanguagePicker = (handleClose?: () => void) => { return { t, disclosure: { isOpen, setValue, onOpen, onClose }, - filteredNames, + languages, } } From 1f295bfe7ca79f8076c67a72b369574ef9199b54 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Mon, 29 Jul 2024 14:34:47 +0200 Subject: [PATCH 07/13] fix bad prop --- src/components/LanguagePicker/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/LanguagePicker/index.tsx b/src/components/LanguagePicker/index.tsx index 4fc9424f72b..595ec11408a 100644 --- a/src/components/LanguagePicker/index.tsx +++ b/src/components/LanguagePicker/index.tsx @@ -109,7 +109,6 @@ const LanguagePicker = ({ From 38dc84d7ed7348c9cb1aa4be3aa36f9341fb3da7 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Mon, 5 Aug 2024 18:27:21 +0200 Subject: [PATCH 08/13] on mobile, set the popover's width to fill all the vw space --- src/components/Nav/Mobile/MenuFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Nav/Mobile/MenuFooter.tsx b/src/components/Nav/Mobile/MenuFooter.tsx index acbe645064f..bde756e192a 100644 --- a/src/components/Nav/Mobile/MenuFooter.tsx +++ b/src/components/Nav/Mobile/MenuFooter.tsx @@ -52,7 +52,7 @@ const MenuFooter = ({ {t(themeLabelKey)} {/* TODO migrate once #13449 is merged */} From 225c9c84ff41e91e5a7b7d661e4a1a908a29c09d Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 8 Aug 2024 15:33:29 +0200 Subject: [PATCH 09/13] remove unused code --- src/components/LanguagePicker/MenuItem.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/LanguagePicker/MenuItem.tsx b/src/components/LanguagePicker/MenuItem.tsx index a5acc4ef5a4..50deb50f51e 100644 --- a/src/components/LanguagePicker/MenuItem.tsx +++ b/src/components/LanguagePicker/MenuItem.tsx @@ -51,15 +51,6 @@ const MenuItem = ({ displayInfo, ...props }: ItemProps) => { : "bg-transparent" )} {...props} - // TODO double check this - // sx={{ - // p: { - // textDecoration: "none", - // overflow: "hidden", - // textOverflow: "ellipsis", - // whiteSpace: "nowrap", - // }, - // }} >
From b67df3cb65f5110129f23361e349546f853565d3 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 8 Aug 2024 17:15:55 +0200 Subject: [PATCH 10/13] display kbd shortcut inside the search input field --- src/components/LanguagePicker/index.tsx | 1 + src/components/ui/command.tsx | 51 ++++++++++++++++--------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/components/LanguagePicker/index.tsx b/src/components/LanguagePicker/index.tsx index 595ec11408a..9b4447bef9e 100644 --- a/src/components/LanguagePicker/index.tsx +++ b/src/components/LanguagePicker/index.tsx @@ -109,6 +109,7 @@ const LanguagePicker = ({ diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 5e165866111..980d569a7ec 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -1,6 +1,6 @@ import * as React from "react" import { Command as CommandPrimitive } from "cmdk" -import { Search } from "lucide-react" +import { MdOutlineSearch } from "react-icons/md" import { type DialogProps } from "@radix-ui/react-dialog" import { Dialog, DialogContent } from "@/components/ui/dialog" @@ -36,25 +36,42 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { ) } +type CommandInputProps = React.ComponentPropsWithoutRef< + typeof CommandPrimitive.Input +> & { + icon?: React.ElementType + kbdShortcut?: string +} + const CommandInput = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -
- (({ className, icon = MdOutlineSearch, kbdShortcut, ...props }, ref) => { + const Icon = icon + return ( +
+ + {kbdShortcut && ( + + {kbdShortcut} + )} - {...props} - /> - -
-)) + {!kbdShortcut && Icon && ( + + )} +
+ ) +}) CommandInput.displayName = CommandPrimitive.Input.displayName From 88bf2b016ab2a2be3aa211db297d96bb6db62e73 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 8 Aug 2024 17:19:24 +0200 Subject: [PATCH 11/13] adjust colors --- src/components/ui/command.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 980d569a7ec..df1325e4c94 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -14,7 +14,7 @@ const Command = React.forwardRef< { return ( - + {children} @@ -56,7 +56,7 @@ const CommandInput = React.forwardRef< ) => { return ( ) From 4c32eed91b6a08940157a13acc77d5dfc3244734 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 13 Aug 2024 18:57:54 +0200 Subject: [PATCH 12/13] fix imports --- src/components/LanguagePicker/MobileCloseBar.tsx | 2 +- src/components/Nav/Desktop/index.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/LanguagePicker/MobileCloseBar.tsx b/src/components/LanguagePicker/MobileCloseBar.tsx index 6238aadfed2..5678294259c 100644 --- a/src/components/LanguagePicker/MobileCloseBar.tsx +++ b/src/components/LanguagePicker/MobileCloseBar.tsx @@ -1,7 +1,7 @@ import { MouseEventHandler } from "react" import { useTranslation } from "next-i18next" -import { Button } from "../../../tailwind/ui/buttons/Button" +import { Button } from "../ui/buttons/Button" type MobileCloseBarProps = { handleClick: MouseEventHandler diff --git a/src/components/Nav/Desktop/index.tsx b/src/components/Nav/Desktop/index.tsx index 911b1d3c659..01f36de203c 100644 --- a/src/components/Nav/Desktop/index.tsx +++ b/src/components/Nav/Desktop/index.tsx @@ -7,11 +7,10 @@ import { HStack, useColorModeValue, useEventListener } from "@chakra-ui/react" import { IconButton } from "@/components/Buttons" import LanguagePicker from "@/components/LanguagePicker" +import { Button } from "@/components/ui/buttons/Button" import { DESKTOP_LANGUAGE_BUTTON_NAME } from "@/lib/constants" -import { Button } from "../../../../tailwind/ui/buttons/Button" - type DesktopNavMenuProps = { toggleColorMode: () => void } From ed4df194ec476a2ea1e6ba0879c55324f23f5692 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 13 Aug 2024 18:58:14 +0200 Subject: [PATCH 13/13] display Lang picker in a dialog on mobile --- src/components/LanguagePicker/index.tsx | 189 +++++++++++------- .../LanguagePicker/useLanguagePicker.tsx | 1 - src/components/Nav/Desktop/index.tsx | 4 +- src/components/Nav/Mobile/MenuFooter.tsx | 5 +- src/components/ui/dialog.tsx | 4 +- 5 files changed, 119 insertions(+), 84 deletions(-) diff --git a/src/components/LanguagePicker/index.tsx b/src/components/LanguagePicker/index.tsx index 9b4447bef9e..0b38e913c12 100644 --- a/src/components/LanguagePicker/index.tsx +++ b/src/components/LanguagePicker/index.tsx @@ -1,9 +1,9 @@ import { useRouter } from "next/router" +import { useTranslation } from "react-i18next" import { BaseLink } from "@/components/Link" import { cn } from "@/lib/utils/cn" -import { isMobile } from "@/lib/utils/isMobile" import { Command, @@ -12,6 +12,7 @@ import { CommandInput, CommandList, } from "../ui/command" +import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog" import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover" import MenuItem from "./MenuItem" @@ -25,15 +26,17 @@ type LanguagePickerProps = { children: React.ReactNode className?: string handleClose?: () => void + dialog?: boolean } const LanguagePicker = ({ children, handleClose, className, + dialog, }: LanguagePickerProps) => { const { asPath, push } = useRouter() - const { t, disclosure, languages } = useLanguagePicker(handleClose) + const { disclosure, languages } = useLanguagePicker(handleClose) const { isOpen, setValue, onClose, onOpen } = disclosure /** @@ -63,6 +66,33 @@ const LanguagePicker = ({ eventName: "/contributing/translation-program", }) + if (dialog) { + return ( + + {children} + + {/* Mobile Close bar */} + + + + onClose({ + eventAction: "Translation program link (no results)", + eventName: "/contributing/translation-program", + }) + } + /> + + + + + ) + } + return ( {children} @@ -73,83 +103,94 @@ const LanguagePicker = ({ className )} > - {/* Mobile Close bar */} - {/* avoid rendering mobile only feature on desktop */} - {isMobile() && ( - - )} - - { - const item = languages.find((name) => name.localeOption === value) + + onClose({ + eventAction: "Translation program link (no results)", + eventName: "/contributing/translation-program", + }) + } + /> + + + + + ) +} - if (!item) return 0 +const LanguagePickerMenu = ({ languages, onClose, onSelect }) => { + const { t } = useTranslation("common") - const { localeOption, sourceName, targetName, englishName } = item + return ( + { + const item = languages.find((name) => name.localeOption === value) + + if (!item) return 0 + + const { localeOption, sourceName, targetName, englishName } = item + + if ( + (localeOption + sourceName + targetName + englishName) + .toLowerCase() + .includes(search.toLowerCase()) + ) { + return 1 + } + + return 0 + }} + > +
+ {t("page-languages-filter-label")}{" "} + + ({languages.length} {t("common:languages")}) + +
+ + + + + + + + + {languages.map((displayInfo) => ( + + ))} + + +
+ ) +} - if ( - (localeOption + sourceName + targetName + englishName) - .toLowerCase() - .includes(search.toLowerCase()) - ) { - return 1 - } +const LanguagePickerFooter = ({ onTranslationProgramClick }) => { + const { t } = useTranslation("common") - return 0 - }} + return ( +
+

+ {t("page-languages-recruit-community")}{" "} + {/* TODO migrate once #13411 is merged */} + -

- {t("page-languages-filter-label")}{" "} - - ({languages.length} {t("common:languages")}) - -
- - - - - - - onClose({ - eventAction: "Translation program link (no results)", - eventName: "/contributing/translation-program", - }) - } - /> - - - {languages.map((displayInfo) => ( - - ))} - - - - - {/* Footer callout */} -
-

- {t("page-languages-recruit-community")}{" "} - {/* TODO migrate once #13411 is merged */} - - {t("common:learn-more")} - -

-
- - + {t("common:learn-more")} + +

+
) } diff --git a/src/components/LanguagePicker/useLanguagePicker.tsx b/src/components/LanguagePicker/useLanguagePicker.tsx index 4ff97b41313..441424ce59b 100644 --- a/src/components/LanguagePicker/useLanguagePicker.tsx +++ b/src/components/LanguagePicker/useLanguagePicker.tsx @@ -95,7 +95,6 @@ export const useLanguagePicker = (handleClose?: () => void) => { } return { - t, disclosure: { isOpen, setValue, onOpen, onClose }, languages, } diff --git a/src/components/Nav/Desktop/index.tsx b/src/components/Nav/Desktop/index.tsx index 01f36de203c..b5cdf216510 100644 --- a/src/components/Nav/Desktop/index.tsx +++ b/src/components/Nav/Desktop/index.tsx @@ -65,9 +65,7 @@ const DesktopNavMenu = ({ toggleColorMode }: DesktopNavMenuProps) => { /> {/* Locale-picker menu */} - +