Skip to content

Commit

Permalink
Update selectable search input state when the input changes (#2776)
Browse files Browse the repository at this point in the history
* feat(selectable search input): update selectable search input state on input change

* feat(search select input): add default value prop

* feat(search select input): update test

* feat(slectable search input): update vrt

* feat(selectable search input): update placeholder value when provided from parent

* feat(selectable search input): update placeholder value when provided

* feat(selectable search input): pass experimental value when available

* feat(selectable search input): tests cases for experimental values

* feat(selectable search input): update changeset

* feat(selectable search input): update uption to also have experimental value state

* feat(selectable search input): update story with expected behaviour

* feat(selectable search input): update hook dependency

* feat(selectable search input): remove unused story code

---------

Co-authored-by: Ddouglasz <[email protected]>
Co-authored-by: Carlos Cortizas <[email protected]>
  • Loading branch information
3 people authored May 22, 2024
1 parent 0caed39 commit 79318f5
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 36 deletions.
6 changes: 6 additions & 0 deletions .changeset/plenty-snails-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@commercetools-uikit/selectable-search-input': minor
---

We're introducing a **temporary** property to handle an internal case.
Please do not use it as it will be removed in the near future.
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ describe('SelectableSearchInput', () => {
).toHaveTextContent('Bar');
});

it('should show the passed experimental value when controlled externally', () => {
render(
<TestComponent
value={{ text: 'foo', option: 'bar' }}
_experimentalValue={{ text: 'experimental', option: 'bar' }}
/>
);
expect(screen.getByLabelText('test-label')).toHaveAttribute(
'value',
'experimental'
);
expect(
screen.getByTestId('selectable-search-input-container')
).toHaveTextContent('Bar');
});

it('should show an empty input when an empty string is passed externally as value', () => {
render(
<TestComponent
value={{ text: 'foo', option: 'bar' }}
_experimentalValue={{ text: '', option: 'bar' }}
/>
);
expect(screen.getByLabelText('test-label')).toHaveAttribute('value', '');
expect(
screen.getByTestId('selectable-search-input-container')
).toHaveTextContent('Bar');
});

it('should call onFocus when the dropdown menu is focused', async () => {
const onFocus = jest.fn();
render(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean, text, select } from '@storybook/addon-knobs/react';
Expand Down Expand Up @@ -88,12 +87,10 @@ storiesOf('Components|Inputs', module)
];

const name = text('name', '') || 'default-name';
const [dropdownValue, setDropdownValue] = useState();
const [textInputValue, setTextInputValue] = useState();

const value = {
text: textInputValue,
option: dropdownValue,
text: '',
option: '',
};

return (
Expand All @@ -114,12 +111,6 @@ storiesOf('Components|Inputs', module)
value={value}
onChange={(event) => {
action('onChange')(event);
if (event.target.name.endsWith('.textInput')) {
setTextInputValue(event.target.value);
}
if (event.target.name.endsWith('.dropdown')) {
setDropdownValue(event.target.value);
}
}}
isAutofocussed={boolean('isAutofocussed', false)}
isDisabled={boolean('isDisabled', false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type TSelectableSearchInputProps = {
* Value of the input. Consists of text input and selected option.
*/
value: TValue;
_experimentalValue?: TValue;
/**
* Called with the event of the input or dropdown when either the selectable dropdown or the text input have changed.
* The change event from the text input has a suffix of `.textInput` and the change event from the dropdown has a suffix of `.dropdown`.
Expand Down Expand Up @@ -258,12 +259,15 @@ const transformDataProps = (dataProps?: Record<string, string>) =>
const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
const [dropdownHasFocus, toggleDropdownHasFocus] = useToggleState(false);
const [searchValue, setSearchValue] = useState(props.value.text || '');
const [searchOption, setSearchOption] = useState(props.value.option || '');
const containerRef = useRef<HTMLDivElement>(null);
const textInputRef = useRef<HTMLInputElement>(null);

const legacyDataProps = filterDataAttributes(props);
const transformedSelectDataProps = transformDataProps(props.selectDataProps);
const transformedInputDataProps = transformDataProps(props.inputDataProps);
const searchInputValue = props._experimentalValue?.text ?? searchValue;
const searchInputOption = props._experimentalValue?.option ?? searchOption;

const optionsWithoutGroups = props.options.flatMap((option) => {
if (isOptionObject(option)) {
Expand All @@ -273,7 +277,7 @@ const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
});

const selectedOption = optionsWithoutGroups.find(
(option) => option.value === props.value.option
(option) => option.value === searchInputOption
);

const selectablSearchInputId = useFieldId(
Expand Down Expand Up @@ -324,7 +328,7 @@ const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
}
};

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const handleTextInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchValue(event.target.value);
if (props.onChange) {
props.onChange({
Expand All @@ -346,7 +350,7 @@ const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
event.preventDefault();
if (props.onSubmit) {
props.onSubmit({
text: searchValue,
text: searchInputValue,
option: selectedOption?.value ?? '',
});
}
Expand Down Expand Up @@ -406,6 +410,23 @@ const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
[onBlur, selectablSearchInputId, name]
);

const handleDropdownChange = useCallback(
(nextSelectedOptions) => {
setSearchOption(nextSelectedOptions.value);
if (props.onChange) {
props.onChange({
target: {
id: SelectableSearchInput.getDropdownId(selectablSearchInputId),
name: getDropdownName(name),
value: nextSelectedOptions.value,
},
});
}
textInputRef.current?.focus();
},
[props.onChange, selectablSearchInputId, name]
);

return (
<Constraints.Horizontal max={props.horizontalConstraint}>
<Container
Expand All @@ -422,6 +443,7 @@ const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
isCondensed={props.isCondensed ?? false}
handleDropdownFocus={handleDropdownFocus}
handleDropdownBlur={handleDropdownBlur}
handleDropdownChange={handleDropdownChange}
textInputRef={textInputRef}
selectedOption={selectedOption}
dataProps={transformedSelectDataProps}
Expand All @@ -445,8 +467,8 @@ const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
id={SelectableSearchInput.getTextInputId(selectablSearchInputId)}
name={getTextInputName(props.name)}
type="text"
value={searchValue}
onChange={handleChange}
value={searchInputValue}
onChange={handleTextInputChange}
onBlur={handleTextInputBlur}
onFocus={handleTextInputFocus}
disabled={props.isDisabled}
Expand All @@ -470,7 +492,7 @@ const SelectableSearchInput = (props: TSelectableSearchInputProps) => {
}}
/>
{props.isClearable &&
searchValue &&
searchInputValue &&
!props.isDisabled &&
!props.isReadOnly && (
<SecondaryIconButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Select, {
type Props as ReactSelectProps,
} from 'react-select';
import { DropdownIndicator, messages } from '@commercetools-uikit/select-utils';
import { type ReactNode, useCallback } from 'react';
import { type ReactNode } from 'react';
import type {
TSelectableSearchInputProps,
TOption,
Expand All @@ -32,6 +32,7 @@ type TSelectableSelect = {
isCondensed: boolean;
handleDropdownFocus: () => void;
handleDropdownBlur: () => void;
handleDropdownChange: ReactSelectProps['onChange'];
textInputRef: React.RefObject<HTMLInputElement>;
selectedOption?: TOption;
dataProps?: Record<string, string>;
Expand All @@ -50,23 +51,6 @@ const SelectableSelect = (props: TSelectableSelect) => {
dropdownHasFocus: props.dropdownHasFocus,
}) as ReactSelectProps['styles'];

const { onChange, name, id, textInputRef } = props;
const handleDropdownChange = useCallback(
(nextSelectedOptions) => {
if (onChange) {
onChange({
target: {
id: id,
name: name,
value: (nextSelectedOptions as TOption).value,
},
});
}
textInputRef.current?.focus();
},
[onChange, id, name, textInputRef]
);

return (
<Select
inputId={props.id}
Expand Down Expand Up @@ -94,7 +78,7 @@ const SelectableSelect = (props: TSelectableSelect) => {
menuPortalTarget={props.menuPortalTarget}
menuShouldBlockScroll={props.menuShouldBlockScroll}
onBlur={props.handleDropdownBlur}
onChange={handleDropdownChange}
onChange={props.handleDropdownChange}
onInputChange={props.onMenuInputChange}
noOptionsMessage={
props.noMenuOptionsMessage ||
Expand Down

0 comments on commit 79318f5

Please sign in to comment.