FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(Select): add typeahead keyboard navigation by kmcfaul · Pull Request #2013 · patternfly/patternfly-react · GitHub

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

Filter by extension

Filter by extension .js  (5) .snap  (2) .ts  (1) All 3 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
      • Select.js
      • SelectOption.js
      • SelectOption.test.js
      • SelectToggle.js
      • SingleSelect.js
      • util.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CheckboxSelect from './CheckboxSelect';
import SelectToggle from './SelectToggle';
import SelectOption from './SelectOption';
import { SelectContext, SelectVariant } from './selectConstants';
import { getNextIndex } from '../../helpers/util';

// seed for the aria-labelledby ID
let currentId = 0;
Expand Down Expand Up @@ -77,9 +78,12 @@ class Select extends React.Component {
parentRef = React.createRef();
state = {
openedOnEnter: false,
typeaheadValue: null,
filteredChildren: this.props.children
typeaheadInputValue: null,
typeaheadActiveChild: null,
typeaheadFilteredChildren: this.props.children,
typeaheadCurrIndex: -1
};
refCollection = [];

onEnter = () => {
this.setState({ openedOnEnter: true });
Expand All @@ -88,8 +92,10 @@ class Select extends React.Component {
onClose = () => {
this.setState({
openedOnEnter: false,
typeaheadValue: null,
filteredChildren: this.props.children
typeaheadInputValue: null,
typeaheadActiveChild: null,
typeaheadFilteredChildren: this.props.children,
typeaheadCurrIndex: -1
});
};

Expand All @@ -100,17 +106,20 @@ class Select extends React.Component {
} catch (err) {
input = new RegExp(e.target.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
}
const filteredChildren =
const typeaheadFilteredChildren =
e.target.value !== ''
? React.Children.toArray(this.props.children).filter(child => child.props.value.search(input) === 0)
: this.props.children;
if (filteredChildren.length === 0) {
filteredChildren.push(<SelectOption isDisabled key={0} value="No results found" />);
if (typeaheadFilteredChildren.length === 0) {
typeaheadFilteredChildren.push(<SelectOption isDisabled key={0} value="No results found" />);
}
this.setState({
typeaheadValue: e.target.value,
filteredChildren
typeaheadInputValue: e.target.value,
typeaheadCurrIndex: -1,
typeaheadFilteredChildren,
typeaheadActiveChild: null
});
this.refCollection = [];
};

onClick = e => {
Expand All @@ -120,11 +129,54 @@ class Select extends React.Component {
clearSelection = e => {
e.stopPropagation();
this.setState({
typeaheadValue: '',
filteredChildren: this.props.children
typeaheadInputValue: '',
typeaheadActiveChild: null,
typeaheadFilteredChildren: this.props.children,
typeaheadCurrIndex: -1
});
};

extendTypeaheadChildren(typeaheadActiveChild) {
return this.state.typeaheadFilteredChildren.map(child =>
React.cloneElement(child, {
isFocused: typeaheadActiveChild && typeaheadActiveChild.innerText === child.props.value
})
);
}

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

handleTypeaheadKeys = position => {
const { isExpanded, onSelect } = this.props;
const { typeaheadActiveChild, typeaheadCurrIndex } = this.state;
if (isExpanded) {
if (position === 'enter' && (typeaheadActiveChild || this.refCollection[0])) {
this.setState({
typeaheadInputValue:
(typeaheadActiveChild && typeaheadActiveChild.innerText) || this.refCollection[0].innerText
});
onSelect &&
onSelect(null, (typeaheadActiveChild && typeaheadActiveChild.innerText) || this.refCollection[0].innerText);
} else {
let nextIndex;
if (typeaheadCurrIndex === -1 && position === 'down') {
nextIndex = 0;
} else if (typeaheadCurrIndex === -1 && position === 'up') {
nextIndex = this.refCollection.length - 1;
} else {
nextIndex = getNextIndex(typeaheadCurrIndex, position, this.refCollection);
}
this.setState({
typeaheadCurrIndex: nextIndex,
typeaheadActiveChild: this.refCollection[nextIndex],
typeaheadInputValue: this.refCollection[nextIndex].innerText
});
}
}
};

render() {
const {
children,
Expand All @@ -146,7 +198,7 @@ class Select extends React.Component {
width,
...props
} = this.props;
const { openedOnEnter, typeaheadValue, filteredChildren } = this.state;
const { openedOnEnter, typeaheadInputValue, typeaheadActiveChild } = this.state;
const selectToggleId = `pf-toggle-id-${currentId++}`;
let childPlaceholderText = null;
if (!selections && !placeholderText) {
Expand Down Expand Up @@ -185,6 +237,7 @@ class Select extends React.Component {
ariaLabelledBy={`${ariaLabelledBy || ''} ${selectToggleId}`}
variant={variant}
ariaLabelToggle={ariaLabelToggle}
handleTypeaheadKeys={this.handleTypeaheadKeys}
>
{variant === SelectVariant.single && (
<div className={css(styles.selectToggleWrapper)}>
Expand All @@ -210,12 +263,14 @@ class Select extends React.Component {
<div className={css(styles.selectToggleWrapper)}>
<input
className={css(formStyles.formControl, styles.selectToggleTypeahead)}
aria-activedescendant={typeaheadActiveChild && typeaheadActiveChild.id}
id="select-single-typeahead-typeahead"
aria-label={ariaLabelTypeAhead}
placeholder={placeholderText}
value={typeaheadValue !== null ? typeaheadValue : selections || ''}
value={typeaheadInputValue !== null ? typeaheadInputValue : selections || ''}
type="text"
onChange={this.onChange}
autoComplete="off"
/>
</div>
{selections && (
Expand All @@ -238,12 +293,14 @@ class Select extends React.Component {
{selections && selections.length > 0 && selectedChips}
<input
className={css(formStyles.formControl, styles.selectToggleTypeahead)}
aria-activedescendant={typeaheadActiveChild}
id="select-multi-typeahead-typeahead"
aria-label={ariaLabelTypeAhead}
placeholder={placeholderText}
value={typeaheadValue !== null ? typeaheadValue : ''}
value={typeaheadInputValue !== null ? typeaheadInputValue : ''}
type="text"
onChange={this.onChange}
autoComplete="off"
/>
</div>
{selections && selections.length > 0 && (
Expand Down Expand Up @@ -290,8 +347,9 @@ class Select extends React.Component {
openedOnEnter={openedOnEnter}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
sendRef={this.sendRef}
>
{filteredChildren}
{this.extendTypeaheadChildren(typeaheadActiveChild)}
</SingleSelect>
)}
</SelectContext.Provider>
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ const propTypes = {
isPlaceholder: PropTypes.bool,
/** Internal flag indicating if the option is selected */
isSelected: PropTypes.bool,
/** Internal flag indicating if the option is focused */
isFocused: PropTypes.bool,
/** Optional on click callback */
onClick: PropTypes.func,
/** Internal callback for ref tracking */
sendRef: PropTypes.func,
/** Internal callback for ref tracking */
sendListRef: PropTypes.func,
/** Internal callback for keyboard navigation */
keyHandler: PropTypes.func,
/** Additional props are spread to the container <button> */
Expand All @@ -35,8 +39,10 @@ const defaultProps = {
isDisabled: false,
isPlaceholder: false,
isSelected: false,
isFocused: false,
onClick: Function.prototype,
sendRef: Function.prototype,
sendListRef: Function.prototype,
keyHandler: Function.prototype
};

Expand All @@ -45,6 +51,11 @@ class SelectOption extends React.Component {

componentDidMount() {
this.props.sendRef(this.ref.current, this.props.index);
this.props.sendListRef(this.ref.current, this.props.index);
}

componentDidUpdate() {
this.props.sendRef(this.ref.current, this.props.index);
}

onKeyDown = event => {
Expand All @@ -67,7 +78,9 @@ class SelectOption extends React.Component {
isDisabled,
isPlaceholder,
isSelected,
isFocused,
sendRef,
sendListRef,
keyHandler,
index,
...props
Expand All @@ -82,6 +95,7 @@ class SelectOption extends React.Component {
styles.selectMenuItem,
isSelected && styles.modifiers.selected,
isDisabled && styles.modifiers.disabled,
isFocused && styles.modifiers.focus,
className
)}
onClick={event => {
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@ import SelectOption from './SelectOption';

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

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

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

describe('is selected', () => {
test('renders selected successfully', () => {
const view = shallow(<SelectOption isSelected value="test" sendRef={jest.fn()} />);
const view = shallow(<SelectOption isSelected value="test" sendRef={jest.fn()} sendListRef={jest.fn()} />);
expect(view).toMatchSnapshot();
});
});
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const propTypes = {
ariaLabelToggle: PropTypes.string,
/** Flag for variant, determines toggle rules and interaction */
variant: PropTypes.oneOf(['single', 'checkbox', 'typeahead', 'typeaheadmulti']),
/** Internal handler for typeahead keyboard navigation */
handleTypeaheadKeys: PropTypes.Function,
/** Additional props are spread to the container <button> */
'': PropTypes.any // eslint-disable-line react/require-default-props
};
Expand All @@ -58,7 +60,8 @@ const defaultProps = {
type: 'button',
onToggle: Function.prototype,
onEnter: Function.prototype,
onClose: Function.prototype
onClose: Function.prototype,
handleTypeaheadKeys: Function.prototype
};

class SelectToggle extends Component {
Expand Down Expand Up @@ -99,7 +102,20 @@ class SelectToggle extends Component {
};

onKeyDown = event => {
const { isExpanded, onToggle, variant, onClose, onEnter } = this.props;
const { isExpanded, onToggle, variant, onClose, onEnter, handleTypeaheadKeys } = this.props;
if (
(event.key === KeyTypes.ArrowDown || event.key === KeyTypes.ArrowUp) &&
(variant === SelectVariant.typeahead || variant === SelectVariant.typeaheadMulti)
)
handleTypeaheadKeys((event.key === KeyTypes.ArrowDown && 'down') || (event.key === KeyTypes.ArrowUp && 'up'));
if (
event.key === KeyTypes.Enter &&
(variant === SelectVariant.typeahead || variant === SelectVariant.typeaheadMulti)
) {
if (isExpanded) handleTypeaheadKeys('enter');
else onToggle && onToggle(!isExpanded);
}

if (
(event.key === KeyTypes.Tab && variant === SelectVariant.checkbox) ||
(event.key === KeyTypes.Tab && !isExpanded) ||
Expand Down Expand Up @@ -132,6 +148,7 @@ class SelectToggle extends Component {
onToggle,
onEnter,
onClose,
handleTypeaheadKeys,
parentRef,
id,
type,
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const propTypes = {
openedOnEnter: PropTypes.bool,
/** Currently selected option */
selected: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
/** Internal handler for ref handling */
sendRef: PropTypes.function,
/** Additional props are spread to the container <select> */
'': PropTypes.any // eslint-disable-line react/require-default-props
};
Expand All @@ -23,44 +25,48 @@ const defaultProps = {
className: '',
isExpanded: false,
openedOnEnter: false,
selected: ''
selected: '',
sendRef: Function.prototype
};

class SingleSelect extends React.Component {
refCollection = [];

componentDidMount() {
if (this.props.openedOnEnter) {
const selectedRef = this.refCollection.filter(ref => ref.classList.contains('pf-c-select__menu-item--match'));
const selectedRef = this.refCollection.filter(ref => ref.classList.contains('pf-m-selected'));
selectedRef && selectedRef[0] ? selectedRef[0].focus() : this.refCollection[0].focus();
}
}

extendChildren() {
const { selected } = this.props;
return React.Children.map(this.props.children, (child, index) =>
React.cloneElement(child, {
isSelected:
selected && selected.constructor === Array
? selected && selected.includes(child.props.value)
: selected === child.props.value,
sendRef: this.sendRef,
const { selected, sendRef } = this.props;
return React.Children.map(this.props.children, (child, index) => {
const isSelected =
selected && selected.constructor === Array
? selected && selected.includes(child.props.value)
: selected === child.props.value;
return React.cloneElement(child, {
id: `${child.props.value}-${index}`,
isSelected,
sendRef,
sendListRef: this.sendListRef,
keyHandler: this.childKeyHandler,
index
})
);
});
});
}

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

childKeyHandler = (index, position) => {
keyHandler(index, position, this.refCollection, this.props.children);
};

sendListRef = (ref, index) => {
this.refCollection[index] = ref;
};

render() {
const { children, className, isExpanded, openedOnEnter, selected, ...props } = this.props;
const { children, className, isExpanded, openedOnEnter, selected, sendRef, ...props } = this.props;
this.renderedChildren = this.extendChildren();
return (
<ul {...props} className={css(styles.selectMenu, className)} role="listbox">
Expand Down
Loading

Back | FazBrowse Home | New Git URL