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 3684130a654..230fab660ca 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.md +++ b/packages/patternfly-4/react-core/src/components/Select/Select.md @@ -312,19 +312,21 @@ import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; class TypeaheadSelectInput extends React.Component { constructor(props) { super(props); - this.options = [ - { value: 'Alabama' }, - { value: 'Florida' }, - { value: 'New Jersey' }, - { value: 'New Mexico' }, - { value: 'New York' }, - { value: 'North Carolina' } - ]; this.state = { + options: [ + { value: 'Alabama' }, + { value: 'Florida' }, + { value: 'New Jersey' }, + { value: 'New Mexico' }, + { value: 'New York' }, + { value: 'North Carolina' } + ], isExpanded: false, selected: null, - isDisabled: false + isDisabled: false, + isCreatable: false, + hasOnCreateOption: false }; this.onToggle = isExpanded => { @@ -344,6 +346,12 @@ class TypeaheadSelectInput extends React.Component { } }; + this.onCreateOption = (newValue) => { + this.setState({ + options: [...this.state.options, {value: newValue}] + }); + } + this.clearSelection = () => { this.setState({ selected: null, @@ -351,15 +359,27 @@ class TypeaheadSelectInput extends React.Component { }); }; - this.toggleDisabled = (checked) => { + this.toggleDisabled = (checked) => { this.setState({ isDisabled: checked }) } + + this.toggleCreatable = (checked) => { + this.setState({ + isCreatable: checked + }) + } + + this.toggleCreateNew = (checked) => { + this.setState({ + hasOnCreateOption: checked + }) + } } render() { - const { isExpanded, selected, isDisabled } = this.state; + const { isExpanded, selected, isDisabled, isCreatable, hasOnCreateOption, options } = this.state; const titleId = 'typeahead-select-id'; return (
@@ -377,8 +397,10 @@ class TypeaheadSelectInput extends React.Component { ariaLabelledBy={titleId} placeholderText="Select a state" isDisabled={isDisabled} + isCreatable={isCreatable} + onCreateOption={hasOnCreateOption && this.onCreateOption || undefined} > - {this.options.map((option, index) => ( + {options.map((option, index) => ( ))} @@ -390,6 +412,22 @@ class TypeaheadSelectInput extends React.Component { id="toggle-disabled-typeahead" name="toggle-disabled-typeahead" /> + +
); } @@ -496,18 +534,26 @@ import { Select, SelectOption, SelectVariant } 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 = { + 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 } + ], isExpanded: false, - selected: [] + selected: [], + isCreatable: false, + hasOnCreateOption: false + }; + + this.onCreateOption = (newValue) => { + this.setState({ + options: [...this.state.options, {value: newValue}] + }); }; this.onToggle = isExpanded => { @@ -537,10 +583,22 @@ class MultiTypeaheadSelectInput extends React.Component { isExpanded: false }); }; + + this.toggleCreatable = (checked) => { + this.setState({ + isCreatable: checked + }) + } + + this.toggleCreateNew = (checked) => { + this.setState({ + hasOnCreateOption: checked + }) + } } render() { - const { isExpanded, selected } = this.state; + const { isExpanded, selected, isCreatable, hasOnCreateOption } = this.state; const titleId = 'multi-typeahead-select-id'; return ( @@ -558,11 +616,29 @@ class MultiTypeaheadSelectInput extends React.Component { isExpanded={isExpanded} ariaLabelledBy={titleId} placeholderText="Select a state" + isCreatable={isCreatable} + onCreateOption={hasOnCreateOption && this.onCreateOption || undefined} > - {this.options.map((option, index) => ( + {this.state.options.map((option, index) => ( ))} + + ); } diff --git a/packages/patternfly-4/react-core/src/components/Select/Select.test.tsx b/packages/patternfly-4/react-core/src/components/Select/Select.test.tsx index 582fdaa82dc..c1e8e8667d0 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.test.tsx +++ b/packages/patternfly-4/react-core/src/components/Select/Select.test.tsx @@ -245,6 +245,24 @@ describe('typeahead select', () => { view.update(); expect(view).toMatchSnapshot(); }); + + test('test creatable option', () => { + const mockEvent = { target: { value: 'test' } } as React.ChangeEvent; + const view = mount( + + ); + const inst = view.instance() as Select; + inst.onChange(mockEvent); + view.update(); + expect(view).toMatchSnapshot(); + }); }); describe('typeahead multi select', () => { diff --git a/packages/patternfly-4/react-core/src/components/Select/Select.tsx b/packages/patternfly-4/react-core/src/components/Select/Select.tsx index cd00928ddc5..3b1201e6661 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.tsx +++ b/packages/patternfly-4/react-core/src/components/Select/Select.tsx @@ -17,7 +17,7 @@ import { Omit } from '../../helpers/typeUtils'; let currentId = 0; export interface SelectProps - extends Omit, 'onSelect' | 'ref' | 'checked' | 'selected' > { + extends Omit, 'onSelect' | 'ref' | 'checked' | 'selected'> { /** Content rendered inside the Select */ children: React.ReactElement[]; /** Classes applied to the root of the Select */ @@ -30,8 +30,10 @@ export interface SelectProps isGrouped?: boolean; /** Display the toggle with no border or background */ isPlain?: boolean; - /** Flag to inficate if select is disabled */ + /** Flag to indicate if select is disabled */ isDisabled?: boolean; + /** Flag to indicate if the typeahead select allows new items */ + isCreatable?: boolean; /** Title text of Select */ placeholderText?: string | React.ReactNode; /** Selected item */ @@ -51,13 +53,19 @@ export interface SelectProps /** Label for remove chip button of multiple type ahead select variant */ ariaLabelRemove?: string; /** Callback for selection behavior */ - onSelect?: (event: React.MouseEvent | React.ChangeEvent, value: string | SelectOptionObject, isPlaceholder?: boolean) => void; + onSelect?: ( + event: React.MouseEvent | React.ChangeEvent, + value: string | SelectOptionObject, + isPlaceholder?: boolean + ) => void; /** Callback for toggle button behavior */ onToggle: (isExpanded: boolean) => void; /** Callback for typeahead clear button */ onClear?: (event: React.MouseEvent) => void; /** Optional callback for custom filtering */ onFilter?: (e: React.ChangeEvent) => React.ReactElement[]; + /** Optional callback for newly created options */ + onCreateOption?: (newOptionValue: string) => void; /** Variant of rendered Select */ variant?: 'single' | 'checkbox' | 'typeahead' | 'typeaheadmulti'; /** Width of the select container as a number of px or string percentage */ @@ -72,6 +80,7 @@ export interface SelectState { typeaheadActiveChild?: HTMLElement; typeaheadFilteredChildren: React.ReactNode[]; typeaheadCurrIndex: number; + creatableValue: string; } export class Select extends React.Component { @@ -87,7 +96,8 @@ export class Select extends React.Component { "isGrouped": false, "isPlain": false, "isDisabled": false, - 'aria-label': '', + "isCreatable": false, + "aria-label": '', "ariaLabelledBy": '', "ariaLabelTypeAhead": '', "ariaLabelClear": 'Clear all', @@ -98,6 +108,7 @@ export class Select extends React.Component { "variant": SelectVariant.single, "width": '', "onClear": Function.prototype, + "onCreateOption": Function.prototype, "toggleIcon": null as React.ReactElement, "onFilter": undefined as () => {} }; @@ -107,7 +118,8 @@ export class Select extends React.Component { typeaheadInputValue: '', typeaheadActiveChild: null as HTMLElement, typeaheadFilteredChildren: React.Children.toArray(this.props.children), - typeaheadCurrIndex: -1 + typeaheadCurrIndex: -1, + creatableValue: '' }; componentDidUpdate = (prevProps: SelectProps, prevState: SelectState) => { @@ -137,7 +149,7 @@ export class Select extends React.Component { } onChange = (e: React.ChangeEvent) => { - const { onFilter } = this.props; + const { onFilter, isCreatable, onCreateOption } = this.props; let typeaheadFilteredChildren; if (onFilter) { typeaheadFilteredChildren = onFilter(e); @@ -151,19 +163,29 @@ export class Select extends React.Component { typeaheadFilteredChildren = e.target.value.toString() !== '' ? React.Children.toArray(this.props.children).filter( - (child: React.ReactNode) => - this.getDisplay((child as React.ReactElement).props.value.toString(), 'text').search(input) === 0 + (child: React.ReactNode) => + this.getDisplay((child as React.ReactElement).props.value.toString(), 'text').search(input) === 0 ) : React.Children.toArray(this.props.children); } if (typeaheadFilteredChildren.length === 0) { - typeaheadFilteredChildren.push(); + !isCreatable && typeaheadFilteredChildren.push(); } + if (isCreatable && e.target.value != '') { + const newValue = e.target.value; + typeaheadFilteredChildren.push( + onCreateOption && onCreateOption(newValue)}> + Create "{newValue}" + + ); + } + this.setState({ typeaheadInputValue: e.target.value, typeaheadCurrIndex: -1, typeaheadFilteredChildren, - typeaheadActiveChild: null + typeaheadActiveChild: null, + creatableValue: e.target.value }); this.refCollection = []; } @@ -187,7 +209,9 @@ export class Select extends React.Component { React.cloneElement(child as React.ReactElement, { isFocused: typeaheadActiveChild && - typeaheadActiveChild.innerText === this.getDisplay((child as React.ReactElement).props.value.toString(), 'text') + (typeaheadActiveChild.innerText === + this.getDisplay((child as React.ReactElement).props.value.toString(), 'text') || + (this.props.isCreatable && typeaheadActiveChild.innerText === `Create "${(child as React.ReactElement).props.value}"`)) }) ); } @@ -207,7 +231,7 @@ export class Select extends React.Component { } handleTypeaheadKeys = (position: string) => { - const { isExpanded, onSelect } = this.props; + const { isExpanded, isCreatable } = this.props; const { typeaheadActiveChild, typeaheadCurrIndex } = this.state; if (isExpanded) { if (position === 'enter' && (typeaheadActiveChild || this.refCollection[0])) { @@ -232,7 +256,10 @@ export class Select extends React.Component { this.setState({ typeaheadCurrIndex: nextIndex, typeaheadActiveChild: this.refCollection[nextIndex], - typeaheadInputValue: this.refCollection[nextIndex].innerText + typeaheadInputValue: + isCreatable && this.refCollection[nextIndex].innerText.includes('Create') + ? this.state.creatableValue + : this.refCollection[nextIndex].innerText }); } } @@ -244,16 +271,18 @@ export class Select extends React.Component { } const { children } = this.props; - const item = children.filter((child) => child.props.value.toString() === value.toString())[0]; - - if (item && item.props.children) { - if (type === 'node') { - return item.props.children; + const item = children.filter(child => child.props.value.toString() === value.toString())[0]; + if (item) { + if (item && item.props.children) { + if (type === 'node') { + return item.props.children; + } + return this.findText(item); } - return this.findText(item); + return item.props.value.toString(); } - return item.props.value.toString(); - } + return value; + }; findText: (item: React.ReactElement) => string = (item: React.ReactElement) => { if (!item.props || !item.props.children) { @@ -282,11 +311,13 @@ export class Select extends React.Component { onSelect, onClear, onFilter, + onCreateOption, toggleId, isExpanded, isGrouped, isPlain, isDisabled, + isCreatable, selections, ariaLabelledBy, ariaLabelTypeAhead, @@ -323,7 +354,12 @@ export class Select extends React.Component { } return (
@@ -367,23 +403,25 @@ export class Select extends React.Component {
{toggleIcon && {toggleIcon}} - +
e.preventDefault()}> + +
{selections && (
+ +
    +
  • + +
  • +
+ , + } + } + type="button" + variant="typeahead" + > +
+
+
+ +
+
+ +
+ + +
    + +
  • + +
  • +
    +
+
+ + +`; + exports[`typeahead select test onChange 1`] = ` +
+ +