Skip to content
Snippets Groups Projects
Commit 1f785b8d authored by Loïc Correnson's avatar Loïc Correnson
Browse files

[dome] text fields

parent a454fcf3
No related branches found
No related tags found
No related merge requests found
...@@ -49,7 +49,7 @@ export function validate<A>( ...@@ -49,7 +49,7 @@ export function validate<A>(
export function isValid(err: Error): boolean { return !err; } export function isValid(err: Error): boolean { return !err; }
type ObjectError = { [key: string]: Error } type ObjectError = { [key: string]: Error };
function isObjectError(err: Error): err is ObjectError { function isObjectError(err: Error): err is ObjectError {
return typeof err === 'object' && !Array.isArray(err); return typeof err === 'object' && !Array.isArray(err);
...@@ -86,14 +86,22 @@ export function useState<A>( ...@@ -86,14 +86,22 @@ export function useState<A>(
const [value, setValue] = React.useState<A>(defaultValue); const [value, setValue] = React.useState<A>(defaultValue);
const [error, setError] = React.useState<Error>(undefined); const [error, setError] = React.useState<Error>(undefined);
const setState = React.useCallback((newValue: A, newError: Error) => { const setState = React.useCallback((newValue: A, newError: Error) => {
const localError = validate(value, checker) || newError; const localError = validate(newValue, checker) || newError;
setValue(newValue); setValue(newValue);
setError(localError); setError(localError);
if (onChange) onChange(newValue, localError); if (onChange) onChange(newValue, localError);
}, [setValue, setError, onChange]); }, [checker, setValue, setError, onChange]);
return [value, error, setState]; return [value, error, setState];
} }
export function useDefault<A>(
state: FieldState<A | undefined>,
defaultValue: A,
): FieldState<A> {
const [value, error, setState] = state;
return [value ?? defaultValue, error, setState];
}
export function useChecker<A>( export function useChecker<A>(
state: FieldState<A>, state: FieldState<A>,
checker?: Checker<A>, checker?: Checker<A>,
...@@ -102,7 +110,7 @@ export function useChecker<A>( ...@@ -102,7 +110,7 @@ export function useChecker<A>(
const update = React.useCallback((newValue: A, newError: Error) => { const update = React.useCallback((newValue: A, newError: Error) => {
const localError = validate(newValue, checker) || newError; const localError = validate(newValue, checker) || newError;
setState(newValue, localError); setState(newValue, localError);
}, [setState]); }, [checker, setState]);
return [value, error, update]; return [value, error, update];
} }
...@@ -110,7 +118,6 @@ export function useProperty<A, K extends keyof A>( ...@@ -110,7 +118,6 @@ export function useProperty<A, K extends keyof A>(
state: FieldState<A>, state: FieldState<A>,
property: K, property: K,
checker?: Checker<A[K]>, checker?: Checker<A[K]>,
onError?: string,
): FieldState<A[K]> { ): FieldState<A[K]> {
const [value, error, setState] = state; const [value, error, setState] = state;
const update = React.useCallback((newProp: A[K], newError: Error) => { const update = React.useCallback((newProp: A[K], newError: Error) => {
...@@ -119,7 +126,7 @@ export function useProperty<A, K extends keyof A>( ...@@ -119,7 +126,7 @@ export function useProperty<A, K extends keyof A>(
const propError = validate(newProp, checker) || newError; const propError = validate(newProp, checker) || newError;
const localError = { ...objError, [property]: propError }; const localError = { ...objError, [property]: propError };
setState(newValue, isValidObject(localError) ? undefined : localError); setState(newValue, isValidObject(localError) ? undefined : localError);
}, [value, error, setState, property, checker, onError]); }, [value, error, setState, property, checker]);
return [value[property], error, update]; return [value[property], error, update];
} }
...@@ -128,12 +135,12 @@ export function useLatency<A>( ...@@ -128,12 +135,12 @@ export function useLatency<A>(
latency?: number, latency?: number,
): FieldState<A> { ): FieldState<A> {
const [initValue, initError, setState] = state; const [initValue, initError, setState] = state;
const period = Math.max(latency ?? 0, 0); const period = latency ?? 0;
const [value, setValue] = React.useState(initValue); const [value, setValue] = React.useState(initValue);
const [error, setError] = React.useState(initError); const [error, setError] = React.useState(initError);
const propagate = React.useCallback( const propagate = React.useMemo(
debounce(setState, period), () => (period > 0 ? debounce(setState, period) : setState),
[latency, setState], [period, setState],
); );
const update = React.useCallback((newValue, newError) => { const update = React.useCallback((newValue, newError) => {
setValue(newValue); setValue(newValue);
...@@ -147,7 +154,6 @@ export function useIndex<A>( ...@@ -147,7 +154,6 @@ export function useIndex<A>(
state: FieldState<A[]>, state: FieldState<A[]>,
index: number, index: number,
checker?: Checker<A>, checker?: Checker<A>,
onError?: string,
): FieldState<A> { ): FieldState<A> {
const [array, error, setState] = state; const [array, error, setState] = state;
const update = React.useCallback((newValue: A, newError: Error) => { const update = React.useCallback((newValue: A, newError: Error) => {
...@@ -157,7 +163,7 @@ export function useIndex<A>( ...@@ -157,7 +163,7 @@ export function useIndex<A>(
const valueError = validate(newValue, checker) || newError; const valueError = validate(newValue, checker) || newError;
localError[index] = valueError; localError[index] = valueError;
setState(newArray, isValidArray(localError) ? undefined : localError); setState(newArray, isValidArray(localError) ? undefined : localError);
}, [array, error, setState, index, checker, onError]); }, [array, error, setState, index, checker]);
const itemError = isArrayError(error) ? error[index] : undefined; const itemError = isArrayError(error) ? error[index] : undefined;
return [array[index], itemError, update]; return [array[index], itemError, update];
} }
...@@ -284,9 +290,7 @@ export function Warning(props: WarningProps) { ...@@ -284,9 +290,7 @@ export function Warning(props: WarningProps) {
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
/** /**
Layout its contents inside a full-width block. Layout its contents inside a full-width container.
The children are _not_ supposed to contain `<Field />` like elements,
only custom controls that fits a full-width containter.
@category Form Containers @category Form Containers
*/ */
export function Block(props: FilterProps & Children) { export function Block(props: FilterProps & Children) {
...@@ -304,6 +308,7 @@ export function Block(props: FilterProps & Children) { ...@@ -304,6 +308,7 @@ export function Block(props: FilterProps & Children) {
// --- Section Container // --- Section Container
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
/** @category Form Fields */
export interface SectionProps extends FilterProps, Children { export interface SectionProps extends FilterProps, Children {
/** Section name. */ /** Section name. */
label: string; label: string;
...@@ -319,7 +324,7 @@ export interface SectionProps extends FilterProps, Children { ...@@ -319,7 +324,7 @@ export interface SectionProps extends FilterProps, Children {
unfold?: boolean; unfold?: boolean;
} }
/** Form Section. */ /** @category Form Fields */
export function Section(props: SectionProps) { export function Section(props: SectionProps) {
const { label, title, children, warning, error, ...filter } = props; const { label, title, children, warning, error, ...filter } = props;
const { disabled, hidden } = useContext(filter); const { disabled, hidden } = useContext(filter);
...@@ -352,10 +357,11 @@ export function Section(props: SectionProps) { ...@@ -352,10 +357,11 @@ export function Section(props: SectionProps) {
} }
/* --------------------------------------------------------------------------*/ /* --------------------------------------------------------------------------*/
/* --- Value Filter --- */ /* --- Generic Field --- */
/* --------------------------------------------------------------------------*/ /* --------------------------------------------------------------------------*/
export interface FieldProps extends FilterProps, Children { /** @category Form Fields */
export interface GenericFieldProps extends FilterProps, Children {
/** Field label. */ /** Field label. */
label: string; label: string;
/** Field tooltip text. */ /** Field tooltip text. */
...@@ -364,10 +370,15 @@ export interface FieldProps extends FilterProps, Children { ...@@ -364,10 +370,15 @@ export interface FieldProps extends FilterProps, Children {
offset?: number; offset?: number;
/** Html tag `<input />` element. */ /** Html tag `<input />` element. */
htmlFor?: string; htmlFor?: string;
/** Warning message (in case of error). */
onError?: string;
/** Error (if any). */
error?: Error;
} }
let FIELDID = 0; let FIELDID = 0;
/** Generates a unique, stable identifier. */
export function useHtmlFor() { export function useHtmlFor() {
return React.useMemo(() => `dome-field ${FIELDID++}`, []); return React.useMemo(() => `dome-field ${FIELDID++}`, []);
} }
...@@ -375,8 +386,9 @@ export function useHtmlFor() { ...@@ -375,8 +386,9 @@ export function useHtmlFor() {
/** /**
Generic Field. Generic Field.
Layout its content in a top-left aligned box on the right of the label. Layout its content in a top-left aligned box on the right of the label.
@category Form Fields
*/ */
export function Field(props: FieldProps) { export function Field(props: GenericFieldProps) {
const { hidden, disabled } = useContext(props); const { hidden, disabled } = useContext(props);
if (hidden) return null; if (hidden) return null;
...@@ -393,6 +405,12 @@ export function Field(props: FieldProps) { ...@@ -393,6 +405,12 @@ export function Field(props: FieldProps) {
disabled && 'dome-disabled', disabled && 'dome-disabled',
); );
const { onError, error } = props;
const WARNING = error ? (
<Warning offset={offset} warning={onError} error={error} />
) : null;
return ( return (
<> <>
<label <label
...@@ -405,10 +423,210 @@ export function Field(props: FieldProps) { ...@@ -405,10 +423,210 @@ export function Field(props: FieldProps) {
</label> </label>
<div className={cssField}> <div className={cssField}>
{children} {children}
{WARNING}
</div> </div>
</> </>
); );
} }
/* --------------------------------------------------------------------------*/
/* --- Input Fields ---*/
/* --------------------------------------------------------------------------*/
/** @category Form Fields */
export interface FieldProps<A> extends FilterProps {
/** Field label. */
label: string;
/** Field tooltip text. */
title?: string;
/** Field state. */
state: FieldState<A>;
/** Checker. */
checker?: Checker<A>;
/** Alternative error message (in case of error). */
onError?: string;
}
type InputEvent = { target: { value: string } };
type InputState = [string, Error, (evt: InputEvent) => void];
function useTextInputField(
props: FieldTextProps,
defaultLatency: number,
): InputState {
const checked = useChecker(props.state, props.checker);
const period = props.latency ?? defaultLatency;
const [value, error, setState] = useLatency(checked, period);
const onChange = (evt: InputEvent) => {
setState(evt.target.value, undefined);
};
return [value || '', error, onChange];
}
/* --------------------------------------------------------------------------*/
/* --- Text Fields ---*/
/* --------------------------------------------------------------------------*/
/** @category Form Fields */
export interface FieldTextProps extends FieldProps<string | undefined> {
placeholder?: string;
className?: string;
style?: React.CSSProperties;
latency?: number;
}
/**
Text Field.
@category Form Fields
*/
export const FieldText = (props: FieldTextProps) => {
const { disabled } = useContext(props);
const id = useHtmlFor();
const css = Utils.classes('dome-xForm-text-field', props.className);
const [value, error, onChange] = useTextInputField(props, 600);
return (
<Field
{...props}
offset={4}
htmlFor={id}
error={error}
>
<input
id={id}
type="text"
value={value}
className={css}
style={props.style}
disabled={disabled}
placeholder={props.placeholder}
onChange={onChange}
/>
</Field>
);
};
/**
Monospaced Text Field.
@category Form Fields
*/
export const FieldCode = (props: FieldTextProps) => {
const { disabled } = useContext(props);
const id = useHtmlFor();
const [value, error, onChange] = useTextInputField(props, 600);
const css = Utils.classes(
'dome-xForm-text-field',
'dome-text-code',
props.className,
);
return (
<Field
{...props}
offset={4}
htmlFor={id}
error={error}
>
<input
id={id}
type="text"
value={value}
className={css}
style={props.style}
disabled={disabled}
placeholder={props.placeholder}
onChange={onChange}
/>
</Field>
);
};
/* --------------------------------------------------------------------------*/
/* --- Text Area Fields ---*/
/* --------------------------------------------------------------------------*/
/** @category Form Fields */
export interface FieldTextAreaProps extends FieldTextProps {
/** Number of columns (default 35, min 5). */
cols?: number;
/** Number of rows (default 5, min 2). */
rows?: number;
}
/**
Text Field Area.
@category Form Fields
*/
export const FieldTextArea = (props: FieldTextAreaProps) => {
const { disabled } = useContext(props);
const id = useHtmlFor();
const [value, error, onChange] = useTextInputField(props, 900);
const cols = Math.max(5, props.cols ?? 35);
const rows = Math.max(2, props.rows ?? 5);
const css = Utils.classes(
'dome-xForm-textarea-field',
props.className,
);
return (
<Field
{...props}
offset={4}
htmlFor={id}
error={error}
>
<textarea
id={id}
wrap="hard"
spellCheck
value={value}
cols={cols}
rows={rows - 1}
className={css}
style={props.style}
disabled={disabled}
placeholder={props.placeholder}
onChange={onChange}
/>
</Field>
);
};
/**
Monospaced Text Field Area.
@category Form Fields
*/
export const FieldCodeArea = (props: FieldTextAreaProps) => {
const { disabled } = useContext(props);
const id = useHtmlFor();
const [value, error, onChange] = useTextInputField(props, 900);
const cols = Math.max(5, props.cols ?? 35);
const rows = Math.max(2, props.rows ?? 5);
const css = Utils.classes(
'dome-xForm-textarea-field',
'dome-text-code',
props.className,
);
return (
<Field
{...props}
offset={4}
htmlFor={id}
error={error}
>
<textarea
id={id}
wrap="off"
spellCheck={false}
value={value}
cols={cols}
rows={rows}
className={css}
style={props.style}
disabled={disabled}
placeholder={props.placeholder}
onChange={onChange}
/>
</Field>
);
};
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment