diff --git a/packages/combobox/docs/Combobox.mdx b/packages/combobox/docs/Combobox.mdx index 0d407c2e..879b7a8a 100644 --- a/packages/combobox/docs/Combobox.mdx +++ b/packages/combobox/docs/Combobox.mdx @@ -308,6 +308,27 @@ function Example() { } ``` +### Providing feedback + +You can provide feedback to the user via the feedback props. This is especially +useful when performing asynchronous operations such as fetching options from an +API as it enables you to tell the user that they need to wait for search results +or that no results were found and so on. Feedback can be provided using any of +the info, warning or error feedback props. + +```ts + +``` + +When any of these props are set to a non empty string, options will not show. +Omit feedback props entirely or set them to empty strings when not in use. + ## Combobox Props ```props packages/combobox/src/component.tsx diff --git a/packages/combobox/src/component.tsx b/packages/combobox/src/component.tsx index 53caf9c3..e18da235 100644 --- a/packages/combobox/src/component.tsx +++ b/packages/combobox/src/component.tsx @@ -61,6 +61,9 @@ export const Combobox = forwardRef( onFocus, onBlur, optional, + feedbackInfo, + feedbackWarn, + feedbackError, ...rest } = props; @@ -249,77 +252,100 @@ export const Combobox = forwardRef( {getAriaText(currentOptions, value)} - + )} + {feedbackWarn && ( +
+
+ {feedbackWarn} +
+
+ )} + {feedbackError && ( +
+
+ {feedbackError} +
+
+ )} + {!feedbackInfo && !feedbackWarn && !feedbackError && ( + + }); + }} + className={classNames({ + [`block cursor-pointer p-8 hover:bg-${OPTION_HIGHLIGHT_COLOR} ${OPTION_CLASS_NAME}`]: + true, + [`bg-${OPTION_HIGHLIGHT_COLOR}`]: + navigationOption?.id === option.id, + })} + > + {matchTextSegments || highlightValueMatch ? match : display} + + ); + })} + + + )} ); }, diff --git a/packages/combobox/src/props.ts b/packages/combobox/src/props.ts index 4817236b..606838c3 100644 --- a/packages/combobox/src/props.ts +++ b/packages/combobox/src/props.ts @@ -117,6 +117,15 @@ export type ComboboxProps = { /** Whether to show optional text */ optional?: boolean; + + /** Feedback string to use to inform users about something. Eg. Show "Søker..." feedback to users as they type. */ + feedbackInfo?: string; + + /** Feedback string to use to warn users about something. Eg. if there are no results when searching. */ + feedbackWarn?: string; + + /** Feedback string to show users if there is an error as they interact with the combobox. */ + feedbackError?: string; } & Omit< React.PropsWithoutRef, 'onChange' | 'type' | 'value' | 'label' diff --git a/packages/combobox/stories/Combobox.stories.tsx b/packages/combobox/stories/Combobox.stories.tsx index 53fb96ae..ba52a2da 100644 --- a/packages/combobox/stories/Combobox.stories.tsx +++ b/packages/combobox/stories/Combobox.stories.tsx @@ -356,3 +356,106 @@ export const Optional = () => { ); }; + +export const Searching = () => { + return ( + console.log('change')} + options={[ + { value: 'Product manager' }, + { value: 'Produktledelse' }, + { value: 'Prosessoperatør' }, + { value: 'Prosjekteier' }, + ]} + /> + ); +}; + +export const AsyncFetchWithFeedback = () => { + const [query, setQuery] = React.useState(''); + const [value, setValue] = React.useState(''); + const [infoFeedback, setInfoFeedback] = React.useState(''); + const [warningFeedback, setWarningFeedback] = React.useState(''); + const [errorFeedback, setErrorFeedback] = React.useState(''); + const characters = useDebouncedSearch(query, 300); + + // Generic debouncer + function useDebouncedSearch(query, delay) { + const [characters, setCharacters] = React.useState([]); + + React.useEffect(() => { + if (!query.length) { + setCharacters([]); + return; + } + + const handler = setTimeout(async () => { + setInfoFeedback('Søker...'); + setWarningFeedback(''); + setErrorFeedback(''); + try { + const res = await fetch('https://swapi.dev/api/people/?search=' + query.trim()) + const { results } = await res.json(); + console.log('Results from API', query); + if (!results.length) { + setWarningFeedback('Ingen treff'); + } + setCharacters(results.map((c) => ({ value: c.name }))); + } catch(err) { + setErrorFeedback('API Fail'); + } finally { + setInfoFeedback(''); + } + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [delay, query]); + + return characters; + } + + return ( + { + setValue(val); + setQuery(val); + }} + onSelect={(val) => { + setValue(val); + action('select')(val); + }} + onBlur={() => { + setInfoFeedback(''); + setWarningFeedback(''); + setErrorFeedback(''); + }} + options={characters} + feedbackInfo={infoFeedback} + feedbackWarn={warningFeedback} + feedbackError={errorFeedback} + > + { + setValue(''); + setQuery(''); + }} + /> + + ); +};