@@ -13,17 +13,25 @@ import {
1313 useListNavigation ,
1414 useRole ,
1515} from "@floating-ui/react" ;
16- import { styled } from "styled-components" ;
16+ import { css , styled } from "styled-components" ;
1717import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" ;
1818import type { Option } from "./options" ;
1919import { findMatchingOptions } from "./options" ;
2020import { SuggestBoxItem } from "./SuggestBoxItem" ;
2121import { LabelText } from "./LabelText" ;
22+ import type { Diagnostic } from "./diagnostics" ;
2223
23- const Input = styled ( VSCodeTextField ) `
24+ const Input = styled ( VSCodeTextField ) < { $error : boolean } > `
2425 width: 430px;
2526
2627 font-family: var(--vscode-editor-font-family);
28+
29+ ${ ( props ) =>
30+ props . $error &&
31+ css `
32+ --dropdown-border: var(--vscode-inputValidation-errorBorder);
33+ --focus-border: var(--vscode-inputValidation-errorBorder);
34+ ` }
2735` ;
2836
2937const Container = styled . div `
@@ -51,7 +59,10 @@ const NoSuggestionsText = styled.div`
5159 padding-left: 22px;
5260` ;
5361
54- export type SuggestBoxProps < T extends Option < T > > = {
62+ export type SuggestBoxProps <
63+ T extends Option < T > ,
64+ D extends Diagnostic = Diagnostic ,
65+ > = {
5566 value ?: string ;
5667 onChange : ( value : string ) => void ;
5768 options : T [ ] ;
@@ -63,6 +74,12 @@ export type SuggestBoxProps<T extends Option<T>> = {
6374 */
6475 parseValueToTokens : ( value : string ) => string [ ] ;
6576
77+ /**
78+ * Validate the value. This is used to show syntax errors in the input.
79+ * @param value The user-entered value to validate.
80+ */
81+ validateValue ?: ( value : string ) => D [ ] ;
82+
6683 /**
6784 * Get the icon to display for an option.
6885 * @param option The option to get the icon for.
@@ -84,20 +101,29 @@ export type SuggestBoxProps<T extends Option<T>> = {
84101 * for easier testing.
85102 * @param props The props returned by `getReferenceProps` of {@link useInteractions}
86103 */
87- renderInputComponent ?: ( props : Record < string , unknown > ) => ReactNode ;
104+ renderInputComponent ?: (
105+ props : Record < string , unknown > ,
106+ hasError : boolean ,
107+ ) => ReactNode ;
88108} ;
89109
90- export const SuggestBox = < T extends Option < T > > ( {
110+ export const SuggestBox = <
111+ T extends Option < T > ,
112+ D extends Diagnostic = Diagnostic ,
113+ > ( {
91114 value = "" ,
92115 onChange,
93116 options,
94117 parseValueToTokens,
118+ validateValue,
95119 getIcon,
96120 getDetails,
97121 disabled,
98122 "aria-label" : ariaLabel ,
99- renderInputComponent = ( props ) => < Input { ...props } /> ,
100- } : SuggestBoxProps < T > ) => {
123+ renderInputComponent = ( props , hasError ) => (
124+ < Input { ...props } $error = { hasError } />
125+ ) ,
126+ } : SuggestBoxProps < T , D > ) => {
101127 const [ isOpen , setIsOpen ] = useState ( false ) ;
102128 const [ activeIndex , setActiveIndex ] = useState < number | null > ( null ) ;
103129
@@ -156,6 +182,13 @@ export const SuggestBox = <T extends Option<T>>({
156182 return findMatchingOptions ( options , tokens ) ;
157183 } , [ options , tokens ] ) ;
158184
185+ const diagnostics = useMemo (
186+ ( ) => validateValue ?.( value ) ?? [ ] ,
187+ [ validateValue , value ] ,
188+ ) ;
189+
190+ const hasSyntaxError = diagnostics . length > 0 ;
191+
159192 useEffect ( ( ) => {
160193 if ( disabled ) {
161194 setIsOpen ( false ) ;
@@ -185,6 +218,7 @@ export const SuggestBox = <T extends Option<T>>({
185218 } ,
186219 disabled,
187220 } ) ,
221+ hasSyntaxError ,
188222 ) }
189223 { isOpen && (
190224 < FloatingPortal >
0 commit comments