Skip to content

Commit

Permalink
feat(TreeSelect): added error state view and hasClear prop (#1885)
Browse files Browse the repository at this point in the history
  • Loading branch information
IsaevAlexandr authored Oct 9, 2024
1 parent 523caf8 commit 9849e4b
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 6 deletions.
28 changes: 28 additions & 0 deletions src/components/TreeSelect/TreeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -51,6 +53,10 @@ export const TreeSelect = React.forwardRef(function TreeSelect<T, P extends {} =
disabled = false,
withExpandedState = true,
defaultExpandedState = 'expanded',
hasClear,
errorMessage: propsErrorMessage,
errorPlacement: propsErrorPlacement,
validationState: propsValidationState,
onClose,
onOpenChange,
onUpdate,
Expand All @@ -75,6 +81,19 @@ export const TreeSelect = React.forwardRef(function TreeSelect<T, P extends {} =
const containerRefLocal = React.useRef<HTMLDivElement>(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({
Expand Down Expand Up @@ -165,6 +184,11 @@ export const TreeSelect = React.forwardRef(function TreeSelect<T, P extends {} =
id: treeSelectId,
activeItemId: list.state.activeItemId,
title,
errorMessage: isErrorIconVisible ? errorMessage : undefined,
errorPlacement,
validationState,
hasClear,
isErrorVisible: isErrorStateVisible,
};

const togglerNode = renderControl ? (
Expand Down Expand Up @@ -234,6 +258,10 @@ export const TreeSelect = React.forwardRef(function TreeSelect<T, P extends {} =

{slotAfterListBody}
</SelectPopup>
<OuterAdditionalContent
errorMessage={isErrorMsgVisible ? errorMessage : null}
errorMessageId={errorMessageId}
/>
</div>
);
}) as <T, P extends {} = {}>(
Expand Down
11 changes: 11 additions & 0 deletions src/components/TreeSelect/__stories__/TreeSelect.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -118,6 +120,15 @@ WithDndList.parameters = {
disableStrictMode: true,
};

const ErrorStateTemplate: StoryFn<ErrorStateExampleProps> = (props) => {
return <ErrorStateExample {...props} />;
};
export const ErrorState = ErrorStateTemplate.bind({});

ErrorState.args = {
size: 'l',
};

const WithDisabledElementsTemplate: StoryFn<WithDisabledElementsExampleProps> = (props) => {
return <WithDisabledElementsExample {...props} />;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TreeSelectProps<Entity>, 'items' | 'mapItemDataToContentProps'> {}

const items: ListItemType<Entity>[] = ['one', 'two', 'free'];
const errorMessage = 'A validation error has occurred';

export const ErrorStateExample = ({...props}: ErrorStateExampleProps) => {
const containerRef = React.useRef<HTMLDivElement>(null);

return (
<Flex gap="5">
<TreeSelect
{...props}
items={items}
getItemId={(id) => id}
placeholder="-"
containerRef={containerRef}
mapItemDataToContentProps={(title) => ({title})}
errorMessage={errorMessage}
errorPlacement="outside"
validationState="invalid"
hasClear
/>
<TreeSelect
{...props}
items={items}
getItemId={(id) => id}
placeholder="-"
containerRef={containerRef}
mapItemDataToContentProps={(title) => ({title})}
errorMessage={errorMessage}
errorPlacement="inside"
validationState="invalid"
hasClear
/>
</Flex>
);
};
29 changes: 23 additions & 6 deletions src/components/TreeSelect/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export type TreeSelectRenderControlProps<T> = {
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<T, P extends {} = {}> = TreeListRenderItem<T, P>;
Expand All @@ -39,15 +52,19 @@ interface TreeSelectBehavioralProps<T> extends UseListParsedStateProps<T> {

export interface TreeSelectProps<T, P extends {} = {}>
extends Omit<TreeListProps<T, P>, 'list' | 'renderContainer' | 'multiple'>,
Pick<
TreeSelectRenderControlProps<T>,
| 'title'
| 'placeholder'
| 'disabled'
| 'hasClear'
| 'errorPlacement'
| 'validationState'
| 'errorMessage'
>,
UseOpenProps,
TreeSelectBehavioralProps<T> {
/**
* Control's title attribute value
*/
title?: string;
value?: ListItemId[];
disabled?: boolean;
placeholder?: string;
defaultValue?: ListItemId[] | undefined;
popupClassName?: string;
popupWidth?: SelectPopupProps['width'];
Expand Down

0 comments on commit 9849e4b

Please sign in to comment.