Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions packages/patternfly-4/react-core/src/components/Select/Select.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,112 @@ class MultiTypeaheadSelectInput extends React.Component {
}
}
```
## Multiple typeahead select input with custom objects

```js
import React from 'react';
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';

class MultiTypeaheadSelectInputCustomObjects extends React.Component {
constructor(props) {
super(props);
this.createState = (name, abbreviation, capital, founded) => {
return {
name: name,
abbreviation: abbreviation,
capital: capital,
founded: founded,
toString: function() {
return `${this.name} (${this.abbreviation}) - Founded: ${this.founded}`;
}
}
}
this.options = [
<SelectOption value={ this.createState('Alabama', 'AL', 'Montgomery', 1846)} />,
<SelectOption value={ this.createState('Florida', 'FL', 'Tailahassee', 1845)} />,
<SelectOption value={ this.createState('New Jersey', 'NJ', 'Trenton', 1787)} />,
<SelectOption value={ this.createState('New Mexico', 'NM', 'Santa Fe', 1912)} />,
<SelectOption value={ this.createState('New York', 'NY', 'Albany', 1788)} />,
<SelectOption value={ this.createState('North Carolina', 'NC', 'Raleigh', 1789)} />
];

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
});
};


this.customFilter = (e) => {
let input;
try {
input = new RegExp(e.target.value.toString(), 'i');
} catch (err) {
input = new RegExp(e.target.value.toString().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
}
let typeaheadFilteredChildren =
e.target.value.toString() !== ''
? this.options.filter(option => input.test(option.props.value.toString()))
: this.options;
return typeaheadFilteredChildren;
}
}

render() {
const { isExpanded, selected } = this.state;
const titleId = 'multi-typeahead-select-id';

return (
<div>
<span id={titleId} hidden>
Select a state
</span>
<Select
variant={SelectVariant.typeaheadMulti}
aria-label="Select a state"
onToggle={this.onToggle}
onSelect={this.onSelect}
onClear={this.clearSelection}
onFilter={this.customFilter}
selections={selected}
isExpanded={isExpanded}
ariaLabelledBy={titleId}
placeholderText="Select a state"
>
{this.options}
</Select>
</div>
);
}
}
```
## Plain multiple typeahead select input

```js
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import * as React from 'react';
import { mount } from 'enzyme';
import { Select } from './Select';
import { SelectOption } from './SelectOption';
import { SelectOption, SelectOptionObject } from './SelectOption';
import { CheckboxSelectOption } from './CheckboxSelectOption';
import { SelectGroup } from './SelectGroup';
import { CheckboxSelectGroup } from './CheckboxSelectGroup';
import { SelectVariant } from './selectConstants';

class User implements SelectOptionObject {
private firstName: string;
private lastName: string;
private title: string;

constructor(title: string, firstName: string, lastName: string) {
this.title = title;
this.firstName = firstName;
this.lastName = lastName;
}

toString = ():string =>`${this.title}: ${this.firstName} ${this.lastName}`;
}

const selectOptions = [
<SelectOption value="Mr" key="0" />,
<SelectOption value="Mrs" key="1" />,
Expand All @@ -21,6 +35,12 @@ const checkboxSelectOptions = [
<CheckboxSelectOption value="Other" key="3" />
];

const selectOptionsCustom = [
<SelectOption value={new User('Mr', 'User', 'One')} key="0" />,
<SelectOption value={new User('Mrs', 'New', 'User')} key="1" />,
<SelectOption value={new User('Ms', 'Test', 'Three')} key="2" />
];

describe('select', () => {
describe('single select', () => {
test('renders closed successfully', () => {
Expand All @@ -40,6 +60,14 @@ describe('select', () => {
);
expect(view).toMatchSnapshot();
});
test('renders expanded successfully with custom objects', () => {
const view = mount(
<Select variant={SelectVariant.single} onSelect={jest.fn()} onToggle={jest.fn()} isExpanded>
{selectOptionsCustom}
</Select>
);
expect(view).toMatchSnapshot();
});
});

describe('custom select filter', () => {
Expand Down Expand Up @@ -125,6 +153,15 @@ describe('checkbox select', () => {
expect(view).toMatchSnapshot();
});

test('renders expanded successfully with custom objects', () => {
const view = mount(
<Select variant={SelectVariant.checkbox} onSelect={jest.fn()} onToggle={jest.fn()} isExpanded>
{selectOptionsCustom}
</Select>
);
expect(view).toMatchSnapshot();
});

test('renders checkbox select groups successfully', () => {
const view = mount(
<Select variant={SelectVariant.checkbox} onSelect={jest.fn()} onToggle={jest.fn()} isExpanded isGrouped>
Expand Down
30 changes: 15 additions & 15 deletions packages/patternfly-4/react-core/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import buttonStyles from '@patternfly/react-styles/css/components/Button/button'
import { css } from '@patternfly/react-styles';
import { TimesCircleIcon } from '@patternfly/react-icons';
import { SelectMenu } from './SelectMenu';
import { SelectOption } from './SelectOption';
import { SelectOption, SelectOptionObject } from './SelectOption';
import { SelectToggle } from './SelectToggle';
import { SelectContext, SelectVariant } from './selectConstants';
import { Chip, ChipGroup } from '../ChipGroup';
Expand All @@ -17,7 +17,7 @@ import { Omit } from '../../helpers/typeUtils';
let currentId = 0;

export interface SelectProps
extends Omit<React.HTMLProps<HTMLDivElement>, 'onSelect' | 'ref' | 'checked' | 'selected'> {
extends Omit<React.HTMLProps<HTMLDivElement>, 'onSelect' | 'ref' | 'checked' | 'selected' > {
/** Content rendered inside the Select */
children: React.ReactElement[];
/** Classes applied to the root of the Select */
Expand All @@ -31,7 +31,7 @@ export interface SelectProps
/** Title text of Select */
placeholderText?: string | React.ReactNode;
/** Selected item */
selections?: string[] | string;
selections?: string | SelectOptionObject | (string | SelectOptionObject)[];
/** Id for select toggle element */
toggleId?: string;
/** Adds accessible text to Select */
Expand All @@ -47,7 +47,7 @@ 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, 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 */
Expand Down Expand Up @@ -135,15 +135,15 @@ export class Select extends React.Component<SelectProps, SelectState> {
} else {
let input: RegExp;
try {
input = new RegExp(e.target.value, 'i');
input = new RegExp(e.target.value.toString(), 'i');
} catch (err) {
input = new RegExp(e.target.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
input = new RegExp(e.target.value.toString().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
}
typeaheadFilteredChildren =
e.target.value !== ''
e.target.value.toString() !== ''
? React.Children.toArray(this.props.children).filter(
(child: React.ReactNode) =>
this.getDisplay((child as React.ReactElement).props.value, 'text').search(input) === 0
this.getDisplay((child as React.ReactElement).props.value.toString(), 'text').search(input) === 0
)
: React.Children.toArray(this.props.children);
}
Expand Down Expand Up @@ -178,7 +178,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
React.cloneElement(child as React.ReactElement, {
isFocused:
typeaheadActiveChild &&
typeaheadActiveChild.innerText === this.getDisplay((child as React.ReactElement).props.value, 'text')
typeaheadActiveChild.innerText === this.getDisplay((child as React.ReactElement).props.value.toString(), 'text')
})
);
}
Expand Down Expand Up @@ -229,21 +229,21 @@ export class Select extends React.Component<SelectProps, SelectState> {
}
};

getDisplay = (value: string, type: 'node' | 'text' = 'node') => {
getDisplay = (value: string | SelectOptionObject, type: 'node' | 'text' = 'node') => {
if (!value) {
return;
}

const { children } = this.props;
const item = children.filter(child => child.props.value === value)[0];
const item = children.filter(child => child.props.value.toString() === value.toString())[0];

if (item && item.props.children) {
if (type === 'node') {
return item.props.children;
}
return this.findText(item);
}
return item.props.value;
return item.props.value.toString();
};

findText: (item: React.ReactElement) => string = (item: React.ReactElement) => {
Expand Down Expand Up @@ -341,7 +341,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
<React.Fragment>
<div className={css(styles.selectToggleWrapper)}>
<span className={css(styles.selectToggleText)}>{placeholderText}</span>
{selections && selections.length > 0 && (
{selections && (Array.isArray(selections) && selections.length > 0) && (
<div className={css(styles.selectToggleBadge)}>
<span className={css(badgeStyles.badge, badgeStyles.modifiers.read)}>{selections.length}</span>
</div>
Expand Down Expand Up @@ -387,7 +387,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
{variant === SelectVariant.typeaheadMulti && (
<React.Fragment>
<div className={css(styles.selectToggleWrapper)}>
{selections && selections.length > 0 && selectedChips}
{selections && (Array.isArray(selections) && selections.length > 0)&& selectedChips}
<input
className={css(formStyles.formControl, styles.selectToggleTypeahead)}
aria-activedescendant={typeaheadActiveChild && typeaheadActiveChild.id}
Expand All @@ -401,7 +401,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
autoComplete="off"
/>
</div>
{selections && selections.length > 0 && (
{selections && (Array.isArray(selections) && selections.length > 0) && (
<button
className={css(buttonStyles.button, buttonStyles.modifiers.plain, styles.selectToggleClear)}
onClick={e => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/Select/select';
import { default as formStyles } from '@patternfly/react-styles/css/components/Form/form';
import { css } from '@patternfly/react-styles';
import { SelectOptionObject } from './SelectOption';
import { SelectConsumer, SelectVariant } from './selectConstants';
import { Omit } from '../../helpers/typeUtils';

Expand All @@ -18,9 +19,9 @@ export interface SelectMenuProps extends Omit<React.HTMLProps<HTMLElement>, 'che
/** Flag indicating the Select options are grouped */
isGrouped?: boolean;
/** Currently selected option (for single, typeahead variants) */
selected?: string | string[];
selected?: string | SelectOptionObject | (string | SelectOptionObject)[];
/** Currently checked options (for checkbox variant) */
checked?: string[];
checked?: (string | SelectOptionObject) [];
/** Internal flag for specifiying how the menu was opened */
openedOnEnter?: boolean;
/** Internal callback for ref tracking */
Expand Down Expand Up @@ -60,10 +61,10 @@ export class SelectMenu extends React.Component<SelectMenuProps> {
const { selected, sendRef, keyHandler } = this.props;
const isSelected =
selected && selected.constructor === Array
? selected && selected.includes(child.props.value)
? selected && (Array.isArray(selected) && selected.includes(child.props.value))
: selected === child.props.value;
return React.cloneElement(child, {
id: `${child.props.value}-${index}`,
id: `${child.props.value ? child.props.value.toString() : ''}-${index}`,
isSelected,
sendRef,
keyHandler,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import { SelectOption } from './SelectOption';
import { SelectOption, SelectOptionObject } from './SelectOption';
import { SelectProvider } from './selectConstants';

class User implements SelectOptionObject {
private firstName: string;
private lastName: string;
private title: string;

constructor(title: string, firstName: string, lastName: string) {
this.title = title;
this.firstName = firstName;
this.lastName = lastName;
}

toString = (): string => `${this.title}: ${this.firstName} ${this.lastName}`;
}

describe('select options', () => {
test('renders with value parameter successfully', () => {
const view = mount(
Expand All @@ -25,6 +39,27 @@ describe('select options', () => {
expect(view).toMatchSnapshot();
});

test('renders with custom user object successfully', () => {
const view = mount(
<SelectProvider value={{ onSelect: () => {}, onClose: () => {}, variant: 'single' }}>
<SelectOption value={new User('Mr.', 'Test', 'User')} sendRef={jest.fn()} />
</SelectProvider>
);
expect(view).toMatchSnapshot();
});


test('renders with custom display and custom user object successfully', () => {
const view = mount(
<SelectProvider value={{ onSelect: () => {}, onClose: () => {}, variant: 'single' }}>
<SelectOption value={new User('Mr.', 'Test', 'User')} sendRef={jest.fn()}>
<div>test display</div>
</SelectOption>
</SelectProvider>
);
expect(view).toMatchSnapshot();
});

describe('disabled', () => {
test('renders disabled successfully', () => {
const view = mount(
Expand Down
Loading