From fbbc9e374d9f97b73b6bb840467793a19d98d7ca Mon Sep 17 00:00:00 2001 From: andresin87 Date: Wed, 29 Nov 2023 16:47:53 +0100 Subject: [PATCH] feat(dropdown): add leadingIcon --- package-lock.json | 1 + packages/components/dropdown/package.json | 1 + packages/components/dropdown/src/Dropdown.doc.mdx | 4 ++++ .../components/dropdown/src/Dropdown.stories.tsx | 4 ++++ .../dropdown/src/DropdownLeadingIcon.tsx | 9 +++++++++ .../components/dropdown/src/DropdownTrigger.tsx | 10 +++++++++- packages/components/dropdown/src/index.ts | 4 ++++ packages/components/dropdown/src/utils.ts | 14 +++++++++++++- 8 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 packages/components/dropdown/src/DropdownLeadingIcon.tsx diff --git a/package-lock.json b/package-lock.json index 5e22f8196..8afcda1fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34260,6 +34260,7 @@ "dependencies": { "@radix-ui/react-id": "1.0.1", "@spark-ui/form-field": "^1.4.0", + "@spark-ui/icon": "^2.1.0", "@spark-ui/popover": "^1.2.7", "@spark-ui/visually-hidden": "^1.2.0", "class-variance-authority": "0.7.0", diff --git a/packages/components/dropdown/package.json b/packages/components/dropdown/package.json index a29ba460a..f586225fb 100644 --- a/packages/components/dropdown/package.json +++ b/packages/components/dropdown/package.json @@ -30,6 +30,7 @@ "dependencies": { "@radix-ui/react-id": "1.0.1", "@spark-ui/form-field": "^1.4.0", + "@spark-ui/icon": "^2.1.0", "@spark-ui/popover": "^1.2.7", "@spark-ui/visually-hidden": "^1.2.0", "class-variance-authority": "0.7.0", diff --git a/packages/components/dropdown/src/Dropdown.doc.mdx b/packages/components/dropdown/src/Dropdown.doc.mdx index bb95e1262..c62def7ce 100644 --- a/packages/components/dropdown/src/Dropdown.doc.mdx +++ b/packages/components/dropdown/src/Dropdown.doc.mdx @@ -39,6 +39,10 @@ import { Dropdown } from '@spark-ui/dropdown' description: "The part that reflects the selected value. By default the selected item's text will be rendered. if you require more control, you can instead control the select and pass your own children. It should not be styled to ensure correct positioning. An optional placeholder prop is also available for when the select has no value.", }, + 'Dropdown.LeadingIcon': { + of: Dropdown.LeadingIcon, + description: "Prepend a decorative icon inside the input (to the left).", + }, 'Dropdown.Popover': { of: Dropdown.Popover, description: 'The part that is toggled and portaled when the trigger element is clicked.', diff --git a/packages/components/dropdown/src/Dropdown.stories.tsx b/packages/components/dropdown/src/Dropdown.stories.tsx index cf153daf5..5bb5f1f4b 100644 --- a/packages/components/dropdown/src/Dropdown.stories.tsx +++ b/packages/components/dropdown/src/Dropdown.stories.tsx @@ -1,5 +1,6 @@ import { Button } from '@spark-ui/button' import { FormField } from '@spark-ui/form-field' +import { Book } from '@spark-ui/icons/dist/icons/Book' import { Tag } from '@spark-ui/tag' import { Meta, StoryFn } from '@storybook/react' import { useState } from 'react' @@ -18,6 +19,9 @@ export const Default: StoryFn = _args => {
+ + + diff --git a/packages/components/dropdown/src/DropdownLeadingIcon.tsx b/packages/components/dropdown/src/DropdownLeadingIcon.tsx new file mode 100644 index 000000000..28c4883c3 --- /dev/null +++ b/packages/components/dropdown/src/DropdownLeadingIcon.tsx @@ -0,0 +1,9 @@ +import { Icon } from '@spark-ui/icon' +import { ReactElement } from 'react' + +export const LeadingIcon = ({ children }: { children: ReactElement }) => { + return {children} +} + +LeadingIcon.id = 'LeadingIcon' +LeadingIcon.displayName = 'Dropdown.LeadingIcon' diff --git a/packages/components/dropdown/src/DropdownTrigger.tsx b/packages/components/dropdown/src/DropdownTrigger.tsx index ead81571f..34f385bc9 100644 --- a/packages/components/dropdown/src/DropdownTrigger.tsx +++ b/packages/components/dropdown/src/DropdownTrigger.tsx @@ -4,6 +4,7 @@ import { cx } from 'class-variance-authority' import { Fragment, ReactNode } from 'react' import { useDropdownContext } from './DropdownContext' +import { findElement } from './utils' interface TriggerProps { 'aria-label'?: string @@ -18,6 +19,10 @@ export const Trigger = ({ 'aria-label': ariaLabel, children, className }: Trigge ? [Popover.Trigger, { asChild: true }] : [Fragment, {}] + const finder = findElement(children) + const leadingIcon = finder('LeadingIcon') + const value = finder('Value') + return ( <> {ariaLabel && ( @@ -34,7 +39,10 @@ export const Trigger = ({ 'aria-label': ariaLabel, children, className }: Trigge )} {...getToggleButtonProps()} > - {children} + + {leadingIcon} + {value} + {isOpen ? <>↑ : <>↓} diff --git a/packages/components/dropdown/src/index.ts b/packages/components/dropdown/src/index.ts index f10dbc26c..4f071f298 100644 --- a/packages/components/dropdown/src/index.ts +++ b/packages/components/dropdown/src/index.ts @@ -8,6 +8,7 @@ import { Item } from './DropdownItem' import { Items } from './DropdownItems' import { ItemText } from './DropdownItemText' import { Label } from './DropdownLabel' +import { LeadingIcon } from './DropdownLeadingIcon' import { Popover } from './DropdownPopover' import { Trigger } from './DropdownTrigger' import { Value } from './DropdownValue' @@ -24,6 +25,7 @@ export const Dropdown: FC & { Divider: typeof Divider Trigger: typeof Trigger Value: typeof Value + LeadingIcon: typeof LeadingIcon } = Object.assign(Root, { Group, Item, @@ -34,6 +36,7 @@ export const Dropdown: FC & { Divider, Trigger, Value, + LeadingIcon, }) Dropdown.displayName = 'Dropdown' @@ -46,3 +49,4 @@ Popover.displayName = 'Dropdown.Popover' Divider.displayName = 'Dropdown.Divider' Trigger.displayName = 'Dropdown.Trigger' Value.displayName = 'Dropdown.Value' +LeadingIcon.displayName = 'Dropdown.LeadingIcon' diff --git a/packages/components/dropdown/src/utils.ts b/packages/components/dropdown/src/utils.ts index f0e4f0729..0a97936ea 100644 --- a/packages/components/dropdown/src/utils.ts +++ b/packages/components/dropdown/src/utils.ts @@ -1,4 +1,4 @@ -import React, { type FC, isValidElement, type ReactElement, type ReactNode } from 'react' +import React, { Children, type FC, isValidElement, type ReactElement, type ReactNode } from 'react' import { type ItemProps } from './DropdownItem' import { type DropdownItem, type ItemsMap } from './types' @@ -35,6 +35,18 @@ const getElementId = (element?: ReactElement) => { return element ? (element.type as FC & { id?: string }).id : '' } +export const findElement = + (children: React.ReactNode) => + (...values: string[]) => { + const validChildren = Children.toArray(children).filter(isValidElement) + + return validChildren.find(child => { + const displayName = getElementId(child) + + return values.includes(displayName || '') + }) + } + export const getOrderedItems = ( children: ReactNode, result: DropdownItem[] = []