diff --git a/package-lock.json b/package-lock.json
index b04638c0d..61f731c71 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2384,6 +2384,7 @@
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
+ "extraneous": true,
"inBundle": true,
"license": "MIT",
"engines": {
diff --git a/packages/components/dropdown/src/Dropdown.doc.mdx b/packages/components/dropdown/src/Dropdown.doc.mdx
index 6049f7499..14e27f431 100644
--- a/packages/components/dropdown/src/Dropdown.doc.mdx
+++ b/packages/components/dropdown/src/Dropdown.doc.mdx
@@ -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 '.'
@@ -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
+
+
+ Dropdowns and Comboboxes are similar in that every both provide visual selectable options for
+ users. However, there are important distinctions to be aware of.
+
+
+#### 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
diff --git a/packages/components/dropdown/src/DropdownDivider.tsx b/packages/components/dropdown/src/DropdownDivider.tsx
index dbe7df964..3653cdbf9 100644
--- a/packages/components/dropdown/src/DropdownDivider.tsx
+++ b/packages/components/dropdown/src/DropdownDivider.tsx
@@ -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
-}
+export const Divider = forwardRef(
+ ({ className }: DividerProps, forwardedRef: Ref) => {
+ return
+ }
+)
-Divider.id = 'Divider'
Divider.displayName = 'Dropdown.Divider'
diff --git a/packages/components/dropdown/src/DropdownGroup.tsx b/packages/components/dropdown/src/DropdownGroup.tsx
index c5f58c9f7..1cc1aee6c 100644
--- a/packages/components/dropdown/src/DropdownGroup.tsx
+++ b/packages/components/dropdown/src/DropdownGroup.tsx
@@ -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'
@@ -8,23 +8,28 @@ interface GroupProps {
className?: string
}
-export const Group = ({ children, ...props }: GroupProps) => {
- return (
-
- {children}
-
- )
-}
+export const Group = forwardRef(
+ ({ children, ...props }: GroupProps, forwardedRef: Ref) => {
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
-const GroupContent = ({ children, className }: GroupProps) => {
- const { labelId } = useDropdownGroupContext()
+const GroupContent = forwardRef(
+ ({ children, className }: GroupProps, forwardedRef: Ref) => {
+ const { labelId } = useDropdownGroupContext()
- return (
-
- {children}
-
- )
-}
+ return (
+
+ {children}
+
+ )
+ }
+)
-Group.id = 'Group'
Group.displayName = 'Dropdown.Group'
diff --git a/packages/components/dropdown/src/DropdownItem.tsx b/packages/components/dropdown/src/DropdownItem.tsx
index 256589257..a2823939c 100644
--- a/packages/components/dropdown/src/DropdownItem.tsx
+++ b/packages/components/dropdown/src/DropdownItem.tsx
@@ -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'
@@ -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) => {
+ const { value, disabled } = props
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+
+const ItemContent = forwardRef(
+ (
+ { className, disabled = false, value, children }: ItemProps,
+ forwardedRef: Ref
+ ) => {
+ const { getItemProps, highlightedItem, lastInteractionType } = useDropdownContext()
+
+ const { textId, index, itemData, isSelected } = useDropdownItemContext()
+
+ const isHighlighted = highlightedItem?.value === value
+
+ return (
+
+ {children}
+
+ )
+ }
+)
- return (
-
- {children}
-
- )
-}
-
-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 (
-
- {children}
-
- )
-}
-
-Item.id = 'Item'
Item.displayName = 'Dropdown.Item'
diff --git a/packages/components/dropdown/src/DropdownItemIndicator.tsx b/packages/components/dropdown/src/DropdownItemIndicator.tsx
index 44d0cb507..2668403f8 100644
--- a/packages/components/dropdown/src/DropdownItemIndicator.tsx
+++ b/packages/components/dropdown/src/DropdownItemIndicator.tsx
@@ -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'
@@ -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 ? : ✓ )
+export const ItemIndicator = forwardRef(
+ ({ className, children, label }: ItemIndicatorProps, forwardedRef: Ref) => {
+ const { disabled, isSelected } = useDropdownItemContext()
+ const { multiple } = useDropdownContext()
+ const childElement =
+ children || (multiple ? : ✓ )
- return (
-
- {isSelected && childElement}
-
- )
-}
+ return (
+
+ {isSelected && childElement}
+
+ )
+ }
+)
-ItemIndicator.id = 'ItemIndicator'
ItemIndicator.displayName = 'Dropdown.ItemIndicator'
diff --git a/packages/components/dropdown/src/DropdownItemText.tsx b/packages/components/dropdown/src/DropdownItemText.tsx
index 94324394f..8a308c543 100644
--- a/packages/components/dropdown/src/DropdownItemText.tsx
+++ b/packages/components/dropdown/src/DropdownItemText.tsx
@@ -1,6 +1,6 @@
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'
@@ -8,23 +8,24 @@ export interface ItemTextProps {
children: string
}
-export const ItemText = ({ children }: ItemTextProps) => {
- const id = useId()
+export const ItemText = forwardRef(
+ ({ children }: ItemTextProps, forwardedRef: Ref) => {
+ const id = useId()
- const { setTextId } = useDropdownItemContext()
+ const { setTextId } = useDropdownItemContext()
- useEffect(() => {
- setTextId(id)
+ useEffect(() => {
+ setTextId(id)
- return () => setTextId(undefined)
- })
+ return () => setTextId(undefined)
+ })
- return (
-
- {children}
-
- )
-}
+ return (
+
+ {children}
+
+ )
+ }
+)
-ItemText.id = 'ItemText'
ItemText.displayName = 'Dropdown.ItemText'
diff --git a/packages/components/dropdown/src/DropdownItems.tsx b/packages/components/dropdown/src/DropdownItems.tsx
index e768b5650..7a32ae94d 100644
--- a/packages/components/dropdown/src/DropdownItems.tsx
+++ b/packages/components/dropdown/src/DropdownItems.tsx
@@ -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'
@@ -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) => {
+ const { isOpen, getMenuProps, hasPopover, setLastInteractionType } = useDropdownContext()
- const downshiftProps = getMenuProps({
- onMouseMove: () => {
- setLastInteractionType('mouse')
- },
- })
+ const downshiftProps = getMenuProps({
+ onMouseMove: () => {
+ setLastInteractionType('mouse')
+ },
+ })
- return (
-
- )
-})
+ return (
+
+ )
+ }
+)
Items.displayName = 'Dropdown.Items'
diff --git a/packages/components/dropdown/src/DropdownLabel.tsx b/packages/components/dropdown/src/DropdownLabel.tsx
index 3ce853466..87052bbc4 100644
--- a/packages/components/dropdown/src/DropdownLabel.tsx
+++ b/packages/components/dropdown/src/DropdownLabel.tsx
@@ -1,4 +1,5 @@
import { cx } from 'class-variance-authority'
+import { forwardRef, type Ref } from 'react'
import { useDropdownGroupContext } from './DropdownItemsGroupContext'
@@ -7,15 +8,20 @@ interface LabelProps {
className?: string
}
-export const Label = ({ children, className }: LabelProps) => {
- const { labelId } = useDropdownGroupContext()
+export const Label = forwardRef(
+ ({ children, className }: LabelProps, forwardedRef: Ref) => {
+ const { labelId } = useDropdownGroupContext()
- return (
-
- {children}
-
- )
-}
+ return (
+
+ {children}
+
+ )
+ }
+)
-Label.id = 'Label'
Label.displayName = 'Dropdown.Label'
diff --git a/packages/components/dropdown/src/DropdownLeadingIcon.tsx b/packages/components/dropdown/src/DropdownLeadingIcon.tsx
index 19fbf636b..1966ef549 100644
--- a/packages/components/dropdown/src/DropdownLeadingIcon.tsx
+++ b/packages/components/dropdown/src/DropdownLeadingIcon.tsx
@@ -9,5 +9,4 @@ export const LeadingIcon = ({ children }: { children: ReactElement }) => {
)
}
-LeadingIcon.id = 'LeadingIcon'
LeadingIcon.displayName = 'Dropdown.LeadingIcon'
diff --git a/packages/components/dropdown/src/DropdownPopover.tsx b/packages/components/dropdown/src/DropdownPopover.tsx
index 03a93e12d..14582586e 100644
--- a/packages/components/dropdown/src/DropdownPopover.tsx
+++ b/packages/components/dropdown/src/DropdownPopover.tsx
@@ -1,38 +1,44 @@
import { Popover as SparkPopover } from '@spark-ui/popover'
import { cx } from 'class-variance-authority'
-import { ComponentProps, useEffect } from 'react'
+import { ComponentProps, forwardRef, Ref, useEffect } from 'react'
import { useDropdownContext } from './DropdownContext'
-export const Popover = ({
- children,
- matchTriggerWidth = true,
- sideOffset = 4,
- ...props
-}: ComponentProps) => {
- const { isOpen, hasPopover, setHasPopover } = useDropdownContext()
+export const Popover = forwardRef(
+ (
+ {
+ children,
+ matchTriggerWidth = true,
+ sideOffset = 4,
+ ...props
+ }: ComponentProps,
+ forwardedRef: Ref
+ ) => {
+ const { isOpen, hasPopover, setHasPopover } = useDropdownContext()
- useEffect(() => {
- setHasPopover(true)
+ useEffect(() => {
+ setHasPopover(true)
- return () => setHasPopover(false)
- }, [setHasPopover])
+ return () => setHasPopover(false)
+ }, [setHasPopover])
- if (!hasPopover) return children
+ if (!hasPopover) return children
- return (
-
- {children}
-
- )
-}
+ return (
+
+ {children}
+
+ )
+ }
+)
-Popover.id = 'Popover'
Popover.displayName = 'Dropdown.Popover'
diff --git a/packages/components/dropdown/src/DropdownTrigger.tsx b/packages/components/dropdown/src/DropdownTrigger.tsx
index 73f476239..962665518 100644
--- a/packages/components/dropdown/src/DropdownTrigger.tsx
+++ b/packages/components/dropdown/src/DropdownTrigger.tsx
@@ -3,7 +3,7 @@ import { ArrowHorizontalDown } from '@spark-ui/icons/dist/icons/ArrowHorizontalD
import { Popover } from '@spark-ui/popover'
import { VisuallyHidden } from '@spark-ui/visually-hidden'
import { cva } from 'class-variance-authority'
-import { Fragment, ReactNode } from 'react'
+import { forwardRef, Fragment, ReactNode, type Ref } from 'react'
import { useDropdownContext } from './DropdownContext'
import { DropdownStateIndicator } from './DropdownStateIndicator'
@@ -33,51 +33,57 @@ const styles = cva(
}
)
-export const Trigger = ({ 'aria-label': ariaLabel, children, className }: TriggerProps) => {
- const {
- getToggleButtonProps,
- getDropdownProps,
- getLabelProps,
- hasPopover,
- state,
- setLastInteractionType,
- } = useDropdownContext()
+export const Trigger = forwardRef(
+ (
+ { 'aria-label': ariaLabel, children, className }: TriggerProps,
+ forwardedRef: Ref
+ ) => {
+ const {
+ getToggleButtonProps,
+ getDropdownProps,
+ getLabelProps,
+ hasPopover,
+ state,
+ setLastInteractionType,
+ } = useDropdownContext()
- const [WrapperComponent, wrapperProps] = hasPopover
- ? [Popover.Trigger, { asChild: true }]
- : [Fragment, {}]
+ const [WrapperComponent, wrapperProps] = hasPopover
+ ? [Popover.Trigger, { asChild: true }]
+ : [Fragment, {}]
- return (
- <>
- {ariaLabel && (
-
- {ariaLabel}
-
- )}
-
- {
- setLastInteractionType('keyboard')
- },
- })}
- >
- {children}
+ return (
+ <>
+ {ariaLabel && (
+
+ {ariaLabel}
+
+ )}
+
+ {
+ setLastInteractionType('keyboard')
+ },
+ })}
+ data-spark-component="dropdown-trigger"
+ >
+ {children}
-
-
-
- >
- )
-}
+
+
+
+ >
+ )
+ }
+)
-Trigger.id = 'Trigger'
Trigger.displayName = 'Dropdown.Trigger'
diff --git a/packages/components/dropdown/src/DropdownValue.tsx b/packages/components/dropdown/src/DropdownValue.tsx
index 43f97629f..0f76f4f98 100644
--- a/packages/components/dropdown/src/DropdownValue.tsx
+++ b/packages/components/dropdown/src/DropdownValue.tsx
@@ -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'
@@ -9,22 +9,23 @@ export interface ValueProps {
placeholder: string
}
-export const Value = ({ children, className, placeholder }: ValueProps) => {
- const { selectedItem, multiple, selectedItems } = useDropdownContext()
+export const Value = forwardRef(
+ ({ children, className, placeholder }: ValueProps, forwardedRef: Ref) => {
+ const { selectedItem, multiple, selectedItems } = useDropdownContext()
- const hasSelectedItems = !!(multiple ? selectedItems.length : selectedItem)
- const text = multiple ? selectedItems[0]?.text : selectedItem?.text
- const suffix = selectedItems.length > 1 ? `, +${selectedItems.length - 1}` : ''
+ const hasSelectedItems = !!(multiple ? selectedItems.length : selectedItem)
+ const text = multiple ? selectedItems[0]?.text : selectedItem?.text
+ const suffix = selectedItems.length > 1 ? `, +${selectedItems.length - 1}` : ''
- return (
-
-
- {!hasSelectedItems ? placeholder : children || text}
+ return (
+
+
+ {!hasSelectedItems ? placeholder : children || text}
+
+ {suffix && {suffix} }
- {suffix && {suffix} }
-
- )
-}
+ )
+ }
+)
-Value.id = 'Value'
Value.displayName = 'Dropdown.Value'
diff --git a/packages/components/dropdown/src/utils.ts b/packages/components/dropdown/src/utils.ts
index f0e4f0729..c3e2d562e 100644
--- a/packages/components/dropdown/src/utils.ts
+++ b/packages/components/dropdown/src/utils.ts
@@ -31,8 +31,8 @@ export const getElementByIndex = (map: ItemsMap, index: number) => {
return key !== undefined ? map.get(key) : undefined
}
-const getElementId = (element?: ReactElement) => {
- return element ? (element.type as FC & { id?: string }).id : ''
+const getElementDisplayName = (element?: ReactElement) => {
+ return element ? (element.type as FC & { displayName?: string }).displayName : ''
}
export const getOrderedItems = (
@@ -42,7 +42,7 @@ export const getOrderedItems = (
React.Children.forEach(children, child => {
if (!isValidElement(child)) return
- if (getElementId(child) === 'Item') {
+ if (getElementDisplayName(child) === 'Dropdown.Item') {
const childProps = child.props as ItemProps
result.push({
value: childProps.value,
@@ -72,7 +72,7 @@ export const getItemText = (children: ReactNode, itemText = ''): string => {
React.Children.forEach(children, child => {
if (!isValidElement(child)) return
- if (getElementId(child) === 'ItemText') {
+ if (getElementDisplayName(child) === 'Dropdown.ItemText') {
itemText = child.props.children
}