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 (