From c720330e85c550004fbea2202cdaca7b43b4eb8b Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Tue, 3 Sep 2019 10:07:03 -0400 Subject: [PATCH 1/4] feat(Select): add creatable and new features, add tests, add to TS demo --- .../src/components/Select/Select.md | 63 +++- .../src/components/Select/Select.test.tsx | 20 ++ .../src/components/Select/Select.tsx | 80 +++-- .../Select/__snapshots__/Select.test.tsx.snap | 334 ++++++++++++++++-- .../cypress/integration/select.spec.ts | 12 + .../demos/SelectDemo/SelectDemo.tsx | 65 +++- 6 files changed, 504 insertions(+), 70 deletions(-) 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..e336541d217 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,13 @@ class TypeaheadSelectInput extends React.Component { } }; + this.onCreateOption = (newValue) => { + console.log(newValue); + this.setState({ + options: [...this.state.options, {value: newValue}] + }); + } + this.clearSelection = () => { this.setState({ selected: null, @@ -351,15 +360,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 +398,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 +413,22 @@ class TypeaheadSelectInput extends React.Component { id="toggle-disabled-typeahead" name="toggle-disabled-typeahead" /> + +
); } 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..d61e022cbe0 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,26 @@ 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..dce2d2831ab 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 storing newly created options */ + onCreateOption?: (value: 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 (
diff --git a/packages/patternfly-4/react-core/src/components/Select/__snapshots__/Select.test.tsx.snap b/packages/patternfly-4/react-core/src/components/Select/__snapshots__/Select.test.tsx.snap index 64984ee0f30..79e81ad47a5 100644 --- a/packages/patternfly-4/react-core/src/components/Select/__snapshots__/Select.test.tsx.snap +++ b/packages/patternfly-4/react-core/src/components/Select/__snapshots__/Select.test.tsx.snap @@ -10,11 +10,13 @@ exports[`checkbox select renders checkbox select groups successfully - old class ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={true} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -634,11 +636,13 @@ exports[`checkbox select renders checkbox select groups successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={true} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -1282,11 +1286,13 @@ exports[`checkbox select renders closed successfully - old classes 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={false} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -1423,11 +1429,13 @@ exports[`checkbox select renders closed successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={false} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -1564,11 +1572,13 @@ exports[`checkbox select renders expanded successfully - old classes 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -1947,11 +1957,13 @@ exports[`checkbox select renders expanded successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -2342,11 +2354,13 @@ exports[`checkbox select renders expanded successfully with custom objects 1`] = ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -2711,11 +2725,13 @@ exports[`select custom select filter filters properly 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onFilter={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} @@ -3034,11 +3050,13 @@ exports[`select renders select groups successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={true} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -3599,11 +3617,13 @@ exports[`select renders up drection successfully 1`] = ` ariaLabelledBy="" className="" direction="up" + isCreatable={false} isDisabled={false} isExpanded={false} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -3744,11 +3764,13 @@ exports[`select single select renders closed successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={false} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -3889,11 +3911,13 @@ exports[`select single select renders disabled successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={true} isExpanded={false} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -4035,11 +4059,13 @@ exports[`select single select renders expanded successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -4378,11 +4404,13 @@ exports[`select single select renders expanded successfully with custom objects ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={true} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -4739,11 +4767,13 @@ exports[`typeahead multi select renders closed successfully 1`] = ` ariaLabelledBy="" className="" direction="down" + isCreatable={false} isDisabled={false} isExpanded={false} isGrouped={false} isPlain={false} onClear={[Function]} + onCreateOption={[Function]} onSelect={[MockFunction]} onToggle={[MockFunction]} placeholderText="" @@ -4763,10 +4793,10 @@ exports[`typeahead multi select renders closed successfully 1`] = ` > + +
    + +
  • + +
  • +
    +
+
+
+ +`; + exports[`typeahead select test onChange 1`] = ` + + ); } @@ -493,7 +534,7 @@ export class SelectDemo extends Component { ariaLabelledBy={titleId} placeholderText="Select a state" > - {this.typeaheadOptions.map((option, index) => ( + {this.state.typeaheadOptions.map((option, index) => ( ))} @@ -557,7 +598,7 @@ export class SelectDemo extends Component { ariaLabelledBy={titleId} placeholderText="Select a state" > - {this.typeaheadOptions.map((option, index) => ( + {this.state.typeaheadOptions.map((option, index) => (
div-{option.value.toString()}-test_span
@@ -592,7 +633,7 @@ export class SelectDemo extends Component { ariaLabelledBy={titleId} placeholderText="Select a state" > - {this.typeaheadOptions.map((option, index) => ( + {this.state.typeaheadOptions.map((option, index) => (
div-{option.value.toString()}-test_span
From 5d307d01120b1336d3b28927f75dd0693886479a Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Wed, 4 Sep 2019 15:48:47 -0400 Subject: [PATCH 2/4] feat(Select): add example toggles to multi typeahead --- .../src/components/Select/Select.md | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) 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 e336541d217..6f47c483dcc 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.md +++ b/packages/patternfly-4/react-core/src/components/Select/Select.md @@ -535,18 +535,27 @@ 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) => { + console.log(newValue); + this.setState({ + options: [...this.state.options, {value: newValue}] + }); }; this.onToggle = isExpanded => { @@ -576,10 +585,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 ( @@ -597,11 +618,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) => ( ))} + + ); } From 13ebede681bd8aadd9661b8e75c70e3606d8dccf Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Thu, 5 Sep 2019 13:40:34 -0400 Subject: [PATCH 3/4] feat(Select): add pr feedback --- .../patternfly-4/react-core/src/components/Select/Select.md | 2 -- .../react-core/src/components/Select/Select.test.tsx | 2 -- .../patternfly-4/react-core/src/components/Select/Select.tsx | 4 ++-- .../src/components/Select/__snapshots__/Select.test.tsx.snap | 3 +-- .../src/components/demos/SelectDemo/SelectDemo.tsx | 1 - 5 files changed, 3 insertions(+), 9 deletions(-) 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 6f47c483dcc..230fab660ca 100644 --- a/packages/patternfly-4/react-core/src/components/Select/Select.md +++ b/packages/patternfly-4/react-core/src/components/Select/Select.md @@ -347,7 +347,6 @@ class TypeaheadSelectInput extends React.Component { }; this.onCreateOption = (newValue) => { - console.log(newValue); this.setState({ options: [...this.state.options, {value: newValue}] }); @@ -552,7 +551,6 @@ class MultiTypeaheadSelectInput extends React.Component { }; this.onCreateOption = (newValue) => { - console.log(newValue); this.setState({ options: [...this.state.options, {value: newValue}] }); 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 d61e022cbe0..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 @@ -251,9 +251,7 @@ describe('typeahead select', () => { const view = mount( +
e.preventDefault()}> + +
{selections && (