Reuse text field component

This commit is contained in:
Elio Struyf
2024-01-19 13:26:42 +01:00
parent 0d19abfa8f
commit ae436e1a0e
9 changed files with 145 additions and 138 deletions
@@ -0,0 +1,78 @@
import { XCircleIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
export interface ITextFieldProps {
name: string;
value?: string;
placeholder?: string;
icon?: JSX.Element;
disabled?: boolean;
autoFocus?: boolean;
multiline?: boolean;
rows?: number;
onChange?: (value: string) => void;
onReset?: () => void;
}
export const TextField: React.FunctionComponent<ITextFieldProps> = ({
name,
value,
placeholder,
icon,
autoFocus,
multiline,
rows,
disabled,
onChange,
onReset
}: React.PropsWithChildren<ITextFieldProps>) => {
return (
<div className="relative flex justify-center">
{
icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
{icon}
</div>
)
}
{
multiline ? (
<textarea
rows={rows || 3}
name={name}
className={`block w-full py-2 ${icon ? "pl-10" : "pl-2"} pr-2 sm:text-sm appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] border-[var(--frontmatter-border)] focus:border-[var(--vscode-focusBorder)] focus:outline-0`}
style={{
boxShadow: "none"
}}
placeholder={placeholder || ""}
value={value}
autoFocus={!!autoFocus}
onChange={(e) => onChange && onChange(e.target.value)}
disabled={!!disabled}
/>
) : (
<input
type="text"
name={name}
className={`block w-full py-2 ${icon ? "pl-10" : "pl-2"} pr-2 sm:text-sm appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] border-[var(--frontmatter-border)] focus:border-[var(--vscode-focusBorder)] focus:outline-0`}
style={{
boxShadow: "none"
}}
placeholder={placeholder || ""}
value={value}
autoFocus={!!autoFocus}
onChange={(e) => onChange && onChange(e.target.value)}
disabled={!!disabled}
/>
)
}
{(value && onReset) && (
<button onClick={onReset} className="absolute inset-y-0 right-0 pr-3 flex items-center text-[var(--vscode-input-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]">
<XCircleIcon className={`h-5 w-5`} aria-hidden="true" />
</button>
)}
</div>
);
};
@@ -1,8 +1,8 @@
import { MagnifyingGlassIcon, XCircleIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import useThemeColors from '../../hooks/useThemeColors';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { TextField } from '../Common/TextField';
export interface IFilterInputProps {
placeholder: string;
@@ -21,7 +21,6 @@ export const FilterInput: React.FunctionComponent<IFilterInputProps> = ({
onReset,
onChange
}: React.PropsWithChildren<IFilterInputProps>) => {
const { getColors } = useThemeColors();
return (
<div className="flex space-x-4 flex-1">
@@ -29,32 +28,19 @@ export const FilterInput: React.FunctionComponent<IFilterInputProps> = ({
<label htmlFor="search" className="sr-only">
{l10n.t(LocalizationKey.commonSearch)}
</label>
<div className="relative flex justify-center">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className={`h-5 w-5 ${getColors(`text-gray-400`, 'text-[var(--vscode-input-foreground)]')}`} aria-hidden="true" />
</div>
<input
type="text"
name="search"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm appearance-none disabled:opacity-50 rounded ${getColors(
'bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
placeholder={placeholder || l10n.t(LocalizationKey.commonSearch)}
value={value}
autoFocus={autoFocus}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.value)}
disabled={!isReady}
/>
{value && onReset && (
<button onClick={onReset} className="absolute inset-y-0 right-0 pr-3 flex items-center">
<XCircleIcon className={`h-5 w-5 ${getColors(`text-gray-400`, 'text-[var(--vscode-input-foreground)]')}`} aria-hidden="true" />
</button>
<TextField
name='search'
icon={(
<MagnifyingGlassIcon className={`h-4 w-4 text-[var(--vscode-input-foreground)]`} aria-hidden="true" />
)}
</div>
value={value}
autoFocus={autoFocus}
placeholder={placeholder || l10n.t(LocalizationKey.commonSearch)}
disabled={!isReady}
onChange={onChange}
onReset={onReset}
/>
</div>
</div>
);
@@ -2,11 +2,11 @@ import { MagnifyingGlassIcon, XCircleIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import useThemeColors from '../../hooks/useThemeColors';
import { SearchAtom, SearchReadyAtom } from '../../state';
import { RefreshDashboardData } from './RefreshDashboardData';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { TextField } from '../Common/TextField';
export interface ISearchboxProps {
placeholder?: string;
@@ -19,10 +19,9 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({
const [debounceSearchValue, setDebounceValue] = useRecoilState(SearchAtom);
const searchReady = useRecoilValue(SearchReadyAtom);
const debounceSearch = useDebounce<string>(value, 500);
const { getColors } = useThemeColors();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
const handleChange = (newValue: string) => {
setValue(newValue);
};
const reset = React.useCallback(() => {
@@ -46,31 +45,18 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({
<label htmlFor="search" className="sr-only">
{l10n.t(LocalizationKey.commonSearch)}
</label>
<div className="relative flex justify-center">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className={`h-5 w-5 ${getColors(`text-gray-400`, 'text-[var(--vscode-input-foreground)]')}`} aria-hidden="true" />
</div>
<input
type="text"
name="search"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm appearance-none disabled:opacity-50 rounded ${getColors(
'bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border, --vscode-editorWidget-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
placeholder={placeholder || l10n.t(LocalizationKey.commonSearch)}
value={value}
onChange={handleChange}
disabled={!searchReady}
/>
{value && (
<button onClick={reset} className="absolute inset-y-0 right-0 pr-3 flex items-center">
<XCircleIcon className={`h-5 w-5 ${getColors(`text-gray-400`, 'text-[var(--vscode-input-foreground)]')}`} aria-hidden="true" />
</button>
<TextField
name='search'
icon={(
<MagnifyingGlassIcon className={`h-4 w-4 text-[var(--vscode-input-foreground)]`} aria-hidden="true" />
)}
</div>
value={value}
placeholder={placeholder || l10n.t(LocalizationKey.commonSearch)}
disabled={!searchReady}
onChange={handleChange}
onReset={reset}
/>
</div>
<RefreshDashboardData />
@@ -1,37 +1,28 @@
import * as React from 'react';
import useThemeColors from '../../hooks/useThemeColors';
import { TextField } from '../Common/TextField';
export interface IDetailsInputProps {
name: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
onChange: (value: string) => void;
isTextArea?: boolean;
}
export const DetailsInput: React.FunctionComponent<IDetailsInputProps> = ({ value, isTextArea, onChange }: React.PropsWithChildren<IDetailsInputProps>) => {
const { getColors } = useThemeColors();
export const DetailsInput: React.FunctionComponent<IDetailsInputProps> = ({ name, value, isTextArea, onChange }: React.PropsWithChildren<IDetailsInputProps>) => {
if (isTextArea) {
return (
<textarea
rows={3}
className={`py-1 px-2 sm:text-sm border w-full ${getColors(
'bg-white dark:bg-vulcan-300 border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
<TextField
name={name}
value={value}
onChange={onChange}
multiline
/>
);
}
return (
<input
className={`py-1 px-2 sm:text-sm border w-full ${getColors(
'bg-white dark:bg-vulcan-300 border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none',
'bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
<TextField
name={name}
value={value}
onChange={onChange}
/>
@@ -206,7 +206,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFieldFileName)}
</label>
<div className="relative mt-1">
<DetailsInput value={name || ""} onChange={(e) => setFilename(`${e.target.value}.${extension}`)} />
<DetailsInput name={`filename`} value={name || ""} onChange={(e) => setFilename(`${e}.${extension}`)} />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className={`sm:text-sm placeholder-[var(--vscode-input-placeholderForeground)]`}>.{extension}</span>
@@ -219,7 +219,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}
</label>
<div className="mt-1">
<DetailsInput value={title || ""} onChange={(e) => setTitle(e.target.value)} />
<DetailsInput name={`title`} value={title || ""} onChange={(e) => setTitle(e)} />
</div>
</div>
@@ -229,7 +229,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{l10n.t(LocalizationKey.dashboardMediaCommonCaption)}
</label>
<div className="mt-1">
<DetailsInput value={caption || ""} onChange={(e) => setCaption(e.target.value)} isTextArea />
<DetailsInput name={`caption`} value={caption || ""} onChange={(e) => setCaption(e)} isTextArea />
</div>
</div>
)}
@@ -239,7 +239,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{l10n.t(LocalizationKey.dashboardMediaCommonAlt)}
</label>
<div className="mt-1">
<DetailsInput value={alt || ""} onChange={(e) => setAlt(e.target.value)} isTextArea />
<DetailsInput name={`alt`} value={alt || ""} onChange={(e) => setAlt(e)} isTextArea />
</div>
</div>
)}
@@ -52,7 +52,7 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({
name='title'
value={title}
placeholder={l10n.t(LocalizationKey.dashboardSnippetsViewNewFormSnippetInputTitlePlaceholder)}
onChange={(e) => onTitleUpdate(e.currentTarget.value)}
onChange={(e) => onTitleUpdate(e)}
/>
</div>
</div>
@@ -66,7 +66,7 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({
name='description'
value={description}
placeholder={l10n.t(LocalizationKey.dashboardSnippetsViewNewFormSnippetInputDescriptionPlaceholder)}
onChange={(e) => onDescriptionUpdate(e.currentTarget.value)}
onChange={(e) => onDescriptionUpdate(e)}
/>
</div>
</div>
@@ -84,7 +84,7 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({
name='snippet'
value={body}
placeholder={l10n.t(LocalizationKey.dashboardSnippetsViewNewFormSnippetInputSnippetPlaceholder)}
onChange={(e) => onBodyUpdate(e.currentTarget.value)}
onChange={(e) => onBodyUpdate(e)}
isTextArea
/>
</div>
@@ -1,48 +1,33 @@
import * as React from 'react';
import useThemeColors from '../../hooks/useThemeColors';
import { TextField } from '../Common/TextField';
export interface ISnippetInputProps {
name: string;
value?: string;
placeholder?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
onChange?: (value: string) => void;
isTextArea?: boolean;
}
export const SnippetInput: React.FunctionComponent<ISnippetInputProps> = ({ name, value, placeholder, isTextArea, onChange }: React.PropsWithChildren<ISnippetInputProps>) => {
const { getColors } = useThemeColors();
if (isTextArea) {
return (
<textarea
<TextField
name={name}
value={value || ''}
placeholder={placeholder}
rows={5}
className={`block w-full sm:text-sm ${
getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
onChange={onChange}
multiline
/>
)
}
return (
<input
type="text"
<TextField
name={name}
value={value || ''}
placeholder={placeholder}
className={`block w-full sm:text-sm ${
getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
onChange={onChange}
/>
onChange={onChange} />
);
};
@@ -3,6 +3,7 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline';
import { Choice, SnippetField, SnippetInfoField } from '../../../models';
import useThemeColors from '../../hooks/useThemeColors';
import { useEffect } from 'react';
import { TextField } from '../Common/TextField';
export interface ISnippetInputFieldProps {
field: SnippetField;
@@ -32,11 +33,10 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
<select
name={field.name}
value={field.value || ''}
className={`block w-full sm:text-sm ${getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
className={`block w-full sm:text-sm pr-2 appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] border-[var(--frontmatter-border)] focus:border-[var(--vscode-focusBorder)] focus:outline-0`}
style={{
boxShadow: "none"
}}
onChange={(e) => onValueChange(field, e.target.value)}
>
{(field.choices || [])?.map((option: string | Choice, index: number) =>
@@ -59,31 +59,21 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
if (field.type === 'string' && !field.single) {
return (
<textarea
<TextField
name={field.name}
value={field.value || ''}
className={`block w-full sm:text-sm h-auto ${getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
onChange={(e) => onValueChange(field, e.currentTarget.value)}
onChange={(e) => onValueChange(field, e)}
rows={4}
multiline
/>
);
}
return (
<input
type="text"
<TextField
name={field.name}
value={field.value || ''}
className={`block w-full sm:text-sm ${getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
onChange={(e) => onValueChange(field, e.currentTarget.value)}
onChange={(e) => onValueChange(field, e)}
/>
);
};
@@ -1,5 +1,6 @@
import { FunnelIcon, XCircleIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { TextField } from '../Common/TextField';
export interface IFilterInputProps {
label?: string;
@@ -29,27 +30,17 @@ export const FilterInput: React.FunctionComponent<IFilterInputProps> = ({
)
}
<div className="relative flex justify-center">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<TextField
name='filter'
icon={(
<FunnelIcon className={`h-4 w-4 text-[var(--vscode-input-foreground)]`} aria-hidden="true" />
</div>
<input
type="text"
name="filter"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-[var(--vscode-input-border)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent`}
placeholder={placeholder || ""}
value={value}
onChange={(e) => onChange && onChange(e.target.value)}
disabled={disabled}
/>
{value && (
<button onClick={onReset} className="absolute inset-y-0 right-0 pr-3 flex items-center">
<XCircleIcon className={`h-5 w-5 text-[var(--vscode-input-foreground)]`} aria-hidden="true" />
</button>
)}
</div>
value={value}
placeholder={placeholder || ""}
disabled={disabled}
onChange={onChange}
onReset={onReset}
/>
</div>
</div>
);