Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve the MultiSelect component #1659

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions integration/specs/MultiSelect/multiSelect-1.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ describe('MultiSelect base', () => {
it('should put the input element focused when clicked', () => {
const input = new PageMultiSelect(MULTI_SELECT);
input.click();
expect(input.hasFocus()).toBe(true);
expect(input.hasTriggerFocus()).toBe(true);
});

it('should put the input element focused when the label element is clicked', () => {
const input = new PageMultiSelect(MULTI_SELECT);
input.clickLabel();
expect(input.hasFocus()).toBe(true);
expect(input.hasTriggerFocus()).toBe(true);
});
});
26 changes: 26 additions & 0 deletions integration/specs/MultiSelect/multiSelect-11.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const PageMultiSelect = require('../../../src/components/MultiSelect/pageObject');

const MULTI_SELECT = '#multiselect-component-11';

describe('MultiSelect base', () => {
beforeAll(() => {
browser.url('/#!/MultiSelect/11');
});
beforeEach(() => {
browser.refresh();
const component = $(MULTI_SELECT);
component.waitForExist();
});

it('should not put the input element focused when clicked', () => {
const input = new PageMultiSelect(MULTI_SELECT);
input.click();
expect(input.hasInputFocus()).toBe(false);
});

it('should not put the input element focused when the label element is clicked', () => {
const input = new PageMultiSelect(MULTI_SELECT);
input.clickLabel();
expect(input.hasInputFocus()).toBe(false);
});
});
4 changes: 3 additions & 1 deletion src/components/InternalDropdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ const InternalDropdown = forwardRef((props, reference) => {
if (multiple) {
if (Array.isArray(value)) {
if (value.some(v => v.name === name)) {
return null;
return onChange(value.filter(v => v.name !== name));
}
return onChange(value.concat([option]));
}
Expand Down Expand Up @@ -258,6 +258,7 @@ const InternalDropdown = forwardRef((props, reference) => {
activeOptionName,
currentValues,
activeChildrenMap,
multiple,
};
}, [
value,
Expand All @@ -267,6 +268,7 @@ const InternalDropdown = forwardRef((props, reference) => {
activeOptionName,
activeChildrenMap,
handleChange,
multiple,
]);

return (
Expand Down
65 changes: 60 additions & 5 deletions src/components/MultiSelect/__test__/multiSelect.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Option from '../../Option';
import HelpText from '../../Input/styled/helpText';
import ErrorText from '../../Input/styled/errorText';
import Label from '../../Input/label/labelText';
import { StyledChip, StyledPlaceholder, StyledInput } from '../styled';
import { StyledChip, StyledPlaceholder, StyledText, StyledButtonIcon } from '../styled';

describe('<MultiSelect />', () => {
it('should render Label when label prop is passed', () => {
Expand All @@ -28,6 +28,21 @@ describe('<MultiSelect />', () => {
expect(component.find(StyledPlaceholder).exists()).toBe(true);
});

it('should render the default variant', () => {
const value = [
{
label: 'First',
name: 'first',
},
{
label: 'Second',
name: 'second',
},
];
const component = mount(<MultiSelect value={value} />);
expect(component.find(StyledText).exists()).toBe(true);
});

it('should render the correct amount of chips', () => {
const value = [
{
Expand All @@ -40,7 +55,7 @@ describe('<MultiSelect />', () => {
},
];
const component = mount(
<MultiSelect value={value}>
<MultiSelect value={value} variant="chip">
<Option name="first" label="First" />
<Option name="second" label="Second" />
</MultiSelect>,
Expand All @@ -57,7 +72,7 @@ describe('<MultiSelect />', () => {
];
const mockOnChange = jest.fn();
const component = mount(
<MultiSelect value={value} onChange={mockOnChange}>
<MultiSelect value={value} variant="chip" onChange={mockOnChange}>
<Option name="first" label="First" />
<Option name="second" label="Second" />
</MultiSelect>,
Expand All @@ -70,14 +85,54 @@ describe('<MultiSelect />', () => {
it('should fire focus event', () => {
const mockOnFocus = jest.fn();
const component = mount(<MultiSelect onFocus={mockOnFocus} />);
component.find(StyledInput).simulate('focus');
component.find(StyledButtonIcon).simulate('focus');
expect(mockOnFocus).toHaveBeenCalledTimes(1);
});

it('should fire blur event', () => {
const mockOnBlur = jest.fn();
const component = mount(<MultiSelect onBlur={mockOnBlur} />);
component.find(StyledInput).simulate('blur');
component.find(StyledButtonIcon).simulate('blur');
expect(mockOnBlur).toHaveBeenCalledTimes(1);
});

it('should not render the buttons when readOnly', () => {
const value = [
{
label: 'First',
name: 'first',
},
{
label: 'Second',
name: 'second',
},
];
const component = mount(
<MultiSelect value={value} variant="chip" readOnly>
<Option name="first" label="First" />
<Option name="second" label="Second" />
</MultiSelect>,
);
expect(component.find('button').exists()).toBe(false);
});

it('should not render the buttons when disabled', () => {
const value = [
{
label: 'First',
name: 'first',
},
{
label: 'Second',
name: 'second',
},
];
const component = mount(
<MultiSelect value={value} variant="chip" disabled>
<Option name="first" label="First" />
<Option name="second" label="Second" />
</MultiSelect>,
);
expect(component.find('button').exists()).toBe(false);
});
});
35 changes: 23 additions & 12 deletions src/components/MultiSelect/chips.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@ import React from 'react';
import PropTypes from 'prop-types';
import { StyledChip } from './styled';

function Chips(props) {
const { value, variant, onDelete } = props;
const Chips = props => {
const { value, variant, onDelete, disabled, readOnly } = props;
const getDeleteCallback = val => {
return disabled || readOnly ? null : () => onDelete(val);
};

if (!value) {
return null;
}

if (Array.isArray(value)) {
return value.map(val => (
<StyledChip
key={val.name}
label={val.label}
variant={variant}
onDelete={() => onDelete(val)}
/>
));
return value.map(val => {
return (
<StyledChip
key={val.name}
label={val.label}
variant={variant}
onDelete={getDeleteCallback(val)}
/>
);
});
}
return <StyledChip label={value.label} variant={variant} onDelete={() => onDelete(value)} />;
}
return <StyledChip label={value.label} variant={variant} onDelete={getDeleteCallback(value)} />;
};

Chips.propTypes = {
value: PropTypes.oneOfType([
Expand All @@ -34,12 +41,16 @@ Chips.propTypes = {
),
]),
variant: PropTypes.oneOf(['base', 'neutral', 'outline-brand', 'brand']),
disabled: PropTypes.bool,
readOnly: PropTypes.bool,
onDelete: PropTypes.func,
};

Chips.defaultProps = {
value: undefined,
variant: 'base',
disabled: undefined,
readOnly: undefined,
onDelete: () => {},
};

Expand Down
50 changes: 50 additions & 0 deletions src/components/MultiSelect/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getContent } from './helpers';
import { StyledText } from './styled';
import Chips from './chips';

const Content = props => {
const { variant, value, chipVariant, readOnly, disabled, onDelete } = props;

if (variant === 'chip') {
return (
<Chips
value={value}
variant={chipVariant}
readOnly={readOnly}
disabled={disabled}
onDelete={onDelete}
/>
);
}

const content = getContent(value);
return <StyledText>{content}</StyledText>;
};

Content.propTypes = {
disabled: PropTypes.bool,
readOnly: PropTypes.bool,
variant: PropTypes.oneOf(['default', 'chip']),
chipVariant: PropTypes.oneOf(['base', 'neutral', 'outline-brand', 'brand']),
value: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.any,
}),
),
onDelete: PropTypes.func,
};

Content.defaultProps = {
disabled: false,
readOnly: false,
variant: 'default',
chipVariant: 'base',
value: undefined,
onDelete: () => {},
};

export default Content;
18 changes: 18 additions & 0 deletions src/components/MultiSelect/helpers/__test__/getContent.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import getContent from '../getContent';

describe('getContent', () => {
it('should return null', () => {
const values = [false, true, undefined, null];
values.forEach(value => {
expect(getContent(value)).toBe(null);
});
});

it('should return the right string', () => {
const values = [{ label: 'Label' }, [{ label: 'Label 1' }, { label: 'Label 2' }]];
const expected = ['Label', 'Label 1, Label 2'];
values.forEach((value, index) => {
expect(getContent(value)).toBe(expected[index]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import hasChips from '../hasChips';
import hasContent from '../hasContent';

describe('hasChips', () => {
describe('hasContent', () => {
it('should return true', () => {
const values = [{ label: 'Label', name: 'Name' }, [{ label: 'Label', name: 'Name' }]];
values.forEach(value => {
expect(hasChips(value)).toBe(true);
expect(hasContent(value)).toBe(true);
});
});

it('should return false', () => {
const values = [undefined, null, 0, false, true, []];
values.forEach(value => {
expect(hasChips(value)).toBe(false);
expect(hasContent(value)).toBe(false);
});
});
});
9 changes: 9 additions & 0 deletions src/components/MultiSelect/helpers/getContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function getContent(value) {
if (Array.isArray(value)) {
return value.map(item => item.label).join(', ');
}
if (value && typeof value === 'object') {
return value.label;
}
return null;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function hasChips(value) {
export default function hasContent(value) {
if (!value) {
return false;
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/MultiSelect/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import hasChips from './hasChips';
import hasContent from './hasContent';
import positionResolver from './positionResolver';
import getContent from './getContent';
import normalizeValue from './normalizeValue';

export { hasChips, positionResolver };
export { hasContent, positionResolver, getContent, normalizeValue };
3 changes: 2 additions & 1 deletion src/components/MultiSelect/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export interface MultiSelectProps extends BaseProps {
required?: boolean;
disabled?: boolean;
readOnly?: boolean;
variant?: 'default' | 'bare';
variant?: 'default' | 'chip';
chipVariant?: 'base' | 'neutral' | 'outline-brand' | 'brand';
isBare?: boolean;
hideLabel?: boolean;
value?: MultiSelectOption[];
onChange?: (value: MultiSelectOption[]) => void;
Expand Down
Loading