diff --git a/integration/specs/MultiSelect/multiSelect-9.spec.js b/integration/specs/MultiSelect/multiSelect-9.spec.js new file mode 100644 index 000000000..e722b7378 --- /dev/null +++ b/integration/specs/MultiSelect/multiSelect-9.spec.js @@ -0,0 +1,26 @@ +const PageMultiSelect = require('../../../src/components/MultiSelect/pageObject'); + +const MULTI_SELECT = '#multiselect-component-9'; + +describe('MultiSelect base', () => { + beforeAll(() => { + browser.url('/#!/MultiSelect/9'); + }); + 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.hasFocus()).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.hasFocus()).toBe(false); + }); +}); diff --git a/src/components/MultiSelect/__test__/multiSelect.spec.js b/src/components/MultiSelect/__test__/multiSelect.spec.js index 664cac575..61de11eff 100644 --- a/src/components/MultiSelect/__test__/multiSelect.spec.js +++ b/src/components/MultiSelect/__test__/multiSelect.spec.js @@ -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, StyledInput, StyledText } from '../styled'; describe('', () => { it('should render Label when label prop is passed', () => { @@ -28,6 +28,21 @@ describe('', () => { 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(); + expect(component.find(StyledText).exists()).toBe(true); + }); + it('should render the correct amount of chips', () => { const value = [ { @@ -40,7 +55,7 @@ describe('', () => { }, ]; const component = mount( - + , @@ -57,7 +72,7 @@ describe('', () => { ]; const mockOnChange = jest.fn(); const component = mount( - + , @@ -80,4 +95,44 @@ describe('', () => { component.find(StyledInput).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( + + , + ); + 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( + + , + ); + expect(component.find('button').exists()).toBe(false); + }); }); diff --git a/src/components/MultiSelect/chips.js b/src/components/MultiSelect/chips.js index 1e51e8c32..b45acdcd7 100644 --- a/src/components/MultiSelect/chips.js +++ b/src/components/MultiSelect/chips.js @@ -3,19 +3,24 @@ import PropTypes from 'prop-types'; import { StyledChip } from './styled'; function Chips(props) { - const { value, variant, onDelete } = props; + const { value, variant, onDelete, disabled, readOnly } = props; if (!value) { return null; } + if (Array.isArray(value)) { - return value.map(val => ( - onDelete(val)} - /> - )); + return value.map(val => { + const onDeleteCallback = disabled || readOnly ? null : () => onDelete(val); + + return ( + + ); + }); } return onDelete(value)} />; } @@ -34,12 +39,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: () => {}, }; diff --git a/src/components/MultiSelect/helpers/__test__/getContent.spec.js b/src/components/MultiSelect/helpers/__test__/getContent.spec.js new file mode 100644 index 000000000..a7df41185 --- /dev/null +++ b/src/components/MultiSelect/helpers/__test__/getContent.spec.js @@ -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]); + }); + }); +}); diff --git a/src/components/MultiSelect/helpers/getContent.js b/src/components/MultiSelect/helpers/getContent.js new file mode 100644 index 000000000..2880509b0 --- /dev/null +++ b/src/components/MultiSelect/helpers/getContent.js @@ -0,0 +1,9 @@ +export default function getContent(value) { + if (!value || typeof value !== 'object') { + return null; + } + if (Array.isArray(value)) { + return value.map(item => item.label).join(', '); + } + return value.label; +} diff --git a/src/components/MultiSelect/index.d.ts b/src/components/MultiSelect/index.d.ts index 423f7675d..770408447 100644 --- a/src/components/MultiSelect/index.d.ts +++ b/src/components/MultiSelect/index.d.ts @@ -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; diff --git a/src/components/MultiSelect/index.js b/src/components/MultiSelect/index.js index 122134c46..9ce8ef0cf 100644 --- a/src/components/MultiSelect/index.js +++ b/src/components/MultiSelect/index.js @@ -7,6 +7,7 @@ import { useErrorMessageId, useReduxForm, useLabelId, + useWindowResize, } from '../../libs/hooks'; import { StyledInput, @@ -15,6 +16,7 @@ import { StyledButtonIcon, StyledPlaceholder, StyledCombobox, + StyledText, } from './styled'; import InternalDropdown from '../InternalDropdown'; import InternalOverlay from '../InternalOverlay'; @@ -26,6 +28,7 @@ import { ENTER_KEY, SPACE_KEY, ESCAPE_KEY, TAB_KEY } from '../../libs/constants' import { hasChips, positionResolver } from './helpers'; import Chips from './chips'; import normalizeValue from './helpers/normalizeValue'; +import getContent from './helpers/getContent'; const MultiSelect = React.forwardRef((props, ref) => { const { @@ -40,9 +43,10 @@ const MultiSelect = React.forwardRef((props, ref) => { required, disabled, readOnly, - tabIndex, + tabIndex: tabIndexInProps, variant, chipVariant, + isBare, value, onChange, onFocus, @@ -55,13 +59,13 @@ const MultiSelect = React.forwardRef((props, ref) => { const comboboxRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { - comboboxRef.current.focus(); + triggerRef.current.focus(); }, click: () => { - comboboxRef.current.click(); + triggerRef.current.click(); }, blur: () => { - comboboxRef.current.blur(); + triggerRef.current.blur(); }, })); @@ -77,7 +81,7 @@ const MultiSelect = React.forwardRef((props, ref) => { setIsOpen(false); // eslint-disable-next-line no-use-before-define stopListeningOutsideClick(); - comboboxRef.current.focus(); + setTimeout(() => triggerRef.current.focus(), 0); }; const handleChange = val => { @@ -141,8 +145,22 @@ const MultiSelect = React.forwardRef((props, ref) => { dropdownRef, handleOutsideClick, ); + useWindowResize(() => setIsOpen(false), isOpen); const shouldRenderChips = hasChips(value); + const tabIndex = disabled || readOnly ? '-1' : tabIndexInProps; + const content = + variant === 'chip' ? ( + + ) : ( + {getContent(value)} + ); return ( @@ -155,7 +173,7 @@ const MultiSelect = React.forwardRef((props, ref) => { /> { onFocus={onFocus} onBlur={onBlur} onKeyDown={handleKeyDown} - tabIndex={tabIndex} + tabIndex="-1" ref={comboboxRef} aria-labelledby={labelId} > @@ -181,20 +199,20 @@ const MultiSelect = React.forwardRef((props, ref) => { {placeholder} - - - + {content} - } - onClick={handleTriggerClick} - disabled={disabled} - ref={triggerRef} - tabIndex="-1" - /> + + } + onClick={handleTriggerClick} + disabled={disabled} + ref={triggerRef} + tabIndex={tabIndex} + /> + {}, onFocus: () => {}, diff --git a/src/components/MultiSelect/readme.md b/src/components/MultiSelect/readme.md index ac8bdd37c..70674e99c 100644 --- a/src/components/MultiSelect/readme.md +++ b/src/components/MultiSelect/readme.md @@ -38,6 +38,47 @@ const MultiSelectExample = props => { ``` +##### MultiSelect with chip variant + +```js +import React, { useState, useRef } from 'react'; +import { MultiSelect, Option } from 'react-rainbow-components'; + +const containerStyles = { + maxWidth: 400, +}; + +const MultiSelectExample = props => { + const [value, setValue] = useState([]); + + return ( + + + ) +} + + +``` + ##### MultiSelect bare ```js @@ -60,7 +101,7 @@ const MultiSelectExample = props => { value={value} onChange={setValue} bottomHelpText="You can select several options" - variant="bare" + isBare >