Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import './NumericArrows.scss';

const b = block('numeric-arrows');

interface NumericArrowsProps extends React.HTMLAttributes<'div'> {
interface NumericArrowsProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
size: InputControlSize;
disabled?: boolean;
Expand Down
42 changes: 41 additions & 1 deletion src/components/controls/TextArea/TextArea.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ $block: '.#{variables.$ns}text-area';
}

&__clear {
position: absolute;
display: flex;
align-items: center;
justify-content: center;

&_size_s,
&_size_m {
Expand All @@ -92,6 +94,28 @@ $block: '.#{variables.$ns}text-area';
}
}

&__actions {
display: flex;
align-items: flex-start;
justify-content: flex-end;
}

&__error-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--g-color-text-danger);
padding-block: var(--_--error-icon-padding-block);
padding-inline: var(--_--error-icon-padding-inline-start)
var(--_--error-icon-padding-inline-end);
}

&__clear {
display: flex;
align-items: center;
justify-content: center;
}

&_size {
&_s {
#{$block}__control {
Expand All @@ -102,6 +126,10 @@ $block: '.#{variables.$ns}text-area';
padding-inline-end: 26px;
}

--_--error-icon-padding-block: 5px;
--_--error-icon-padding-inline-start: 0;
--_--error-icon-padding-inline-end: 5px;

--_--border-radius: var(--g-border-radius-s);
}

Expand All @@ -114,6 +142,10 @@ $block: '.#{variables.$ns}text-area';
padding-inline-end: 26px;
}

--_--error-icon-padding-block: 6px;
--_--error-icon-padding-inline-start: 0;
--_--error-icon-padding-inline-end: 6px;

--_--border-radius: var(--g-border-radius-m);
}

Expand All @@ -126,6 +158,10 @@ $block: '.#{variables.$ns}text-area';
padding-inline-end: 36px;
}

--_--error-icon-padding-block: 9px;
--_--error-icon-padding-inline-start: 0;
--_--error-icon-padding-inline-end: 9px;

--_--border-radius: var(--g-border-radius-l);
}

Expand All @@ -138,6 +174,10 @@ $block: '.#{variables.$ns}text-area';
padding-inline-end: 36px;
}

--_--error-icon-padding-block: 13px;
--_--error-icon-padding-inline-start: 0;
--_--error-icon-padding-inline-end: 13px;

--_--border-radius: var(--g-border-radius-xl);
}
}
Expand Down
55 changes: 39 additions & 16 deletions src/components/controls/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import * as React from 'react';

import {TriangleExclamation} from '@gravity-ui/icons';

import {useControlledState, useForkRef, useUniqId} from '../../../hooks';
import {useFormResetHandler} from '../../../hooks/private';
import {Icon} from '../../Icon';
import {Popover} from '../../legacy';
import {block} from '../../utils/cn';
import {ClearButton, mapTextInputSizeToButtonSize} from '../common';
import {OuterAdditionalContent} from '../common/OuterAdditionalContent/OuterAdditionalContent';
Expand All @@ -22,17 +26,15 @@ import './TextArea.scss';
const b = block('text-area');

export type TextAreaProps = BaseInputControlProps<HTMLTextAreaElement> & {
/** The control's html attributes */
controlProps?: React.TextareaHTMLAttributes<HTMLTextAreaElement>;
/** The number of visible text lines for the control. If not specified, the hight will be automatically calculated based on the content */
rows?: number;
/** The number of minimum visible text lines for the control. Ignored if `rows` is specified */
minRows?: number;
/** The number of maximum visible text lines for the control. Ignored if `rows` is specified */
maxRows?: number;
/** An optional element displayed under the lower right corner of the control and sharing the place with the error container */
note?: React.ReactNode;
/** Controls where error message is displayed */
errorPlacement?: 'inside' | 'outside';
};

export type TextAreaPin = InputControlPin;
export type TextAreaSize = InputControlSize;
export type TextAreaView = InputControlView;
Expand All @@ -51,6 +53,7 @@ export const TextArea = React.forwardRef<HTMLSpanElement, TextAreaProps>(
hasClear = false,
error,
errorMessage: errorMessageProp,
errorPlacement: errorPlacementProp = 'outside',
validationState: validationStateProp,
autoComplete,
id: idProp,
Expand All @@ -64,9 +67,10 @@ export const TextArea = React.forwardRef<HTMLSpanElement, TextAreaProps>(
onChange,
} = props;

const {errorMessage, validationState} = errorPropsMapper({
const {errorMessage, errorPlacement, validationState} = errorPropsMapper({
error,
errorMessage: errorMessageProp,
errorPlacement: errorPlacementProp,
validationState: validationStateProp,
});

Expand All @@ -78,12 +82,16 @@ export const TextArea = React.forwardRef<HTMLSpanElement, TextAreaProps>(
const state = getInputControlState(validationState);
const innerId = useUniqId();

const isErrorMsgVisible = validationState === 'invalid' && Boolean(errorMessage);
const isErrorMsgVisible =
validationState === 'invalid' && Boolean(errorMessage) && errorPlacement === 'outside';
const isErrorIconVisible =
validationState === 'invalid' && Boolean(errorMessage) && errorPlacement === 'inside';
const isClearControlVisible = Boolean(hasClear && !disabled && !readOnly && inputValue);
const id = idProp || innerId;

const id = idProp || innerId;
const errorMessageId = useUniqId();
const noteId = useUniqId();

const ariaDescribedBy = [
controlProps?.['aria-describedby'],
note ? noteId : undefined,
Expand Down Expand Up @@ -132,10 +140,8 @@ export const TextArea = React.forwardRef<HTMLSpanElement, TextAreaProps>(

React.useEffect(() => {
const control = innerControlRef.current;

if (control) {
const currHasVerticalScrollbar = control.scrollHeight > control.clientHeight;

if (hasVerticalScrollbar !== currHasVerticalScrollbar) {
setHasVerticalScrollbar(currHasVerticalScrollbar);
}
Expand All @@ -154,6 +160,7 @@ export const TextArea = React.forwardRef<HTMLSpanElement, TextAreaProps>(
state,
pin: view === 'clear' ? undefined : pin,
'has-clear': isClearControlVisible,
'has-error-icon': isErrorIconVisible,
'has-scrollbar': hasVerticalScrollbar,
},
className,
Expand All @@ -162,14 +169,30 @@ export const TextArea = React.forwardRef<HTMLSpanElement, TextAreaProps>(
>
<span className={b('content')}>
<TextAreaControl {...props} {...commonProps} controlRef={handleRef} />
{isClearControlVisible && (
<ClearButton
className={b('clear', {size})}
size={mapTextInputSizeToButtonSize(size)}
onClick={handleClear}
/>

{(isErrorIconVisible || isClearControlVisible) && (
<span className={b('actions')}>
{isClearControlVisible && (
<ClearButton
className={b('clear', {size})}
size={mapTextInputSizeToButtonSize(size)}
onClick={handleClear}
/>
)}
{isErrorIconVisible && (
<Popover content={errorMessage}>
<span className={b('error-icon')}>
<Icon
data={TriangleExclamation}
size={size === 's' ? 12 : 16}
/>
</span>
</Popover>
)}
</span>
)}
</span>

<OuterAdditionalContent
errorMessage={isErrorMsgVisible ? errorMessage : null}
errorMessageId={errorMessageId}
Expand Down
Loading