office-ui-fabric-react
Version:
Reusable React components for building experiences for Office 365.
496 lines (495 loc) • 28.5 kB
JavaScript
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
define(["require", "exports", 'react', './PeoplePicker.Props', '../../Persona', '../../Spinner', '../../utilities/string', '../../FocusZone', '../../utilities/css', '../../utilities/KeyCodes', '../../utilities/eventGroup/EventGroup', '../../utilities/DomUtils', './PeoplePicker.scss'], function (require, exports, React, PeoplePicker_Props_1, Persona_1, Spinner_1, string_1, FocusZone_1, css_1, KeyCodes_1, EventGroup_1, DomUtils_1) {
"use strict";
var INVALID_INDEX = -1;
var PeoplePicker = (function (_super) {
__extends(PeoplePicker, _super);
function PeoplePicker(props) {
_super.call(this, props);
this._suggestionsCount = 0;
this._focusedPersonaIndex = INVALID_INDEX;
this._events = new EventGroup_1.EventGroup(this);
this._activatePeoplePicker = this._activatePeoplePicker.bind(this);
this._dismissPeoplePicker = this._dismissPeoplePicker.bind(this);
this._addPersonaToSelectedList = this._addPersonaToSelectedList.bind(this);
this._searchForMoreResults = this._searchForMoreResults.bind(this);
this._onSearchFieldTextChanged = this._onSearchFieldTextChanged.bind(this);
this._onSearchFieldKeyDown = this._onSearchFieldKeyDown.bind(this);
this._onFocusCapture = this._onFocusCapture.bind(this);
this._removeSelectedPersona = this._removeSelectedPersona.bind(this);
this._onSelectedPersonaFocus = this._onSelectedPersonaFocus.bind(this);
this._onSearchBoxKeyDown = this._onSearchBoxKeyDown.bind(this);
var selectedPersonas = props.initialItems ? props.initialItems : [];
this.state = {
isActive: false,
isSearching: false,
searchTextValue: '',
highlightedSearchResultIndex: INVALID_INDEX,
selectedPersonas: selectedPersonas
};
}
PeoplePicker.prototype.componentDidMount = function () {
this._events.on(window, 'focus', this._onFocusCapture, true);
this._events.on(window, 'click', this._onClickCapture, true);
this._events.on(window, 'touchstart', this._onClickCapture, true);
};
PeoplePicker.prototype.componentWillUnmount = function () {
this._events.dispose();
};
PeoplePicker.prototype.componentDidUpdate = function () {
this._setScollPosition();
var suggestions = this.props.suggestions;
if (this.state.isActive && this._suggestionsCount !== suggestions.length) {
this._setSelectedSearchResultIndex(0);
}
this._suggestionsCount = suggestions.length;
// if the selected persona is out of range after an update, it means the user deleted it
// and we need to set focus on the last one (which isn't handled by the FocusZone).
// Unless there are no more personas, then set focus on the input field.
var selectedPersonaCount = this.state.selectedPersonas.length;
if (this._focusedPersonaIndex !== INVALID_INDEX && this._focusedPersonaIndex >= selectedPersonaCount) {
if (selectedPersonaCount > 0) {
this._focusedPersonaIndex = selectedPersonaCount - 1;
this.refs['persona' + this._focusedPersonaIndex].focus();
}
else {
this._focusedPersonaIndex = INVALID_INDEX;
this.refs.searchField.focus();
}
}
};
PeoplePicker.prototype.render = function () {
var _this = this;
var type = this.props.type;
var searchField = this._renderSearchField();
var searchResults = type === PeoplePicker_Props_1.PeoplePickerType.memberList ? this._renderSearchResultsForMemberList() : this._renderSearchResults();
// Render the selected personas.
// There are two layouts to choose from, based on the Persona type.
var selectedPersonas = type === PeoplePicker_Props_1.PeoplePickerType.memberList ? null : this._renderSelectedPersonas();
var memberList = type === PeoplePicker_Props_1.PeoplePickerType.memberList ? this._renderSelectedPersonasAsMemberList() : null;
var className = css_1.css('ms-PeoplePicker', {
'is-active': this.state.isActive,
'ms-PeoplePicker--compact': type === PeoplePicker_Props_1.PeoplePickerType.compact,
'ms-PeoplePicker--membersList': type === PeoplePicker_Props_1.PeoplePickerType.memberList,
});
return (React.createElement("div", {className: className, ref: 'root', key: 'root'}, React.createElement("div", {className: 'ms-PeoplePicker-searchBox', ref: function (searchBox) { return _this.refs._searchBox = searchBox; }, onKeyDown: this._onSearchBoxKeyDown}, React.createElement(FocusZone_1.FocusZone, {onActiveElementChanged: this._onSelectedPersonaFocus, ref: function (focusZone) { return _this.refs.focusZone = focusZone; }}, selectedPersonas, searchField)), searchResults, memberList));
};
PeoplePicker.prototype._onSearchBoxKeyDown = function (ev) {
switch (ev.which) {
// remove focused persona
case KeyCodes_1.KeyCodes.backspace:
case KeyCodes_1.KeyCodes.del:
if (this._focusedPersonaIndex !== INVALID_INDEX) {
this._removeSelectedPersona(this._focusedPersonaIndex);
ev.stopPropagation();
ev.preventDefault();
}
}
};
/**
*
*/
PeoplePicker.prototype._onSelectedPersonaFocus = function (element, ev) {
// store a reference to this element
// in keydown handler, if there's a focused persona, we want to delete it on certain key press events
var index = element.getAttribute('data-selection-index');
if (index) {
this._focusedPersonaIndex = Number(index);
}
};
/**
* Handles closing the people picker whenever focus is lost
*/
PeoplePicker.prototype._onFocusCapture = function (ev) {
// onBlur, relatedTarget refers to the element that got focus
var target = ev.target;
if (!DomUtils_1.elementContains(this.refs.root, target)) {
this._dismissPeoplePicker();
}
};
/**
* Handles closing the people picker whenever focus is lost through mouse.
*/
PeoplePicker.prototype._onClickCapture = function (ev) {
if (!this.refs.searchField.contains(ev.target)
&& !this.refs.pickerResults.contains(ev.target)) {
this._dismissPeoplePicker();
}
};
/**
* Click handler for when the user clicks on the Search For Results button.
*/
PeoplePicker.prototype._searchForMoreResults = function (event) {
var onSearchForMoreResults = this.props.onSearchForMoreResults;
this.setState({
'isSearching': true
});
event.preventDefault();
event.stopPropagation();
if (typeof onSearchForMoreResults !== 'undefined') {
onSearchForMoreResults(this.refs.searchField.value);
}
};
/**
* Opens the people picker dropdown.
*/
PeoplePicker.prototype._activatePeoplePicker = function () {
this.setState({
'isActive': true,
'highlightedSearchResultIndex': INVALID_INDEX,
});
this._highlightedSearchResult = undefined;
};
/**
* Closes the people picker dropdown.
*/
PeoplePicker.prototype._dismissPeoplePicker = function () {
this.setState({
'isActive': false,
'isSearching': false,
'highlightedSearchResultIndex': INVALID_INDEX,
});
this._highlightedSearchResult = undefined;
};
/**
*
*/
PeoplePicker.prototype._removeSuggestedPersona = function (index, personaInfo) {
var onRemoveSuggestion = this.props.onRemoveSuggestion;
if (onRemoveSuggestion) {
onRemoveSuggestion(index, personaInfo);
}
};
/**
* Selects the persona, dismisses the people picker, and clears out the search field.
*/
PeoplePicker.prototype._addPersonaToSelectedList = function (personaInfo) {
var selectedPersonas = this.state.selectedPersonas;
var onItemAdded = this.props.onItemAdded;
if (onItemAdded) {
onItemAdded(personaInfo);
}
selectedPersonas.push(personaInfo);
this._dismissPeoplePicker();
this.refs.searchField.value = '';
this.setState({
searchTextValue: '',
selectedPersonas: selectedPersonas
});
this.refs.searchField.focus();
this._onSearchFieldTextChanged();
};
/**
* Creates a new persona based on what the user has typed (non search result persona)
*/
PeoplePicker.prototype._addManualPersonaToSelectedList = function () {
var newPersonaName = this.state.searchTextValue;
if (newPersonaName.length > 0) {
var personaInfo = {
imageInitials: newPersonaName.charAt(0).toUpperCase(),
primaryText: newPersonaName,
secondaryText: newPersonaName
};
this._addPersonaToSelectedList(personaInfo);
}
};
/**
* Handles keyboard inputs for the PeoplePicker.
*/
PeoplePicker.prototype._onSearchFieldKeyDown = function (ev) {
var type = this.props.type;
var _a = this.state, isActive = _a.isActive, highlightedSearchResultIndex = _a.highlightedSearchResultIndex, selectedPersonas = _a.selectedPersonas;
switch (ev.which) {
// Enter behavior:
// - Adds the highlighted persona from the search results (autocomplete)
// - creates a new persona from the user's input (not from the search results)
case KeyCodes_1.KeyCodes.enter:
if (isActive && highlightedSearchResultIndex !== INVALID_INDEX) {
this._addPersonaToSelectedList(this._highlightedSearchResult);
}
else {
this._addManualPersonaToSelectedList();
}
break;
// Escape behavior:
// - closes the people picker if it is open
case KeyCodes_1.KeyCodes.escape:
if (isActive) {
this._dismissPeoplePicker();
}
break;
// Backspace behavior:
// - closes the people picker if it is open
// - sets focus to the last selected persona if people picker is closed
// - removes the focused persona
case KeyCodes_1.KeyCodes.backspace:
// allow normal event handling when there is text entered
if (this.refs.searchField.value.length !== 0) {
return; // continue propagation
}
if (isActive) {
this._dismissPeoplePicker();
}
else if (selectedPersonas.length > 0 && type !== PeoplePicker_Props_1.PeoplePickerType.memberList) {
var index = selectedPersonas.length - 1;
this.refs['persona' + index].focus();
}
break;
// Up behavior:
// - Moves the focus through the people picker dropdown if it is open
// - Blurs out of the search field so that the Focus Zone sets focus on a selected personas
case KeyCodes_1.KeyCodes.up:
if (isActive && highlightedSearchResultIndex !== INVALID_INDEX) {
this._setSelectedSearchResultIndex(highlightedSearchResultIndex - 1);
}
else {
return; // continue propagation
}
break;
// Down behavior:
// - Activates the people picker if it is not open
// - Moves the focus through the people picker dropdown if it is open
case KeyCodes_1.KeyCodes.down:
if (isActive) {
this._setSelectedSearchResultIndex(highlightedSearchResultIndex + 1);
}
else {
this._activatePeoplePicker();
this._setSelectedSearchResultIndex(0);
}
break;
// Left behavior:
// - Default cursor behavior if there is any text entered
// - Blurs out of the search field so that the Focus Zone sets focus on a selected personas
case KeyCodes_1.KeyCodes.left:
if (this.refs.searchField.value.length !== 0) {
ev.stopPropagation();
}
return; // continue propagation
// Tab behavior:
// - Adds the highlighted persona from the search results (autocomplete)
// - Shift-tab out of the FocusZone
case KeyCodes_1.KeyCodes.tab:
// allow default behavior for shift tab
if (ev.shiftKey) {
return;
}
if (isActive && highlightedSearchResultIndex !== INVALID_INDEX) {
this._addPersonaToSelectedList(this._highlightedSearchResult);
}
else {
return; // continue propagation
}
break;
// Semicolon and comma behavior:
// - creates a new persona from the user's input (not from the search results)
case KeyCodes_1.KeyCodes.semicolon:
case KeyCodes_1.KeyCodes.comma:
this._addManualPersonaToSelectedList();
break;
// Default keyboard behavior
// - If any key is pressed on the search field input, activate the people picker
// and set the first search result as selected.
default:
if (!isActive) {
this._activatePeoplePicker();
}
return; // continue propagation
}
// Only stop propagation if the event was handles and we didn't return
ev.stopPropagation();
ev.preventDefault();
};
/**
* Sets which persona in the search results is currently selected/highlighted.
*/
PeoplePicker.prototype._setSelectedSearchResultIndex = function (index) {
var highlightedSearchResultIndex = this.state.highlightedSearchResultIndex;
var suggestions = this.props.suggestions;
if (suggestions.length > 0) {
// Cap index to stay in bounds of available search results
index = Math.max(0, Math.min(suggestions.length - 1, index));
}
else {
index = INVALID_INDEX;
}
if (index !== highlightedSearchResultIndex) {
// Set the selected option.
this.setState({
highlightedSearchResultIndex: index
});
}
};
/**
* Handles changes in the input text box value, so we can notify the host
* of the search value change.
*/
PeoplePicker.prototype._onSearchFieldTextChanged = function () {
var onSearchFieldChanged = this.props.onSearchFieldChanged;
var textValue = this.refs.searchField.value;
this.setState({
searchTextValue: textValue
});
if (typeof onSearchFieldChanged !== 'undefined') {
onSearchFieldChanged(textValue);
}
};
/**
* Handles keeping the currently selected persona in view.
* If there's no search result selected, then reset the scroll to 0.
*/
PeoplePicker.prototype._setScollPosition = function () {
var selectedSearchResult = this.refs.selectedSearchResult;
var newTop = 0;
if (selectedSearchResult) {
var selectedResultTop = selectedSearchResult.offsetTop;
var menuItemHeight = selectedSearchResult.clientHeight;
var currentTop = this.refs.pickerResultGroups.scrollTop;
var totalHeight = this.refs.pickerResultGroups.clientHeight;
newTop = currentTop;
// check to scroll down
var amountCutOffDown = (currentTop + totalHeight) - (selectedResultTop + menuItemHeight);
if (amountCutOffDown < 0) {
newTop = currentTop - amountCutOffDown;
}
// check to scroll up
var amountCutOffUp = selectedResultTop - menuItemHeight;
if (amountCutOffUp < currentTop) {
newTop = amountCutOffUp;
}
}
// set the new scroll
this.refs.pickerResultGroups.scrollTop = newTop;
};
/**
* Removes one of the selected personas
*/
PeoplePicker.prototype._removeSelectedPersona = function (index) {
var selectedPersonas = this.state.selectedPersonas;
var onItemRemoved = this.props.onItemRemoved;
if (onItemRemoved) {
onItemRemoved(selectedPersonas[index]);
}
selectedPersonas.splice(index, 1);
this.setState({
selectedPersonas: selectedPersonas
});
};
/**
* Renders a list of personas using the list of selected personas, for the Member List variant.
*/
PeoplePicker.prototype._renderSelectedPersonasAsMemberList = function () {
var _this = this;
var selectedPersonas = this.state.selectedPersonas;
var addedMemberCountFormatText = this.props.addedMemberCountFormatText;
var count = selectedPersonas.length;
var className = css_1.css('ms-PeoplePicker-selected', {
'is-active': count > 0
});
var id = 0;
return React.createElement("div", {className: className}, addedMemberCountFormatText ?
React.createElement("div", {className: 'ms-PeoplePicker-selectedHeader'}, React.createElement("span", {className: 'ms-PeoplePicker-selectedCount'}, string_1.format(addedMemberCountFormatText, count))) : React.createElement("div", {className: 'ms-PeoplePicker-memberListTopMargin'}), React.createElement("ul", {className: 'ms-PeoplePicker-selectedPeople'}, React.createElement(FocusZone_1.FocusZone, null, selectedPersonas.map(function (child) {
return (React.createElement("li", {className: 'ms-PeoplePicker-selectedPerson', key: id++}, React.createElement(Persona_1.Persona, React.__spread({}, child, {size: Persona_1.PersonaSize.small, presence: child.presence ? child.presence : Persona_1.PersonaPresence.online})), React.createElement("button", {className: 'ms-PeoplePicker-resultAction', onClick: function () {
_this._removeSelectedPersona(selectedPersonas.indexOf(child));
}}, React.createElement("i", {className: 'ms-Icon ms-Icon--x'}))));
}))));
};
/**
* Renders a list of personas using the list of selected personas. Uses the default layout
* of displaying the personas in the search box.
*/
PeoplePicker.prototype._renderSelectedPersonas = function () {
var _this = this;
var id = 0;
var selectedPersonas = this.state.selectedPersonas;
return selectedPersonas.map(function (child) {
var key = id++;
return (React.createElement("div", {className: 'ms-PeoplePicker-persona', ref: 'persona' + key, key: key, "data-selection-index": key, "data-is-focusable": true, tabIndex: -1}, React.createElement("div", {className: 'ms-PeoplePicker-personaContent'}, React.createElement(Persona_1.Persona, React.__spread({}, child, {size: Persona_1.PersonaSize.extraSmall, presence: child.presence ? child.presence : Persona_1.PersonaPresence.online})), React.createElement("button", {className: 'ms-PeoplePicker-personaRemove', tabIndex: -1, "data-is-focusable": false, onClick: function () {
_this._removeSelectedPersona(selectedPersonas.indexOf(child));
}}, React.createElement("i", {className: 'ms-Icon ms-Icon--x'})))));
});
};
/**
* Renders the search field, which is the input element inside the searchbox.
*/
PeoplePicker.prototype._renderSearchField = function () {
var _this = this;
return (React.createElement("input", {className: 'ms-PeoplePicker-searchField', type: 'text', ref: 'searchField', key: 'searchField', "data-is-focusable": true, onFocus: function () { _this._focusedPersonaIndex = INVALID_INDEX; }, onChange: this._onSearchFieldTextChanged, onKeyDown: this._onSearchFieldKeyDown}));
};
/**
* Renders the popup search results
*/
PeoplePicker.prototype._renderSearchResults = function () {
var _this = this;
var _a = this.props, suggestions = _a.suggestions, searchCategoryName = _a.searchCategoryName, noResultsText = _a.noResultsText, type = _a.type, isConnected = _a.isConnected, primarySearchText = _a.primarySearchText, secondarySearchText = _a.secondarySearchText, disconnectedText = _a.disconnectedText, canSearchMore = _a.canSearchMore;
var isSearching = this.state.isSearching;
// Generate a result group section for each item in the array of suggestions
var resultItemId = 0;
var resultGroupId = 0;
var searchResultItems = [];
suggestions.forEach(function (persona) {
searchResultItems.push(_this._renderSearchResultItem(persona, resultItemId++));
});
var searchResults = (React.createElement("div", {className: 'ms-PeoplePicker-resultGroup', key: resultGroupId++}, React.createElement("div", {className: 'ms-PeoplePicker-resultGroupTitle'}, suggestions.length > 0 ? searchCategoryName : noResultsText), React.createElement("ul", {className: 'ms-PeoplePicker-resultList'}, searchResultItems)));
var searchMoreClassName = css_1.css('ms-PeoplePicker-searchMore', {
'is-searching': isSearching,
'ms-PeoplePicker-searchMore--disconnected': !isConnected
});
var searchMoreButtonClassName = css_1.css('ms-PeoplePicker-searchMoreBtn', {
'ms-PeoplePicker-searchMoreBtn--compact': type === PeoplePicker_Props_1.PeoplePickerType.compact
});
var searchIconClassName = css_1.css('ms-Icon', {
'ms-Icon--search': isConnected,
'ms-Icon--alert': !isConnected
});
var searchMore = canSearchMore ? (React.createElement("div", {className: searchMoreClassName, onClick: isConnected ? this._searchForMoreResults : null}, React.createElement("button", {className: searchMoreButtonClassName}, isSearching ? React.createElement(Spinner_1.Spinner, {type: Spinner_1.SpinnerType.large}) :
React.createElement("div", {className: 'ms-PeoplePicker-searchMoreIcon'}, React.createElement("i", {className: searchIconClassName})), isConnected ? React.createElement("div", {className: 'ms-PeoplePicker-searchMoreSecondary'}, secondarySearchText) : null, React.createElement("div", {className: 'ms-PeoplePicker-searchMorePrimary'}, isSearching ? 'Searching for ' + this.refs.searchField.value : isConnected ? primarySearchText : disconnectedText)))) : undefined;
return (React.createElement("div", {className: 'ms-PeoplePicker-results', key: 'pickerResults', ref: function (pickerResults) { return _this.refs.pickerResults = pickerResults; }}, React.createElement("div", {className: 'ms-PeoplePicker-resultGroups', ref: 'pickerResultGroups'}, searchResults), searchMore));
};
/**
* Renders the popup search results, for the Member List variant.
*/
PeoplePicker.prototype._renderSearchResultsForMemberList = function () {
var _this = this;
var suggestions = this.props.suggestions;
// MemberList variant doesn't show groups
var resultItemId = 0;
var searchResultItems = [];
suggestions.forEach(function (persona) {
searchResultItems.push(_this._renderSearchResultItem(persona, resultItemId++));
});
var searchResults = (React.createElement("div", {className: 'ms-PeoplePicker-resultGroup'}, React.createElement("ul", {className: 'ms-PeoplePicker-resultList'}, searchResultItems)));
return (React.createElement("div", {className: 'ms-PeoplePicker-results', key: 'pickerResults', ref: 'pickerResults'}, React.createElement("div", {className: 'ms-PeoplePicker-resultGroups', ref: 'pickerResultGroups'}, searchResults)));
};
/**
* Renders a single persona as part of a list to be displayed in the search results.
*/
PeoplePicker.prototype._renderSearchResultItem = function (personaInfo, id) {
var _this = this;
var type = this.props.type;
var isSelected = id === this.state.highlightedSearchResultIndex;
var buttonClassName = css_1.css('ms-PeoplePicker-resultBtn', {
'ms-PeoplePicker-resultBtn--compact': type === PeoplePicker_Props_1.PeoplePickerType.compact,
'ms-PeoplePicker-resultBtn--selected': isSelected
});
if (isSelected) {
this._highlightedSearchResult = personaInfo;
}
var personaSize = type === PeoplePicker_Props_1.PeoplePickerType.compact ? Persona_1.PersonaSize.extraSmall : Persona_1.PersonaSize.regular;
return (React.createElement("li", {className: 'ms-PeoplePicker-result', key: id, ref: isSelected ? 'selectedSearchResult' : null}, React.createElement("div", {role: 'button', className: buttonClassName}, React.createElement(Persona_1.Persona, React.__spread({}, personaInfo, {presence: personaInfo.presence ? personaInfo.presence : Persona_1.PersonaPresence.online, size: personaSize, onMouseDown: function () { _this._addPersonaToSelectedList(personaInfo); }, onClick: function () { _this._addPersonaToSelectedList(personaInfo); }})), type !== PeoplePicker_Props_1.PeoplePickerType.memberList ?
React.createElement("button", {className: 'ms-PeoplePicker-resultAction', tabIndex: -1, onClick: function () { _this._removeSuggestedPersona(id, personaInfo); }}, React.createElement("i", {className: 'ms-Icon ms-Icon--x'}))
: null)));
};
PeoplePicker.defaultProps = {
type: PeoplePicker_Props_1.PeoplePickerType.normal,
isConnected: true,
canSearchMore: true
};
return PeoplePicker;
}(React.Component));
exports.PeoplePicker = PeoplePicker;
});