diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 2864ba3f47..424d78ff32 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -8,6 +8,8 @@ import {SelectControl} from '../Select/components'; import {SelectPopup} from '../Select/components/SelectPopup/SelectPopup'; import {TreeList} from '../TreeList'; import type {TreeListRenderItem} from '../TreeList/types'; +import {OuterAdditionalContent} from '../controls/common/OuterAdditionalContent/OuterAdditionalContent'; +import {errorPropsMapper} from '../controls/utils'; import {useMobile} from '../mobile'; import {ListItemView, getListItemClickHandler, useList} from '../useList'; import type {ListOnItemClick} from '../useList'; @@ -51,6 +53,10 @@ export const TreeSelect = React.forwardRef(function TreeSelect(null); const containerRef = propsContainerRef ?? containerRefLocal; + const {errorMessage, errorPlacement, validationState} = errorPropsMapper({ + errorMessage: propsErrorMessage, + errorPlacement: propsErrorPlacement || 'outside', + validationState: propsValidationState, + }); + const errorMessageId = useUniqId(); + + const isErrorStateVisible = validationState === 'invalid'; + const isErrorMsgVisible = + isErrorStateVisible && Boolean(errorMessage) && errorPlacement === 'outside'; + const isErrorIconVisible = + isErrorStateVisible && Boolean(errorMessage) && errorPlacement === 'inside'; + const handleControlRef = useForkRef(ref, controlRef); const {toggleOpen, open} = useOpenState({ @@ -165,6 +184,11 @@ export const TreeSelect = React.forwardRef(function TreeSelect + ); }) as ( diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index a2f20a9d13..c306c46059 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -8,6 +8,8 @@ import {createRandomizedData} from '../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../TreeSelect'; import type {TreeSelectProps} from '../types'; +import {ErrorStateExample} from './components/ErrorStateExample'; +import type {ErrorStateExampleProps} from './components/ErrorStateExample'; import {InfinityScrollExample} from './components/InfinityScrollExample'; import type {InfinityScrollExampleProps} from './components/InfinityScrollExample'; import {WithDisabledElementsExample} from './components/WithDisabledElementsExample'; @@ -118,6 +120,15 @@ WithDndList.parameters = { disableStrictMode: true, }; +const ErrorStateTemplate: StoryFn = (props) => { + return ; +}; +export const ErrorState = ErrorStateTemplate.bind({}); + +ErrorState.args = { + size: 'l', +}; + const WithDisabledElementsTemplate: StoryFn = (props) => { return ; }; diff --git a/src/components/TreeSelect/__stories__/components/ErrorStateExample.tsx b/src/components/TreeSelect/__stories__/components/ErrorStateExample.tsx new file mode 100644 index 0000000000..1ed946e82f --- /dev/null +++ b/src/components/TreeSelect/__stories__/components/ErrorStateExample.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import {Flex} from '../../../layout'; +import type {ListItemType} from '../../../useList'; +import {TreeSelect} from '../../TreeSelect'; +import type {TreeSelectProps} from '../../types'; + +type Entity = string; + +export interface ErrorStateExampleProps + extends Omit, 'items' | 'mapItemDataToContentProps'> {} + +const items: ListItemType[] = ['one', 'two', 'free']; +const errorMessage = 'A validation error has occurred'; + +export const ErrorStateExample = ({...props}: ErrorStateExampleProps) => { + const containerRef = React.useRef(null); + + return ( + + id} + placeholder="-" + containerRef={containerRef} + mapItemDataToContentProps={(title) => ({title})} + errorMessage={errorMessage} + errorPlacement="outside" + validationState="invalid" + hasClear + /> + id} + placeholder="-" + containerRef={containerRef} + mapItemDataToContentProps={(title) => ({title})} + errorMessage={errorMessage} + errorPlacement="inside" + validationState="invalid" + hasClear + /> + + ); +}; diff --git a/src/components/TreeSelect/types.ts b/src/components/TreeSelect/types.ts index 8d95ba67e8..6038b64ae9 100644 --- a/src/components/TreeSelect/types.ts +++ b/src/components/TreeSelect/types.ts @@ -26,6 +26,19 @@ export type TreeSelectRenderControlProps = { activeItemId?: ListItemId; title?: string; hasClear?: boolean; + /** + * Determines content of the error message + */ + errorMessage?: React.ReactNode; + /** + * Determines whether the error message will be placed under the input field as text or in the tooltip + */ + errorPlacement?: 'outside' | 'inside'; + /** + * Describes the validation state + */ + validationState?: 'invalid'; + isErrorVisible?: boolean; }; export type TreeSelectRenderItem = TreeListRenderItem; @@ -39,15 +52,19 @@ interface TreeSelectBehavioralProps extends UseListParsedStateProps { export interface TreeSelectProps extends Omit, 'list' | 'renderContainer' | 'multiple'>, + Pick< + TreeSelectRenderControlProps, + | 'title' + | 'placeholder' + | 'disabled' + | 'hasClear' + | 'errorPlacement' + | 'validationState' + | 'errorMessage' + >, UseOpenProps, TreeSelectBehavioralProps { - /** - * Control's title attribute value - */ - title?: string; value?: ListItemId[]; - disabled?: boolean; - placeholder?: string; defaultValue?: ListItemId[] | undefined; popupClassName?: string; popupWidth?: SelectPopupProps['width'];