From d971925633750591dfeb3f89c4df322a9e5e0370 Mon Sep 17 00:00:00 2001
From: Byron Wall <87667330+ByronDWall@users.noreply.github.com>
Date: Wed, 2 Oct 2024 09:06:03 -0400
Subject: [PATCH] FCT 1189 - add `hideSelectedOptions` prop to `SelectInput`
and `AsyncSelectInput` (#2934)
* feat(select-input): add 'hideSelectedOptions' prop to 'SelectInput', add tests for when 'hideSelectedOptions' is true, false, and undefined, update storybook with boolean control for 'hideSelectedOptions', update tsconfig.json to exclude dist and node modules so ts doesnt crash all the time
* feat(async-select-input): add hideSelectedOptions prop to async-select-menu, update tests and storybook
* feat(select input props): update readmes and generate changeset
---
.changeset/sour-weeks-camp.md | 6 ++
packages/components/filters/README.md | 6 +-
.../inputs/async-select-input/README.md | 1 +
.../src/async-select-input.spec.js | 68 ++++++++++++++++++-
.../src/async-select-input.stories.tsx | 1 +
.../src/async-select-input.tsx | 7 ++
.../components/inputs/select-input/README.md | 1 +
.../select-input/src/select-input.spec.js | 66 ++++++++++++++++--
.../select-input/src/select-input.stories.tsx | 1 +
.../inputs/select-input/src/select-input.tsx | 8 ++-
tsconfig.json | 2 +
11 files changed, 154 insertions(+), 13 deletions(-)
create mode 100644 .changeset/sour-weeks-camp.md
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",