From 1f7f2588040785ae88acb1658bae807a5dc8be3c Mon Sep 17 00:00:00 2001 From: Powerplex Date: Thu, 14 Dec 2023 17:43:13 +0100 Subject: [PATCH] feat(dropdown): dropdown states --- .../components/dropdown/src/Dropdown.doc.mdx | 12 ++++-- .../dropdown/src/Dropdown.stories.tsx | 33 ++++++++++++++- .../dropdown/src/DropdownContext.tsx | 7 ++++ .../dropdown/src/DropdownStateIndicator.tsx | 28 +++++++++++++ .../dropdown/src/DropdownTrigger.tsx | 40 ++++++++++++++----- 5 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 packages/components/dropdown/src/DropdownStateIndicator.tsx diff --git a/packages/components/dropdown/src/Dropdown.doc.mdx b/packages/components/dropdown/src/Dropdown.doc.mdx index b7726da1f7..3ddfc843fb 100644 --- a/packages/components/dropdown/src/Dropdown.doc.mdx +++ b/packages/components/dropdown/src/Dropdown.doc.mdx @@ -124,16 +124,22 @@ You can style this element directly, or you can use it as a wrapper to put an ic -### Trigger leading icon +### States -Use `Dropdown.LeadingIcon` inside `Dropdown.Trigger` to prefix your trigger with an icon. +Use `state` prop to assign a specific state to the dropdown, choosing from: `error`, `alert` and `success`. By doing so, the outline styles will be updated, and a state indicator will be displayed accordingly. - + ### Read only TODO +### Trigger leading icon + +Use `Dropdown.LeadingIcon` inside `Dropdown.Trigger` to prefix your trigger with an icon. + + + ## Multiple selection When using `multiple` mode, the component manages an array of values and no longer a single value. diff --git a/packages/components/dropdown/src/Dropdown.stories.tsx b/packages/components/dropdown/src/Dropdown.stories.tsx index 04b19785bd..3281ecd64d 100644 --- a/packages/components/dropdown/src/Dropdown.stories.tsx +++ b/packages/components/dropdown/src/Dropdown.stories.tsx @@ -4,7 +4,7 @@ import { FormField } from '@spark-ui/form-field' import { BookmarkFill } from '@spark-ui/icons/dist/icons/BookmarkFill' import { Tag } from '@spark-ui/tag' import { Meta, StoryFn } from '@storybook/react' -import { useState } from 'react' +import { ComponentProps, useState } from 'react' import { Dropdown } from '.' @@ -266,6 +266,37 @@ export const ItemIndicator: StoryFn = _args => { ) } +export const States: StoryFn = () => { + type State = ComponentProps['state'] + + const states: State[] = ['error', 'alert', 'success'] + + return ( +
+ {states.map(state => { + return ( + + + + + + + + To Kill a Mockingbird + War and Peace + The Idiot + A Picture of Dorian Gray + 1984 + Pride and Prejudice + + + + ) + })} +
+ ) +} + export const FormFieldLabel: StoryFn = _args => { return (
diff --git a/packages/components/dropdown/src/DropdownContext.tsx b/packages/components/dropdown/src/DropdownContext.tsx index ec6589e0d9..e309d12084 100644 --- a/packages/components/dropdown/src/DropdownContext.tsx +++ b/packages/components/dropdown/src/DropdownContext.tsx @@ -21,6 +21,7 @@ export interface DropdownContextState extends DownshiftState { hasPopover: boolean setHasPopover: Dispatch> multiple: boolean + state?: 'error' | 'alert' | 'success' } export type DropdownContextCommonProps = PropsWithChildren<{ @@ -36,6 +37,10 @@ export type DropdownContextCommonProps = PropsWithChildren<{ * The open state of the select when it is initially rendered. Use when you do not need to control its open state. */ defaultOpen?: boolean + /** + * Use `state` prop to assign a specific state to the dropdown, choosing from: `error`, `alert` and `success`. By doing so, the outline styles will be updated, and a state indicator will be displayed accordingly. + */ + state?: 'error' | 'alert' | 'success' }> interface DropdownPropsSingle { @@ -92,6 +97,7 @@ export const DropdownProvider = ({ onOpenChange, defaultOpen, multiple = false, + state, }: DropdownContextProps) => { const [itemsMap, setItemsMap] = useState(getItemsFromChildren(children)) const [hasPopover, setHasPopover] = useState(false) @@ -226,6 +232,7 @@ export const DropdownProvider = ({ highlightedItem: getElementByIndex(itemsMap, downshift.highlightedIndex), hasPopover, setHasPopover, + state, }} > {children} diff --git a/packages/components/dropdown/src/DropdownStateIndicator.tsx b/packages/components/dropdown/src/DropdownStateIndicator.tsx new file mode 100644 index 0000000000..b174d57d17 --- /dev/null +++ b/packages/components/dropdown/src/DropdownStateIndicator.tsx @@ -0,0 +1,28 @@ +import { Icon } from '@spark-ui/icon' +import { AlertOutline } from '@spark-ui/icons/dist/icons/AlertOutline' +import { Check } from '@spark-ui/icons/dist/icons/Check' +import { WarningOutline } from '@spark-ui/icons/dist/icons/WarningOutline' +import { cx } from 'class-variance-authority' + +import { useDropdownContext } from './DropdownContext' + +const icons = { + error: , + alert: , + success: , +} + +export const DropdownStateIndicator = () => { + const { state } = useDropdownContext() + + if (!state) return null + + return ( + + {icons[state]} + + ) +} + +DropdownStateIndicator.id = 'StateIndicator' +DropdownStateIndicator.displayName = 'Dropdown.StateIndicator' diff --git a/packages/components/dropdown/src/DropdownTrigger.tsx b/packages/components/dropdown/src/DropdownTrigger.tsx index b9e80e5bb3..927d9c52a2 100644 --- a/packages/components/dropdown/src/DropdownTrigger.tsx +++ b/packages/components/dropdown/src/DropdownTrigger.tsx @@ -2,10 +2,11 @@ import { Icon } from '@spark-ui/icon' import { ArrowHorizontalDown } from '@spark-ui/icons/dist/icons/ArrowHorizontalDown' import { Popover } from '@spark-ui/popover' import { VisuallyHidden } from '@spark-ui/visually-hidden' -import { cx } from 'class-variance-authority' +import { cva } from 'class-variance-authority' import { Fragment, ReactNode } from 'react' import { useDropdownContext } from './DropdownContext' +import { DropdownStateIndicator } from './DropdownStateIndicator' interface TriggerProps { 'aria-label'?: string @@ -13,8 +14,28 @@ interface TriggerProps { className?: string } +const styles = cva( + [ + 'flex w-full cursor-pointer items-center justify-between', + 'min-h-sz-44 rounded-lg bg-surface px-lg', + // outline styles + 'ring-1 outline-none ring-inset focus:ring-2', + ], + { + variants: { + state: { + undefined: 'ring-outline focus:ring-outline-high hover:ring-outline-high', + error: 'ring-error', + alert: 'ring-alert', + success: 'ring-success', + }, + }, + } +) + export const Trigger = ({ 'aria-label': ariaLabel, children, className }: TriggerProps) => { - const { getToggleButtonProps, getDropdownProps, getLabelProps, hasPopover } = useDropdownContext() + const { getToggleButtonProps, getDropdownProps, getLabelProps, hasPopover, state } = + useDropdownContext() const [WrapperComponent, wrapperProps] = hasPopover ? [Popover.Trigger, { asChild: true }] @@ -30,18 +51,17 @@ export const Trigger = ({ 'aria-label': ariaLabel, children, className }: Trigge