Add diagnostics to suggest box

This adds the ability for consumers of the suggest box to add
diagnostics to the suggest box. When a diagnostic is returned for a
value, the input will be shown with a red border.
This commit is contained in:
Koen Vlaswinkel
2024-01-16 16:56:27 +01:00
parent 6ec727a8a2
commit 39c17291aa
3 changed files with 82 additions and 7 deletions

View File

@@ -5,6 +5,7 @@ import { styled } from "styled-components";
import { Codicon } from "../../view/common";
import { SuggestBox as SuggestBoxComponent } from "../../view/common/SuggestBox/SuggestBox";
import { useCallback, useState } from "react";
import type { Diagnostic } from "../../view/common/SuggestBox/diagnostics";
export default {
title: "Suggest Box",
@@ -141,6 +142,25 @@ export const AccessPath = Template.bind({});
AccessPath.args = {
options: suggestedOptions,
parseValueToTokens: (value: string) => value.split("."),
validateValue: (value: string) => {
let index = value.indexOf("|");
const diagnostics: Diagnostic[] = [];
while (index !== -1) {
index = value.indexOf("|", index + 1);
diagnostics.push({
message: "This cannot contain |",
range: {
start: index,
end: index + 1,
},
});
}
return diagnostics;
},
getIcon: (option: StoryOption) => <Icon name={option.icon} />,
getDetails: (option: StoryOption) => option.details,
};

View File

@@ -13,16 +13,24 @@ import {
useListNavigation,
useRole,
} from "@floating-ui/react";
import { styled } from "styled-components";
import { css, styled } from "styled-components";
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react";
import type { Option } from "./options";
import { findMatchingOptions } from "./options";
import { SuggestBoxItem } from "./SuggestBoxItem";
import type { Diagnostic } from "./diagnostics";
const Input = styled(VSCodeTextField)`
const Input = styled(VSCodeTextField)<{ $error: boolean }>`
width: 430px;
font-family: var(--vscode-editor-font-family);
${(props) =>
props.$error &&
css`
--dropdown-border: var(--vscode-inputValidation-errorBorder);
--focus-border: var(--vscode-inputValidation-errorBorder);
`}
`;
const Container = styled.div`
@@ -50,7 +58,10 @@ const NoSuggestionsText = styled.div`
padding-left: 22px;
`;
export type SuggestBoxProps<T extends Option<T>> = {
export type SuggestBoxProps<
T extends Option<T>,
D extends Diagnostic = Diagnostic,
> = {
value?: string;
onChange: (value: string) => void;
options: T[];
@@ -62,6 +73,12 @@ export type SuggestBoxProps<T extends Option<T>> = {
*/
parseValueToTokens: (value: string) => string[];
/**
* Validate the value. This is used to show syntax errors in the input.
* @param value The user-entered value to validate.
*/
validateValue?: (value: string) => D[];
/**
* Get the icon to display for an option.
* @param option The option to get the icon for.
@@ -83,20 +100,29 @@ export type SuggestBoxProps<T extends Option<T>> = {
* for easier testing.
* @param props The props returned by `getReferenceProps` of {@link useInteractions}
*/
renderInputComponent?: (props: Record<string, unknown>) => ReactNode;
renderInputComponent?: (
props: Record<string, unknown>,
hasError: boolean,
) => ReactNode;
};
export const SuggestBox = <T extends Option<T>>({
export const SuggestBox = <
T extends Option<T>,
D extends Diagnostic = Diagnostic,
>({
value = "",
onChange,
options,
parseValueToTokens,
validateValue,
getIcon,
getDetails,
disabled,
"aria-label": ariaLabel,
renderInputComponent = (props) => <Input {...props} />,
}: SuggestBoxProps<T>) => {
renderInputComponent = (props, hasError) => (
<Input {...props} $error={hasError} />
),
}: SuggestBoxProps<T, D>) => {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
@@ -151,6 +177,13 @@ export const SuggestBox = <T extends Option<T>>({
return findMatchingOptions(options, parseValueToTokens(value));
}, [options, value, parseValueToTokens]);
const diagnostics = useMemo(
() => validateValue?.(value) ?? [],
[validateValue, value],
);
const hasSyntaxError = diagnostics.length > 0;
useEffect(() => {
if (disabled) {
setIsOpen(false);
@@ -180,6 +213,7 @@ export const SuggestBox = <T extends Option<T>>({
},
disabled,
}),
hasSyntaxError,
)}
{isOpen && (
<FloatingPortal>

View File

@@ -0,0 +1,21 @@
/**
* A range of characters in a value. The start position is inclusive, the end position is exclusive.
*/
type DiagnosticRange = {
/**
* Zero-based index of the first character of the token.
*/
start: number;
/**
* Zero-based index of the character after the last character of the token.
*/
end: number;
};
/**
* A diagnostic message.
*/
export type Diagnostic = {
range: DiagnosticRange;
message: string;
};