mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 14:55:44 +00:00
feat(FE-Storyless): add FieldMessage component for consistent field feedback across inputs
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
type FieldMessageTone = 'error' | 'info' | 'success';
|
||||||
|
|
||||||
|
export interface FieldMessageProps {
|
||||||
|
message?: ReactNode;
|
||||||
|
tone?: FieldMessageTone;
|
||||||
|
isVisible?: boolean;
|
||||||
|
persistent?: boolean;
|
||||||
|
className?: string;
|
||||||
|
ariaLive?: 'off' | 'polite' | 'assertive';
|
||||||
|
}
|
||||||
|
|
||||||
|
const toneClassName: Record<FieldMessageTone, string> = {
|
||||||
|
error: 'text-error',
|
||||||
|
info: 'text-base-content/60',
|
||||||
|
success: 'text-success',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared helper to render bottom field feedback without causing layout shift.
|
||||||
|
* Keeps a minimal slot height, but expands when the content wraps onto multiple lines.
|
||||||
|
*/
|
||||||
|
export const FieldMessage = ({
|
||||||
|
message,
|
||||||
|
tone = 'info',
|
||||||
|
isVisible,
|
||||||
|
persistent = true,
|
||||||
|
className,
|
||||||
|
ariaLive,
|
||||||
|
}: FieldMessageProps) => {
|
||||||
|
const hasMessage = Boolean(message);
|
||||||
|
const visible = isVisible ?? hasMessage;
|
||||||
|
const liveRegion = ariaLive ?? (tone === 'error' ? 'assertive' : 'polite');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-live={liveRegion}
|
||||||
|
aria-hidden={!visible && !hasMessage}
|
||||||
|
className={cn(
|
||||||
|
'relative w-full text-sm leading-5 transition-[opacity,transform] duration-150 ease-out',
|
||||||
|
persistent && 'min-h-[1.25rem]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'block whitespace-pre-line',
|
||||||
|
toneClassName[tone],
|
||||||
|
visible
|
||||||
|
? 'opacity-100 translate-y-0'
|
||||||
|
: 'opacity-0 -translate-y-1 pointer-events-none'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{visible || persistent ? (message ?? '\u00A0') : message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FieldMessage;
|
||||||
@@ -2,6 +2,7 @@ import { Ref } from 'react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { TextInputProps } from '@/components/input/TextInput';
|
import { TextInputProps } from '@/components/input/TextInput';
|
||||||
|
import FieldMessage from '@/components/helper/FieldMessage';
|
||||||
|
|
||||||
interface FileInputProps
|
interface FileInputProps
|
||||||
extends Omit<
|
extends Omit<
|
||||||
@@ -37,6 +38,9 @@ const FileInput = ({
|
|||||||
onBlur,
|
onBlur,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}: FileInputProps) => {
|
}: FileInputProps) => {
|
||||||
|
const showErrorMessage = Boolean(isError && errorMessage);
|
||||||
|
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -76,11 +80,11 @@ const FileInput = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{bottomLabel && (
|
<FieldMessage
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
message={feedbackMessage}
|
||||||
)}
|
tone={showErrorMessage ? 'error' : 'info'}
|
||||||
|
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import FieldMessage from '@/components/helper/FieldMessage';
|
||||||
|
|
||||||
// Utility Functions
|
// Utility Functions
|
||||||
const formatNumber = (
|
const formatNumber = (
|
||||||
@@ -25,7 +26,10 @@ const formatNumber = (
|
|||||||
if (isNaN(numValue)) return '';
|
if (isNaN(numValue)) return '';
|
||||||
|
|
||||||
const parts = numValue.toFixed(decimals).split('.');
|
const parts = numValue.toFixed(decimals).split('.');
|
||||||
const integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
|
const integerPart = parts[0].replace(
|
||||||
|
/\B(?=(\d{3})+(?!\d))/g,
|
||||||
|
thousandSeparator
|
||||||
|
);
|
||||||
const decimalPart = parts[1];
|
const decimalPart = parts[1];
|
||||||
|
|
||||||
return decimals > 0 && decimalPart
|
return decimals > 0 && decimalPart
|
||||||
@@ -180,11 +184,13 @@ const NumberInput = ({
|
|||||||
const [displayValue, setDisplayValue] = useState<string>('');
|
const [displayValue, setDisplayValue] = useState<string>('');
|
||||||
|
|
||||||
// Determine if decimals are allowed based on maskType
|
// Determine if decimals are allowed based on maskType
|
||||||
const allowDecimal = maskType === 'decimal' || maskType === 'weight' || decimals > 0;
|
const allowDecimal =
|
||||||
|
maskType === 'decimal' || maskType === 'weight' || decimals > 0;
|
||||||
|
|
||||||
// Format value for display based on maskType
|
// Format value for display based on maskType
|
||||||
const getFormattedValue = (rawValue: number | string): string => {
|
const getFormattedValue = (rawValue: number | string): string => {
|
||||||
if (rawValue === '' || rawValue === null || rawValue === undefined) return '';
|
if (rawValue === '' || rawValue === null || rawValue === undefined)
|
||||||
|
return '';
|
||||||
|
|
||||||
switch (maskType) {
|
switch (maskType) {
|
||||||
case 'currency':
|
case 'currency':
|
||||||
@@ -193,7 +199,12 @@ const NumberInput = ({
|
|||||||
return formatWeight(rawValue, weightUnit, decimals);
|
return formatWeight(rawValue, weightUnit, decimals);
|
||||||
case 'decimal':
|
case 'decimal':
|
||||||
case 'number':
|
case 'number':
|
||||||
return formatNumber(rawValue, decimals, thousandSeparator, decimalSeparator);
|
return formatNumber(
|
||||||
|
rawValue,
|
||||||
|
decimals,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return String(rawValue);
|
return String(rawValue);
|
||||||
}
|
}
|
||||||
@@ -216,10 +227,18 @@ const NumberInput = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean input
|
// Clean input
|
||||||
const cleaned = cleanNumericInput(inputValue, allowDecimal, decimalSeparator);
|
const cleaned = cleanNumericInput(
|
||||||
|
inputValue,
|
||||||
|
allowDecimal,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
|
|
||||||
// Parse to number
|
// Parse to number
|
||||||
let numericValue = parseNumber(cleaned, thousandSeparator, decimalSeparator);
|
let numericValue = parseNumber(
|
||||||
|
cleaned,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
|
|
||||||
// Apply validation
|
// Apply validation
|
||||||
if (!allowNegative && numericValue < 0) {
|
if (!allowNegative && numericValue < 0) {
|
||||||
@@ -262,7 +281,11 @@ const NumberInput = ({
|
|||||||
const handleIncrement = () => {
|
const handleIncrement = () => {
|
||||||
if (disabled || readOnly) return;
|
if (disabled || readOnly) return;
|
||||||
|
|
||||||
const currentValue = parseNumber(displayValue, thousandSeparator, decimalSeparator);
|
const currentValue = parseNumber(
|
||||||
|
displayValue,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
let newValue = currentValue + step;
|
let newValue = currentValue + step;
|
||||||
|
|
||||||
// Apply max validation
|
// Apply max validation
|
||||||
@@ -296,7 +319,11 @@ const NumberInput = ({
|
|||||||
const handleDecrement = () => {
|
const handleDecrement = () => {
|
||||||
if (disabled || readOnly) return;
|
if (disabled || readOnly) return;
|
||||||
|
|
||||||
const currentValue = parseNumber(displayValue, thousandSeparator, decimalSeparator);
|
const currentValue = parseNumber(
|
||||||
|
displayValue,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
let newValue = currentValue - step;
|
let newValue = currentValue - step;
|
||||||
|
|
||||||
// Apply min validation (prevent negative if not allowed)
|
// Apply min validation (prevent negative if not allowed)
|
||||||
@@ -329,6 +356,9 @@ const NumberInput = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showErrorMessage = Boolean(isError && errorMessage);
|
||||||
|
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -431,12 +461,11 @@ const NumberInput = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
<FieldMessage
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
message={feedbackMessage}
|
||||||
)}
|
tone={showErrorMessage ? 'error' : 'info'}
|
||||||
{isError && errorMessage && (
|
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
||||||
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { ChangeEventHandler, ReactNode } from 'react';
|
import { ChangeEventHandler, ReactNode } from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import FieldMessage from '@/components/helper/FieldMessage';
|
||||||
|
|
||||||
export interface RadioOption {
|
export interface RadioOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -47,6 +48,8 @@ const RadioInput = ({
|
|||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
}: RadioInputProps) => {
|
}: RadioInputProps) => {
|
||||||
|
const showErrorMessage = Boolean(isError && errorMessage);
|
||||||
|
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
||||||
return (
|
return (
|
||||||
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
||||||
{/* Label atas */}
|
{/* Label atas */}
|
||||||
@@ -97,15 +100,11 @@ const RadioInput = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Label bawah */}
|
<FieldMessage
|
||||||
{!isError && bottomLabel && (
|
message={feedbackMessage}
|
||||||
<p className='text-sm opacity-60'>{bottomLabel}</p>
|
tone={showErrorMessage ? 'error' : 'info'}
|
||||||
)}
|
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
||||||
|
/>
|
||||||
{/* Pesan error */}
|
|
||||||
{isError && errorMessage && (
|
|
||||||
<p className='text-sm text-error'>{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
ComponentType,
|
|
||||||
ReactNode,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import Select, {
|
import Select, {
|
||||||
OptionProps,
|
OptionProps,
|
||||||
GroupBase,
|
GroupBase,
|
||||||
@@ -18,6 +12,7 @@ import CreatableSelect from 'react-select/creatable';
|
|||||||
import makeAnimated from 'react-select/animated';
|
import makeAnimated from 'react-select/animated';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import FieldMessage from '@/components/helper/FieldMessage';
|
||||||
|
|
||||||
export interface OptionType {
|
export interface OptionType {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
@@ -98,10 +93,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
return { ...base, IndicatorSeparator: () => null };
|
return { ...base, IndicatorSeparator: () => null };
|
||||||
}, [isAnimated]);
|
}, [isAnimated]);
|
||||||
|
|
||||||
const internalInputChangeHandler = (
|
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
|
||||||
val: string,
|
|
||||||
meta: InputActionMeta
|
|
||||||
) => {
|
|
||||||
if (meta.action === 'input-change') setInternalInputValue(val);
|
if (meta.action === 'input-change') setInternalInputValue(val);
|
||||||
if (meta.action === 'menu-close') setInternalInputValue('');
|
if (meta.action === 'menu-close') setInternalInputValue('');
|
||||||
};
|
};
|
||||||
@@ -113,9 +105,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
const SelectComponent = createables ? CreatableSelect : Select;
|
const SelectComponent = createables ? CreatableSelect : Select;
|
||||||
|
|
||||||
/** 🎯 handleChange tanpa any */
|
/** 🎯 handleChange tanpa any */
|
||||||
const handleChange = (
|
const handleChange = (val: MultiValue<T> | SingleValue<T>): void => {
|
||||||
val: MultiValue<T> | SingleValue<T>
|
|
||||||
): void => {
|
|
||||||
if (!val) {
|
if (!val) {
|
||||||
onChange?.(null);
|
onChange?.(null);
|
||||||
return;
|
return;
|
||||||
@@ -128,6 +118,9 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showErrorMessage = Boolean(isError && errorMessage);
|
||||||
|
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -145,15 +138,15 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{required && (
|
{required && (
|
||||||
<span className="tooltip tooltip-error" data-tip="required">
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
<span className="text-error"> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SelectComponent<T, boolean, GroupBase<T>>
|
<SelectComponent<T, boolean, GroupBase<T>>
|
||||||
instanceId="select"
|
instanceId='select'
|
||||||
value={value ?? (isMulti ? [] : null)}
|
value={value ?? (isMulti ? [] : null)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
options={options}
|
options={options}
|
||||||
@@ -225,10 +218,11 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isError && <p className="w-full text-sm text-error">{errorMessage}</p>}
|
<FieldMessage
|
||||||
{!isError && bottomLabel && (
|
message={feedbackMessage}
|
||||||
<p className="w-full text-sm opacity-60">{bottomLabel}</p>
|
tone={showErrorMessage ? 'error' : 'info'}
|
||||||
)}
|
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react';
|
import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import FieldMessage from '@/components/helper/FieldMessage';
|
||||||
|
|
||||||
export interface TagInputProps {
|
export interface TagInputProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -73,6 +74,9 @@ const TagInput: React.FC<TagInputProps> = ({
|
|||||||
setInputValue(e.target.value);
|
setInputValue(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showErrorMessage = Boolean(isError && errorMessage);
|
||||||
|
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -157,11 +161,11 @@ const TagInput: React.FC<TagInputProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom label or error message */}
|
<FieldMessage
|
||||||
{!isError && bottomLabel && (
|
message={feedbackMessage}
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
tone={showErrorMessage ? 'error' : 'info'}
|
||||||
)}
|
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
FocusEventHandler,
|
|
||||||
ReactNode,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import FieldMessage from '@/components/helper/FieldMessage';
|
||||||
|
|
||||||
export interface TextAreaProps {
|
export interface TextAreaProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -52,8 +49,11 @@ const TextArea = ({
|
|||||||
onBlur,
|
onBlur,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
rows = 3
|
rows = 3,
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
|
const showErrorMessage = Boolean(isError && errorMessage);
|
||||||
|
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -83,40 +83,41 @@ const TextArea = ({
|
|||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{startAdornment && startAdornment}
|
{startAdornment && startAdornment}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all',
|
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
|
||||||
id={name}
|
|
||||||
name={name}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
rows={rows}
|
|
||||||
onChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
disabled={disabled}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(isLoading || endAdornment) && (
|
|
||||||
<div className='flex flex-row gap-2'>
|
|
||||||
{isLoading && <span className='loading loading-spinner' />}
|
|
||||||
|
|
||||||
{endAdornment && endAdornment}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
rows={rows}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
{(isLoading || endAdornment) && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<div className='flex flex-row gap-2'>
|
||||||
|
{isLoading && <span className='loading loading-spinner' />}
|
||||||
|
|
||||||
|
{endAdornment && endAdornment}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
|
||||||
|
<FieldMessage
|
||||||
|
message={feedbackMessage}
|
||||||
|
tone={showErrorMessage ? 'error' : 'info'}
|
||||||
|
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import FieldMessage from '@/components/helper/FieldMessage';
|
||||||
|
|
||||||
export interface TextInputProps {
|
export interface TextInputProps {
|
||||||
type?: HTMLInputTypeAttribute;
|
type?: HTMLInputTypeAttribute;
|
||||||
@@ -55,6 +56,9 @@ const TextInput = ({
|
|||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: TextInputProps) => {
|
}: TextInputProps) => {
|
||||||
|
const showErrorMessage = Boolean(isError && errorMessage);
|
||||||
|
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -119,12 +123,11 @@ const TextInput = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
<FieldMessage
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
message={feedbackMessage}
|
||||||
)}
|
tone={showErrorMessage ? 'error' : 'info'}
|
||||||
{isError && errorMessage && (
|
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
||||||
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user