Skip to content

Commit

Permalink
FCT 1189 - add hideSelectedOptions prop to SelectInput and `Async…
Browse files Browse the repository at this point in the history
…SelectInput` (#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
  • Loading branch information
ByronDWall authored Oct 2, 2024
1 parent 13e4f0a commit d971925
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/sour-weeks-camp.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions packages/components/filters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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! |
1 change: 1 addition & 0 deletions packages/components/inputs/async-select-input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)&#xA;<br>&#xA;[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&#xA;<br>&#xA;[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&#xA;<br>&#xA;[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&#xA;<br>&#xA;[Props from React select was used](https://react-select.com/props) |
| `id` | `AsyncProps['inputId']` | | | The id of the search input&#xA;<br>&#xA;[Props from React select was used](https://react-select.com/props) |
| `inputValue` | `AsyncProps['inputValue']` | | | The value of the search input&#xA;<br>&#xA;[Props from React select was used](https://react-select.com/props) |
| `containerId` | `AsyncProps['id']` | | | The id to set on the SelectContainer component&#xA;<br>&#xA;[Props from React select was used](https://react-select.com/props) |
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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();
});
Expand All @@ -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();
});
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const meta: Meta<typeof AsyncSelectInput> = {
'aria-errormessage': { control: 'text' },
backspaceRemovesValue: { control: 'boolean' },
controlShouldRenderValue: { control: 'boolean' },
hideSelectedOptions: { control: 'boolean' },
id: { control: 'text' },
containerId: { control: 'text' },
isClearable: { control: 'boolean' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <br>
* [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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/components/inputs/select-input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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&#xA;<br>&#xA;[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&#xA;<br>&#xA;[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&#xA;<br>&#xA;[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.&#xA;This forwarded as react-select's "inputId"&#xA;<br>&#xA;[Props from React select was used](https://react-select.com/props) |
| `inputValue` | `ReactSelectProps['inputValue']` | | | The value of the search input&#xA;<br>&#xA;[Props from React select was used](https://react-select.com/props) |
| `containerId` | `ReactSelectProps['id']` | | | The id to set on the SelectContainer component&#xA;This is forwarded as react-select's "id"&#xA;<br>&#xA;[Props from React select was used](https://react-select.com/props) |
Expand Down
66 changes: 60 additions & 6 deletions packages/components/inputs/select-input/src/select-input.spec.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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();
});

Expand All @@ -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', () => {
Expand All @@ -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`', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const meta: Meta<typeof SelectInput> = {
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' } },
Expand Down
8 changes: 7 additions & 1 deletion packages/components/inputs/select-input/src/select-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <br>
* [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"
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"exclude": ["**/node_modules/**", "**/dist/**"],
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": false,
Expand Down Expand Up @@ -28,6 +29,7 @@
"stripInternal": true,
"target": "ES2019",
"allowJs": true,
"noEmit": true,
"typeRoots": [
"@types",
// "@types-extensions",
Expand Down

0 comments on commit d971925

Please sign in to comment.