Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0f63d78
feat(Select): add single select variant
kmcfaul Jan 29, 2019
06cdfb1
refactor(Select): update TS declarations
kmcfaul Jan 29, 2019
9b987ab
feat(Select): update TS, clean up ref and openedOnEnter syntax
kmcfaul Jan 31, 2019
53103cb
fix(Select): fix onKeyDown syntax
kmcfaul Feb 1, 2019
6c3ae82
fix(Select): add missing toggle text class, update example
kmcfaul Feb 5, 2019
5611b70
feat(Select): add alternate placeholder option
kmcfaul Feb 5, 2019
8ae8634
feat(Select): incorporate pr feedback
kmcfaul Feb 6, 2019
82d87ca
fix(Select): update styles imports
kmcfaul Feb 6, 2019
67d779e
feat(Select): add width control to select container
kmcfaul Feb 6, 2019
3aa421b
feat(Select): update with accessibility feedback, fix selected option…
kmcfaul Feb 6, 2019
0045629
feat(Select): add aria-labelledby & label id prop
kmcfaul Feb 11, 2019
3d8d74c
fix(Select): fix for toggle width, fix for disabling keyboard input w…
kmcfaul Feb 12, 2019
a3e0083
fix(Select): remove unused Select prop, fix missing case for clearing…
kmcfaul Feb 13, 2019
15cfdaf
feat(Select): use helpers, use key types from constants
kmcfaul Feb 18, 2019
0cc32ed
feat(Select): update with PR feedback
kmcfaul Feb 19, 2019
ca7a3fe
feat(Select): remove alternate declarations, update tests for console…
kmcfaul Feb 19, 2019
3cfd99e
fix(Select): remove toggle ts
kmcfaul Feb 21, 2019
86c23ac
fix(Select): fix eslint errors
kmcfaul Feb 21, 2019
027b6c1
feat(Select): add Select to TS demo, export SelectVariant for TS
kmcfaul Feb 21, 2019
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
18 changes: 18 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,18 @@
import { HTMLProps, FormEvent } from 'react';

export const SelectVariant = {
single: 'single'
};

export interface SelectProps extends HTMLProps<HTMLOptionElement> {
isExpanded?: boolean;
onToggle(value: boolean): void;
placeholderText?: string;
selections?: string;
variant?: string;
width?: string | number;
}

declare const Select: React.FunctionComponent<SelectProps>;

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

export default {
title: 'Select',
components: {
Select,
SelectOption
},
examples: [{ component: SingleSelectInput, title: 'Single Select Input' }]
};
125 changes: 125 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,125 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/Select/select.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';
import SingleSelect from './SingleSelect';
import SelectToggle from './SelectToggle';
import { SelectContext, SelectVariant } from './selectConstants';

// seed for the aria-labelledby ID
let currentId = 0;

const propTypes = {
/** Content rendered inside the Select */
children: PropTypes.node,
/** Classes applied to the root of the Select */
className: PropTypes.string,
/** Flag to indicate if select is expanded */
isExpanded: PropTypes.bool,
/** Placeholder text of Select */
placeholderText: PropTypes.string,
/** Selected item */
selections: PropTypes.string,
/** Id of label for the Select aria-labelledby */
ariaLabelledBy: PropTypes.string,
/** Callback for selection behavior */
onSelect: PropTypes.func.isRequired,
/** Callback for toggle button behavior */
onToggle: PropTypes.func.isRequired,
/** Variant of rendered Select */
variant: PropTypes.oneOf(['single']),
/** Width of the select container as a number of px or string percentage */
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** Additional props are spread to the container <ul> */
'': PropTypes.any
};

const defaultProps = {
children: null,
className: '',
isExpanded: false,
ariaLabelledBy: '',
selections: null,
placeholderText: null,
variant: SelectVariant.single,
width: '100%'
};

class Select extends React.Component {
parentRef = React.createRef();
state = { openedOnEnter: false };

onEnter = () => {
this.setState({ openedOnEnter: true });
};

onClose = () => {
this.setState({ openedOnEnter: false });
};

render() {
const {
children,
className,
variant,
onToggle,
onSelect,
isExpanded,
selections,
ariaLabelledBy,
placeholderText,
width,
...props
} = this.props;
const { openedOnEnter } = this.state;
const selectToggleId = `pf-toggle-id-${currentId++}`;
let childPlaceholderText = null;
if (!selections && !placeholderText) {
const childPlaceholder = children.filter(child => child.props.isPlaceholder === true);
childPlaceholderText =
(childPlaceholder[0] && childPlaceholder[0].props.value) || (children[0] && children[0].props.value);
}

return (
<div
className={css(styles.select, isExpanded && styles.modifiers.expanded, className)}
ref={this.parentRef}
style={{ width }}
>
<SelectContext.Provider value={{ onSelect, onClose: this.onClose }}>
{variant === 'single' && (
<React.Fragment>
<SelectToggle
id={selectToggleId}
parentRef={this.parentRef.current}
isExpanded={isExpanded}
onToggle={onToggle}
onEnter={this.onEnter}
onClose={this.onClose}
aria-labelledby={`${ariaLabelledBy} ${selectToggleId}`}
style={{ width }}
>
{selections || placeholderText || childPlaceholderText}
</SelectToggle>
{isExpanded && (
<SingleSelect
{...props}
selected={selections}
openedOnEnter={openedOnEnter}
aria-labelledby={ariaLabelledBy}
>
{children}
</SingleSelect>
)}
</React.Fragment>
)}
</SelectContext.Provider>
</div>
);
}
}

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

export default Select;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { mount } from 'enzyme';
import Select from './Select';
import SelectOption from './SelectOption';

const selectOptions = [
<SelectOption value="Mr" key="0" />,
<SelectOption value="Mrs" key="1" />,
<SelectOption value="Ms" key="2" />,
<SelectOption value="Other" key="3" />
];

describe('select', () => {
describe('single select', () => {
test('renders closed successfully', () => {
const view = mount(
<Select variant="single" onSelect={jest.fn()} onToggle={jest.fn()}>
{selectOptions}
</Select>
);
expect(view).toMatchSnapshot();
});

test('renders expanded successfully', () => {
const view = mount(
<Select variant="single" onSelect={jest.fn()} onToggle={jest.fn()} isExpanded>
{selectOptions}
</Select>
);
expect(view).toMatchSnapshot();
});
});
});

describe('API', () => {
test('click on item', () => {
const mockToggle = jest.fn();
const mockSelect = jest.fn();
const view = mount(
<Select variant="single" onToggle={mockToggle} onSelect={mockSelect} isExpanded>
{selectOptions}
</Select>
);
view
.find('button')
.at(1)
.simulate('click');
expect(mockToggle.mock.calls).toHaveLength(0);
expect(mockSelect.mock.calls).toHaveLength(1);
});

test('children only, no console error', () => {
const myMock = jest.fn();
global.console = { error: myMock };
mount(
<Select variant="single" onSelect={jest.fn()} onToggle={jest.fn()} isExpanded>
{selectOptions}
</Select>
);
expect(myMock).not.toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { HTMLProps, FormEvent } from 'react';
import { Omit } from '../../typeUtils';

export interface SelectOptionProps extends Omit<HTMLProps<HTMLOptionElement>, 'disabled'> {
value?: string;
index?: number;
isValid?: boolean;
isDisabled?: boolean;
isPlaceholder?: boolean;
isSelected?: boolean;
onClick?: Function;
sendRef?: Function;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this used for?

keyHandler?: Function;
}

declare const SelectOption: React.FunctionComponent<SelectOptionProps>;

export default SelectOption;
110 changes: 110 additions & 0 deletions packages/patternfly-4/react-core/src/components/Select/SelectOption.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/Select/select.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';
import { SelectContext, KeyTypes } from './selectConstants';

const propTypes = {
/** additional classes added to the Select Option */
className: PropTypes.string,
/** the value for the option */
value: PropTypes.string,
/** internal index of the option */
index: PropTypes.number,
/** flag indicating if the option is disabled */
isDisabled: PropTypes.bool,
/** flag indicating if the option acts as a placeholder */
isPlaceholder: PropTypes.bool,
/** Internal flag indicating if the option is selected */
isSelected: PropTypes.bool,
/** Optional on click callback */
onClick: PropTypes.func,
/** Internal callback for ref tracking */
sendRef: PropTypes.func,
/** Internal callback for keyboard navigation */
keyHandler: PropTypes.func,
/** Additional props are spread to the container <button> */
'': PropTypes.any
};

const defaultProps = {
className: '',
value: null,
index: 0,
isDisabled: false,
isPlaceholder: false,
isSelected: false,
onClick: Function.prototype,
sendRef: Function.prototype,
keyHandler: Function.prototype
};

class SelectOption extends React.Component {
ref = React.createRef();

componentDidMount() {
this.props.sendRef(this.ref.current, this.props.index);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linting error no props validation on index.

}

onKeyDown = event => {
if (event.key === KeyTypes.Tab) return;
event.preventDefault();
if (event.key === KeyTypes.ArrowUp) {
this.props.keyHandler(this.props.index, 'up');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linting error no props validation on keyHandler.

} else if (event.key === KeyTypes.ArrowDown) {
this.props.keyHandler(this.props.index, 'down');
} else if (event.key === KeyTypes.Enter) {
this.ref.current.click && this.ref.current.click();
}
};

render() {
const {
className,
value,
onClick,
isDisabled,
isPlaceholder,
isSelected,
sendRef,
keyHandler,
index,
...props
} = this.props;
return (
<SelectContext.Consumer>
{({ onSelect, onClose }) => (
<li role="presentation">
<button
{...props}
className={css(
styles.selectMenuItem,
isSelected && styles.selectMenuItemMatch,
isDisabled && styles.modifiers.disabled,
className
)}
onClick={event => {
if (!isDisabled) {
onClick && onClick(event);
onSelect && onSelect(event, value, isPlaceholder);
onClose && onClose();
}
}}
role="option"
aria-selected={isSelected || null}
ref={this.ref}
onKeyDown={this.onKeyDown}
>
{value}
</button>
</li>
)}
</SelectContext.Consumer>
);
}
}

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

export default SelectOption;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import SelectOption from './SelectOption';

describe('select options', () => {
test('renders with value parameter successfully', () => {
const view = shallow(<SelectOption value="test" sendRef={jest.fn()} />);
expect(view).toMatchSnapshot();
});

describe('hover', () => {
test('renders with hover successfully', () => {
const view = shallow(<SelectOption isHovered value="test" sendRef={jest.fn()} />);
expect(view).toMatchSnapshot();
});
});

describe('disabled', () => {
test('renders disabled successfully', () => {
const view = shallow(<SelectOption isDisabled value="test" sendRef={jest.fn()} />);
expect(view).toMatchSnapshot();
});
});
});
Loading