Skip to content

Commit

Permalink
Merge pull request #1975 from adevinta/1895-component-combobox-loadin…
Browse files Browse the repository at this point in the history
…g-state

feat(combobox): isLoading + SearchModal example
  • Loading branch information
andresin87 authored Mar 19, 2024
2 parents b274678 + ae4a0a9 commit 76c0a8b
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 2 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/components/combobox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@spark-ui/icon-button": "^2.2.3",
"@spark-ui/icons": "^1.21.6",
"@spark-ui/popover": "^1.5.5",
"@spark-ui/spinner": "^2.2.2",
"@spark-ui/use-merge-refs": "^0.4.0",
"@spark-ui/visually-hidden": "^1.2.0",
"class-variance-authority": "0.7.0",
Expand Down
11 changes: 11 additions & 0 deletions packages/components/combobox/src/Combobox.doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ Use `readOnly` prop to indicate the combobox is only readable.

<Canvas of={stories.ReadOnly} />

### isLoading

Use the `isLoading` prop to render the combobox in loading state.
This will prepend a spinner inside the items-list.

<Canvas of={stories.IsLoading} />

### Status

Use `state` prop to assign a specific state to the combobox, choosing from: `error`, `alert` and `success`. By doing so, the outline styles will be updated, and a status indicator will be displayed accordingly.
Expand Down Expand Up @@ -215,6 +222,10 @@ If your `Combobox.Item` contains anything else than raw text, you may use any JS

<Canvas of={stories.CustomItem} />

### Search Modal

<Canvas of={stories.ModalSearch} />

## Form field

### Label
Expand Down
122 changes: 122 additions & 0 deletions packages/components/combobox/src/Combobox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable max-lines */
// import { Button } from '@spark-ui/button'
import { Button } from '@spark-ui/button'
import { Checkbox, CheckboxGroup } from '@spark-ui/checkbox'
import { Chip } from '@spark-ui/chip'
import { Dialog } from '@spark-ui/dialog'
import { FormField } from '@spark-ui/form-field'
import { PenOutline } from '@spark-ui/icons/dist/icons/PenOutline'
import { RadioGroup } from '@spark-ui/radio-group'
Expand Down Expand Up @@ -780,3 +783,122 @@ export const FormFieldValidation: StoryFn = () => {
</div>
)
}

export const IsLoading: StoryFn = _args => {
return (
<div className="pb-[300px]">
<Combobox isLoading defaultOpen={true}>
<Combobox.Trigger>
<Combobox.LeadingIcon>
<PenOutline />
</Combobox.LeadingIcon>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
<Combobox.ClearButton aria-label="Clear input" />
<Combobox.Disclosure openedLabel="Close popup" closedLabel="Open popup" />
</Combobox.Trigger>

<Combobox.Popover>
<Combobox.Items>
<Combobox.Empty>No results found</Combobox.Empty>
<Combobox.Item value="book-1">To Kill a Mockingbird</Combobox.Item>
<Combobox.Item value="book-2">War and Peace</Combobox.Item>
<Combobox.Item value="book-3">The Idiot</Combobox.Item>
<Combobox.Item value="book-4">A Picture of Dorian Gray</Combobox.Item>
<Combobox.Item value="book-5">1984</Combobox.Item>
<Combobox.Item value="book-6">
Pride and Prejudice but it is an extremely long title
</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
</div>
)
}
export const ModalSearch: StoryFn = () => {
const books = [
{ id: 1, name: 'Things Fall Apart' },
{ id: 2, name: 'The Catcher in the Rye' },
{ id: 3, name: 'The Great Gatsby' },
{ id: 4, name: 'Fairy tales' },
{ id: 5, name: 'The Hobbit' },
{ id: 6, name: 'The Lord of the Rings' },
{ id: 7, name: 'And Then There Were None' },
{ id: 8, name: 'The Da Vinci Code' },
{ id: 9, name: 'The Alchemist' },
{ id: 10, name: 'The Epic Of Gilgamesh' },
{ id: 11, name: 'The Book Thief' },
{ id: 12, name: 'The Little Prince' },
{ id: 13, name: 'The Book Of Job' },
{ id: 14, name: 'The Grapes Of Wrath' },
{ id: 15, name: 'Pride and Prejudice' },
{ id: 16, name: 'The Odyssey' },
{ id: 17, name: 'One Hundred Years of Solitude' },
{ id: 18, name: 'Crime and Punishment' },
{ id: 19, name: 'Gypsy Ballads' },
{ id: 20, name: 'Love in the Time of Cholera' },
{ id: 21, name: 'Hunger' },
{ id: 22, name: 'The Old Man and the Sea' },
{ id: 23, name: 'To Kill a Mockingbird' },
{ id: 24, name: 'War and Peace' },
{ id: 25, name: 'The Idiot' },
{ id: 26, name: 'Scaramouche' },
{ id: 27, name: 'A Picture of Dorian Gray' },
{ id: 28, name: '1984' },
]
const [value, setValue] = useState<string | undefined>()
const [isOpen, setIsOpen] = useState<boolean>(false)

return (
<div className="m-sm flex h-[600px] w-full items-center justify-center border-sm border-dashed bg-gradient-to-br from-main to-support-variant text-surface">
<Dialog open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
<Combobox
onValueChange={value => {
if (value === undefined) {
setValue(undefined)
} else {
const [_, id] = value.split('-')
const book = books.find(({ id: bookId }) => `${bookId}` === id)
setValue(book ? book?.name : undefined)
}
setIsOpen(false)
}}
defaultValue={value}
>
{value ? (
<Chip intent="surface" onClear={() => setValue(undefined)}>
<Chip.Content>{value}</Chip.Content>
<Chip.ClearButton label={'Clear'} />
</Chip>
) : (
<>
<Dialog.Trigger asChild>
<Button design="outlined" intent="surface" onClick={() => setIsOpen(true)}>
Search a book...
</Button>
</Dialog.Trigger>
<Dialog.Overlay />
<Dialog.Portal>
<Dialog.Content size="sm">
<Dialog.Header>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
<Combobox.ClearButton aria-label={'Clear input'} />
</Combobox.Trigger>
</Dialog.Header>
<Dialog.Body>
<Combobox.Items>
<Combobox.Empty>No results found</Combobox.Empty>
{books.map(({ name, id }) => (
<Combobox.Item value={`book-${id}`}>{name}</Combobox.Item>
))}
</Combobox.Items>
</Dialog.Body>
</Dialog.Content>
</Dialog.Portal>
</>
)}
</Combobox>
</Dialog>
</div>
)
}
7 changes: 7 additions & 0 deletions packages/components/combobox/src/ComboboxContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ComboboxContextState extends DownshiftState {
setLastInteractionType: (type: 'mouse' | 'keyboard') => void
innerInputRef: React.RefObject<HTMLInputElement>
triggerAreaRef: React.RefObject<HTMLDivElement>
isLoading?: boolean
}

export type ComboboxContextCommonProps = PropsWithChildren<{
Expand Down Expand Up @@ -75,6 +76,10 @@ export type ComboboxContextCommonProps = PropsWithChildren<{
* If you wish to keep every item on a single line, disabled this property.
*/
wrap?: boolean
/**
* Display a spinner to indicate to the user that the combobox is loading results for .
*/
isLoading?: boolean
}>

interface ComboboxPropsSingle {
Expand Down Expand Up @@ -145,6 +150,7 @@ export const ComboboxProvider = ({
open: controlledOpen,
defaultOpen,
onOpenChange,
isLoading,
}: ComboboxContextProps) => {
const isMounted = useRef(false)

Expand Down Expand Up @@ -395,6 +401,7 @@ export const ComboboxProvider = ({
setInputValue,
selectItem: onInternalSelectedItemChange,
setSelectedItems: onInternalSelectedItemsChange,
isLoading,
}}
>
<WrapperComponent {...wrapperProps}>{children}</WrapperComponent>
Expand Down
11 changes: 9 additions & 2 deletions packages/components/combobox/src/ComboboxItems.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Spinner } from '@spark-ui/spinner'
import { useMergeRefs } from '@spark-ui/use-merge-refs'
import { cx } from 'class-variance-authority'
import { forwardRef, ReactNode, type Ref } from 'react'
Expand All @@ -21,19 +22,25 @@ export const Items = forwardRef(

const ref = useMergeRefs(forwardedRef, downshiftRef)

const isOpen = ctx.hasPopover ? ctx.isOpen : true

console.log('isLoading', ctx.isLoading)

return (
<ul
ref={ref}
className={cx(
className,
'flex flex-col',
ctx.isOpen ? 'block' : 'pointer-events-none opacity-0',
'relative flex flex-col',
isOpen ? 'block' : 'pointer-events-none opacity-0',
ctx.hasPopover && 'p-lg'
)}
{...props}
{...downshiftMenuProps}
aria-busy={ctx.isLoading}
data-spark-component="combobox-items"
>
{ctx.isLoading && <Spinner size="sm" className="absolute right-lg" />}
{children}
</ul>
)
Expand Down

0 comments on commit 76c0a8b

Please sign in to comment.