diff --git a/packages/patternfly-4/react-core/src/components/Select/CheckboxSelectInput.js b/packages/patternfly-4/react-core/src/components/Select/CheckboxSelectInput.js deleted file mode 100644 index 09febe47119..00000000000 --- a/packages/patternfly-4/react-core/src/components/Select/CheckboxSelectInput.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Select, SelectVariant, CheckboxSelectOption } from '@patternfly/react-core'; - -class CheckboxSelectInput extends React.Component { - state = { - isExpanded: false, - selected: [] - }; - - onToggle = isExpanded => { - this.setState({ - isExpanded - }); - }; - - onSelect = (event, selection) => { - const { selected } = this.state; - if (selected.includes(selection)) { - this.setState( - prevState => ({ selected: prevState.selected.filter(item => item !== selection) }), - () => console.log('selections: ', this.state.selected) - ); - } else { - this.setState( - prevState => ({ selected: [...prevState.selected, selection] }), - () => console.log('selections: ', this.state.selected) - ); - } - }; - - clearSelection = () => { - this.setState({ - selected: [] - }); - }; - - options = [ - , - , - , - , - - ]; - - render() { - const { isExpanded, selected } = this.state; - const titleId = 'checkbox-select-id'; - return ( -
- - -
- ); - } -} - -export default CheckboxSelectInput; diff --git a/packages/patternfly-4/react-core/src/components/Select/GroupedCheckboxSelectInput.js b/packages/patternfly-4/react-core/src/components/Select/GroupedCheckboxSelectInput.js deleted file mode 100644 index 2da1cc9e3b5..00000000000 --- a/packages/patternfly-4/react-core/src/components/Select/GroupedCheckboxSelectInput.js +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { Select, SelectVariant, CheckboxSelectGroup, CheckboxSelectOption } from '@patternfly/react-core'; - -class GroupedCheckboxSelectInput extends React.Component { - state = { - isExpanded: false, - selected: [] - }; - - onToggle = isExpanded => { - this.setState({ - isExpanded - }); - }; - - onSelect = (event, selection) => { - const { selected } = this.state; - if (selected.includes(selection)) { - this.setState( - prevState => ({ selected: prevState.selected.filter(item => item !== selection) }), - () => console.log('selections: ', this.state.selected) - ); - } else { - this.setState( - prevState => ({ selected: [...prevState.selected, selection] }), - () => console.log('selections: ', this.state.selected) - ); - } - }; - - clearSelection = () => { - this.setState({ - selected: [] - }); - }; - - options = [ - - - - - - - , - - - - - - ]; - - render() { - const { isExpanded, selected } = this.state; - const titleId = 'grouped-checkbox-select-id'; - return ( -
- - -
- ); - } -} - -export default GroupedCheckboxSelectInput; diff --git a/packages/patternfly-4/react-core/src/components/Select/Select.d.ts b/packages/patternfly-4/react-core/src/components/Select/Select.d.ts index 29e4409cd48..e7e0a6278d8 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.d.ts +++ b/packages/patternfly-4/react-core/src/components/Select/Select.d.ts @@ -3,17 +3,24 @@ import { HTMLProps, FormEvent, ReactNode } from 'react'; export const SelectVariant: { single: 'single'; checkbox: 'checkbox'; + typeahead: 'typeahead'; + typeaheadMulti: 'typeaheadmulti'; }; export interface SelectProps extends HTMLProps { isExpanded?: boolean; isGrouped?: boolean; onToggle(value: boolean): void; + onClear?() : void; placeholderText?: string | ReactNode; selections?: string | Array; variant?: string; width?: string | number; ariaLabelledBy?: string; + ariaLabelTypeAhead?: string; + ariaLabelClear?: string; + ariaLabelToggle?: string; + ariaLabelRemove?: string; } declare const Select: React.FunctionComponent; diff --git a/packages/patternfly-4/react-core/src/components/Select/Select.js b/packages/patternfly-4/react-core/src/components/Select/Select.js index 30b6773dfd8..efc6b68b699 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.js +++ b/packages/patternfly-4/react-core/src/components/Select/Select.js @@ -1,12 +1,16 @@ import React from 'react'; import styles from '@patternfly/patternfly/components/Select/select.css'; import badgeStyles from '@patternfly/patternfly/components/Badge/badge.css'; +import formStyles from '@patternfly/patternfly/components/FormControl/form-control.css'; +import buttonStyles from '@patternfly/patternfly/components/Button/button.css'; import { css } from '@patternfly/react-styles'; +import { TimesCircleIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; import SingleSelect from './SingleSelect'; import CheckboxSelect from './CheckboxSelect'; import SelectToggle from './SelectToggle'; import { SelectContext, SelectVariant } from './selectConstants'; +import { Chip, ChipGroup } from '../ChipGroup'; // seed for the aria-labelledby ID let currentId = 0; @@ -28,12 +32,22 @@ const propTypes = { 'aria-label': PropTypes.string, /** Id of label for the Select aria-labelledby */ ariaLabelledBy: PropTypes.string, + /** Label for input field of type ahead select variants */ + ariaLabelTypeAhead: PropTypes.string, + /** Label for clear selection button of type ahead select variants */ + ariaLabelClear: PropTypes.string, + /** Label for toggle of type ahead select variants */ + ariaLabelToggle: PropTypes.string, + /** Label for remove chip button of multiple type ahead select variant */ + ariaLabelRemove: PropTypes.string, /** Callback for selection behavior */ onSelect: PropTypes.func.isRequired, /** Callback for toggle button behavior */ onToggle: PropTypes.func.isRequired, + /** Callback for typeahead clear button */ + onClear: PropTypes.func, /** Variant of rendered Select */ - variant: PropTypes.oneOf(['single', 'checkbox']), + variant: PropTypes.oneOf(['single', 'checkbox', 'typeahead', 'typeaheadmulti']), /** Width of the select container as a number of px or string percentage */ width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Additional props are spread to the container
    */ @@ -47,22 +61,67 @@ const defaultProps = { isGrouped: false, 'aria-label': null, ariaLabelledBy: null, + ariaLabelTypeAhead: null, + ariaLabelClear: 'Clear all', + ariaLabelToggle: 'Options menu', + ariaLabelRemove: 'Remove', selections: null, placeholderText: null, variant: SelectVariant.single, width: null, + onClear: Function.prototype }; class Select extends React.Component { parentRef = React.createRef(); - state = { openedOnEnter: false }; + state = { + openedOnEnter: false, + typeaheadValue: null, + filteredChildren: this.props.children + }; onEnter = () => { this.setState({ openedOnEnter: true }); }; onClose = () => { - this.setState({ openedOnEnter: false }); + this.setState({ + openedOnEnter: false, + typeaheadValue: null, + filteredChildren: this.props.children + }); + }; + + onChange = e => { + let input; + try { + input = new RegExp(e.target.value, 'i'); + } catch (err) { + input = new RegExp(e.target.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); + } + const filteredChildren = + e.target.value !== '' + ? this.props.children.filter(child => child.props.value.search(input) === 0) + : this.props.children; + if (filteredChildren.length === 0) { + filteredChildren.push(
    No results found
    ); + } + this.setState({ + typeaheadValue: e.target.value, + filteredChildren + }); + }; + + onClick = e => { + e.stopPropagation(); + }; + + clearSelection = e => { + e.stopPropagation(); + this.setState({ + typeaheadValue: '', + filteredChildren: this.props.children + }); }; render() { @@ -72,16 +131,21 @@ class Select extends React.Component { variant, onToggle, onSelect, + onClear, isExpanded, isGrouped, selections, ariaLabelledBy, + ariaLabelTypeAhead, + ariaLabelClear, + ariaLabelToggle, + ariaLabelRemove, 'aria-label': ariaLabel, placeholderText, width, ...props } = this.props; - const { openedOnEnter } = this.state; + const { openedOnEnter, typeaheadValue, filteredChildren } = this.state; const selectToggleId = `pf-toggle-id-${currentId++}`; let childPlaceholderText = null; if (!selections && !placeholderText) { @@ -89,6 +153,19 @@ class Select extends React.Component { childPlaceholderText = (childPlaceholder[0] && childPlaceholder[0].props.value) || (children[0] && children[0].props.value); } + let selectedChips = null; + if (variant === SelectVariant.typeaheadMulti) { + selectedChips = ( + + {selections && + selections.map(item => ( + onSelect(e, item)} closeBtnAriaLabel={ariaLabelRemove}> + {item} + + ))} + + ); + } return (
    {variant === SelectVariant.single && (
    @@ -126,6 +204,61 @@ class Select extends React.Component {
    )} + {variant === SelectVariant.typeahead && ( + +
    + +
    + {selections && ( + + )} +
    + )} + {variant === SelectVariant.typeaheadMulti && ( + +
    + {selections && selections.length > 0 && selectedChips} + +
    + {selections && selections.length > 0 && ( + + )} +
    + )} {variant === SelectVariant.single && isExpanded && ( )} + {(variant === SelectVariant.typeahead || variant === SelectVariant.typeaheadMulti) && isExpanded && ( + + {filteredChildren} + + )}
    ); diff --git a/packages/patternfly-4/react-core/src/components/Select/Select.md b/packages/patternfly-4/react-core/src/components/Select/Select.md index 0356599b91c..1d8e7388986 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.md +++ b/packages/patternfly-4/react-core/src/components/Select/Select.md @@ -243,3 +243,168 @@ class GroupedCheckboxSelectInput extends React.Component { } } ``` + +## Typeahead select input +```js +import React from 'react'; +import { Select, SelectOption, SelectVariant, CheckboxSelectGroup, CheckboxSelectOption } from '@patternfly/react-core'; + +class TypeaheadSelectInput extends React.Component { + constructor(props) { + super(props); + this.options = [ + { value: 'Alabama', disabled: false }, + { value: 'Florida', disabled: false }, + { value: 'New Jersey', disabled: false }, + { value: 'New Mexico', disabled: false }, + { value: 'New York', disabled: false }, + { value: 'North Carolina', disabled: false } + ]; + + this.state = { + isExpanded: false, + selected: null + }; + + this.onToggle = isExpanded => { + this.setState({ + isExpanded + }); + }; + + this.onSelect = (event, selection, isPlaceholder) => { + if (isPlaceholder) this.clearSelection(); + else { + this.setState({ + selected: selection, + isExpanded: false + }); + console.log('selected:', selection); + } + }; + + this.clearSelection = () => { + this.setState({ + selected: null, + isExpanded: false + }); + }; + } + + render() { + const { isExpanded, selected } = this.state; + const titleId = 'typeahead-select-id'; + return ( +
    + + +
    + ); + } +} +``` + +## Multiple typeahead select input +```js +import React from 'react'; +import { Select, SelectOption, SelectVariant, CheckboxSelectGroup, CheckboxSelectOption } from '@patternfly/react-core'; + +class MultiTypeaheadSelectInput extends React.Component { + constructor(props) { + super(props); + this.options = [ + { value: 'Alabama', disabled: false }, + { value: 'Florida', disabled: false }, + { value: 'New Jersey', disabled: false }, + { value: 'New Mexico', disabled: false }, + { value: 'New York', disabled: false }, + { value: 'North Carolina', disabled: false } + ]; + + this.state = { + isExpanded: false, + selected: [] + }; + + this.onToggle = isExpanded => { + this.setState({ + isExpanded + }); + }; + + this.onSelect = (event, selection) => { + const { selected } = this.state; + if (selected.includes(selection)) { + this.setState( + prevState => ({ selected: prevState.selected.filter(item => item !== selection) }), + () => console.log('selections: ', this.state.selected) + ); + } else { + this.setState( + prevState => ({ selected: [...prevState.selected, selection] }), + () => console.log('selections: ', this.state.selected) + ); + } + }; + + this.clearSelection = () => { + this.setState({ + selected: [], + isExpanded: false, + }); + }; + } + + render() { + const { isExpanded, selected } = this.state; + const titleId = 'multi-typeahead-select-id'; + + return ( +
    + + +
    + ); + } +} +``` \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Select/Select.test.js b/packages/patternfly-4/react-core/src/components/Select/Select.test.js index cdd86e06a25..10c34b960f8 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.test.js +++ b/packages/patternfly-4/react-core/src/components/Select/Select.test.js @@ -72,6 +72,108 @@ describe('checkbox select', () => { }); }); +describe('typeahead select', () => { + test('renders closed successfully', () => { + const view = mount( + + ); + expect(view).toMatchSnapshot(); + }); + + test('renders expanded successfully', () => { + const view = mount( + + ); + expect(view).toMatchSnapshot(); + }); + + test('renders selected successfully', () => { + const view = mount( + + ); + expect(view).toMatchSnapshot(); + }); + + test('test onChange', () => { + const mockEvent = { target: { value: 'test' } }; + const view = mount( + + ); + const inst = view.instance(); + inst.onChange(mockEvent); + view.update(); + expect(view).toMatchSnapshot(); + }); +}); + +describe('typeahead multi select', () => { + test('renders closed successfully', () => { + const view = mount( + + ); + expect(view).toMatchSnapshot(); + }); + + test('renders expanded successfully', () => { + const view = mount( + + ); + expect(view).toMatchSnapshot(); + }); + + test('renders selected successfully', () => { + const view = mount( + + ); + expect(view).toMatchSnapshot(); + }); + + test('test onChange', () => { + const mockEvent = { target: { value: 'test' } }; + const view = mount( + + ); + const inst = view.instance(); + inst.onChange(mockEvent); + view.update(); + expect(view).toMatchSnapshot(); + }); +}); + describe('API', () => { test('click on item', () => { const mockToggle = jest.fn(); diff --git a/packages/patternfly-4/react-core/src/components/Select/SelectOption.js b/packages/patternfly-4/react-core/src/components/Select/SelectOption.js index 60726180efd..fb7a050bd3d 100644 --- a/packages/patternfly-4/react-core/src/components/Select/SelectOption.js +++ b/packages/patternfly-4/react-core/src/components/Select/SelectOption.js @@ -1,9 +1,9 @@ import React from 'react'; import styles from '@patternfly/patternfly/components/Select/select.css'; import { css } from '@patternfly/react-styles'; +import { CheckIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; import { SelectContext, KeyTypes } from './selectConstants'; -import { CheckIcon } from '@patternfly/react-icons'; const propTypes = { /** additional classes added to the Select Option */ diff --git a/packages/patternfly-4/react-core/src/components/Select/SelectToggle.js b/packages/patternfly-4/react-core/src/components/Select/SelectToggle.js index d77e882ef31..84f4fb635ff 100644 --- a/packages/patternfly-4/react-core/src/components/Select/SelectToggle.js +++ b/packages/patternfly-4/react-core/src/components/Select/SelectToggle.js @@ -1,9 +1,10 @@ import React, { Component } from 'react'; import styles from '@patternfly/patternfly/components/Select/select.css'; +import buttonStyles from '@patternfly/patternfly/components/Button/button.css'; import { css } from '@patternfly/react-styles'; import PropTypes from 'prop-types'; import { CaretDownIcon } from '@patternfly/react-icons'; -import { KeyTypes } from './selectConstants'; +import { KeyTypes, SelectVariant } from './selectConstants'; const propTypes = { /** HTML ID of dropdown toggle */ @@ -32,8 +33,12 @@ const propTypes = { isPlain: PropTypes.bool, /** Type of the toggle button, defaults to 'button' */ type: PropTypes.string, - /** Flag for checkbox variant keyboard interaction */ - isCheckbox: PropTypes.bool, + /** Id of label for the Select aria-labelledby */ + ariaLabelledBy: PropTypes.string, + /** Label for toggle of select variants */ + ariaLabelToggle: PropTypes.string, + /** Flag for variant, determines toggle rules and interaction */ + variant: PropTypes.oneOf(['single', 'checkbox', 'typeahead', 'typeaheadmulti']), /** Additional props are spread to the container + {isTypeahead && ( + + )} + {!isTypeahead && } + ); } } diff --git a/packages/patternfly-4/react-core/src/components/Select/SingleSelect.js b/packages/patternfly-4/react-core/src/components/Select/SingleSelect.js index 65ee640f042..754eb437a46 100644 --- a/packages/patternfly-4/react-core/src/components/Select/SingleSelect.js +++ b/packages/patternfly-4/react-core/src/components/Select/SingleSelect.js @@ -14,7 +14,7 @@ const propTypes = { /** Internal flag indicating whether select was opened via keyboard */ openedOnEnter: PropTypes.bool, /** Currently selected option */ - selected: PropTypes.string, + selected: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), /** Additional props are spread to the container + + + + +`; + +exports[`typeahead multi select renders expanded successfully 1`] = ` + + + + + + +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      +
    +
    + + +`; + +exports[`typeahead multi select renders selected successfully 1`] = ` + + + + + + + +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      +
    +
    + + +`; + +exports[`typeahead multi select test onChange 1`] = ` + + + + +
      +
      + No results found +
      +
    + + } + type="button" + variant="typeahead" + > +
    +
    + +
    + +
    + + +
      +
      + No results found +
      +
    +
    + + +`; + +exports[`typeahead select renders closed successfully 1`] = ` + + + + + + + +`; + +exports[`typeahead select renders expanded successfully 1`] = ` + + + + + + +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      +
    +
    + + +`; + +exports[`typeahead select renders selected successfully 1`] = ` + + + + + + + +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + +
    • + +
    • +
      +
    +
    + + +`; + +exports[`typeahead select test onChange 1`] = ` + + + + +
      +
      + No results found +
      +
    + + } + type="button" + variant="typeahead" + > +
    +
    + +
    + +
    + + +
      +
      + No results found +
      +
    +
    + + +`; diff --git a/packages/patternfly-4/react-core/src/components/Select/__snapshots__/SelectToggle.test.js.snap b/packages/patternfly-4/react-core/src/components/Select/__snapshots__/SelectToggle.test.js.snap index 130de7c6516..ba933fa593a 100644 --- a/packages/patternfly-4/react-core/src/components/Select/__snapshots__/SelectToggle.test.js.snap +++ b/packages/patternfly-4/react-core/src/components/Select/__snapshots__/SelectToggle.test.js.snap @@ -2,10 +2,11 @@ exports[`state active 1`] = ` } type="button" + variant={false} >