UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

496 lines (495 loc) • 28.5 kB
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; });