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
52 changes: 43 additions & 9 deletions src/components/controls/TextArea/TextArea.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ $block: '.#{variables.$ns}text-area';
--_--placeholder-color: var(--g-color-text-hint);
--_--background-color: transparent;
--_--border-width: #{control-variables.$border-width};
--_--clear-offset: calc(
var(--g-text-area-border-width, #{control-variables.$border-width}) + 1px
);
--_--clear-offset: calc(var(--g-text-area-border-width, #{control-variables.$border-width}));
--_--focus-outline-color: var(--g-text-area-focus-outline-color);

display: inline-block;
Expand Down Expand Up @@ -77,8 +75,6 @@ $block: '.#{variables.$ns}text-area';
}

&__clear {
position: absolute;

&_size_s,
&_size_m {
inset-inline-end: var(--_--clear-offset);
Expand All @@ -92,16 +88,42 @@ $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 {
@include control-mixins.input-control(s);
}

&#{$block}_has-clear #{$block}__control {
padding-inline-end: 26px;
padding-inline-end: 0;
}

--_--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 @@ -111,9 +133,13 @@ $block: '.#{variables.$ns}text-area';
}

&#{$block}_has-clear #{$block}__control {
padding-inline-end: 26px;
padding-inline-end: 0;
}

--_--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 @@ -123,9 +149,13 @@ $block: '.#{variables.$ns}text-area';
}

&#{$block}_has-clear #{$block}__control {
padding-inline-end: 36px;
padding-inline-end: 0;
}

--_--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 @@ -135,9 +165,13 @@ $block: '.#{variables.$ns}text-area';
}

&#{$block}_has-clear #{$block}__control {
padding-inline-end: 36px;
padding-inline-end: 0;
}

--_--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
46 changes: 37 additions & 9 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 Down Expand Up @@ -33,6 +37,7 @@ export type TextAreaProps = BaseInputControlProps<HTMLTextAreaElement> & {
/** An optional element displayed under the lower right corner of the control and sharing the place with the error container */
note?: React.ReactNode;
};

export type TextAreaPin = InputControlPin;
export type TextAreaSize = InputControlSize;
export type TextAreaView = InputControlView;
Expand All @@ -51,6 +56,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 +70,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 +85,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 @@ -154,6 +165,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 +174,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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {TextArea} from '../TextArea';

import {
disabledCases,
errorPlacementCases,
hasClearCases,
maxRowsCases,
minRowsCases,
Expand Down Expand Up @@ -94,6 +95,7 @@ test.describe('TextArea', {tag: '@TextArea'}, () => {
} as const,
{
value: valueCases,
errorPlacement: errorPlacementCases,
},
);

Expand Down
2 changes: 2 additions & 0 deletions src/components/controls/TextArea/__tests__/cases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const hasClearCases: Array<TextAreaProps['hasClear']> = [true];

export const validationStateCases: Array<TextAreaProps['validationState']> = ['invalid'];

export const errorPlacementCases: Cases<TextAreaProps['errorPlacement']> = ['outside', 'inside'];

export const pinCases: Cases<TextAreaProps['pin']> = [
'round-round',
'brick-brick',
Expand Down
Loading