Skip to content

Commit

Permalink
Merge pull request #1784 from adevinta/dropdown-disabled-state
Browse files Browse the repository at this point in the history
feat(dropdown): disabled and readOnly modes
  • Loading branch information
Powerplex authored Jan 3, 2024
2 parents 14b440b + 2bbf561 commit fbd1d04
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 27 deletions.
30 changes: 24 additions & 6 deletions packages/components/dropdown/src/Dropdown.doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Use `open` and `onOpenChange` props to control when the dropdown is opened or cl

Use `disabled` on the root component to disable the dropdown entirely.

TODO
<Canvas of={stories.Disabled} />

### Disabled Item

Expand All @@ -162,6 +162,12 @@ You can style this element directly, or you can use it as a wrapper to put an ic

<Canvas of={stories.ItemIndicator} />

### Read only

Use `readOnly` prop to indicate the dropdown is only readable.

<Canvas of={stories.ReadOnly} />

### Status

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 status indicator will be displayed accordingly.
Expand All @@ -170,10 +176,6 @@ You could also wrap `Dropdown` with a `FormField` and pass the prop to `Formfiel

<Canvas of={stories.Statuses} />

### Read only

TODO

### Trigger leading icon

Use `Dropdown.LeadingIcon` inside `Dropdown.Trigger` to prefix your trigger with an icon.
Expand Down Expand Up @@ -208,6 +210,22 @@ If your `Dropdown.Item` contains anything else than raw text, you may use any JS

<Canvas of={stories.CustomItem} />

### With form field label
## Form field

### Disabled

Apply `disabled` to the wrapping `FormField` to disable the dropdown.

<Canvas of={stories.FormFieldDisabled} />

### Label

Use `FormField.Label` to add a label to the input.

<Canvas of={stories.FormFieldLabel} />

### ReadOnly

Apply `readOnly` to the wrapping `FormField` to indicate the dropdown is only readable.

<Canvas of={stories.FormFieldReadOnly} />
98 changes: 98 additions & 0 deletions packages/components/dropdown/src/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,52 @@ export const CustomItem: StoryFn = _args => {
)
}

export const Disabled: StoryFn = _args => {
return (
<div className="pb-[300px]">
<Dropdown disabled>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>

<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">To Kill a Mockingbird</Dropdown.Item>
<Dropdown.Item value="book-2">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-3">The Idiot</Dropdown.Item>
<Dropdown.Item value="book-4">A Picture of Dorian Gray</Dropdown.Item>
<Dropdown.Item value="book-5">1984</Dropdown.Item>
<Dropdown.Item value="book-6">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
</div>
)
}

export const ReadOnly: StoryFn = _args => {
return (
<div className="pb-[300px]">
<Dropdown readOnly>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>

<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">To Kill a Mockingbird</Dropdown.Item>
<Dropdown.Item value="book-2">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-3">The Idiot</Dropdown.Item>
<Dropdown.Item value="book-4">A Picture of Dorian Gray</Dropdown.Item>
<Dropdown.Item value="book-5">1984</Dropdown.Item>
<Dropdown.Item value="book-6">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
</div>
)
}

export const DisabledItem: StoryFn = _args => {
return (
<div className="pb-[300px]">
Expand Down Expand Up @@ -322,6 +368,58 @@ export const FormFieldLabel: StoryFn = _args => {
)
}

export const FormFieldDisabled: StoryFn = _args => {
return (
<div className="pb-[300px]">
<FormField disabled>
<FormField.Label>Book</FormField.Label>
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>

<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">To Kill a Mockingbird</Dropdown.Item>
<Dropdown.Item value="book-2">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-3">The Idiot</Dropdown.Item>
<Dropdown.Item value="book-4">A Picture of Dorian Gray</Dropdown.Item>
<Dropdown.Item value="book-5">1984</Dropdown.Item>
<Dropdown.Item value="book-6">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
</FormField>
</div>
)
}

export const FormFieldReadOnly: StoryFn = _args => {
return (
<div className="pb-[300px]">
<FormField readOnly>
<FormField.Label>Book</FormField.Label>
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>

<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">To Kill a Mockingbird</Dropdown.Item>
<Dropdown.Item value="book-2">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-3">The Idiot</Dropdown.Item>
<Dropdown.Item value="book-4">A Picture of Dorian Gray</Dropdown.Item>
<Dropdown.Item value="book-5">1984</Dropdown.Item>
<Dropdown.Item value="book-6">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
</FormField>
</div>
)
}

export const MultipleSelection: StoryFn = _args => {
return (
<div className="pb-[300px]">
Expand Down
18 changes: 18 additions & 0 deletions packages/components/dropdown/src/DropdownContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface DropdownContextState extends DownshiftState {
hasPopover: boolean
setHasPopover: Dispatch<SetStateAction<boolean>>
multiple: boolean
disabled: boolean
readOnly: boolean
state?: 'error' | 'alert' | 'success'
lastInteractionType: 'mouse' | 'keyboard'
setLastInteractionType: (type: 'mouse' | 'keyboard') => void
Expand All @@ -43,6 +45,14 @@ export type DropdownContextCommonProps = PropsWithChildren<{
* 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'
/**
* When true, prevents the user from interacting with the dropdown.
*/
disabled?: boolean
/**
* Sets the dropdown as interactive or not.
*/
readOnly?: boolean
}>

interface DropdownPropsSingle {
Expand Down Expand Up @@ -90,6 +100,7 @@ export type DropdownContextProps = DropdownContextCommonProps &

const DropdownContext = createContext<DropdownContextState | null>(null)

// eslint-disable-next-line complexity
export const DropdownProvider = ({
children,
defaultValue,
Expand All @@ -99,6 +110,8 @@ export const DropdownProvider = ({
onOpenChange,
defaultOpen,
multiple = false,
disabled: disabledProp = false,
readOnly: readOnlyProp = false,
state: stateProp,
}: DropdownContextProps) => {
const [itemsMap, setItemsMap] = useState<ItemsMap>(getItemsFromChildren(children))
Expand All @@ -112,6 +125,9 @@ export const DropdownProvider = ({
const id = useId(field.id)
const labelId = useId(field.labelId)

const disabled = field.disabled ?? disabledProp
const readOnly = field.readOnly ?? readOnlyProp

const downshiftMultipleSelection = useMultipleSelection<DropdownItem>({
selectedItems: value
? items.filter(item => (value as string[]).includes(item.value))
Expand Down Expand Up @@ -230,6 +246,8 @@ export const DropdownProvider = ({
<DropdownContext.Provider
value={{
multiple,
disabled,
readOnly,
...downshift,
...downshiftMultipleSelection,
itemsMap,
Expand Down
33 changes: 33 additions & 0 deletions packages/components/dropdown/src/DropdownTrigger.styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { cva } from 'class-variance-authority'

export const styles = cva(
[
'flex w-full items-center justify-between',
'min-h-sz-44 rounded-lg bg-surface text-on-surface px-lg',
// outline styles
'ring-1 outline-none ring-inset focus:ring-2',
],
{
variants: {
state: {
undefined: 'ring-outline focus:ring-outline-high',
error: 'ring-error',
alert: 'ring-alert',
success: 'ring-success',
},
disabled: {
true: 'disabled:bg-on-surface/dim-5 cursor-not-allowed text-on-surface/dim-3',
},
readOnly: {
true: 'disabled:bg-on-surface/dim-5 cursor-not-allowed text-on-surface/dim-3',
},
},
compoundVariants: [
{
disabled: false,
state: undefined,
class: 'hover:ring-outline-high',
},
],
}
)
26 changes: 5 additions & 21 deletions packages/components/dropdown/src/DropdownTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,18 @@ 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 { cva } from 'class-variance-authority'
import { forwardRef, Fragment, ReactNode, type Ref } from 'react'

import { useDropdownContext } from './DropdownContext'
import { DropdownStateIndicator } from './DropdownStateIndicator'
import { styles } from './DropdownTrigger.styles'

interface TriggerProps {
'aria-label'?: string
children: ReactNode
className?: string
}

const styles = cva(
[
'flex w-full cursor-pointer items-center justify-between',
'min-h-sz-44 rounded-lg bg-surface text-on-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 = forwardRef(
(
{ 'aria-label': ariaLabel, children, className }: TriggerProps,
Expand All @@ -43,6 +24,8 @@ export const Trigger = forwardRef(
getDropdownProps,
getLabelProps,
hasPopover,
disabled,
readOnly,
state,
setLastInteractionType,
} = useDropdownContext()
Expand All @@ -62,7 +45,8 @@ export const Trigger = forwardRef(
<button
type="button"
ref={forwardedRef}
className={styles({ className, state })}
disabled={disabled || readOnly}
className={styles({ className, state, disabled, readOnly })}
{...getToggleButtonProps({
...getDropdownProps(),
onKeyDown: () => {
Expand Down

0 comments on commit fbd1d04

Please sign in to comment.