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

docs(dropdown): FWD ref and component-names #1754

Merged
merged 4 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 39 additions & 1 deletion packages/components/dropdown/src/Dropdown.doc.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Meta, Canvas } from '@storybook/addon-docs'
import { ArgTypes as ExtendedArgTypes } from '@docs/helpers/ArgTypes'
import { Callout } from '@docs/helpers/Callout'

import { Dropdown } from '.'

Expand All @@ -9,7 +10,44 @@ import * as stories from './Dropdown.stories'

# Dropdown

Displays a list of options for the user to pick from—triggered by a button.
Dropdown is an interactive element
that allows users to select an option from a list of choices presented in a collapsible menu.
It saves space on the interface by concealing the options until the user interacts with the component.

Displays a **closed** list of options for the user to pick triggered by a button.

#### Dropdown is not a ComboBox

<Callout kind="warning">
Dropdowns and Comboboxes are similar in that every both provide visual selectable options for
users. However, there are important distinctions to be aware of.
</Callout>

#### Dropdown, Select and Combobox

Dropdown, Select and Combobox are visually similar when its options are not displayed
(unfocused closed trigger element).
All of them also share the same behavior on its options/items, toggling its own item's
value to its parent selected state.

However, they have different behaviors and purposes:

- Dropdown:
- **Closed** list of options for the user to pick triggered by a button.
- The trigger element is rolled as a **button**.
- The options are displayed fully customizable inside a **popover** (normally).
- Select:
- **Closed** list of options for the user to pick triggered by a button.
- The structural select role, an abstract role, is superclass role for four form widgets, `listbox`, `menu`,
`radiogroup`, and `tree`, which allow users to make selections from a set of choices.
- The options can only be text and are its look and feel is managed by the browser natively.
- Cannot be used for multiple selections.
- Combobox
- **Open** list of options for the user to pick triggered by a button.
- The trigger element is rolled as an **input**.
- The options can be text or any other JSX markup.
- Can be used for multiple selections.
- Can be used inside a form.

## Install

Expand Down
10 changes: 6 additions & 4 deletions packages/components/dropdown/src/DropdownDivider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { cx } from 'class-variance-authority'
import { forwardRef, type Ref } from 'react'

interface DividerProps {
className?: string
}

export const Divider = ({ className }: DividerProps) => {
return <div className={cx('my-md border-b-sm border-outline', className)} />
}
export const Divider = forwardRef(
({ className }: DividerProps, forwardedRef: Ref<HTMLDivElement>) => {
return <div ref={forwardedRef} className={cx('my-md border-b-sm border-outline', className)} />
}
)

Divider.id = 'Divider'
Divider.displayName = 'Dropdown.Divider'
39 changes: 22 additions & 17 deletions packages/components/dropdown/src/DropdownGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cx } from 'class-variance-authority'
import { ReactNode } from 'react'
import { forwardRef, ReactNode, type Ref } from 'react'

import { DropdownGroupProvider, useDropdownGroupContext } from './DropdownItemsGroupContext'

Expand All @@ -8,23 +8,28 @@ interface GroupProps {
className?: string
}

export const Group = ({ children, ...props }: GroupProps) => {
return (
<DropdownGroupProvider>
<GroupContent {...props}>{children}</GroupContent>
</DropdownGroupProvider>
)
}
export const Group = forwardRef(
({ children, ...props }: GroupProps, forwardedRef: Ref<HTMLDivElement>) => {
return (
<DropdownGroupProvider>
<GroupContent ref={forwardedRef} {...props}>
{children}
</GroupContent>
</DropdownGroupProvider>
)
}
)

const GroupContent = ({ children, className }: GroupProps) => {
const { labelId } = useDropdownGroupContext()
const GroupContent = forwardRef(
({ children, className }: GroupProps, forwardedRef: Ref<HTMLDivElement>) => {
const { labelId } = useDropdownGroupContext()

return (
<div role="group" aria-labelledby={labelId} className={cx(className)}>
{children}
</div>
)
}
return (
<div ref={forwardedRef} role="group" aria-labelledby={labelId} className={cx(className)}>
{children}
</div>
)
}
)

Group.id = 'Group'
Group.displayName = 'Dropdown.Group'
83 changes: 46 additions & 37 deletions packages/components/dropdown/src/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cx } from 'class-variance-authority'
import { ReactNode } from 'react'
import { forwardRef, ReactNode, type Ref } from 'react'

import { useDropdownContext } from './DropdownContext'
import { DropdownItemProvider, useDropdownItemContext } from './DropdownItemContext'
Expand All @@ -11,41 +11,50 @@ export interface ItemProps {
className?: string
}

export const Item = ({ children, ...props }: ItemProps) => {
const { value, disabled } = props
export const Item = forwardRef(
({ children, ...props }: ItemProps, forwardedRef: Ref<HTMLLIElement>) => {
const { value, disabled } = props

return (
<DropdownItemProvider value={value} disabled={disabled}>
<ItemContent ref={forwardedRef} {...props}>
{children}
</ItemContent>
</DropdownItemProvider>
)
}
)

const ItemContent = forwardRef(
(
{ className, disabled = false, value, children }: ItemProps,
forwardedRef: Ref<HTMLLIElement>
) => {
const { getItemProps, highlightedItem, lastInteractionType } = useDropdownContext()

const { textId, index, itemData, isSelected } = useDropdownItemContext()

const isHighlighted = highlightedItem?.value === value

return (
<li
ref={forwardedRef}
className={cx(
isHighlighted && (lastInteractionType === 'mouse' ? 'bg-surface-hovered' : 'u-ring'),
isSelected && 'font-bold',
disabled && 'opacity-dim-3',
'px-lg py-md text-body-1',
className
)}
key={value}
{...getItemProps({ item: itemData, index })}
aria-selected={isSelected}
aria-labelledby={textId}
>
{children}
</li>
)
}
)

return (
<DropdownItemProvider value={value} disabled={disabled}>
<ItemContent {...props}>{children}</ItemContent>
</DropdownItemProvider>
)
}

const ItemContent = ({ className, disabled = false, value, children }: ItemProps) => {
const { getItemProps, highlightedItem, lastInteractionType } = useDropdownContext()

const { textId, index, itemData, isSelected } = useDropdownItemContext()

const isHighlighted = highlightedItem?.value === value

return (
<li
className={cx(
isHighlighted && (lastInteractionType === 'mouse' ? 'bg-surface-hovered' : 'u-ring'),
isSelected && 'font-bold',
disabled && 'opacity-dim-3',
'px-lg py-md text-body-1',
className
)}
key={value}
{...getItemProps({ item: itemData, index })}
aria-selected={isSelected}
aria-labelledby={textId}
>
{children}
</li>
)
}

Item.id = 'Item'
Item.displayName = 'Dropdown.Item'
30 changes: 17 additions & 13 deletions packages/components/dropdown/src/DropdownItemIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Check } from '@spark-ui/icons/dist/icons/Check'
import { cx } from 'class-variance-authority'
import { ReactNode } from 'react'
import { forwardRef, ReactNode, type Ref } from 'react'

import { useDropdownContext } from './DropdownContext'
import { useDropdownItemContext } from './DropdownItemContext'
Expand All @@ -11,18 +11,22 @@ export interface ItemIndicatorProps {
label?: string
}

export const ItemIndicator = ({ className, children, label }: ItemIndicatorProps) => {
const { disabled, isSelected } = useDropdownItemContext()
const { multiple } = useDropdownContext()
const childElement =
children || (multiple ? <Check aria-label={label} /> : <span aria-label={label}>✓</span>)
export const ItemIndicator = forwardRef(
({ className, children, label }: ItemIndicatorProps, forwardedRef: Ref<HTMLSpanElement>) => {
const { disabled, isSelected } = useDropdownItemContext()
const { multiple } = useDropdownContext()
const childElement =
children || (multiple ? <Check aria-label={label} /> : <span aria-label={label}>✓</span>)

return (
<span className={cx('flex min-h-sz-16 min-w-sz-16', disabled && 'opacity-dim-3', className)}>
{isSelected && childElement}
</span>
)
}
return (
<span
ref={forwardedRef}
className={cx('flex min-h-sz-16 min-w-sz-16', disabled && 'opacity-dim-3', className)}
>
{isSelected && childElement}
</span>
)
}
)

ItemIndicator.id = 'ItemIndicator'
ItemIndicator.displayName = 'Dropdown.ItemIndicator'
31 changes: 16 additions & 15 deletions packages/components/dropdown/src/DropdownItemText.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
import { useId } from '@radix-ui/react-id'
import { cx } from 'class-variance-authority'
import { useEffect } from 'react'
import { forwardRef, type Ref, useEffect } from 'react'

import { useDropdownItemContext } from './DropdownItemContext'

export interface ItemTextProps {
children: string
}

export const ItemText = ({ children }: ItemTextProps) => {
const id = useId()
export const ItemText = forwardRef(
({ children }: ItemTextProps, forwardedRef: Ref<HTMLSpanElement>) => {
const id = useId()

const { setTextId } = useDropdownItemContext()
const { setTextId } = useDropdownItemContext()

useEffect(() => {
setTextId(id)
useEffect(() => {
setTextId(id)

return () => setTextId(undefined)
})
return () => setTextId(undefined)
})

return (
<span id={id} className={cx('inline')}>
{children}
</span>
)
}
return (
<span id={id} className={cx('inline')} ref={forwardedRef}>
{children}
</span>
)
}
)

ItemText.id = 'ItemText'
ItemText.displayName = 'Dropdown.ItemText'
51 changes: 27 additions & 24 deletions packages/components/dropdown/src/DropdownItems.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cx } from 'class-variance-authority'
import React, { ReactNode } from 'react'
import { forwardRef, ReactNode, type Ref } from 'react'

import { useDropdownContext } from './DropdownContext'

Expand All @@ -8,30 +8,33 @@ interface ItemsProps {
className?: string
}

export const Items = React.forwardRef(({ children, className, ...props }: ItemsProps, ref) => {
const { isOpen, getMenuProps, hasPopover, setLastInteractionType } = useDropdownContext()
export const Items = forwardRef(
({ children, className, ...props }: ItemsProps, forwardedRef: Ref<HTMLUListElement>) => {
const { isOpen, getMenuProps, hasPopover, setLastInteractionType } = useDropdownContext()

const downshiftProps = getMenuProps({
onMouseMove: () => {
setLastInteractionType('mouse')
},
})
const downshiftProps = getMenuProps({
onMouseMove: () => {
setLastInteractionType('mouse')
},
})

return (
<ul
ref={ref}
className={cx(
className,
'flex flex-col',
isOpen ? 'block' : 'pointer-events-none opacity-0',
hasPopover && 'p-lg'
)}
{...props}
{...downshiftProps}
>
{children}
</ul>
)
})
return (
<ul
ref={forwardedRef}
className={cx(
className,
'flex flex-col',
isOpen ? 'block' : 'pointer-events-none opacity-0',
hasPopover && 'p-lg'
)}
{...props}
{...downshiftProps}
data-spark-component="dropdown-items"
>
{children}
</ul>
)
}
)

Items.displayName = 'Dropdown.Items'
Loading
Loading