Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/vite/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ body {
min-height: 0;
height: 100%;
width: 100%;
background: var(--str-chat__background-color);
}

/* Fills viewport minus in-flow `.str-chat__system-notification` when present */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
padding: var(--button-padding-y-sm) var(--spacing-xs);
background-color: inherit;
border-radius: var(--button-radius-lg);
border: 1px solid var(--control-playback-toggle-border);
border: 1px solid var(--chat-border-on-chat-incoming);
color: var(--control-playback-toggle-text, var(--text-primary));
font: var(--str-chat__metadata-emphasis-text);

Expand Down
1 change: 1 addition & 0 deletions src/components/ChatView/styling/ChatView.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
height: 100%;
min-height: 0;
position: relative;
background-color: var(--str-chat__background-color);

.str-chat__chat-view__selector {
display: flex;
Expand Down
7 changes: 3 additions & 4 deletions src/components/Form/NumericInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,10 @@ export const NumericInput = forwardRef<HTMLInputElement, NumericInputProps>(
)}
disabled={disabled || atMin}
onClick={handleDecrement}
size='xs'
variant='secondary'
>
<span aria-hidden className='str-chat__form-numeric-input__stepper-icon'>
<IconMinus />
</span>
<IconMinus className='str-chat__form-numeric-input__stepper-icon' />
</Button>
<input
className='str-chat__form-numeric-input__input'
Expand All @@ -151,7 +150,7 @@ export const NumericInput = forwardRef<HTMLInputElement, NumericInputProps>(
)}
disabled={disabled || atMax}
onClick={handleIncrement}
size='sm'
size='xs'
variant='secondary'
>
<IconPlusSmall className='str-chat__form-numeric-input__stepper-icon' />
Expand Down
248 changes: 177 additions & 71 deletions src/components/Form/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ import { IconCheckmark, IconExclamationMark } from '../Icons';

export type TextInputVariant = 'outline' | 'ghost';

/** Where the active field message (error, success, or neutral) sits relative to the bordered control */
export type TextInputFieldMessagePlacement = 'outside' | 'inside';

export type TextInputProps = Omit<ComponentProps<'input'>, 'className'> & {
/** Root class name */
className?: string;
/**
* `outside` (default): message below the bordered wrapper.
* `inside`: message under the value row, inside the border (error, success, or neutral).
*/
fieldMessagePlacement?: TextInputFieldMessagePlacement;
/** Optional label above the input */
label?: string;
/** Optional leading content (e.g. icon) inside the input area */
Expand All @@ -17,20 +27,109 @@ export type TextInputProps = Omit<ComponentProps<'input'>, 'className'> & {
trailingText?: string;
/** Neutral/helper message below the input (no icon) */
message?: ReactNode;
/** Error message below the input; shown when error is true, with errorMessageIcon */
/** Error message; shown when `error` is true, with `errorMessageIcon` */
errorMessage?: ReactNode;
/** Icon shown before error message (default: IconExclamationMark) */
/** Icon before error text (default: exclamation) */
errorMessageIcon?: ReactNode;
/** Success message below the input; shown when provided, with successMessageIcon */
/** Success message below the input */
successMessage?: ReactNode;
/** Icon shown before success message (default: IconCheckmark) */
/** Icon before success text (default: checkmark) */
successMessageIcon?: ReactNode;
/** When true, shows error border and error message styling */
/** When true, error border and error styling */
error?: boolean;
/** Visual variant: outline = border always visible, ghost = border only on focus */
/** `outline` = border always; `ghost` = border on focus */
variant?: TextInputVariant;
/** Optional class name for the root wrapper */
className?: string;
};

type TextInputIconMessageLineProps = {
icon: ReactNode;
text: ReactNode;
};

const TextInputIconMessageLine = ({ icon, text }: TextInputIconMessageLineProps) => (
<>
<span aria-hidden className='str-chat__form-text-input__message-icon'>
{icon}
</span>
<span className='str-chat__form-text-input__message-text'>{text}</span>
</>
);

/** At most one of error / success / neutral is shown under the field */
type TextInputFieldMessageProps =
| {
kind: 'error';
id?: string;
insidePlacement: boolean;
errorMessageIcon?: ReactNode;
text: ReactNode;
}
| {
kind: 'success';
id?: string;
insidePlacement: boolean;
successMessageIcon?: ReactNode;
text: ReactNode;
}
| {
kind: 'neutral';
id?: string;
insidePlacement: boolean;
text: ReactNode;
};

const TextInputFieldMessage = (props: TextInputFieldMessageProps) => {
if (props.kind === 'neutral') {
return (
<div
className={clsx(
'str-chat__form-text-input__message',
props.insidePlacement &&
'str-chat__form-text-input__message--field-message-inside',
)}
id={props.id}
>
{props.text}
</div>
);
} else if (props.kind === 'success') {
return (
<div
className={clsx(
'str-chat__form-text-input__message',
'str-chat__form-text-input__message--success',
props.insidePlacement &&
'str-chat__form-text-input__message--field-message-inside',
)}
id={props.id}
>
<TextInputIconMessageLine
icon={props.successMessageIcon ?? <IconCheckmark />}
text={props.text}
/>
</div>
);
} else if (props.kind === 'error') {
return (
<div
className={clsx(
'str-chat__form-text-input__message',
'str-chat__form-field-error',
props.insidePlacement &&
'str-chat__form-text-input__message--field-message-inside',
)}
id={props.id}
role='alert'
>
<TextInputIconMessageLine
icon={props.errorMessageIcon ?? <IconExclamationMark />}
text={props.text}
/>
</div>
);
}

return null;
};

export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
Expand All @@ -40,6 +139,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
error = false,
errorMessage,
errorMessageIcon,
fieldMessagePlacement = 'outside',
id: idProp,
label,
leading,
Expand All @@ -53,93 +153,99 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
},
ref,
) {
const generatedId = useStableId();
const id = idProp ?? generatedId;
const autoId = useStableId();
const id = idProp ?? autoId;

const displayError = error && (errorMessage != null || message != null);
const displaySuccess = successMessage != null;
const displayNeutralMessage = message != null && !error;
const displayMessage = displayError || displaySuccess || displayNeutralMessage;
const hasError = error && (errorMessage != null || message != null);
const showSuccess = !hasError && successMessage != null;
const showNeutral = !hasError && !showSuccess && message != null;
const hasFeedback = hasError || showSuccess || showNeutral;
const messageInside = fieldMessagePlacement === 'inside' && hasFeedback;

const messageId = displayMessage ? `${id}-message` : undefined;
const messageId = hasError
? `${id}-field-error`
: showSuccess || showNeutral
? `${id}-message`
: undefined;
const describedBy = messageId;

const messageContent = displayError ? (
<>
<span aria-hidden className='str-chat__form-text-input__message-icon'>
{errorMessageIcon ?? <IconExclamationMark />}
</span>
{errorMessage ?? message}
</>
) : displaySuccess ? (
<>
<span aria-hidden className='str-chat__form-text-input__message-icon'>
{successMessageIcon ?? <IconCheckmark />}
</span>
{successMessage}
</>
) : displayNeutralMessage ? (
(message as ReactNode)
const fieldMessage = hasError ? (
<TextInputFieldMessage
errorMessageIcon={errorMessageIcon}
id={messageId}
insidePlacement={messageInside}
kind='error'
text={errorMessage ?? message}
/>
) : showSuccess ? (
<TextInputFieldMessage
id={messageId}
insidePlacement={messageInside}
kind='success'
successMessageIcon={successMessageIcon}
text={successMessage}
/>
) : showNeutral ? (
<TextInputFieldMessage
id={messageId}
insidePlacement={messageInside}
kind='neutral'
text={message}
/>
) : null;

return (
<div
className={clsx(
'str-chat__form-text-input',
error && 'str-chat__form-text-input--error',
displaySuccess && 'str-chat__form-text-input--success',
showSuccess && 'str-chat__form-text-input--success',
disabled && 'str-chat__form-text-input--disabled',
messageInside && 'str-chat__form-text-input--field-message-inside',
className,
)}
>
{!!label && (
{label ? (
<label className='str-chat__form-text-input__label' htmlFor={id}>
{label}
</label>
)}
) : null}
<div
className={clsx(
'str-chat__form-text-input__wrapper',
`str-chat__form-text-input__wrapper--${variant}`,
messageInside && 'str-chat__form-text-input__wrapper--field-message-inside',
)}
>
{!!leading && (
<span aria-hidden className='str-chat__form-text-input__leading'>
{leading}
</span>
)}
<input
aria-describedby={messageId}
aria-invalid={error}
className='str-chat__form-text-input__input'
disabled={disabled}
id={id}
ref={ref}
{...inputProps}
/>
{trailingText != null && (
<span aria-hidden className='str-chat__form-text-input__suffix'>
{trailingText}
</span>
)}
{!!trailing && (
<span aria-hidden className='str-chat__form-text-input__trailing'>
{trailing}
</span>
)}
</div>
{messageContent != null && (
<div
className={clsx(
'str-chat__form-text-input__message',
displayError && 'str-chat__form-field-error',
displaySuccess && 'str-chat__form-text-input__message--success',
)}
id={messageId}
role={error ? 'alert' : undefined}
>
{messageContent}
<div className='str-chat__form-text-input__control-row'>
{leading ? (
<span aria-hidden className='str-chat__form-text-input__leading'>
{leading}
</span>
) : null}
<input
aria-describedby={describedBy}
aria-invalid={error}
className='str-chat__form-text-input__input'
disabled={disabled}
id={id}
ref={ref}
{...inputProps}
/>
{trailingText != null ? (
<span aria-hidden className='str-chat__form-text-input__suffix'>
{trailingText}
</span>
) : null}
{trailing ? (
<span aria-hidden className='str-chat__form-text-input__trailing'>
{trailing}
</span>
) : null}
</div>
)}
{messageInside ? fieldMessage : null}
</div>
{messageInside ? null : fieldMessage}
</div>
);
});
Loading
Loading