Skip to content

Commit

Permalink
Implemented BasePicker itemLimit (#2553)
Browse files Browse the repository at this point in the history
* Picker itemLimit implemented

* added change request

* removed leftover variable assignment from previous iteration

* removed debug remnant..

* added jsDoc defualt value for itemLimit Prop

* simplified logic and changed function name to be more consistent

* added more tests for itemLimit

* fixes in focus handling when itemLimit set. (needs test)
  • Loading branch information
ohritz authored and dzearing committed Aug 24, 2017
1 parent 00dcae5 commit cd5b68b
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 6 deletions.
11 changes: 11 additions & 0 deletions common/changes/office-ui-fabric-react/master_2017-08-20-21-11.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "office-ui-fabric-react",
"comment": "BasePicker: added itemLimit property, which will allow preventing adding more items than set limit.",
"type": "minor"
}
],
"packageName": "office-ui-fabric-react",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export interface IBasePickerProps<T> extends React.Props<any> {
* @default false
*/
disabled?: boolean;

/**
* Restrict the amount of selectable items.
* @default undefined
*/
itemLimit?: number;
/**
* Function that specifies how arbitrary text entered into the well is handled.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,73 @@ describe('Pickers', () => {

});

it('can will not render input when items reach itemLimit', () => {
let root = document.createElement('div');
document.body.appendChild(root);
let picker: TypedBasePicker = ReactDOM.render(
<BasePickerWithType
onResolveSuggestions={ onResolveSuggestions }
onRenderItem={ (props: IPickerItemProps<{ key: string, name: string }>) => <div key={ props.item.name }>{ basicRenderer(props) }</div> }
onRenderSuggestionsItem={ basicSuggestionRenderer }
itemLimit={ 1 }
/>,
root
) as TypedBasePicker;
let input = document.querySelector('.ms-BasePicker-input') as HTMLInputElement;
input.focus();
input.value = 'bl';
ReactTestUtils.Simulate.change(input);

let suggestions = document.querySelector('.ms-Suggestions') as HTMLInputElement;
let suggestionOptions = document.querySelectorAll('.ms-Suggestions-itemButton');
ReactTestUtils.Simulate.click(suggestionOptions[0]);
expect(picker.items.length).to.be.equal(1, 'There was not only 1 item selected');
input = document.querySelector('.ms-BasePicker-input') as HTMLInputElement;
expect(input).to.be.null;

ReactDOM.unmountComponentAtNode(root);
});

it('will still render with itemLimit set to 0', () => {
let root = document.createElement('div');
document.body.appendChild(root);
let picker: TypedBasePicker = ReactDOM.render(
<BasePickerWithType
onResolveSuggestions={ onResolveSuggestions }
onRenderItem={ (props: IPickerItemProps<{ key: string, name: string }>) => <div key={ props.item.name }>{ basicRenderer(props) }</div> }
onRenderSuggestionsItem={ basicSuggestionRenderer }
itemLimit={ 0 }
/>,
root
) as TypedBasePicker;

let input = document.querySelector('.ms-BasePicker-input') as HTMLInputElement;
expect(input).to.be.null;

ReactDOM.unmountComponentAtNode(root);
});

it('can be set with selectedItems and a lower itemLimit', () => {
let root = document.createElement('div');
document.body.appendChild(root);
let picker: TypedBasePicker = ReactDOM.render(
<BasePickerWithType
selectedItems={ [{ key: '1', name: 'blue' }, { key: '2', name: 'black' }] }
onResolveSuggestions={ onResolveSuggestions }
onRenderItem={ (props: IPickerItemProps<{ key: string, name: string }>) => <div key={ props.item.name }>{ basicRenderer(props) }</div> }
onRenderSuggestionsItem={ basicSuggestionRenderer }
itemLimit={ 0 }
/>,
root
) as TypedBasePicker;

let input = document.querySelector('.ms-BasePicker-input') as HTMLInputElement;
expect(input).to.be.null;
expect(picker.items.length).to.equal(2);

ReactDOM.unmountComponentAtNode(root);
});

});

describe('TagPicker', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends BaseComponent<
<SelectionZone selection={ this.selection } selectionMode={ SelectionMode.multiple }>
<div className={ css('ms-BasePicker-text', styles.pickerText) } role={ 'list' }>
{ this.renderItems() }
<BaseAutoFill
{ this.canAddItems() && (<BaseAutoFill
{ ...inputProps as any }
className={ css('ms-BasePicker-input', styles.pickerInput) }
ref={ this._resolveRef('input') }
Expand All @@ -171,7 +171,7 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends BaseComponent<
autoComplete='off'
role='combobox'
disabled={ disabled }
/>
/>) }
</div>
</SelectionZone>
</FocusZone>
Expand All @@ -180,6 +180,12 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends BaseComponent<
);
}

protected canAddItems(): boolean {
const { items } = this.state;
const { itemLimit } = this.props;
return itemLimit === undefined || items.length < itemLimit;
}

protected renderSuggestions(): JSX.Element | null {
let TypedSuggestion = this.SuggestionOfProperType;
return this.state.suggestionsVisible ? (
Expand Down Expand Up @@ -230,10 +236,12 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends BaseComponent<

if (items.length && index! >= 0) {
let newEl: HTMLElement = this.root.querySelectorAll('[data-selection-index]')[Math.min(index!, items.length - 1)] as HTMLElement;

if (newEl) {
this.focusZone.focusElement(newEl);
}
} else if (!this.canAddItems()) {
(items[items.length - 1] as IPickerItemProps<T>).selected = true;
this.resetFocus(items.length - 1);
} else {
this.input.focus();
}
Expand Down Expand Up @@ -387,7 +395,6 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends BaseComponent<

@autobind
protected onKeyDown(ev: React.KeyboardEvent<HTMLElement>) {
let value = this.input.value;
switch (ev.which) {
case KeyCodes.escape:
if (this.state.suggestionsVisible) {
Expand All @@ -414,7 +421,7 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends BaseComponent<
break;

case KeyCodes.del:
if (ev.target === this.input.inputElement && this.state.suggestionsVisible && this.suggestionStore.currentIndex !== -1) {
if (this.input && ev.target === this.input.inputElement && this.state.suggestionsVisible && this.suggestionStore.currentIndex !== -1) {
if (this.props.onRemoveSuggestion) {
(this.props.onRemoveSuggestion as any)(this.suggestionStore.currentSuggestion!.item);
}
Expand Down Expand Up @@ -552,7 +559,7 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends BaseComponent<
// This is protected because we may expect the backspace key to work differently in a different kind of picker.
// This lets the subclass override it and provide it's own onBackspace. For an example see the BasePickerListBelow
protected onBackspace(ev: React.KeyboardEvent<HTMLElement>) {
if (this.state.items.length && !this.input.isValueSelected && this.input.cursorLocation === 0) {
if (this.state.items.length && !this.input || (!this.input.isValueSelected && this.input.cursorLocation === 0)) {
if (this.selection.getSelectedCount() > 0) {
this.removeItems(this.selection.getSelection());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class TagPickerBasicExample extends React.Component<{}, ITagPickerDemoPag
noResultsFoundText: 'No Color Tags Found'
}
}
itemLimit={ 2 }
disabled={ this.state.isPickerDisabled }
inputProps={ {
onBlur: (ev: React.FocusEvent<HTMLInputElement>) => console.log('onBlur called'),
Expand Down

0 comments on commit cd5b68b

Please sign in to comment.