diff --git a/src/components/controls/TextArea/TextArea.scss b/src/components/controls/TextArea/TextArea.scss index 72dc5a1ca7..0014d01161 100644 --- a/src/components/controls/TextArea/TextArea.scss +++ b/src/components/controls/TextArea/TextArea.scss @@ -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; @@ -77,8 +75,6 @@ $block: '.#{variables.$ns}text-area'; } &__clear { - position: absolute; - &_size_s, &_size_m { inset-inline-end: var(--_--clear-offset); @@ -92,6 +88,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 { @@ -99,9 +117,13 @@ $block: '.#{variables.$ns}text-area'; } &#{$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); } @@ -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); } @@ -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); } @@ -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); } } diff --git a/src/components/controls/TextArea/TextArea.tsx b/src/components/controls/TextArea/TextArea.tsx index 7de57cc0db..77a96ffd80 100644 --- a/src/components/controls/TextArea/TextArea.tsx +++ b/src/components/controls/TextArea/TextArea.tsx @@ -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'; @@ -33,6 +37,7 @@ export type TextAreaProps = BaseInputControlProps & { /** 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; @@ -51,6 +56,7 @@ export const TextArea = React.forwardRef( hasClear = false, error, errorMessage: errorMessageProp, + errorPlacement: errorPlacementProp = 'outside', validationState: validationStateProp, autoComplete, id: idProp, @@ -64,9 +70,10 @@ export const TextArea = React.forwardRef( onChange, } = props; - const {errorMessage, validationState} = errorPropsMapper({ + const {errorMessage, errorPlacement, validationState} = errorPropsMapper({ error, errorMessage: errorMessageProp, + errorPlacement: errorPlacementProp, validationState: validationStateProp, }); @@ -78,12 +85,16 @@ export const TextArea = React.forwardRef( 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, @@ -154,6 +165,7 @@ export const TextArea = React.forwardRef( state, pin: view === 'clear' ? undefined : pin, 'has-clear': isClearControlVisible, + 'has-error-icon': isErrorIconVisible, 'has-scrollbar': hasVerticalScrollbar, }, className, @@ -162,14 +174,30 @@ export const TextArea = React.forwardRef( > - {isClearControlVisible && ( - + + {(isErrorIconVisible || isClearControlVisible) && ( + + {isClearControlVisible && ( + + )} + {isErrorIconVisible && ( + + + + + + )} + )} + { } as const, { value: valueCases, + errorPlacement: errorPlacementCases, }, ); diff --git a/src/components/controls/TextArea/__tests__/cases.tsx b/src/components/controls/TextArea/__tests__/cases.tsx index 3dbadba904..1b59a63934 100644 --- a/src/components/controls/TextArea/__tests__/cases.tsx +++ b/src/components/controls/TextArea/__tests__/cases.tsx @@ -13,6 +13,8 @@ export const hasClearCases: Array = [true]; export const validationStateCases: Array = ['invalid']; +export const errorPlacementCases: Cases = ['outside', 'inside']; + export const pinCases: Cases = [ 'round-round', 'brick-brick',