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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user