Skip to content

Commit

Permalink
STCOM-715 Correct validation for User Record Custom Fields MultiSelec…
Browse files Browse the repository at this point in the history
…tion (#1328)

Correctly present validation error for `<MultiSelection>` component 
if it has no value and disable `Save&Close` button

Refs STCOM-715

Co-authored-by: Denys Bohdan <[email protected]>
  • Loading branch information
zburke and BogdanDenis authored Jul 10, 2020
1 parent 8c2628a commit 06e0f36
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 5 deletions.
12 changes: 11 additions & 1 deletion lib/MultiSelection/MultiSelect.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,24 @@
cursor: pointer;
}

.multiSelectInput {
.multiSelectFilterField {
margin: 2px;
background-color: transparent;
padding: 0 4px;
border-width: 0;
outline: 0;
}

.multiSelectValueInput {
position: absolute;
top: 0;
width: 100%;
height: 100%;
border: none;
z-index: -1;
color: transparent;
}

.multiSelectValueListContainer {
display: flex;
flex: 0 1 auto;
Expand Down
2 changes: 1 addition & 1 deletion lib/MultiSelection/MultiSelectFilterField.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class MultiSelectFilterField extends React.Component {
onFocus,
onBlur,
placeholder,
'className': css.multiSelectInput,
'className': css.multiSelectFilterField,
disabled,
})}
/>
Expand Down
40 changes: 40 additions & 0 deletions lib/MultiSelection/MultiSelectValueInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import css from './MultiSelect.css';

const propTypes = {
ariaLabelledBy: PropTypes.string,
getInputProps: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
required: PropTypes.bool,
value: PropTypes.string,
};

const MultiSelectValueInput = ({
ariaLabelledBy,
id,
required,
getInputProps,
value,
...rest
}) => (
<input
{...getInputProps({
'aria-labelledby': rest['aria-labelledby'] || ariaLabelledBy,
type: 'text',
id,
className: css.multiSelectValueInput,
required,
value,
tabindex: '-1',
})}
/>
);

MultiSelectValueInput.propTypes = propTypes;

MultiSelectValueInput.defaultProps = {
required: false,
};

export default MultiSelectValueInput;
14 changes: 14 additions & 0 deletions lib/MultiSelection/MultiSelection.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FormattedMessage } from 'react-intl';
import MultiDownshift from './MultiDownshift';
import SelectedValuesList from './SelectedValuesList';
import MultiSelectFilterField from './MultiSelectFilterField';
import MultiSelectValueInput from './MultiSelectValueInput';
import MultiSelectResponsiveRenderer from './MultiSelectResponsiveRenderer';
import SRStatus from '../SRStatus';
import DefaultOptionFormatter from '../Selection/DefaultOptionFormatter';
Expand Down Expand Up @@ -251,6 +252,10 @@ class MultiSelection extends React.Component {
}
};

renderValueInput = (inputProps) => {
return <MultiSelectValueInput {...inputProps} />;
};

handleDownshiftStateChange = (changes) => {
if (this.props.asyncFiltering) {
if (Object.prototype.hasOwnProperty.call(changes, 'isOpen') &&
Expand Down Expand Up @@ -384,6 +389,14 @@ class MultiSelection extends React.Component {
disabled,
};

const inputProps = {
ariaLabelledBy,
id: `multiselect-input-${uiId}`,
required,
getInputProps,
value: Array.isArray(value) ? value.map(this.props.itemToString).join(',') : value,
};

const optionListProps = {
actions,
ariaLabelledBy,
Expand Down Expand Up @@ -465,6 +478,7 @@ class MultiSelection extends React.Component {
<div className={css.multiSelectControlGroup}>
<SelectedValuesList {...valueListProps} />
{this.renderInputOrPlaceholder(filterProps)}
{this.renderValueInput(inputProps)}
</div>
<button
className={css.multiSelectToggleButton}
Expand Down
12 changes: 12 additions & 0 deletions lib/MultiSelection/tests/MultiSelection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ describe('MultiSelect', () => {
expect(multiselection.controlAriaLabelledBy).to.equal(`multi-value-status-${testId}`);
});

it('should have empty hidden value', () => {
expect(multiselection.inputValue).to.equal('');
});

describe('supplying a label prop', () => {
beforeEach(async () => {
await mountWithContext(
Expand Down Expand Up @@ -120,6 +124,10 @@ describe('MultiSelect', () => {
it('does not render placeholder', () => {
expect(multiselection.filterPlaceholder).to.equal('');
});

it('sets correct hidden input value', () => {
expect(multiselection.inputValue).to.equal(listOptions[2].label);
});
});

describe('clicking multiple options', () => {
Expand Down Expand Up @@ -215,6 +223,10 @@ describe('MultiSelect', () => {
expect(multiselection.values(2).valLabel).to.equal(listOptions[5].label);
});

it('sets correct value in hidden input', () => {
expect(multiselection.inputValue).to.equal(`${listOptions[1].label},${listOptions[3].label},${listOptions[5].label}`);
});

describe('Keyboard : navigating selected values', () => {
describe('Keyboard: pressing the Home key when middle selected value is focused', () => {
beforeEach(async () => {
Expand Down
9 changes: 6 additions & 3 deletions lib/MultiSelection/tests/interactor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isPresent,
text,
count,
value,
} from '@bigtest/interactor';

import SelectionCss from '../../Selection/Selection.css';
Expand All @@ -26,8 +27,9 @@ const controlClass = selectorFromClassnameString(`.${css.multiSelectControl}`);
const menuClass = selectorFromClassnameString(`.${css.multiSelectMenu}`);
const errorClass = selectorFromClassnameString(`.${formCss.feedbackError}`);
const warningClass = selectorFromClassnameString(`.${formCss.feedbackWarning}`);
const filterClass = selectorFromClassnameString(`.${css.multiSelectInput}`);
const filterClass = selectorFromClassnameString(`.${css.multiSelectFilterField}`);
const emptyClass = selectorFromClassnameString(`.${css.multiSelectEmptyMessage}`);
const inputClass = selectorFromClassnameString(`.${css.multiSelectValueInput}`);

const OptionInteractor = interactor(class OptionInteractor {
defaultScope = optionClass;
Expand Down Expand Up @@ -60,6 +62,7 @@ export default interactor(class MultiSelectInteractor {
emptyMessagePresent = isPresent(emptyClass);
filterPlaceholder = attribute(filterClass, 'placeholder');
loaderPresent = isPresent(`.${iconCss.spinner}`);
inputValue = value(inputClass);

// aria-attributes
controlAriaLabelledBy = attribute(controlClass, 'aria-labelledby');
Expand Down Expand Up @@ -105,10 +108,10 @@ export default interactor(class MultiSelectInteractor {
.click(`li:nth-of-type(${index}`);
}

expandAndFilter(value) {
expandAndFilter(fieldValue) {
return this
.click('button')
.fill('input', value);
.fill('input', fieldValue);
}

moveToNextOption(sel, setFocus = false) {
Expand Down

0 comments on commit 06e0f36

Please sign in to comment.