From dea4ecb6b2be6fc0f7c7ae605b5fe1e9de65a02f Mon Sep 17 00:00:00 2001 From: Krotov Petr <83280920+KrotovPetr@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:46:10 +0300 Subject: [PATCH 1/2] fix: add HTMLDivElement type to NumericArrowsProps --- src/components/NumberInput/NumericArrows/NumericArrows.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NumberInput/NumericArrows/NumericArrows.tsx b/src/components/NumberInput/NumericArrows/NumericArrows.tsx index 9b3b4ff8d2..c5dee4efcf 100644 --- a/src/components/NumberInput/NumericArrows/NumericArrows.tsx +++ b/src/components/NumberInput/NumericArrows/NumericArrows.tsx @@ -15,7 +15,7 @@ import './NumericArrows.scss'; const b = block('numeric-arrows'); -interface NumericArrowsProps extends React.HTMLAttributes<'div'> { +interface NumericArrowsProps extends React.HTMLAttributes { className?: string; size: InputControlSize; disabled?: boolean; From 6ccca7ed56192814a739bc87ff79c350392a261d Mon Sep 17 00:00:00 2001 From: Krotov Petr <83280920+KrotovPetr@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:04:42 +0300 Subject: [PATCH 2/2] feat: add error-placement to TextArea --- .../controls/TextArea/TextArea.scss | 42 +++++++++++++- src/components/controls/TextArea/TextArea.tsx | 55 +++++++++++++------ 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/components/controls/TextArea/TextArea.scss b/src/components/controls/TextArea/TextArea.scss index 72dc5a1ca7..fde393ec4d 100644 --- a/src/components/controls/TextArea/TextArea.scss +++ b/src/components/controls/TextArea/TextArea.scss @@ -77,7 +77,9 @@ $block: '.#{variables.$ns}text-area'; } &__clear { - position: absolute; + display: flex; + align-items: center; + justify-content: center; &_size_s, &_size_m { @@ -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 { @@ -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); } @@ -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); } @@ -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); } @@ -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); } } diff --git a/src/components/controls/TextArea/TextArea.tsx b/src/components/controls/TextArea/TextArea.tsx index 7de57cc0db..53ace05f7a 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'; @@ -22,17 +26,15 @@ import './TextArea.scss'; const b = block('text-area'); export type TextAreaProps = BaseInputControlProps & { - /** The control's html attributes */ controlProps?: React.TextareaHTMLAttributes; - /** 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; @@ -51,6 +53,7 @@ export const TextArea = React.forwardRef( hasClear = false, error, errorMessage: errorMessageProp, + errorPlacement: errorPlacementProp = 'outside', validationState: validationStateProp, autoComplete, id: idProp, @@ -64,9 +67,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 +82,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, @@ -132,10 +140,8 @@ export const TextArea = React.forwardRef( React.useEffect(() => { const control = innerControlRef.current; - if (control) { const currHasVerticalScrollbar = control.scrollHeight > control.clientHeight; - if (hasVerticalScrollbar !== currHasVerticalScrollbar) { setHasVerticalScrollbar(currHasVerticalScrollbar); } @@ -154,6 +160,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 +169,30 @@ export const TextArea = React.forwardRef( > - {isClearControlVisible && ( - + + {(isErrorIconVisible || isClearControlVisible) && ( + + {isClearControlVisible && ( + + )} + {isErrorIconVisible && ( + + + + + + )} + )} +