From 8f2f07b798b5776adc94a4442fd6a725911b8048 Mon Sep 17 00:00:00 2001 From: Kirill Kharitonov Date: Wed, 31 Jan 2024 09:54:17 -0500 Subject: [PATCH] feat(Select): added errorMessage, errorPlacement and validationType props (#1291) --- src/components/Select/README.md | 22 +++++++ src/components/Select/Select.tsx | 25 ++++++- .../Select/__stories__/SelectShowcase.tsx | 34 ++++++++++ .../Select/__tests__/Select.error.test.tsx | 66 +++++++++++++++++++ .../SelectControl/SelectControl.scss | 7 +- .../SelectControl/SelectControl.tsx | 30 ++++++--- src/components/Select/i18n/en.json | 3 +- src/components/Select/i18n/ru.json | 3 +- src/components/Select/types.ts | 9 +++ 9 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 src/components/Select/__tests__/Select.error.test.tsx diff --git a/src/components/Select/README.md b/src/components/Select/README.md index ebbbe02789..98e2a6cf14 100644 --- a/src/components/Select/README.md +++ b/src/components/Select/README.md @@ -974,6 +974,25 @@ const MyComponent = () => { +### Error + +The state of the `Select` in which you want to indicate incorrect user input. To change `Select` appearance, use the `validationState` property with the `"invalid"` value. An optional message text can be added via the `errorMessage` property. By default, message text is rendered outside the component. +This behaviour can be changed with the `errorPlacement` property. + + + + + ## Properties | Name | Description | Type | Default | @@ -1016,3 +1035,6 @@ const MyComponent = () => { | view | Control view | `string` | `'normal'` | | [virtualizationThreshold](#virtualized-list) | The threshold of the options count after which virtualization is enabled | `number` | `50` | | [width](#control-width) | Control width | `string \| number` | `undefined` | +| errorMessage | Error text | `string` | | +| errorPlacement | Error placement | `outside` `inside` | `outside` | +| validationState | Validation state | `"invalid"` | | diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 668320c650..e4fc6cde3b 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -3,6 +3,8 @@ import React from 'react'; import {KeyCode} from '../../constants'; import {useFocusWithin, useForkRef, useSelect, useUniqId} from '../../hooks'; import type {List} from '../List'; +import {OuterAdditionalContent} from '../controls/common/OuterAdditionalContent/OuterAdditionalContent'; +import {errorPropsMapper} from '../controls/utils'; import {useMobile} from '../mobile'; import type {CnMods} from '../utils/cn'; @@ -122,6 +124,20 @@ export const Select = React.forwardRef(function ); const virtualized = filteredFlattenOptions.length >= virtualizationThreshold; + const {errorMessage, errorPlacement, validationState} = errorPropsMapper({ + error, + errorMessage: props.errorMessage, + errorPlacement: props.errorPlacement || 'outside', + validationState: props.validationState, + }); + const errorMessageId = useUniqId(); + + const isErrorMsgVisible = + validationState === 'invalid' && Boolean(errorMessage) && errorPlacement === 'outside'; + const isErrorIconVisible = + validationState === 'invalid' && Boolean(errorMessage) && errorPlacement === 'inside'; + const isErrorStateVisible = isErrorMsgVisible || isErrorIconVisible; + const handleOptionClick = React.useCallback( (option?: FlattenOption) => { if (!option || option?.disabled || 'label' in option) { @@ -250,7 +266,8 @@ export const Select = React.forwardRef(function label={label} placeholder={placeholder} selectedOptionsContent={selectedOptionsContent} - error={error} + isErrorVisible={isErrorStateVisible} + errorMessage={isErrorIconVisible ? errorMessage : undefined} open={open} disabled={disabled} onKeyDown={handleControlKeyDown} @@ -260,6 +277,7 @@ export const Select = React.forwardRef(function selectId={`select-${selectId}`} activeIndex={activeIndex} /> + (function )} + + ); }) as unknown as SelectComponent; diff --git a/src/components/Select/__stories__/SelectShowcase.tsx b/src/components/Select/__stories__/SelectShowcase.tsx index 6dee20adf7..d91da09b8b 100644 --- a/src/components/Select/__stories__/SelectShowcase.tsx +++ b/src/components/Select/__stories__/SelectShowcase.tsx @@ -380,6 +380,40 @@ export const SelectShowcase = (props: SelectProps) => { + +
+

Select (with text error)

+ + + + + + + + + + + + + + +
); }; diff --git a/src/components/Select/__tests__/Select.error.test.tsx b/src/components/Select/__tests__/Select.error.test.tsx new file mode 100644 index 0000000000..94f4054428 --- /dev/null +++ b/src/components/Select/__tests__/Select.error.test.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import {render, screen} from '@testing-library/react'; + +import {CONTROL_ERROR_MESSAGE_QA} from '../../controls/utils'; +import {Select} from '../Select'; + +describe('Select error', () => { + test('render error message with error prop (if it is not an empty string)', () => { + render(); + + expect(screen.queryByText('Some Error with errorMessage prop')).not.toBeInTheDocument(); + }); + + test('render error message with errorMessage prop and invalid state (if it is not an empty string)', () => { + render( + , + ); + + expect(screen.getByLabelText('Show popup with error info')).toBeInTheDocument(); + }); + + test('do not show error message without error/errorMessage prop', () => { + render(); + + expect(screen.queryByTestId(CONTROL_ERROR_MESSAGE_QA)).not.toBeInTheDocument(); + }); + + test('do not show error message if errorMessage prop value is an empty string', () => { + render(); + + expect(screen.queryByLabelText('Show popup with error info')).not.toBeInTheDocument(); + }); + + test('do not show error icon if errorMessage prop is an empty string', () => { + render(