diff --git a/.changeset/sour-weeks-camp.md b/.changeset/sour-weeks-camp.md new file mode 100644 index 0000000000..6d26459a93 --- /dev/null +++ b/.changeset/sour-weeks-camp.md @@ -0,0 +1,6 @@ +--- +'@commercetools-uikit/async-select-input': patch +'@commercetools-uikit/select-input': patch +--- + +feat(select input props): add hideSelectedOptions prop from 'react-select' to SelectInput and AsyncSelectInput diff --git a/packages/components/filters/README.md b/packages/components/filters/README.md index 4ad4aafb61..c69b57da46 100644 --- a/packages/components/filters/README.md +++ b/packages/components/filters/README.md @@ -42,6 +42,6 @@ export default Example; ## Properties -| Props | Type | Required | Default | Description | -| ------- | -------- | :------: | ------- | ------------------- | -| `label` | `string` | | | This is a stub prop | +| Props | Type | Required | Default | Description | +| ------- | -------- | :------: | ------- | -------------------- | +| `label` | `string` | ✅ | | This is a stub prop! | diff --git a/packages/components/inputs/async-select-input/README.md b/packages/components/inputs/async-select-input/README.md index 45795afa45..a27f59b1ce 100644 --- a/packages/components/inputs/async-select-input/README.md +++ b/packages/components/inputs/async-select-input/README.md @@ -64,6 +64,7 @@ export default Example; | `components` | `AsyncProps['components']` | | | Map of components to overwrite the default ones, see [what components you can override](https://react-select.com/components)
[Props from React select was used](https://react-select.com/props) | | `controlShouldRenderValue` | `AsyncProps['controlShouldRenderValue']` | | `true` | Control whether the selected values should be rendered in the control
[Props from React select was used](https://react-select.com/props) | | `filterOption` | `AsyncProps['filterOption']` | | | Custom method to filter whether an option should be displayed in the menu
[Props from React select was used](https://react-select.com/props) | +| `hideSelectedOptions` | `AsyncProps['hideSelectedOptions']` | | | Custom method to determine whether selected options should be displayed in the menu
[Props from React select was used](https://react-select.com/props) | | `id` | `AsyncProps['inputId']` | | | The id of the search input
[Props from React select was used](https://react-select.com/props) | | `inputValue` | `AsyncProps['inputValue']` | | | The value of the search input
[Props from React select was used](https://react-select.com/props) | | `containerId` | `AsyncProps['id']` | | | The id to set on the SelectContainer component
[Props from React select was used](https://react-select.com/props) | diff --git a/packages/components/inputs/async-select-input/src/async-select-input.spec.js b/packages/components/inputs/async-select-input/src/async-select-input.spec.js index 76a148c2c2..cfd0110a84 100644 --- a/packages/components/inputs/async-select-input/src/async-select-input.spec.js +++ b/packages/components/inputs/async-select-input/src/async-select-input.spec.js @@ -1,6 +1,11 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import { render, fireEvent, waitFor } from '../../../../../test/test-utils'; +import { + render, + fireEvent, + waitFor, + within, +} from '../../../../../test/test-utils'; import AsyncSelectInput from './async-select-input'; // We use this component to simulate the whole flow of @@ -83,7 +88,7 @@ it('should have an open menu if menuIsOpen is true', async () => { const { findByLabelText, getByText } = renderInput({ menuIsOpen: true, }); - const input = await findByLabelText('Fruit'); + await findByLabelText('Fruit'); expect(getByText('Mango')).toBeInTheDocument(); }); @@ -94,7 +99,7 @@ it('should not have an open menu if menuIsOpen is true and isReadOnly is true', isReadOnly: true, }); - const input = await findByLabelText('Fruit'); + await findByLabelText('Fruit'); expect(queryByText('Mango')).not.toBeInTheDocument(); }); @@ -119,6 +124,34 @@ it('should call onBlur when input loses focus', async () => { expect(onBlur).toHaveBeenCalled(); }); +it('should not display selected options in menu when hideSelectedOptions is true', async () => { + const { findByLabelText, findByRole } = renderInput({ + hideSelectedOptions: true, + }); + const input = await findByLabelText('Fruit', { text: 'Banana' }); + + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = await findByRole('listbox'); + expect(within(menu).queryByText('Banana')).not.toBeInTheDocument(); +}); + +it('should display selected options in menu when hideSelectedOptions is false', async () => { + const { findByLabelText, findByRole } = renderInput({ + hideSelectedOptions: false, + }); + const input = await findByLabelText('Fruit', { text: 'Banana' }); + + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = await findByRole('listbox'); + expect(within(menu).getByText('Banana')).toBeInTheDocument(); +}); + describe('in single mode', () => { describe('when no value is specified', () => { it('should render a select input', async () => { @@ -136,6 +169,18 @@ describe('in single mode', () => { expect(input).toBeInTheDocument(); expect(getByText('Banana')).toBeInTheDocument(); }); + it('should display selected option in menu when hideSelectedOptions is undefined', async () => { + const { findByLabelText, findByRole } = renderInput(); + const input = await findByLabelText('Fruit', { text: 'Banana' }); + expect(input).toBeInTheDocument(); + + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = await findByRole('listbox'); + expect(within(menu).getByText('Banana')).toBeInTheDocument(); + }); }); describe('interacting', () => { it('should open the list and all options should be visible', async () => { @@ -209,6 +254,23 @@ describe('in multi mode', () => { expect(getByText('Mango')).toBeInTheDocument(); expect(getByText('Raspberry')).toBeInTheDocument(); }); + it('should not display selected options in menu when hideSelectedOptions is undefined', async () => { + const { findByLabelText, findByRole } = renderInput({ + isMulti: true, + value: [ + { value: 'mango', label: 'Mango' }, + { value: 'raspberry', label: 'Raspberry' }, + ], + }); + const input = await findByLabelText('Fruit'); + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = await findByRole('listbox'); + expect(within(menu).queryByText('Mango')).not.toBeInTheDocument(); + expect(within(menu).queryByText('Raspberry')).not.toBeInTheDocument(); + }); }); }); describe('interacting', () => { diff --git a/packages/components/inputs/async-select-input/src/async-select-input.stories.tsx b/packages/components/inputs/async-select-input/src/async-select-input.stories.tsx index 4da00cb5d2..8bd03bb65b 100644 --- a/packages/components/inputs/async-select-input/src/async-select-input.stories.tsx +++ b/packages/components/inputs/async-select-input/src/async-select-input.stories.tsx @@ -15,6 +15,7 @@ const meta: Meta = { 'aria-errormessage': { control: 'text' }, backspaceRemovesValue: { control: 'boolean' }, controlShouldRenderValue: { control: 'boolean' }, + hideSelectedOptions: { control: 'boolean' }, id: { control: 'text' }, containerId: { control: 'text' }, isClearable: { control: 'boolean' }, diff --git a/packages/components/inputs/async-select-input/src/async-select-input.tsx b/packages/components/inputs/async-select-input/src/async-select-input.tsx index 69bc3371d3..c10c796222 100644 --- a/packages/components/inputs/async-select-input/src/async-select-input.tsx +++ b/packages/components/inputs/async-select-input/src/async-select-input.tsx @@ -137,6 +137,12 @@ export type TAsyncSelectInputProps = { * [Props from React select was used](https://react-select.com/props) */ filterOption?: ReactSelectAsyncProps['filterOption']; + /** + * Custom method to determine whether selected options should be displayed in the menu + *
+ * [Props from React select was used](https://react-select.com/props) + */ + hideSelectedOptions?: ReactSelectAsyncProps['hideSelectedOptions']; // This forwarded as react-select's "inputId" /** * The id of the search input @@ -398,6 +404,7 @@ const AsyncSelectInput = (props: TAsyncSelectInputProps) => { }) as ReactSelectAsyncProps['styles'] } filterOption={props.filterOption} + hideSelectedOptions={props.hideSelectedOptions} // react-select uses "id" (for the container) and "inputId" (for the input), // but we use "id" (for the input) and "containerId" (for the container) // instead. diff --git a/packages/components/inputs/select-input/README.md b/packages/components/inputs/select-input/README.md index 96c9a2d131..8c285f0454 100644 --- a/packages/components/inputs/select-input/README.md +++ b/packages/components/inputs/select-input/README.md @@ -76,6 +76,7 @@ export default Example; | `isCondensed` | `boolean` | | | Whether the input and options are rendered with condensed paddings | | `controlShouldRenderValue` | `ReactSelectProps['controlShouldRenderValue']` | | | Control whether the selected values should be rendered in the control
[Props from React select was used](https://react-select.com/props) | | `filterOption` | `ReactSelectProps['filterOption']` | | | Custom method to filter whether an option should be displayed in the menu
[Props from React select was used](https://react-select.com/props) | +| `hideSelectedOptions` | `ReactSelectProps['hideSelectedOptions']` | | | Custom method to determine whether selected options should be displayed in the menu
[Props from React select was used](https://react-select.com/props) | | `id` | `ReactSelectProps['inputId']` | | | Used as HTML id property. An id is generated automatically when not provided. This forwarded as react-select's "inputId"
[Props from React select was used](https://react-select.com/props) | | `inputValue` | `ReactSelectProps['inputValue']` | | | The value of the search input
[Props from React select was used](https://react-select.com/props) | | `containerId` | `ReactSelectProps['id']` | | | The id to set on the SelectContainer component This is forwarded as react-select's "id"
[Props from React select was used](https://react-select.com/props) | diff --git a/packages/components/inputs/select-input/src/select-input.spec.js b/packages/components/inputs/select-input/src/select-input.spec.js index f3a01c069c..4625804143 100644 --- a/packages/components/inputs/select-input/src/select-input.spec.js +++ b/packages/components/inputs/select-input/src/select-input.spec.js @@ -1,6 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import { render, fireEvent } from '../../../../../test/test-utils'; +import { render, fireEvent, within } from '../../../../../test/test-utils'; import SelectInput from './select-input'; // We use this component to simulate the whole flow of @@ -78,22 +78,19 @@ it('should have focus automatically when isAutofocussed is passed', () => { }); it('should have an open menu if menuIsOpen is true', () => { - const { getByLabelText, getByText } = renderInput({ + const { getByText } = renderInput({ menuIsOpen: true, }); - const input = getByLabelText('Fruit'); expect(getByText('Mango')).toBeInTheDocument(); }); it('should not have an open menu if menuIsOpen is true and isReadOnly is true', () => { - const { getByLabelText, queryByText } = renderInput({ + const { queryByText } = renderInput({ menuIsOpen: true, isReadOnly: true, }); - const input = getByLabelText('Fruit'); - expect(queryByText('Mango')).not.toBeInTheDocument(); }); @@ -117,6 +114,36 @@ it('should call onBlur when input loses focus', () => { expect(onBlur).toHaveBeenCalled(); }); +it('should not display selected options in menu when hideSelectedOptions is true', () => { + const { getByLabelText, getByRole } = renderInput({ + hideSelectedOptions: true, + }); + const input = getByLabelText('Fruit', { text: 'Banana' }); + expect(input).toBeInTheDocument(); + + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = getByRole('listbox'); + expect(within(menu).queryByText('Banana')).not.toBeInTheDocument(); +}); + +it('should display selected options in menu when hideSelectedOptions is false', () => { + const { getByLabelText, getByRole } = renderInput({ + hideSelectedOptions: false, + }); + const input = getByLabelText('Fruit', { text: 'Banana' }); + expect(input).toBeInTheDocument(); + + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = getByRole('listbox'); + expect(within(menu).getByText('Banana')).toBeInTheDocument(); +}); + describe('in single mode', () => { describe('when no value is specified', () => { it('should render a select input', () => { @@ -134,6 +161,18 @@ describe('in single mode', () => { expect(input).toBeInTheDocument(); expect(getByText('Banana')).toBeInTheDocument(); }); + it('should display selected option in menu when hideSelectedOptions is undefined', () => { + const { getByLabelText, getByRole } = renderInput(); + const input = getByLabelText('Fruit', { text: 'Banana' }); + expect(input).toBeInTheDocument(); + + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = getByRole('listbox'); + expect(within(menu).getByText('Banana')).toBeInTheDocument(); + }); }); describe('interacting', () => { describe('when isAutofocussed is `true`', () => { @@ -218,8 +257,23 @@ describe('in multi mode', () => { expect(getByText('Mango')).toBeInTheDocument(); expect(getByText('Raspberry')).toBeInTheDocument(); }); + it('should not display selected options in menu when hideSelectedOptions is undefined', () => { + const { getByLabelText, getByRole } = renderInput({ + isMulti: true, + value: ['mango', 'raspberry'], + }); + const input = getByLabelText('Fruit'); + fireEvent.keyDown(input, { + key: 'ArrowDown', + }); + + const menu = getByRole('listbox'); + expect(within(menu).queryByText('Mango')).not.toBeInTheDocument(); + expect(within(menu).queryByText('Raspberry')).not.toBeInTheDocument(); + }); }); }); + describe('interacting', () => { describe('when disabled', () => { it('should not call onChange when value is cleared', () => { diff --git a/packages/components/inputs/select-input/src/select-input.stories.tsx b/packages/components/inputs/select-input/src/select-input.stories.tsx index 2a8346b25f..ad521fc320 100644 --- a/packages/components/inputs/select-input/src/select-input.stories.tsx +++ b/packages/components/inputs/select-input/src/select-input.stories.tsx @@ -15,6 +15,7 @@ const meta: Meta = { backspaceRemovesValue: { control: { type: 'boolean' } }, controlShouldRenderValue: { control: { type: 'boolean' } }, filterOption: { type: 'function' }, + hideSelectedOptions: { type: 'boolean' }, id: { control: { type: 'text' } }, inputValue: { control: { type: 'text' } }, containerId: { control: { type: 'text' } }, diff --git a/packages/components/inputs/select-input/src/select-input.tsx b/packages/components/inputs/select-input/src/select-input.tsx index 94919565c2..4b5cfd6d98 100644 --- a/packages/components/inputs/select-input/src/select-input.tsx +++ b/packages/components/inputs/select-input/src/select-input.tsx @@ -161,7 +161,12 @@ export type TSelectInputProps = { // formatOptionLabel: PropTypes.func, // getOptionLabel: PropTypes.func, // getOptionValue: PropTypes.func, - // hideSelectedOptions: PropTypes.bool, + /** + * Custom method to determine whether selected options should be displayed in the menu + *
+ * [Props from React select was used](https://react-select.com/props) + */ + hideSelectedOptions?: ReactSelectProps['hideSelectedOptions']; /** * Used as HTML id property. An id is generated automatically when not provided. * This forwarded as react-select's "inputId" @@ -491,6 +496,7 @@ const SelectInput = (props: TSelectInputProps) => { isClearable={props.isReadOnly ? false : props.isClearable} isDisabled={props.isDisabled} isOptionDisabled={props.isOptionDisabled} + hideSelectedOptions={props.hideSelectedOptions} // @ts-ignore isReadOnly={props.isReadOnly} isMulti={props.isMulti} diff --git a/tsconfig.json b/tsconfig.json index 43c82a0814..7e898117ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,5 @@ { + "exclude": ["**/node_modules/**", "**/dist/**"], "compilerOptions": { "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": false, @@ -28,6 +29,7 @@ "stripInternal": true, "target": "ES2019", "allowJs": true, + "noEmit": true, "typeRoots": [ "@types", // "@types-extensions",