Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Improve Dropdown Component #2492

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
32 changes: 16 additions & 16 deletions apps/site/middleware.ts
Original file line number Diff line number Diff line change
@@ -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-')}`
// )
// }
Comment on lines +4 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uncomment before the merge

return NextResponse.next()
}
78 changes: 72 additions & 6 deletions apps/site/pages/components/molecules/dropdown.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -88,18 +89,16 @@ Displays a set of actions/items to the user, usually used to show a menu of opti
<Tab>
<OverviewSection>
<Dropdown>
<DropdownButton icon={<Icon name="CaretDown" />}>
{'Dropdown'}
</DropdownButton>
<DropdownButton icon={<Icon name="CaretDown" />}>Dropdown</DropdownButton>
<DropdownMenu>
<DropdownItem icon={<Icon name="ArrowElbowDownRight" />}>
{'Dropdown Item 1'}
Dropdown Item 1
</DropdownItem>
<DropdownItem icon={<Icon name="ArrowElbowDownRight" />}>
{'Dropdown Item 2'}
Dropdown Item 2
</DropdownItem>
<DropdownItem icon={<Icon name="ArrowElbowDownRight" />}>
{'Dropdown Item 3'}
Dropdown Item 3
</DropdownItem>
</DropdownMenu>
</Dropdown>
Expand Down Expand Up @@ -186,6 +185,8 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c

## Usage

#### Default

<Tabs items={['Example', 'Code']} defaultIndex="0">
<Tab>
<OverviewSection>
Expand Down Expand Up @@ -231,6 +232,71 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c
</Tab>
</Tabs>

#### Controlled

export const ControlledDropdown = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<Dropdown isOpen={isOpen} onDismiss={() => {console.log("Dismiss"); setIsOpen(false)}}>
<DropdownButton asChild>
<button onClick={() => setIsOpen(old => !old)}>{'Controlled Dropdown with trigger as child'}</button>
</DropdownButton>
<DropdownMenu style={{ backgroundColor: "white"}}>
<DropdownItem asChild>
<button>A - Dropdown item as child</button>
</DropdownItem>
<DropdownItem asChild>
<button>B - Dropdown item as child</button>
</DropdownItem>
<DropdownItem asChild>
<button>C - Dropdown item as child</button>
</DropdownItem>
<DropdownItem asChild>
<button>D - Dropdown item as child</button>
</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds controlled option for Dropdown Component


<Tabs items={['Example', 'Code']} defaultIndex="0">
<Tab>
<OverviewSection>
<ControlledDropdown/>
</OverviewSection>
</Tab>
<Tab>
```tsx
<OverviewSection>
export const ControlledDropdown = () => {
const [isOpen, setIsOpen] = useState(true)
return (
<Dropdown isOpen={isOpen} onDismiss={() => {console.log("Dismiss"); setIsOpen(false)}}>
<DropdownButton asChild>
<button onClick={() => setIsOpen(old => !old)}>{'Controlled Dropdown with trigger as child'}</button>
</DropdownButton>
<DropdownMenu style={{ backgroundColor: "white"}}>
<DropdownItem asChild>
<button>A - Dropdown item as child</button>
</DropdownItem>
<DropdownItem asChild>
<button>B - Dropdown item as child</button>
</DropdownItem>
<DropdownItem asChild>
<button>C - Dropdown item as child</button>
</DropdownItem>
<DropdownItem asChild>
<button>D - Dropdown item as child</button>
</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}
</OverviewSection>
```
</Tab>
</Tabs>

---

## Props
Expand Down
42 changes: 24 additions & 18 deletions packages/components/src/molecules/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,44 @@ export interface DropdownProps {

const Dropdown = ({
children,
isOpen: isOpenDefault = false,
isOpen: isOpenControlled,
onDismiss,
id = 'fs-dropdown',
}: PropsWithChildren<DropdownProps>) => {
const [isOpen, setIsOpen] = useState(isOpenDefault)
const dropdownItemsRef = useRef<HTMLButtonElement[]>([])
const [isOpenInternal, setIsOpenInternal] = useState(false)
const dropdownItemsRef = useRef<HTMLElement[]>([])
const selectedDropdownItemIndexRef = useRef(0)
const dropdownButtonRef = useRef<HTMLButtonElement>(null)
const dropdownTriggerRef = useRef<HTMLElement | null>(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(<T extends HTMLElement = HTMLElement>(ref: T) => {
dropdownTriggerRef.current = ref
}, [])

useEffect(() => {
setIsOpen(isOpenDefault)
}, [isOpenDefault])
setIsOpenInternal(isOpenControlled ?? false)
}, [isOpenControlled])

useEffect(() => {
isOpen && dropdownItemsRef?.current[0]?.focus()
Expand All @@ -61,8 +67,8 @@ const Dropdown = ({
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) {
Expand All @@ -71,7 +77,7 @@ const Dropdown = ({
return
}

!someItemWasClicked && close()
!wasSomeItemClicked && close()
}

if (isOpen) {
Expand All @@ -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 (
<DropdownContext.Provider value={value}>
Expand Down
57 changes: 25 additions & 32 deletions packages/components/src/molecules/Dropdown/DropdownButton.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,44 @@
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<ButtonProps, 'variant' | 'inverse'> {
/**
* 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']
/** Replace the default rendered element with the one passed as a child, merging their props and behavior. */
ArthurTriis1 marked this conversation as resolved.
Show resolved Hide resolved
asChild?: boolean
}
ArthurTriis1 marked this conversation as resolved.
Show resolved Hide resolved

const DropdownButton = forwardRef<HTMLButtonElement, DropdownButtonProps>(
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 (
<Button
data-fs-dropdown-button
onClick={toggle}
data-testid={testId}
ref={dropdownButtonRef}
aria-label={ariaLabel}
aria-expanded={isOpen}
aria-haspopup="menu"
aria-controls={id}
variant="tertiary"
{...otherProps}
>
{children}
</Button>
<>
{asChild ? (
asChildrenTrigger
) : (
<Button
data-fs-dropdown-button
data-testid={testId}
variant="tertiary"
{...triggerProps}
{...otherProps}
>
{children}
</Button>
)}
</>
)
}
)
Expand Down
Loading
Loading