Skip to content

Commit

Permalink
feat(dropdown): dropdown readOnly mode
Browse files Browse the repository at this point in the history
Powerplex committed Jan 3, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 44f0e2b commit 2bbf561
Showing 5 changed files with 107 additions and 37 deletions.
16 changes: 12 additions & 4 deletions packages/components/dropdown/src/Dropdown.doc.mdx
Original file line number Diff line number Diff line change
@@ -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.
@@ -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.
@@ -221,3 +223,9 @@ Apply `disabled` to the wrapping `FormField` to disable the dropdown.
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} />
49 changes: 49 additions & 0 deletions packages/components/dropdown/src/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -166,6 +166,29 @@ export const Disabled: StoryFn = _args => {
)
}

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]">
@@ -371,6 +394,32 @@ export const FormFieldDisabled: StoryFn = _args => {
)
}

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]">
10 changes: 9 additions & 1 deletion packages/components/dropdown/src/DropdownContext.tsx
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ export interface DropdownContextState extends DownshiftState {
setHasPopover: Dispatch<SetStateAction<boolean>>
multiple: boolean
disabled: boolean
readOnly: boolean
state?: 'error' | 'alert' | 'success'
lastInteractionType: 'mouse' | 'keyboard'
setLastInteractionType: (type: 'mouse' | 'keyboard') => void
@@ -48,6 +49,10 @@ export type DropdownContextCommonProps = PropsWithChildren<{
* When true, prevents the user from interacting with the dropdown.
*/
disabled?: boolean
/**
* Sets the dropdown as interactive or not.
*/
readOnly?: boolean
}>

interface DropdownPropsSingle {
@@ -106,6 +111,7 @@ export const DropdownProvider = ({
defaultOpen,
multiple = false,
disabled: disabledProp = false,
readOnly: readOnlyProp = false,
state: stateProp,
}: DropdownContextProps) => {
const [itemsMap, setItemsMap] = useState<ItemsMap>(getItemsFromChildren(children))
@@ -119,7 +125,8 @@ export const DropdownProvider = ({
const id = useId(field.id)
const labelId = useId(field.labelId)

const disabled = field.disabled != null ? field.disabled : disabledProp
const disabled = field.disabled ?? disabledProp
const readOnly = field.readOnly ?? readOnlyProp

const downshiftMultipleSelection = useMultipleSelection<DropdownItem>({
selectedItems: value
@@ -240,6 +247,7 @@ export const DropdownProvider = ({
value={{
multiple,
disabled,
readOnly,
...downshift,
...downshiftMultipleSelection,
itemsMap,
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',
},
],
}
)
36 changes: 4 additions & 32 deletions packages/components/dropdown/src/DropdownTrigger.tsx
Original file line number Diff line number Diff line change
@@ -2,47 +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 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',
},
},
compoundVariants: [
{
disabled: false,
state: undefined,
class: 'hover:ring-outline-high',
},
],
}
)

export const Trigger = forwardRef(
(
{ 'aria-label': ariaLabel, children, className }: TriggerProps,
@@ -54,6 +25,7 @@ export const Trigger = forwardRef(
getLabelProps,
hasPopover,
disabled,
readOnly,
state,
setLastInteractionType,
} = useDropdownContext()
@@ -73,8 +45,8 @@ export const Trigger = forwardRef(
<button
type="button"
ref={forwardedRef}
disabled={disabled}
className={styles({ className, state, disabled })}
disabled={disabled || readOnly}
className={styles({ className, state, disabled, readOnly })}
{...getToggleButtonProps({
...getDropdownProps(),
onKeyDown: () => {

0 comments on commit 2bbf561

Please sign in to comment.