Skip to content

Commit

Permalink
feat(patternfly-4/react-core/): added Select Component (patternfly#590)
Browse files Browse the repository at this point in the history
* feat(patternfly-4/react-core/): added Select Componentt

affects: @patternfly/react-core

ISSUES CLOSED: patternfly#461

* feat(patternfly-4/react-core): added Select component for PF4

affects: @patternfly/react-core

ISSUES CLOSED: patternfly#461

* feat(/patternfly-4/react-core): add Select Component

affects: @patternfly/react-core

ISSUES CLOSED: patternfly#461

* feat(patternfly-4/react-core): added the Patternfly 4 Select component

affects: @patternfly/react-core

ISSUES CLOSED:  patternfly#461

* feat(patternfly-4/react-core): added the Patternfly 4 Select component

affects: @patternfly/react-core

ISSUES CLOSED: patternfly#461

* feat(patternfly-4/react-core): added the Patternfly 4 Select component

affects: @patternfly/react-core

ISSUES CLOSED: patternfly#461
  • Loading branch information
tlabaj authored and amarie401 committed Sep 12, 2018
1 parent cb052e1 commit 0a51e65
Show file tree
Hide file tree
Showing 16 changed files with 835 additions and 0 deletions.
16 changes: 16 additions & 0 deletions packages/patternfly-4/react-core/src/components/Select/Select.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HTMLProps, FormEvent } from 'react';
import { Omit } from '../../typeUtils';

export interface SelectProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'onBlur' | 'onFocus' | 'disabled'> {
children: any;
value?: any;
isValid?: boolean;
isDisabled?: boolean;
onBlur(event: React.FormEvent<HTMLSelectElement>): void;
onFocus(event: React.FormEvent<HTMLSelectElement>): void;
onChange(event: React.FormEvent<HTMLSelectElement>): void;
}

declare const Select: React.SFC<SelectProps>;

export default Select;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Select, SelectOption, SelectOptionGroup } from '@patternfly/react-core';
import SelectInput from './examples/SelectInput';
import SelectInputDisabled from './examples/SelectInputDisabled';
import SelectInputGrouped from './examples/SelectInputGrouped';

export default {
title: 'Select',
components: {
Select,
SelectOption,
SelectOptionGroup
},
examples: [SelectInput, SelectInputGrouped, SelectInputDisabled]
};
68 changes: 68 additions & 0 deletions packages/patternfly-4/react-core/src/components/Select/Select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import styles from '@patternfly/patternfly-next/components/FormControls/styles.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';

const propTypes = {
/** content rendered inside the Select */
children: PropTypes.node.isRequired,
/** additional classes added to the Select control */
className: PropTypes.string,
/** value of selected option */
value: PropTypes.any,
/** Flag indicating selection is valid */
isValid: PropTypes.bool,
/** Flag indicating the Select is disabled */
isDisabled: PropTypes.bool,
/** Optional callback for updating when selection loses focus */
onBlur: PropTypes.func,
/** Optional callback for updating when selection gets focus */
onFocus: PropTypes.func,
/** Optional callback for updating when selection changes */
onChange: PropTypes.func,
/** Custom flag to show that the Select requires an associated id or aria-label. */
'aria-label': props => {
if (!props.id && !props['aria-label']) {
return new Error('Select requires either an id or aria-label to be specified');
}
return null;
}
};

const defaultProps = {
className: '',
value: '',
isValid: true,
isDisabled: false,
onBlur: () => undefined,
onFocus: () => undefined,
onChange: () => undefined,
'aria-label': null
};

class Select extends React.Component {
handleChange = event => {
this.props.onChange(event.currentTarget.value, event);
};

render() {
const { children, className, value, isValid, isDisabled, ...props } = this.props;
return (
<select
{...props}
className={css(styles.formControl, className, !isValid && styles.modifiers.invalid)}
aria-invalid={!isValid}
onChange={this.handleChange}
disabled={isDisabled}
value={value}
>
{children}
</select>
);
}
}

Select.propTypes = propTypes;
Select.defaultProps = defaultProps;

export default Select;
152 changes: 152 additions & 0 deletions packages/patternfly-4/react-core/src/components/Select/Select.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React from 'react';
import { shallow } from 'enzyme';
import Select from './Select';
import SelectOption from './SelectOption';
import SelectOptionGroup from './SelectOptionGroup';

const props = {
options: [
{ value: 'please choose', label: 'Please Choose', disabled: true },
{ value: 'mr', label: 'Mr', disabled: false },
{ value: 'miss', label: 'Miss', disabled: false },
{ value: 'mrs', label: 'Mrs', disabled: false },
{ value: 'ms', label: 'Ms', disabled: false },
{ value: 'dr', label: 'Dr', disabled: false },
{ value: 'other', label: 'Other', disabled: true }
],
value: 'mrs'
};

const groupedProps = {
groups: [
{
groupLabel: 'Group1',
disabled: false,
options: [
{ value: '1', label: 'The First Option', disabled: false },
{ value: '2', label: 'Second option is selected by default', disabled: false }
]
},
{
groupLabel: 'Group2',
disabled: false,
options: [
{ value: '3', label: 'The Third Option', disabled: false },
{ value: '4', label: 'The Fourth option', disabled: false }
]
},
{
groupLabel: 'Group3',
disabled: true,
options: [
{ value: '5', label: 'The Fifth Option', disabled: false },
{ value: '6', label: 'The Sixth option', disabled: false }
]
}
],
value: '2'
};

test('Simple Select input', () => {
const view = shallow(
<Select value={props.value} aria-label="simple Select">
{props.options.map((option, index) => (
<SelectOption isDisabled={option.disabled} key={index} value={option.value} label={option.label} />
))}
</Select>
);
expect(view).toMatchSnapshot();
});

test('Grouped Select input', () => {
const view = shallow(
<Select value={groupedProps.value} aria-label=" grouped Select">
{groupedProps.groups.map((group, index) => (
<SelectOptionGroup isDisabled={group.disabled} key={index} label={group.groupLabel}>
{group.options.map((option, i) => (
<SelectOption isDisabled={option.disabled} key={i} value={option.value} label={option.label} />
))}
</SelectOptionGroup>
))}
</Select>
);
expect(view).toMatchSnapshot();
});

test('Disabled Select input ', () => {
const view = shallow(
<Select isDisabled aria-label="disabled Select">
<SelectOption key={1} value={props.options[1].value} label={props.options[1].label} />
</Select>
);
expect(view).toMatchSnapshot();
});

test('Select input with aria-label does not generate console error', () => {
const myMock = jest.fn();
global.console = { error: myMock };
const view = shallow(
<Select aria-label="Select with aria-label">
<SelectOption key={1} value={props.options[1].value} label={props.options[1].label} />
</Select>
);
expect(view).toMatchSnapshot();
expect(myMock).not.toBeCalled();
});

test('Select input with id does not generate console error', () => {
const myMock = jest.fn();
global.console = { error: myMock };
const view = shallow(
<Select id="id">
<SelectOption key={1} value={props.options[1].value} label={props.options[1].label} />
</Select>
);
expect(view).toMatchSnapshot();
expect(myMock).not.toBeCalled();
});

test('Select input with no aria-label or id generates console error', () => {
const myMock = jest.fn();
global.console = { error: myMock };
const view = shallow(
<Select>
<SelectOption key={1} value={props.options[1].value} label={props.options[1].label} />
</Select>
);
expect(view).toMatchSnapshot();
expect(myMock).toBeCalled();
});

test('invalid Select input', () => {
const view = shallow(
<Select isValid={false} aria-label="invalid Select">
<SelectOption key={1} value={props.options[1].value} label={props.options[1].label} />
</Select>
);
expect(view).toMatchSnapshot();
});

test('required Select input', () => {
const view = shallow(
<Select required aria-label="required Select">
<SelectOption key={1} value={props.options[1].value} label={props.options[1].label} />
</Select>
);
expect(view).toMatchSnapshot();
});

test('Select passes value and event to onChange handler', () => {
const myMock = jest.fn();
const newValue = 1;
const event = {
currentTarget: { value: newValue }
};
const view = shallow(
<Select onChange={myMock} aria-label="onchange Select">
<SelectOption key={1} value={props.options[1].value} label={props.options[1].label} />
</Select>
);
view.find('select').simulate('change', event);
expect(myMock).toBeCalledWith(newValue, event);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { HTMLProps, FormEvent } from 'react';
import { Omit } from '../../typeUtils';

export interface SelectOptionProps extends Omit<HTMLProps<HTMLOptionElement>, 'disabled'> {
value?: any;
label: string;
isValid?: boolean;
isDisabled?: boolean;
}

declare const SelectOption: React.SFC<SelectOptionProps>;

export default SelectOption;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
/** additional classes added to the Select Option */
className: PropTypes.string,
/** the value for the option */
value: PropTypes.any,
/** the label for the option */
label: PropTypes.string.isRequired,
/** flag indicating if the option is disabled */
isDisabled: PropTypes.bool
};

const defaultProps = {
className: '',
value: '',
isDisabled: false
};

const SelectOption = ({ className, value, label, isDisabled, ...props }) => (
<option {...props} className={className} value={value} disabled={isDisabled}>
{label}
</option>
);

SelectOption.propTypes = propTypes;
SelectOption.defaultProps = defaultProps;

export default SelectOption;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { HTMLProps, FormEvent } from 'react';
import { Omit } from '../../typeUtils';

export interface SelectOptionGroupProps extends Omit<HTMLProps<HTMLOptGroupElement>, 'disabled'> {
children: any;
label: string;
isDisabled?: boolean;
}

declare const SelectOptionGroup: React.SFC<SelectOptionGroupProps>;

export default SelectOptionGroup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
/** content rendered inside the Select Option Group */
children: PropTypes.node,
/** additional classes added to the Select Option */
className: PropTypes.string,
/** the label for the option */
label: PropTypes.string.isRequired,
/** flag indicating if the Option Group is disabled */
isDisabled: PropTypes.bool
};

const defaultProps = {
children: null,
className: '',
isDisabled: false
};

const SelectOptionGroup = ({ children, className, label, isDisabled, ...props }) => (
<optgroup {...props} disabled={!!isDisabled} className={className} label={label}>
{children}
</optgroup>
);

SelectOptionGroup.propTypes = propTypes;
SelectOptionGroup.defaultProps = defaultProps;

export default SelectOptionGroup;
Loading

0 comments on commit 0a51e65

Please sign in to comment.