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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FunctionComponent, HTMLProps, ReactNode } from 'react';
import { OneOf } from '../../typeUtils';

export interface ContextSelectorProps extends HTMLProps<HTMLDivElement> {
children?: ReactNode;
isOpen?: boolean;
onToggle?(value: boolean): void;
onSelect?(event: React.SyntheticEvent<HTMLButtonElement>, value: ReactNode): void;
screenReaderLabel?: string;
toggleText?: string;
searchButtonAriaLabel?: string;
searchInputValue?: string;
onSearchInputChange?(value: string): void;
searchInputPlaceholder?: string;
onSearchButtonClick?(event: React.SyntheticEvent<HTMLButtonElement>): void;
}

declare const ContextSelector: FunctionComponent<ContextSelectorProps>;

export default ContextSelector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ContextSelector, ContextSelectorItem } from '@patternfly/react-core';
import Simple from './examples/SimpleContextSelector';

export default {
title: 'ContextSelector',
components: {
ContextSelector,
ContextSelectorItem
},
examples: [{ component: Simple, title: 'Simple ContextSelector' }]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/ContextSelector/context-selector.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';
import FocusTrap from 'focus-trap-react';
import ContextSelectorToggle from './ContextSelectorToggle';
import ContextSelectorMenuList from './ContextSelectorMenuList';
import { ContextSelectorContext } from './contextSelectorConstants';
import { Button, ButtonVariant } from '../Button';
import { TextInput } from '../TextInput';
import { SearchIcon } from '@patternfly/react-icons';
import { InputGroup } from '../InputGroup';
import { KEY_CODES } from '../../helpers/constants';

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

const propTypes = {
/** content rendered inside the Context Selector */
children: PropTypes.node,
/** Classes applied to root element of Context Selector */
className: PropTypes.string,
/** Flag to indicate if Context Selector is opened */
isOpen: PropTypes.bool,
/** Function callback called when user clicks toggle button */
onToggle: PropTypes.func,
/** Function callback called when user selects item */
onSelect: PropTypes.func,
/** Labels the Context Selector for Screen Readers */
screenReaderLabel: PropTypes.string,
/** Text that appears in the Context Selector Toggle */
toggleText: PropTypes.string,
/** aria-label for the Context Selector Search Button */
searchButtonAriaLabel: PropTypes.string,
/** Value in the Search field */
searchInputValue: PropTypes.string,
/** Function callback called when user changes the Search Input */
onSearchInputChange: PropTypes.func,
/** Search Input placeholder */
searchInputPlaceholder: PropTypes.string,
/** Function callback for when Search Button is clicked */
onSearchButtonClick: PropTypes.func
};

const defaultProps = {
children: null,
className: '',
isOpen: false,
onToggle: () => {},
onSelect: () => {},
screenReaderLabel: '',
toggleText: '',
searchButtonAriaLabel: 'Search menu items',
searchInputValue: '',
onSearchInputChange: () => {},
searchInputPlaceholder: 'Search',
onSearchButtonClick: () => {}
};

class ContextSelector extends React.Component {
parentRef = React.createRef();

onEnterPressed = event => {
if (event.charCode === KEY_CODES.ENTER) {
this.props.onSearchButtonClick();
}
};

render() {
const toggleId = `pf-context-selector-toggle-id-${newId}`;
const screenReaderLabelId = `pf-context-selector-label-id-${newId}`;
const searchButtonId = `pf-context-selector-search-button-id-${newId}`;
const {
children,
className,
isOpen,
onToggle,
onSelect,
screenReaderLabel,
toggleText,
searchButtonAriaLabel,
searchInputValue,
onSearchInputChange,
searchInputPlaceholder,
onSearchButtonClick,
...props
} = this.props;
return (
<div
className={css(styles.contextSelector, isOpen && styles.modifiers.expanded, className)}
ref={this.parentRef}
{...props}
>
{screenReaderLabel && (
<span id={screenReaderLabelId} hidden>
{screenReaderLabel}
</span>
)}
<ContextSelectorToggle
onToggle={onToggle}
isOpen={isOpen}
toggleText={toggleText}
id={toggleId}
parentRef={this.parentRef.current}
aria-labelledby={`${screenReaderLabelId} ${toggleId}`}
/>
{isOpen && (
<div className={css(styles.contextSelectorMenu)}>
{isOpen && (
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
<div className={css(styles.contextSelectorMenuInput)}>
<InputGroup>
<TextInput
value={searchInputValue}
type="search"
placeholder={searchInputPlaceholder}
onChange={onSearchInputChange}
onKeyPress={this.onEnterPressed}
aria-labelledby={searchButtonId}
/>
<Button
variant={ButtonVariant.tertiary}
aria-label={searchButtonAriaLabel}
id={searchButtonId}
onClick={onSearchButtonClick}
>
<SearchIcon aria-hidden="true" />
</Button>
</InputGroup>
</div>
<ContextSelectorContext.Provider value={{ onSelect }}>
<ContextSelectorMenuList isOpen={isOpen}>{children}</ContextSelectorMenuList>
</ContextSelectorContext.Provider>
</FocusTrap>
)}
</div>
)}
</div>
);
}
}

ContextSelector.propTypes = propTypes;
ContextSelector.defaultProps = defaultProps;

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

const items = [
<ContextSelectorItem key="0">My Project</ContextSelectorItem>,
<ContextSelectorItem key="1">OpenShift Cluster</ContextSelectorItem>,
<ContextSelectorItem key="2">Production Ansible</ContextSelectorItem>,
<ContextSelectorItem key="3">AWS</ContextSelectorItem>,
<ContextSelectorItem key="4">Azure</ContextSelectorItem>
];

test('Renders ContextSelector', () => {
const view = shallow(<ContextSelector> {items} </ContextSelector>);
expect(view).toMatchSnapshot();
});

test('Renders ContextSelector open', () => {
const view = shallow(<ContextSelector isOpen> {items} </ContextSelector>);
expect(view).toMatchSnapshot();
});

test('Verify onToggle is called ', () => {
const mockfn = jest.fn();
const view = mount(<ContextSelector onToggle={mockfn}> {items} </ContextSelector>);
view
.find('button')
.at(0)
.simulate('click');
expect(mockfn.mock.calls).toHaveLength(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FunctionComponent, HTMLProps, ReactType } from 'react';

export interface ContextSelectorItemProps extends HTMLProps<HTMLButtonElement> {
isDisabled?: boolean;
isSelected?: boolean;
isHovered?: boolean;
onClick?: Function;
}

declare const ContextSelectorItem: FunctionComponent<ContextSelectorItemProps>;

export default ContextSelectorItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/ContextSelector/context-selector.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';
import { ContextSelectorContext } from './contextSelectorConstants';

const propTypes = {
/** Anything which can be rendered as Context Selector item */
children: PropTypes.node,
/** Classes applied to root element of the Context Selector item */
className: PropTypes.string,
/** Render Context Selector item as disabled */
isDisabled: PropTypes.bool,
/** Forces display of the hover state of the element */
isHovered: PropTypes.bool,
/** Callback for click event */
onClick: PropTypes.func,
/** Additional props are spread to the button element */
'': PropTypes.any
};

const defaultProps = {
children: null,
className: '',
isHovered: false,
isDisabled: false,
onClick: () => {}
};

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

componentDidMount() {
/* eslint-disable-next-line */
this.props.sendRef(this.props.index, this.ref.current);
}

render() {
const { className, children, isHovered, onClick, isDisabled, index, sendRef, ...props } = this.props;
return (
<ContextSelectorContext.Consumer>
{({ onSelect }) => (
<li role="none">
<button
className={css(
styles.contextSelectorMenuListItem,
isDisabled && styles.modifiers.disabled,
isHovered && styles.modifiers.hover,
className
)}
ref={this.ref}
onClick={event => {
if (!isDisabled) {
onClick && onClick(event);
onSelect && onSelect(event, children);
}
}}
{...props}
>
{children}
</button>
</li>
)}
</ContextSelectorContext.Consumer>
);
}
}

ContextSelectorItem.propTypes = propTypes;
ContextSelectorItem.defaultProps = defaultProps;

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

test('Renders ContextSelectorItem', () => {
const view = shallow(
<ContextSelectorItem sendRef={jest.fn()} index="0">
My Project
</ContextSelectorItem>
);
expect(view).toMatchSnapshot();
});

test('Renders ContextSelectorItem disabled and hovered', () => {
const view = shallow(
<ContextSelectorItem isDisabled isHovered sendRef={jest.fn()} index="0">
My Project
</ContextSelectorItem>
);
expect(view).toMatchSnapshot();
});

test('Verify onClick is called ', () => {
const mockfn = jest.fn();
const view = mount(
<ContextSelectorItem isHovered onClick={mockfn} sendRef={jest.fn()} index="0">
My Project
</ContextSelectorItem>
);
view
.find('button')
.at(0)
.simulate('click');
expect(mockfn.mock.calls).toHaveLength(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import styles from '@patternfly/patternfly/components/ContextSelector/context-selector.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';

const propTypes = {
/** Content rendered inside the Context Selector Menu */
children: PropTypes.node,
/** Classess applied to root element of Context Selector menu */
className: PropTypes.string,
/** Flag to indicate if Context Selector menu is opened */
isOpen: PropTypes.bool,
/** Additional props are spread to the container component */
'': PropTypes.any
};

const defaultProps = {
children: null,
className: '',
isOpen: true
};

class ContextSelectorMenuList extends React.Component {
refsCollection = [];

sendRef = (index, ref) => {
this.refsCollection[index] = ref;
};

extendChildren() {
return React.Children.map(this.props.children, (child, index) =>
React.cloneElement(child, {
sendRef: this.sendRef,
index
})
);
}

render() {
const { className, isOpen, children, ...props } = this.props;

return (
<ul className={css(styles.contextSelectorMenuList, className)} hidden={!isOpen} role="menu" {...props}>
{this.extendChildren()}
</ul>
);
}
}

ContextSelectorMenuList.propTypes = propTypes;
ContextSelectorMenuList.defaultProps = defaultProps;

export default ContextSelectorMenuList;
Loading