-
Notifications
You must be signed in to change notification settings - Fork 377
feat(Select): add checkbox select variant #1487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4a0667e
1d45bf3
30a00fd
a576d77
484ab96
512dfa6
360970b
91e84fd
0874943
391f5c6
f10b221
b24fa18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import React from 'react'; | ||
| import styles from '@patternfly/patternfly/components/Select/select.css'; | ||
| import { default as formStyles } from '@patternfly/patternfly/components/Form/form.css'; | ||
| import { css } from '@patternfly/react-styles'; | ||
| import PropTypes from 'prop-types'; | ||
| import { keyHandler } from '../../helpers/util'; | ||
| import FocusTrap from 'focus-trap-react'; | ||
|
|
||
| const propTypes = { | ||
| /** Content rendered inside the CheckboxSelect */ | ||
| children: PropTypes.node.isRequired, | ||
| /** Additional classes added to the CheckboxSelect control */ | ||
| className: PropTypes.string, | ||
| /** Flag indicating the Select is expanded */ | ||
| isExpanded: PropTypes.bool, | ||
| /** Flag indicating whether checkboxes are grouped */ | ||
| isGrouped: PropTypes.bool, | ||
| /** Currently checked options */ | ||
| checked: PropTypes.arrayOf(PropTypes.string), | ||
| /** Additional props are spread to the container <select> */ | ||
| '': PropTypes.any | ||
| }; | ||
|
|
||
| const defaultProps = { | ||
| className: '', | ||
| isExpanded: false, | ||
| isGrouped: false, | ||
| checked: [] | ||
| }; | ||
|
|
||
| class CheckboxSelect extends React.Component { | ||
| refCollection = []; | ||
|
|
||
| extendChildren(props) { | ||
| const { children, isGrouped, checked } = this.props; | ||
| const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy } = props; | ||
| if (isGrouped) { | ||
| let index = 0; | ||
| return React.Children.map(children, group => | ||
| React.cloneElement(group, { | ||
| titleId: group.props.label.replace(/\W/g, '-'), | ||
| children: ( | ||
| <fieldset aria-labelledby={group.props.label.replace(/\W/g, '-')} className={css(formStyles.formFieldset)}> | ||
| {group.props.children.map(option => | ||
| React.cloneElement(option, { | ||
| isChecked: checked && checked.includes(option.props.value), | ||
| sendRef: this.sendRef, | ||
| keyHandler: this.childKeyHandler, | ||
| index: index++ | ||
| }) | ||
| )} | ||
| </fieldset> | ||
| ) | ||
| }) | ||
| ); | ||
| } | ||
| return ( | ||
| <fieldset | ||
| {...props} | ||
| aria-label={ariaLabel} | ||
| aria-labelledby={(!ariaLabel && ariaLabelledBy) || null} | ||
| className={css(formStyles.formFieldset)} | ||
| > | ||
| {React.Children.map(children, (child, index) => | ||
| React.cloneElement(child, { | ||
| isChecked: checked && checked.includes(child.props.value), | ||
| sendRef: this.sendRef, | ||
| keyHandler: this.childKeyHandler, | ||
| index | ||
| }) | ||
| )} | ||
| </fieldset> | ||
| ); | ||
| } | ||
|
|
||
| sendRef = (ref, index) => { | ||
| this.refCollection[index] = ref; | ||
| }; | ||
|
|
||
| childKeyHandler = (index, position) => { | ||
| keyHandler(index, position, this.refCollection, this.props.isGrouped ? this.refCollection : this.props.children); | ||
| }; | ||
|
|
||
| render() { | ||
| const { children, className, isExpanded, checked, isGrouped, ...props } = this.props; | ||
| this.renderedChildren = this.extendChildren(props); | ||
| return ( | ||
| <FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}> | ||
| <div className={css(styles.selectMenu, className)}> | ||
| <form noValidate className={css(formStyles.form)}> | ||
| <div className={css(formStyles.formGroup)}>{this.renderedChildren}</div> | ||
| </form> | ||
| </div> | ||
| </FocusTrap> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| CheckboxSelect.propTypes = propTypes; | ||
| CheckboxSelect.defaultProps = defaultProps; | ||
|
|
||
| export default CheckboxSelect; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { HTMLProps, FormEvent } from 'react'; | ||
| import { Omit } from '../../typeUtils'; | ||
|
|
||
| export interface CheckboxSelectGroupProps extends Omit<HTMLProps<HTMLOptionElement>> { | ||
| label?: string; | ||
| } | ||
|
|
||
| declare const CheckboxSelectGroup: React.FunctionComponent<CheckboxSelectGroupProps>; | ||
|
|
||
| export default CheckboxSelectGroup; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import React from 'react'; | ||
| import styles from '@patternfly/patternfly/components/Select/select.css'; | ||
| import { css } from '@patternfly/react-styles'; | ||
| import PropTypes from 'prop-types'; | ||
|
|
||
| const propTypes = { | ||
| /** Checkboxes within group */ | ||
| children: PropTypes.node, | ||
| /** Additional classes added to the CheckboxSelectGroup control */ | ||
| className: PropTypes.string, | ||
| /** Group label */ | ||
| label: PropTypes.string, | ||
| /** Additional props are spread to the container <select> */ | ||
| '': PropTypes.any | ||
| }; | ||
|
|
||
| const defaultProps = { | ||
| children: null, | ||
| className: '', | ||
| label: '' | ||
| }; | ||
|
|
||
| const CheckboxSelectGroup = ({ children, className, label, titleId, ...props }) => ( | ||
| <React.Fragment> | ||
| <div {...props} className={css(styles.selectMenuGroup, className)}> | ||
| <div className={css(styles.selectMenuGroupTitle)} id={titleId || ''} aria-hidden> | ||
| {label} | ||
| </div> | ||
| {children} | ||
| </div> | ||
| </React.Fragment> | ||
| ); | ||
|
|
||
| CheckboxSelectGroup.propTypes = propTypes; | ||
| CheckboxSelectGroup.defaultProps = defaultProps; | ||
|
|
||
| export default CheckboxSelectGroup; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import React from 'react'; | ||
| import { shallow } from 'enzyme'; | ||
| import CheckboxSelectGroup from './CheckboxSelectGroup'; | ||
|
|
||
| describe('checkbox select options', () => { | ||
| test('renders with children successfully', () => { | ||
| const view = shallow( | ||
| <CheckboxSelectGroup label="test"> | ||
| <div>child</div> | ||
| </CheckboxSelectGroup> | ||
| ); | ||
| expect(view).toMatchSnapshot(); | ||
| }); | ||
| }); |
| 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 CheckboxSelectOptionProps extends Omit<HTMLProps<HTMLOptionElement>, 'disabled'> { | ||
| value?: string; | ||
| isDisabled?: boolean; | ||
| isChecked?: boolean; | ||
| onClick?: Function; | ||
| } | ||
|
|
||
| declare const CheckboxSelectOption: React.FunctionComponent<CheckboxSelectOptionProps>; | ||
|
|
||
| export default CheckboxSelectOption; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import React from 'react'; | ||
| import styles from '@patternfly/patternfly/components/Select/select.css'; | ||
| import { default as checkStyles } from '@patternfly/patternfly/components/Check/check.css'; | ||
| import { css } from '@patternfly/react-styles'; | ||
| import PropTypes from 'prop-types'; | ||
| import { SelectContext, KeyTypes } from './selectConstants'; | ||
|
|
||
| const propTypes = { | ||
| /** the value for the option */ | ||
| children: PropTypes.string, | ||
| /** additional classes added to the Select Option */ | ||
| className: PropTypes.string, | ||
| /** the value for the option */ | ||
| value: PropTypes.string, | ||
| /** flag indicating if the option is disabled */ | ||
| isDisabled: PropTypes.bool, | ||
| /** Optional on click callback */ | ||
| onClick: PropTypes.func, | ||
| /** Additional props are spread to the container <button> */ | ||
| '': PropTypes.any | ||
| }; | ||
|
|
||
| const defaultProps = { | ||
| children: null, | ||
| className: '', | ||
| value: null, | ||
| isDisabled: false, | ||
| onClick: Function.prototype | ||
| }; | ||
|
|
||
| class CheckboxSelectOption extends React.Component { | ||
| ref = React.createRef(); | ||
|
|
||
| componentDidMount() { | ||
| this.props.sendRef(this.ref.current, this.props.index); | ||
| } | ||
|
|
||
| onKeyDown = event => { | ||
| if (event.key === KeyTypes.Tab) return; | ||
| event.preventDefault(); | ||
| if (event.key === KeyTypes.ArrowUp) { | ||
| this.props.keyHandler(this.props.index, 'up'); | ||
| } else if (event.key === KeyTypes.ArrowDown) { | ||
| this.props.keyHandler(this.props.index, 'down'); | ||
| } else if (event.key === KeyTypes.Enter || event.key === KeyTypes.Space) { | ||
| this.ref.current.click && this.ref.current.click(); | ||
| this.ref.current.focus(); | ||
| } | ||
| }; | ||
|
|
||
| render() { | ||
| const { | ||
| children, | ||
| className, | ||
| value, | ||
| onClick, | ||
| isDisabled, | ||
| isChecked, | ||
| sendRef, | ||
| keyHandler, | ||
| index, | ||
| ...props | ||
| } = this.props; | ||
| return ( | ||
| <SelectContext.Consumer> | ||
| {({ onSelect }) => ( | ||
| <label | ||
| {...props} | ||
| className={css( | ||
| checkStyles.check, | ||
| styles.selectMenuItem, | ||
| isDisabled && styles.modifiers.disabled, | ||
| className | ||
| )} | ||
| onKeyDown={this.onKeyDown} | ||
| > | ||
| <input | ||
| id={value} | ||
| className={css(checkStyles.checkInput)} | ||
| type="checkbox" | ||
| onChange={event => { | ||
| if (!isDisabled) { | ||
| onClick && onClick(event); | ||
| onSelect && onSelect(event, value); | ||
| } | ||
| }} | ||
| ref={this.ref} | ||
| checked={isChecked || false} | ||
| disabled={isDisabled} | ||
| /> | ||
| <span className={css(checkStyles.checkLabel, isDisabled && styles.modifiers.disabled)}>{value}</span> | ||
| </label> | ||
| )} | ||
| </SelectContext.Consumer> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| CheckboxSelectOption.propTypes = propTypes; | ||
| CheckboxSelectOption.defaultProps = defaultProps; | ||
|
|
||
| export default CheckboxSelectOption; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import React from 'react'; | ||
| import { mount } from 'enzyme'; | ||
| import CheckboxSelectOption from './CheckboxSelectOption'; | ||
|
|
||
| describe('checkbox select options', () => { | ||
| test('renders with value parameter successfully', () => { | ||
| const view = mount(<CheckboxSelectOption value="test" sendRef={jest.fn()} />); | ||
| expect(view).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| describe('hover', () => { | ||
| test('renders with checked successfully', () => { | ||
| const view = mount(<CheckboxSelectOption isChecked value="test" sendRef={jest.fn()} />); | ||
| expect(view).toMatchSnapshot(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('disabled', () => { | ||
| test('renders disabled successfully', () => { | ||
| const view = mount(<CheckboxSelectOption isDisabled value="test" sendRef={jest.fn()} />); | ||
| expect(view).toMatchSnapshot(); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,20 @@ | ||
| import { HTMLProps, FormEvent } from 'react'; | ||
|
|
||
| export const SelectVariant : { | ||
| single: 'single' | ||
| export const SelectVariant: { | ||
| single: 'single'; | ||
| checkbox: 'checkbox'; | ||
| }; | ||
|
|
||
| export interface SelectProps extends HTMLProps<HTMLOptionElement> { | ||
| isExpanded?: boolean; | ||
| isGrouped?: boolean; | ||
| onToggle(value: boolean): void; | ||
| placeholderText?: string; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to maintain the API and not cause breaking changes because we are not ready for a major release.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, will revert that until we can change the API. |
||
| selections?: string; | ||
| onSelect(event: React.SyntheticEvent<HTMLDivElement>, selection: string): void; | ||
| placeholderText?: string | ReactNode; | ||
| selections?: string | Array<string>; | ||
| variant?: string; | ||
| width?: string | number; | ||
| ariaLabelledBy?: string; | ||
| } | ||
|
|
||
| declare const Select: React.FunctionComponent<SelectProps>; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,20 @@ | ||
| import { Select, SelectOption } from '@patternfly/react-core'; | ||
| import { Select, SelectOption, CheckboxSelectOption, CheckboxSelectGroup } from '@patternfly/react-core'; | ||
| import SingleSelectInput from './examples/SingleSelectInput'; | ||
| import CheckboxSelectInput from './examples/CheckboxSelectInput'; | ||
| import GroupedCheckboxSelectInput from './examples/GroupedCheckboxSelectInput'; | ||
|
|
||
| export default { | ||
| title: 'Select', | ||
| components: { | ||
| Select, | ||
| SelectOption | ||
| SelectOption, | ||
| CheckboxSelectOption, | ||
| CheckboxSelectGroup | ||
| }, | ||
| variablesRoot: 'pf-c-select', | ||
| examples: [{ component: SingleSelectInput, title: 'Single Select Input' }] | ||
| examples: [ | ||
| { component: SingleSelectInput, title: 'Single Select Input' }, | ||
| { component: CheckboxSelectInput, title: 'Checkbox Select Input' }, | ||
| { component: GroupedCheckboxSelectInput, title: 'Grouped Checkbox Select Input' } | ||
| ] | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should also be a
disabledattribute on the<input>.