From 54f4bb91c18efe56f5f9c8c8e4c4ca38954e158a Mon Sep 17 00:00:00 2001 From: Richard Walker Date: Tue, 27 Sep 2022 16:13:57 +1300 Subject: [PATCH 1/2] feat: add feedback prop to combobox --- packages/combobox/docs/Combobox.mdx | 14 +++ packages/combobox/src/component.tsx | 4 + packages/combobox/src/props.ts | 3 + .../combobox/stories/Combobox.stories.tsx | 93 +++++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/packages/combobox/docs/Combobox.mdx b/packages/combobox/docs/Combobox.mdx index 0d407c2e..c062ab27 100644 --- a/packages/combobox/docs/Combobox.mdx +++ b/packages/combobox/docs/Combobox.mdx @@ -308,6 +308,20 @@ function Example() { } ``` +### Providing feedback + +You can provide feedback to the user via the feedback prop. 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. + +```ts + +``` + +When feedback is set to a non empty string, options will not show. Omit feedback entirely or set it to an empty string 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..976e6a14 100644 --- a/packages/combobox/src/component.tsx +++ b/packages/combobox/src/component.tsx @@ -61,6 +61,7 @@ export const Combobox = forwardRef( onFocus, onBlur, optional, + feedback, ...rest } = props; @@ -249,6 +250,8 @@ export const Combobox = forwardRef( {getAriaText(currentOptions, value)} + {feedback &&
{feedback}
} + {!feedback && + } ); }, diff --git a/packages/combobox/src/props.ts b/packages/combobox/src/props.ts index 4817236b..552013f5 100644 --- a/packages/combobox/src/props.ts +++ b/packages/combobox/src/props.ts @@ -117,6 +117,9 @@ export type ComboboxProps = { /** Whether to show optional text */ optional?: boolean; + + /** Feedback string to show users as they interact with the combobox */ + feedback?: 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..2ed921f2 100644 --- a/packages/combobox/stories/Combobox.stories.tsx +++ b/packages/combobox/stories/Combobox.stories.tsx @@ -356,3 +356,96 @@ export const Optional = () => { ); }; + +export const Feedback = () => { + 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 [feedback, setFeedback] = 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 () => { + setFeedback('Søker...'); + try { + const res = await fetch('https://swapi.dev/api/people/?search=' + query.trim()) + const { results } = await res.json(); + console.log('Results from API', query); + setCharacters(results.map((c) => ({ value: c.name }))); + if (results.length) { + setFeedback(''); + } else { + setFeedback('Ingen treff'); + } + } catch(err) { + setFeedback('API Fail'); + } + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [delay, query]); + + return characters; + } + + return ( + { + setValue(val); + setQuery(val); + }} + onSelect={(val) => { + setValue(val); + action('select')(val); + }} + onBlur={() => setFeedback('')} + options={characters} + feedback={feedback} + > + { + setValue(''); + setQuery(''); + }} + /> + + ); +}; From 8cf7cf124c71b64746c1dbf689889ea18c50c948 Mon Sep 17 00:00:00 2001 From: Richard Walker Date: Mon, 7 Nov 2022 14:31:43 +1300 Subject: [PATCH 2/2] refactor: split feedback into levels (info, warn and error) --- packages/combobox/docs/Combobox.mdx | 13 +- packages/combobox/src/component.tsx | 166 ++++++++++-------- packages/combobox/src/props.ts | 10 +- .../combobox/stories/Combobox.stories.tsx | 34 ++-- 4 files changed, 134 insertions(+), 89 deletions(-) diff --git a/packages/combobox/docs/Combobox.mdx b/packages/combobox/docs/Combobox.mdx index c062ab27..879b7a8a 100644 --- a/packages/combobox/docs/Combobox.mdx +++ b/packages/combobox/docs/Combobox.mdx @@ -310,17 +310,24 @@ function Example() { ### Providing feedback -You can provide feedback to the user via the feedback prop. 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. +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 feedback is set to a non empty string, options will not show. Omit feedback entirely or set it to an empty string when not in use. +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 diff --git a/packages/combobox/src/component.tsx b/packages/combobox/src/component.tsx index 976e6a14..e18da235 100644 --- a/packages/combobox/src/component.tsx +++ b/packages/combobox/src/component.tsx @@ -61,7 +61,9 @@ export const Combobox = forwardRef( onFocus, onBlur, optional, - feedback, + feedbackInfo, + feedbackWarn, + feedbackError, ...rest } = props; @@ -250,80 +252,100 @@ export const Combobox = forwardRef( {getAriaText(currentOptions, value)} - {feedback &&
{feedback}
} - {!feedback && - + )} + {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 552013f5..606838c3 100644 --- a/packages/combobox/src/props.ts +++ b/packages/combobox/src/props.ts @@ -118,8 +118,14 @@ export type ComboboxProps = { /** Whether to show optional text */ optional?: boolean; - /** Feedback string to show users as they interact with the combobox */ - feedback?: string; + /** 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 2ed921f2..ba52a2da 100644 --- a/packages/combobox/stories/Combobox.stories.tsx +++ b/packages/combobox/stories/Combobox.stories.tsx @@ -357,7 +357,7 @@ export const Optional = () => { ); }; -export const Feedback = () => { +export const Searching = () => { return ( { matchTextSegments openOnFocus value="asd" - feedback="Søker..." + feedbackInfo='Søker...' onChange={(val) => console.log('change')} options={[ { value: 'Product manager' }, @@ -380,7 +380,9 @@ export const Feedback = () => { export const AsyncFetchWithFeedback = () => { const [query, setQuery] = React.useState(''); const [value, setValue] = React.useState(''); - const [feedback, setFeedback] = React.useState(''); + const [infoFeedback, setInfoFeedback] = React.useState(''); + const [warningFeedback, setWarningFeedback] = React.useState(''); + const [errorFeedback, setErrorFeedback] = React.useState(''); const characters = useDebouncedSearch(query, 300); // Generic debouncer @@ -394,19 +396,21 @@ export const AsyncFetchWithFeedback = () => { } const handler = setTimeout(async () => { - setFeedback('Søker...'); + 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 }))); - if (results.length) { - setFeedback(''); - } else { - setFeedback('Ingen treff'); - } } catch(err) { - setFeedback('API Fail'); + setErrorFeedback('API Fail'); + } finally { + setInfoFeedback(''); } }, delay); @@ -433,9 +437,15 @@ export const AsyncFetchWithFeedback = () => { setValue(val); action('select')(val); }} - onBlur={() => setFeedback('')} + onBlur={() => { + setInfoFeedback(''); + setWarningFeedback(''); + setErrorFeedback(''); + }} options={characters} - feedback={feedback} + feedbackInfo={infoFeedback} + feedbackWarn={warningFeedback} + feedbackError={errorFeedback} >