From a2ff711d1101b123db8459057f1f9e9c453d7db5 Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Wed, 2 Oct 2024 20:04:50 -0300 Subject: [PATCH 01/10] feat: Improve Dropdown --- apps/site/middleware.ts | 32 ++++----- .../pages/components/molecules/dropdown.mdx | 32 ++++++--- .../src/molecules/Dropdown/Dropdown.tsx | 48 +++++++------ .../src/molecules/Dropdown/DropdownButton.tsx | 57 +++++++--------- .../src/molecules/Dropdown/DropdownItem.tsx | 43 ++---------- .../src/molecules/Dropdown/DropdownMenu.tsx | 67 +++++++++++++++---- .../Dropdown/contexts/DropdownContext.ts | 26 ++++--- .../molecules/Dropdown/hooks/useDropdown.ts | 8 +-- .../Dropdown/hooks/useDropdownItem.ts | 63 +++++++++++++++++ .../Dropdown/hooks/useDropdownPosition.ts | 4 +- .../Dropdown/hooks/useDropdownTrigger.ts | 27 ++++++++ 11 files changed, 262 insertions(+), 145 deletions(-) create mode 100644 packages/components/src/molecules/Dropdown/hooks/useDropdownItem.ts create mode 100644 packages/components/src/molecules/Dropdown/hooks/useDropdownTrigger.ts diff --git a/apps/site/middleware.ts b/apps/site/middleware.ts index 5fcf566fd4..b3167eaec0 100644 --- a/apps/site/middleware.ts +++ b/apps/site/middleware.ts @@ -1,21 +1,21 @@ import { NextResponse, NextRequest } from 'next/server' export function middleware(request: NextRequest) { - const { pathname } = new URL(request.url) - if (pathname.startsWith('/docs')) { - return NextResponse.redirect( - `https://developers.vtex.com/docs/guides/faststore${pathname.replace( - '/docs', - '' - )}` - ) - } - if (pathname.startsWith('/components')) { - return NextResponse.redirect( - `https://developers.vtex.com/docs/guides/faststore${pathname - .replace('/components', '') - .replace(/\/([^\/]*)\//, '/$1-')}` - ) - } + // const { pathname } = new URL(request.url) + // if (pathname.startsWith('/docs')) { + // return NextResponse.redirect( + // `https://developers.vtex.com/docs/guides/faststore${pathname.replace( + // '/docs', + // '' + // )}` + // ) + // } + // if (pathname.startsWith('/components')) { + // return NextResponse.redirect( + // `https://developers.vtex.com/docs/guides/faststore${pathname + // .replace('/components', '') + // .replace(/\/([^\/]*)\//, '/$1-')}` + // ) + // } return NextResponse.next() } diff --git a/apps/site/pages/components/molecules/dropdown.mdx b/apps/site/pages/components/molecules/dropdown.mdx index 3945e39c96..22adb2a124 100644 --- a/apps/site/pages/components/molecules/dropdown.mdx +++ b/apps/site/pages/components/molecules/dropdown.mdx @@ -29,6 +29,7 @@ import { OverviewSection } from 'site/components/OverviewSection' import path from 'path' import { useSSG } from 'nextra/ssg' import { getComponentPropsFrom } from 'site/components/utilities/propsSection' +import { useState } from 'react' export const getStaticProps = () => { const dropdownPath = path.resolve(__filename) @@ -84,25 +85,38 @@ Displays a set of actions/items to the user, usually used to show a menu of opti ## Overview - - - - - }> - {'Dropdown'} +export const ControlledDropdown = () => { + const [isOpen, setIsOpen] = useState(true) + return ( + {console.log("Dismiss"); setIsOpen(false)}}> + + }> - {'Dropdown Item 1'} + + + + }> - {'Dropdown Item 2'} + {'B - Dropdown Item 2'} }> - {'Dropdown Item 3'} + {'C - Dropdown Item 3'} + + }> + {'B - Dropdown Item 3'} + ) +} + + + + + }> {'Dropdown Small'} diff --git a/packages/components/src/molecules/Dropdown/Dropdown.tsx b/packages/components/src/molecules/Dropdown/Dropdown.tsx index aacec6e08a..96a857de03 100644 --- a/packages/components/src/molecules/Dropdown/Dropdown.tsx +++ b/packages/components/src/molecules/Dropdown/Dropdown.tsx @@ -1,7 +1,7 @@ import type { PropsWithChildren } from 'react' import React, { useRef, useMemo, useState, useEffect, useCallback } from 'react' -import DropdownContext from '../Dropdown/contexts/DropdownContext' +import DropdownContext, { DropdownItemElement, DropdownTriggerElement } from '../Dropdown/contexts/DropdownContext' export interface DropdownProps { /** @@ -20,49 +20,55 @@ export interface DropdownProps { const Dropdown = ({ children, - isOpen: isOpenDefault = false, + isOpen: isOpenControlled, onDismiss, id = 'fs-dropdown', }: PropsWithChildren) => { - const [isOpen, setIsOpen] = useState(isOpenDefault) - const dropdownItemsRef = useRef([]) + const [isOpenInternal, setIsOpenInternal] = useState(false) + const dropdownItemsRef = useRef([]) const selectedDropdownItemIndexRef = useRef(0) - const dropdownButtonRef = useRef(null) + const dropdownTriggerRef = useRef(null) + + const isOpen = isOpenControlled ?? isOpenInternal const close = useCallback(() => { - setIsOpen(false) + setIsOpenInternal(false) onDismiss?.() }, [onDismiss]) const open = () => { - setIsOpen(true) + setIsOpenInternal(true) } const toggle = useCallback(() => { - setIsOpen((old) => { - if (old) { + setIsOpenInternal((currentIsOpen) => { + if (currentIsOpen) { onDismiss?.() - dropdownButtonRef.current?.focus() + dropdownTriggerRef.current?.focus() } - return !old + return !currentIsOpen }) }, [onDismiss]) + const addDropdownTriggerRef = useCallback((ref: T) => { + dropdownTriggerRef.current = ref + }, []) + useEffect(() => { - setIsOpen(isOpenDefault) - }, [isOpenDefault]) + setIsOpenInternal(isOpenControlled ?? false) + }, [isOpenControlled]) useEffect(() => { - isOpen && dropdownItemsRef?.current[0]?.focus() - }, [isOpen]) + isOpenInternal && dropdownItemsRef?.current[0]?.focus() + }, [isOpenInternal]) useEffect(() => { let firstClick = true const event = (e: MouseEvent) => { - const someItemWasClicked = dropdownItemsRef?.current.some( - (item) => e.target === item + const wasSomeItemClicked = dropdownItemsRef?.current.some( + (item) => e.target === item || item.contains(e.target as Node) ) if (firstClick) { @@ -71,7 +77,7 @@ const Dropdown = ({ return } - !someItemWasClicked && close() + !wasSomeItemClicked && close() } if (isOpen) { @@ -91,13 +97,13 @@ const Dropdown = ({ close, open, toggle, - dropdownButtonRef, - onDismiss, + dropdownTriggerRef, + addDropdownTriggerRef, selectedDropdownItemIndexRef, dropdownItemsRef, id, } - }, [close, id, isOpen, onDismiss, toggle]) + }, [isOpen, close, toggle, addDropdownTriggerRef, id]) return ( diff --git a/packages/components/src/molecules/Dropdown/DropdownButton.tsx b/packages/components/src/molecules/Dropdown/DropdownButton.tsx index 6ca2305cde..f3d91964f3 100644 --- a/packages/components/src/molecules/Dropdown/DropdownButton.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownButton.tsx @@ -1,7 +1,6 @@ -import React, { forwardRef, useImperativeHandle, AriaAttributes } from 'react' +import React, { cloneElement, forwardRef } from 'react' import Button, { ButtonProps } from '../../atoms/Button' - -import { useDropdown } from './hooks/useDropdown' +import { useDropdownTrigger } from './hooks/useDropdownTrigger' export interface DropdownButtonProps extends Omit { @@ -9,43 +8,37 @@ export interface DropdownButtonProps * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). */ testId?: string - /** - * For accessibility purposes, add an ARIA label to the element when it doesn't have a label. - */ - 'aria-label'?: AriaAttributes['aria-label'] + + asChild?: boolean } const DropdownButton = forwardRef( function DropdownButton( - { - testId = 'fs-dropdown-button', - 'aria-label': ariaLabel, - children, - ...otherProps - }, - ref + { testId = 'fs-dropdown-button', children, asChild = false, ...otherProps }, + triggerRef ) { - const { toggle, dropdownButtonRef, isOpen, id } = useDropdown() + const triggerProps = useDropdownTrigger({ triggerRef }) - useImperativeHandle(ref, () => dropdownButtonRef!.current!, [ - dropdownButtonRef, - ]) + const asChildrenTrigger = React.isValidElement(children) + ? cloneElement(children, { ...triggerProps, ...children.props }) + : children; return ( - + <> + {asChild ? ( + asChildrenTrigger + ) : ( + + )} + ) } ) diff --git a/packages/components/src/molecules/Dropdown/DropdownItem.tsx b/packages/components/src/molecules/Dropdown/DropdownItem.tsx index 7fd3652fb3..3e6342323f 100644 --- a/packages/components/src/molecules/Dropdown/DropdownItem.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownItem.tsx @@ -1,7 +1,7 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react' -import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react' +import React, { forwardRef } from 'react' -import { useDropdown } from './hooks/useDropdown' +import { useDropdownItem } from './hooks/useDropdownItem' export interface DropdownItemProps extends ButtonHTMLAttributes { @@ -20,48 +20,13 @@ const DropdownItem = forwardRef( { children, icon, onClick, testId = 'fs-dropdown-item', ...otherProps }, ref ) { - const { dropdownItemsRef, selectedDropdownItemIndexRef, close } = - useDropdown() - - const [dropdownItemIndex, setDropdownItemIndex] = useState(0) - const dropdownItemRef = useRef() - - const addToRefs = (el: HTMLButtonElement) => { - if (el && !dropdownItemsRef?.current.includes(el)) { - dropdownItemsRef?.current.push(el) - setDropdownItemIndex( - dropdownItemsRef?.current.findIndex((element) => element === el) ?? 0 - ) - } - - dropdownItemRef.current = el - } - - const onFocusItem = () => { - selectedDropdownItemIndexRef!.current = dropdownItemIndex - dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus() - } - - const handleOnClickItem = ( - event: React.MouseEvent - ) => { - onClick?.(event) - close?.() - } - - useImperativeHandle(ref, () => dropdownItemRef.current!, []) + const itemProps = useDropdownItem({ ref, onClick }) return ( + <> + {asChild ? ( + asChildrenItem + ) : ( + + )} + ) } ) diff --git a/packages/components/src/molecules/Dropdown/DropdownMenu.tsx b/packages/components/src/molecules/Dropdown/DropdownMenu.tsx index c336a20396..00e6f2a8ee 100644 --- a/packages/components/src/molecules/Dropdown/DropdownMenu.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownMenu.tsx @@ -59,7 +59,7 @@ const DropdownMenu = ({ id, } = useDropdown() - const dropdownPosition = useDropdownPosition() + const { loading: loadingPosition, ...dropdownPosition } = useDropdownPosition() const childrenLength = React.Children.toArray(children).length @@ -108,7 +108,7 @@ const DropdownMenu = ({ ]; const matchItem = rearrangedDropdownItems.find( - (item) => item.textContent?.[0].toLowerCase() === key.toLowerCase() + (item) => item.textContent?.[0]?.toLowerCase() === key.toLowerCase() ); if (matchItem) { @@ -119,11 +119,9 @@ const DropdownMenu = ({ const handleBackdropKeyDown = (event: KeyboardEvent) => { - console.log(event.keyCode) - if (event.defaultPrevented || event.key === 'Enter' || event.keyCode === 32) { + if (event.defaultPrevented || event.key === 'Enter' || event.key === ' ') { return } - event.preventDefault() @@ -157,7 +155,7 @@ const DropdownMenu = ({ return null } - return isOpen + return (isOpen && !loadingPosition) ? createPortal(
= { ref: React.ForwardedRef onClick?: React.MouseEventHandler + dismissOnClick?: boolean } export const useDropdownItem = < @@ -14,9 +15,10 @@ export const useDropdownItem = < >({ ref, onClick, + dismissOnClick = true }: UseDropdownItemProps) => { const { dropdownItemsRef, selectedDropdownItemIndexRef, - // close + close } = useDropdown< never, E @@ -45,7 +47,7 @@ export const useDropdownItem = < event: React.MouseEvent ) => { onClick?.(event) - // close?.() + dismissOnClick && close?.() } useImperativeHandle(ref, () => dropdownItemRef.current!, []) diff --git a/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts b/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts index 7f84c0567d..736511918f 100644 --- a/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts +++ b/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts @@ -1,33 +1,60 @@ +import { useEffect, useState } from 'react' import { useDropdown } from './useDropdown' -type DropdownPosition = Pick +type DropdownPosition = { + loading: boolean +} & Pick /** * Hook used to find the DropdownMenu position in relation to DropdownButton * @returns Style with positions. */ export const useDropdownPosition = (): DropdownPosition => { - const { dropdownTriggerRef } = useDropdown() - - // Necessary to use this component in SSR - const isBrowser = typeof window !== 'undefined' - - const buttonRect = dropdownTriggerRef?.current?.getBoundingClientRect() - const topLevel = buttonRect?.top ?? 0 - const topOffset = buttonRect?.height ?? 0 - const leftLevel = buttonRect?.left ?? 0 - - // The scroll properties fix the position of DropdownMenu when the scroll is activated. - const scrollTop = isBrowser ? document?.documentElement?.scrollTop : 0 - const scrollLeft = isBrowser ? document?.documentElement?.scrollLeft : 0 - - const topPosition = topLevel + topOffset + scrollTop - - const leftPosition = leftLevel + scrollLeft - - return { - position: 'absolute', - top: topPosition, - left: leftPosition, - } + const { dropdownTriggerRef, isOpen } = useDropdown() + + const [positionProps, setPositionProps] = useState({ + top: 0, + left: 0, + loading: true + }) + + useEffect(() => { + const updateMenuPosition = () => { + // Necessary to use this component in SSR + const isBrowser = typeof window !== 'undefined' + + const buttonRect = dropdownTriggerRef?.current?.getBoundingClientRect() + const topLevel = buttonRect?.top ?? 0 + const topOffset = buttonRect?.height ?? 0 + const leftLevel = buttonRect?.left ?? 0 + + // The scroll properties fix the position of DropdownMenu when the scroll is activated. + const scrollTop = isBrowser ? document?.documentElement?.scrollTop : 0 + const scrollLeft = isBrowser ? document?.documentElement?.scrollLeft : 0 + + const topPosition = topLevel + topOffset + scrollTop + + const leftPosition = leftLevel + scrollLeft + + setPositionProps({ + top: topPosition, + left: leftPosition, + loading: false + }) + } + + if (isOpen) { + // Update the position of the menu + updateMenuPosition() + window.addEventListener('resize', updateMenuPosition) + } + + // Cleanup listener on unmount or close + return () => { + window.removeEventListener('resize', updateMenuPosition) + } + } + , [dropdownTriggerRef, isOpen]) + + return { ...positionProps, position: 'absolute' as const } } From 814264f1f5b11944c72ad0a550642b6173f5cd14 Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Fri, 4 Oct 2024 10:01:13 -0300 Subject: [PATCH 04/10] docs: Adjust Dropdown Docs --- .../pages/components/molecules/dropdown.mdx | 98 ++++++++++++++----- .../src/molecules/Dropdown/DropdownButton.tsx | 2 +- .../src/molecules/Dropdown/DropdownItem.tsx | 8 +- 3 files changed, 82 insertions(+), 26 deletions(-) diff --git a/apps/site/pages/components/molecules/dropdown.mdx b/apps/site/pages/components/molecules/dropdown.mdx index 22adb2a124..1166bf896c 100644 --- a/apps/site/pages/components/molecules/dropdown.mdx +++ b/apps/site/pages/components/molecules/dropdown.mdx @@ -85,38 +85,23 @@ Displays a set of actions/items to the user, usually used to show a menu of opti ## Overview -export const ControlledDropdown = () => { - const [isOpen, setIsOpen] = useState(true) - return ( - {console.log("Dismiss"); setIsOpen(false)}}> - - - + + + + + }>Dropdown }> - - - - + Dropdown Item 1 }> - {'B - Dropdown Item 2'} + Dropdown Item 2 }> - {'C - Dropdown Item 3'} - - }> - {'B - Dropdown Item 3'} + Dropdown Item 3 - ) -} - - - - - }> {'Dropdown Small'} @@ -200,6 +185,8 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c ## Usage +#### Default + @@ -245,6 +232,71 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c +#### Controlled + +export const ControlledDropdown = () => { + const [isOpen, setIsOpen] = useState(false) + return ( + {console.log("Dismiss"); setIsOpen(false)}}> + + + + + + + + + + + + + + + + + + + ) +} + + + + + + + + + ```tsx + + export const ControlledDropdown = () => { + const [isOpen, setIsOpen] = useState(true) + return ( + {console.log("Dismiss"); setIsOpen(false)}}> + + + + + + + + + + + + + + + + + + + ) + } + + ``` + + + --- ## Props diff --git a/packages/components/src/molecules/Dropdown/DropdownButton.tsx b/packages/components/src/molecules/Dropdown/DropdownButton.tsx index f3d91964f3..6068ff2218 100644 --- a/packages/components/src/molecules/Dropdown/DropdownButton.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownButton.tsx @@ -8,7 +8,7 @@ export interface DropdownButtonProps * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). */ testId?: string - + /** Replace the default rendered element with the one passed as a child, merging their props and behavior. */ asChild?: boolean } diff --git a/packages/components/src/molecules/Dropdown/DropdownItem.tsx b/packages/components/src/molecules/Dropdown/DropdownItem.tsx index d907675542..2acf17b5eb 100644 --- a/packages/components/src/molecules/Dropdown/DropdownItem.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownItem.tsx @@ -13,9 +13,13 @@ export interface DropdownItemProps * A React component that will be rendered as an icon. */ icon?: ReactNode - + /** + * Replace the default rendered element with the one passed as a child, merging their props and behavior. + * */ asChild?: boolean - + /** + * Emit onDismiss eventwhen the component is clicked. + */ dismissOnClick?: boolean } From aba94bd9cf987af6e5541d69e6c2e716b854cf7d Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Mon, 7 Oct 2024 23:07:34 -0300 Subject: [PATCH 05/10] feat: Add align position in DropdownMenu --- .../pages/components/molecules/dropdown.mdx | 122 +++++++++++------- packages/components/src/atoms/Button/index.ts | 2 +- .../src/molecules/Dropdown/Dropdown.tsx | 9 +- .../src/molecules/Dropdown/DropdownButton.tsx | 24 +++- .../src/molecules/Dropdown/DropdownItem.tsx | 1 + .../src/molecules/Dropdown/DropdownMenu.tsx | 9 +- .../Dropdown/hooks/useDropdownPosition.ts | 46 ++++--- 7 files changed, 144 insertions(+), 69 deletions(-) diff --git a/apps/site/pages/components/molecules/dropdown.mdx b/apps/site/pages/components/molecules/dropdown.mdx index 1166bf896c..89120b839e 100644 --- a/apps/site/pages/components/molecules/dropdown.mdx +++ b/apps/site/pages/components/molecules/dropdown.mdx @@ -90,15 +90,15 @@ Displays a set of actions/items to the user, usually used to show a menu of opti }>Dropdown - + }> - Dropdown Item 1 + {'Dropdown Item 1'} }> - Dropdown Item 2 + {'Dropdown Item 2'} }> - Dropdown Item 3 + {'Dropdown Item 3'} @@ -106,7 +106,7 @@ Displays a set of actions/items to the user, usually used to show a menu of opti }> {'Dropdown Small'} - + }> {'Dropdown Item 1'} @@ -237,23 +237,13 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c export const ControlledDropdown = () => { const [isOpen, setIsOpen] = useState(false) return ( - {console.log("Dismiss"); setIsOpen(false)}}> - - - - - - - - - - - - - - - - + {setIsOpen(false)}}> + setIsOpen(true)}>Controlled Dropdown + + A - Dropdown item as child + B - Dropdown item as child + C - Dropdown item as child + D - Dropdown item as child ) @@ -268,30 +258,70 @@ export const ControlledDropdown = () => { ```tsx - export const ControlledDropdown = () => { - const [isOpen, setIsOpen] = useState(true) - return ( - {console.log("Dismiss"); setIsOpen(false)}}> - - - - - - - - - - - - - - - - - - - ) - } + const [isOpen, setIsOpen] = useState(false) + return ( + {setIsOpen(false)}}> + Controlled Dropdown + + A - Dropdown item as child + B - Dropdown item as child + C - Dropdown item as child + D - Dropdown item as child + + + + ``` + + + +#### As Child + + + + + + + + + + + + + + + + + + + + + + + + + + + ```tsx + + + + + + + + + + + + + + + + + + + + ``` diff --git a/packages/components/src/atoms/Button/index.ts b/packages/components/src/atoms/Button/index.ts index c378f002c0..f19f72d177 100644 --- a/packages/components/src/atoms/Button/index.ts +++ b/packages/components/src/atoms/Button/index.ts @@ -1,2 +1,2 @@ export { default } from './Button' -export type { ButtonProps } from './Button' +export type { ButtonProps, Variant, Size, IconPosition } from './Button' diff --git a/packages/components/src/molecules/Dropdown/Dropdown.tsx b/packages/components/src/molecules/Dropdown/Dropdown.tsx index 00c47228a8..552c4000f6 100644 --- a/packages/components/src/molecules/Dropdown/Dropdown.tsx +++ b/packages/components/src/molecules/Dropdown/Dropdown.tsx @@ -60,7 +60,14 @@ const Dropdown = ({ }, [isOpenControlled]) useEffect(() => { - isOpen && dropdownItemsRef?.current[0]?.focus() + if(isOpen) { + dropdownItemsRef?.current[0]?.focus() + document.body.style.overflow = 'hidden' + + return + } + + document.body.style.overflow = 'auto' }, [isOpen]) useEffect(() => { diff --git a/packages/components/src/molecules/Dropdown/DropdownButton.tsx b/packages/components/src/molecules/Dropdown/DropdownButton.tsx index 6068ff2218..53327cacd9 100644 --- a/packages/components/src/molecules/Dropdown/DropdownButton.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownButton.tsx @@ -1,15 +1,33 @@ -import React, { cloneElement, forwardRef } from 'react' -import Button, { ButtonProps } from '../../atoms/Button' +import React, { cloneElement, forwardRef, ReactNode } from 'react' +import Button, { ButtonProps, IconPosition } from '../../atoms/Button' import { useDropdownTrigger } from './hooks/useDropdownTrigger' export interface DropdownButtonProps - extends Omit { + extends Omit { /** * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). */ testId?: string /** Replace the default rendered element with the one passed as a child, merging their props and behavior. */ asChild?: boolean + /** + * Boolean that represents a loading state. + */ + loading?: boolean + /** + * Specifies a label for loading state. + */ + loadingLabel?: string + /** + * @deprecated + * A React component that will be rendered as an icon. + */ + icon?: ReactNode + /** + * @deprecated + * Specifies where the icon should be positioned + */ + iconPosition?: IconPosition } const DropdownButton = forwardRef( diff --git a/packages/components/src/molecules/Dropdown/DropdownItem.tsx b/packages/components/src/molecules/Dropdown/DropdownItem.tsx index 2acf17b5eb..5cfd30bcae 100644 --- a/packages/components/src/molecules/Dropdown/DropdownItem.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownItem.tsx @@ -10,6 +10,7 @@ export interface DropdownItemProps */ testId?: string /** + * @deprecated * A React component that will be rendered as an icon. */ icon?: ReactNode diff --git a/packages/components/src/molecules/Dropdown/DropdownMenu.tsx b/packages/components/src/molecules/Dropdown/DropdownMenu.tsx index 00e6f2a8ee..b213f9cf96 100644 --- a/packages/components/src/molecules/Dropdown/DropdownMenu.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownMenu.tsx @@ -22,17 +22,19 @@ export interface DropdownMenuProps extends ModalContentProps { * @see aria-labelledby https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby */ 'aria-labelledby'?: AriaAttributes['aria-label'] - /** * This function is called whenever the user hits "Escape" or clicks outside * the dialog. */ onDismiss?: (event: MouseEvent | KeyboardEvent) => void - /** * Specifies the size variant. */ size?: 'small' | 'regular' + /** + * Alignment for the dropdown + */ + align?: 'left' | 'right' | 'center' children: ReactNode[] | ReactNode } @@ -47,6 +49,7 @@ const DropdownMenu = ({ children, testId = 'fs-dropdown-menu', size = 'regular', + align = 'left', style, ...otherProps }: PropsWithChildren) => { @@ -59,7 +62,7 @@ const DropdownMenu = ({ id, } = useDropdown() - const { loading: loadingPosition, ...dropdownPosition } = useDropdownPosition() + const { loading: loadingPosition, ...dropdownPosition } = useDropdownPosition(align) const childrenLength = React.Children.toArray(children).length diff --git a/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts b/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts index 736511918f..73fbe1f95b 100644 --- a/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts +++ b/packages/components/src/molecules/Dropdown/hooks/useDropdownPosition.ts @@ -3,18 +3,20 @@ import { useDropdown } from './useDropdown' type DropdownPosition = { loading: boolean -} & Pick +} & Pick /** * Hook used to find the DropdownMenu position in relation to DropdownButton * @returns Style with positions. */ -export const useDropdownPosition = (): DropdownPosition => { +export const useDropdownPosition = (align: 'left' | 'center' | 'right' = 'left'): DropdownPosition => { const { dropdownTriggerRef, isOpen } = useDropdown() const [positionProps, setPositionProps] = useState({ top: 0, - left: 0, + left: 0 as React.CSSProperties['left'], + right: 'auto', + transform: 'none', loading: true }) @@ -23,10 +25,13 @@ export const useDropdownPosition = (): DropdownPosition => { // Necessary to use this component in SSR const isBrowser = typeof window !== 'undefined' - const buttonRect = dropdownTriggerRef?.current?.getBoundingClientRect() + if (!dropdownTriggerRef?.current) return + + const buttonRect = dropdownTriggerRef.current.getBoundingClientRect() const topLevel = buttonRect?.top ?? 0 const topOffset = buttonRect?.height ?? 0 const leftLevel = buttonRect?.left ?? 0 + const buttonWidth = buttonRect?.width ?? 0 // The scroll properties fix the position of DropdownMenu when the scroll is activated. const scrollTop = isBrowser ? document?.documentElement?.scrollTop : 0 @@ -34,27 +39,38 @@ export const useDropdownPosition = (): DropdownPosition => { const topPosition = topLevel + topOffset + scrollTop - const leftPosition = leftLevel + scrollLeft + let leftPosition: React.CSSProperties['left'] = leftLevel + scrollLeft + let rightPosition = 'auto' + let transform = 'none' + + if (align === 'right') { + rightPosition = `${document.documentElement.clientWidth - leftLevel - buttonWidth}px` + leftPosition = 'auto' + } else if (align === 'center') { + leftPosition = leftLevel + (buttonWidth / 2) + scrollLeft + transform = 'translateX(-50%)' + } setPositionProps({ top: topPosition, left: leftPosition, + right: rightPosition, + transform, loading: false }) } - if (isOpen) { - // Update the position of the menu - updateMenuPosition() - window.addEventListener('resize', updateMenuPosition) - } + if (isOpen) { + // Update the position of the menu + updateMenuPosition() + window.addEventListener('resize', updateMenuPosition) + } - // Cleanup listener on unmount or close - return () => { - window.removeEventListener('resize', updateMenuPosition) - } + // Cleanup listener on unmount or close + return () => { + window.removeEventListener('resize', updateMenuPosition) } - , [dropdownTriggerRef, isOpen]) + }, [dropdownTriggerRef, isOpen, align]) return { ...positionProps, position: 'absolute' as const } } From 9aa21f86f6c207b871231f41ab355290ce258781 Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Wed, 16 Oct 2024 14:32:13 -0300 Subject: [PATCH 06/10] Update packages/components/src/molecules/Dropdown/DropdownButton.tsx Co-authored-by: Fanny Chien --- packages/components/src/molecules/Dropdown/DropdownButton.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/src/molecules/Dropdown/DropdownButton.tsx b/packages/components/src/molecules/Dropdown/DropdownButton.tsx index 53327cacd9..99400b3933 100644 --- a/packages/components/src/molecules/Dropdown/DropdownButton.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownButton.tsx @@ -8,7 +8,9 @@ export interface DropdownButtonProps * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). */ testId?: string - /** Replace the default rendered element with the one passed as a child, merging their props and behavior. */ + /** + * Replace the default rendered element with the provided child element, merging their props and behavior. + */ asChild?: boolean /** * Boolean that represents a loading state. From b64df8dbabddcd4d3f867e2aba137d92d960832a Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Fri, 18 Oct 2024 08:38:51 -0300 Subject: [PATCH 07/10] Update packages/components/src/atoms/Button/index.ts --- packages/components/src/atoms/Button/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/atoms/Button/index.ts b/packages/components/src/atoms/Button/index.ts index f19f72d177..8dbc1909ca 100644 --- a/packages/components/src/atoms/Button/index.ts +++ b/packages/components/src/atoms/Button/index.ts @@ -1,2 +1,2 @@ export { default } from './Button' -export type { ButtonProps, Variant, Size, IconPosition } from './Button' +export type { ButtonProps, Variant as ButtonVariant, Size as ButtonSize, IconPosition as ButtonIconPosition } from './Button' From 450d8b4e034c3c6e35f3928a125c8ee224db93f6 Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Tue, 22 Oct 2024 11:36:16 -0300 Subject: [PATCH 08/10] feat: apply adjusts in Dropdown --- .../pages/components/molecules/dropdown.mdx | 16 +++---- .../src/molecules/Dropdown/DropdownButton.tsx | 10 ++--- .../src/molecules/Dropdown/DropdownItem.tsx | 44 +++++++++++-------- .../Dropdown/contexts/DropdownContext.ts | 3 ++ .../Dropdown/hooks/useDropdownItem.ts | 22 +++------- 5 files changed, 49 insertions(+), 46 deletions(-) diff --git a/apps/site/pages/components/molecules/dropdown.mdx b/apps/site/pages/components/molecules/dropdown.mdx index 89120b839e..998a53eca8 100644 --- a/apps/site/pages/components/molecules/dropdown.mdx +++ b/apps/site/pages/components/molecules/dropdown.mdx @@ -240,10 +240,10 @@ export const ControlledDropdown = () => { {setIsOpen(false)}}> setIsOpen(true)}>Controlled Dropdown - A - Dropdown item as child - B - Dropdown item as child - C - Dropdown item as child - D - Dropdown item as child + A - Dropdown item + B - Dropdown item + C - Dropdown item + D - Dropdown item ) @@ -263,10 +263,10 @@ export const ControlledDropdown = () => { {setIsOpen(false)}}> Controlled Dropdown - A - Dropdown item as child - B - Dropdown item as child - C - Dropdown item as child - D - Dropdown item as child + A - Dropdown item + B - Dropdown item + C - Dropdown item + D - Dropdown item diff --git a/packages/components/src/molecules/Dropdown/DropdownButton.tsx b/packages/components/src/molecules/Dropdown/DropdownButton.tsx index 99400b3933..115d1b8aaa 100644 --- a/packages/components/src/molecules/Dropdown/DropdownButton.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownButton.tsx @@ -1,9 +1,9 @@ import React, { cloneElement, forwardRef, ReactNode } from 'react' -import Button, { ButtonProps, IconPosition } from '../../atoms/Button' +import Button, { ButtonProps, ButtonIconPosition } from '../../atoms/Button' import { useDropdownTrigger } from './hooks/useDropdownTrigger' export interface DropdownButtonProps - extends Omit { + extends Omit { /** * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). */ @@ -29,7 +29,7 @@ export interface DropdownButtonProps * @deprecated * Specifies where the icon should be positioned */ - iconPosition?: IconPosition + iconPosition?: ButtonIconPosition } const DropdownButton = forwardRef( @@ -40,8 +40,8 @@ const DropdownButton = forwardRef( const triggerProps = useDropdownTrigger({ triggerRef }) const asChildrenTrigger = React.isValidElement(children) - ? cloneElement(children, { ...triggerProps, ...children.props }) - : children; + ? cloneElement(children, { ...triggerProps, ...children.props }) + : children return ( <> diff --git a/packages/components/src/molecules/Dropdown/DropdownItem.tsx b/packages/components/src/molecules/Dropdown/DropdownItem.tsx index 5cfd30bcae..2c48c4bc6d 100644 --- a/packages/components/src/molecules/Dropdown/DropdownItem.tsx +++ b/packages/components/src/molecules/Dropdown/DropdownItem.tsx @@ -14,41 +14,49 @@ export interface DropdownItemProps * A React component that will be rendered as an icon. */ icon?: ReactNode - /** - * Replace the default rendered element with the one passed as a child, merging their props and behavior. + /** + * Replace the default rendered element with the one passed as a child, merging their props and behavior. * */ asChild?: boolean /** - * Emit onDismiss eventwhen the component is clicked. + * Emit onDismiss event when the component is clicked. */ dismissOnClick?: boolean } const DropdownItem = forwardRef( function Button( - { children, asChild, icon, onClick, dismissOnClick = true, testId = 'fs-dropdown-item', ...otherProps }, + { + children, + asChild, + icon, + onClick, + dismissOnClick = true, + testId = 'fs-dropdown-item', + ...otherProps + }, ref ) { const itemProps = useDropdownItem({ ref, onClick, dismissOnClick }) const asChildrenItem = React.isValidElement(children) - ? cloneElement(children, { ...itemProps, ...children.props }) - : children; + ? cloneElement(children, { ...itemProps, ...children.props }) + : children return ( <> - {asChild ? ( - asChildrenItem - ) : ( - + {asChild ? ( + asChildrenItem + ) : ( + )} ) diff --git a/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts b/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts index 378a63fea6..0ad7871bb9 100644 --- a/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts +++ b/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts @@ -38,6 +38,9 @@ export type DropdownContextState< */ id: string + /** + * Associates the dropdown trigger element's ref for managing its position and interaction events. + */ addDropdownTriggerRef?(ref: T | null): void } diff --git a/packages/components/src/molecules/Dropdown/hooks/useDropdownItem.ts b/packages/components/src/molecules/Dropdown/hooks/useDropdownItem.ts index bdab0098b6..112f46bba8 100644 --- a/packages/components/src/molecules/Dropdown/hooks/useDropdownItem.ts +++ b/packages/components/src/molecules/Dropdown/hooks/useDropdownItem.ts @@ -2,24 +2,18 @@ import React, { useImperativeHandle, useRef, useState } from 'react' import { useDropdown } from './useDropdown' -export type UseDropdownItemProps< - E extends HTMLElement = HTMLElement, -> = { +export type UseDropdownItemProps = { ref: React.ForwardedRef onClick?: React.MouseEventHandler dismissOnClick?: boolean } -export const useDropdownItem = < - E extends HTMLElement = HTMLElement, ->({ +export const useDropdownItem = ({ ref, onClick, - dismissOnClick = true + dismissOnClick = true, }: UseDropdownItemProps) => { - const { dropdownItemsRef, selectedDropdownItemIndexRef, - close - } = useDropdown< + const { dropdownItemsRef, selectedDropdownItemIndexRef, close } = useDropdown< never, E >() @@ -39,13 +33,11 @@ export const useDropdownItem = < } const onFocusItem = () => { - selectedDropdownItemIndexRef!.current = dropdownItemIndex; - (dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current])?.focus() + selectedDropdownItemIndexRef!.current = dropdownItemIndex + dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus() } - const handleOnClickItem = ( - event: React.MouseEvent - ) => { + const handleOnClickItem = (event: React.MouseEvent) => { onClick?.(event) dismissOnClick && close?.() } From 9ae264e5d34618806210eaa4ba93e96e2668fe4c Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Tue, 22 Oct 2024 11:40:56 -0300 Subject: [PATCH 09/10] feat: apply adjusts in Dropdown --- .../src/molecules/Dropdown/contexts/DropdownContext.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts b/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts index 0ad7871bb9..8ada094460 100644 --- a/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts +++ b/packages/components/src/molecules/Dropdown/contexts/DropdownContext.ts @@ -32,12 +32,10 @@ export type DropdownContextState< * Function responsible for switch the the DropdownMenu state in this context. */ toggle?(): void - /** * Identifier to be used in aria-controls */ id: string - /** * Associates the dropdown trigger element's ref for managing its position and interaction events. */ From 07912b165ffca84b1da7f39a02e664d542815830 Mon Sep 17 00:00:00 2001 From: Arthur Andrade Date: Thu, 31 Oct 2024 09:52:06 -0300 Subject: [PATCH 10/10] feat: remove lock scroll --- .../src/molecules/Dropdown/Dropdown.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/components/src/molecules/Dropdown/Dropdown.tsx b/packages/components/src/molecules/Dropdown/Dropdown.tsx index 552c4000f6..40c58ef362 100644 --- a/packages/components/src/molecules/Dropdown/Dropdown.tsx +++ b/packages/components/src/molecules/Dropdown/Dropdown.tsx @@ -51,23 +51,19 @@ const Dropdown = ({ }) }, [onDismiss]) - const addDropdownTriggerRef = useCallback((ref: T) => { - dropdownTriggerRef.current = ref - }, []) + const addDropdownTriggerRef = useCallback( + (ref: T) => { + dropdownTriggerRef.current = ref + }, + [] + ) useEffect(() => { setIsOpenInternal(isOpenControlled ?? false) }, [isOpenControlled]) useEffect(() => { - if(isOpen) { - dropdownItemsRef?.current[0]?.focus() - document.body.style.overflow = 'hidden' - - return - } - - document.body.style.overflow = 'auto' + isOpen && dropdownItemsRef?.current[0]?.focus() }, [isOpen]) useEffect(() => {