UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

392 lines (391 loc) 24.1 kB
import { __assign, __extends, __spreadArray } from "tslib"; import "../../CommonImports"; import "../../Core/core.css"; import "./IdentityPickerDropdown.css"; import * as React from "react"; import * as Resources from '../../Resources.IdentityPicker'; import { ObservableArray, ObservableLike, ObservableValue } from '../../Core/Observable'; import { TimerManagement } from '../../Core/TimerManagement'; import { makeCancelable } from '../../Core/Util/Promise'; import { format, startsWith } from '../../Core/Util/String'; import { FocusWithin } from '../../FocusWithin'; import { Icon, IconSize } from '../../Icon'; import { Measure } from '../../Measure'; import { Observer } from '../../Observer'; import { Persona, PersonaSize } from '../../Persona'; import { TextField } from '../../TextField'; import { css, KeyCode } from '../../Util'; import { Location } from '../../Utilities/Position'; import { IdentityPickerSuggestionItem } from "../IdentityPickerSuggestionsList/IdentityPickerSuggestionItem"; import { IdentityPickerSuggestionsList } from "../IdentityPickerSuggestionsList/IdentityPickerSuggestionsList"; import { shouldShowIdentityCard } from "./IdentityPickerUtils"; import { IdentityType } from "./SharedIdentityPicker.Props"; var textFieldId = 1; var CustomIdentityPickerDropdown = /** @class */ (function (_super) { __extends(CustomIdentityPickerDropdown, _super); function CustomIdentityPickerDropdown(props) { var _this = _super.call(this, props) || this; _this.focusWithin = React.createRef(); _this.inputElement = React.createRef(); _this.itemRefs = {}; _this.openedIdentityCard = new ObservableValue(undefined); _this.outerElement = React.createRef(); _this.nonIdentitySuggestions = []; _this.suggestionsLoading = new ObservableValue(false); _this.isEditing = new ObservableValue(false); _this.selectedIndex = new ObservableValue(-1); _this.suggestions = new ObservableArray([]); _this.textFieldId = "identity-picker-downdown-textfield-".concat(textFieldId++); _this.onPickerDismiss = function () { _this.props.onSuggestionsVisibleChanged(false); }; _this.renderSuggestionItem = function (suggestion) { return (React.createElement("div", { className: "flex-row flex-grow scroll-hidden", onKeyDown: _this.onKeyDownSuggestionItem }, React.createElement(IdentityPickerSuggestionItem, __assign({}, suggestion, { onOpenPersonaCard: _this.openPersonaCard, ref: function (itemRef) { return (_this.itemRefs[suggestion.item.entityId] = itemRef); }, renderSuggestion: _this.props.renderCustomIdentitySuggestion && _this.renderCustomSuggestionItem })))); }; _this.renderCustomSuggestionItem = function (suggestion) { return _this.props.renderCustomIdentitySuggestion && _this.props.renderCustomIdentitySuggestion(suggestion.item); }; _this.renderPersonaCoin = function (className) { return (React.createElement(Observer, { selectedIdentity: _this.props.value, isEditing: _this.isEditing }, function (props) { return props.selectedIdentity && !props.isEditing ? (React.createElement(Persona, { className: css("flex-row flex-center justify-center", className), identity: props.selectedIdentity, size: PersonaSize.size24 })) : (React.createElement(Icon, { className: css("flex-row flex-center justify-center", className), iconName: "Contact", size: IconSize.medium })); })); }; _this.openPersonaCard = function (identity) { if (shouldShowIdentityCard(identity)) { _this.openedIdentityCard.value = identity; } }; _this.closePersonaCard = function () { _this.openedIdentityCard.value = undefined; if (_this.inputElement.current) { _this.inputElement.current.focus(); var suggestionsVisible = ObservableLike.getValue(_this.props.suggestionsVisible); if (!suggestionsVisible) { _this.props.onSuggestionsVisibleChanged(true); } } }; _this.completeSuggestion = function () { var selectedIdentity; if (_this.suggestions.value.length > 0 && ObservableLike.getValue(_this.props.suggestionsVisible) && _this.selectedIndex.value !== -1) { selectedIdentity = _this.suggestions.value[_this.selectedIndex.value]; } else { _this.selectedIndex.value = -1; selectedIdentity = _this.props.resolveUnrecognizedIdentity && _this.props.resolveUnrecognizedIdentity(ObservableLike.getValue(_this.props.textValue)); } _this.selectedPersonaChanged(selectedIdentity); if (selectedIdentity) { _this.props.onSuggestionsVisibleChanged(false); } return Boolean(selectedIdentity); }; _this.onBlur = function () { _this.props.onSuggestionsVisibleChanged(false); if (!_this.openedIdentityCard.value) { ObservableLike.getValue(_this.props.textValue) === "" && _this.selectedPersonaChanged(); _this.props.onBlur && _this.props.onBlur(); } }; _this.onClearClicked = function (ev) { _this.clear(); ev.preventDefault(); }; _this.onClearKeyDown = function (ev) { if (ev.which === KeyCode.enter) { _this.clear(); ev.preventDefault(); } }; _this.clear = function () { var _a; _this.props.onClear && _this.props.onClear(); _this.props.onChange(undefined); _this.lastPickedIdentity = undefined; _this.suggestions.value = []; (_a = _this.inputElement.current) === null || _a === void 0 ? void 0 : _a.focus(); }; _this.onClick = function () { if ((ObservableLike.getValue(_this.props.textValue) === "" && _this.props.pickerProvider.onEmptyInputFocus) || (ObservableLike.getValue(_this.props.value) && _this.suggestions.length === 0)) { _this.updateSuggestionsList(_this.props.pickerProvider.onEmptyInputFocus()); _this.props.onSuggestionsVisibleChanged(!ObservableLike.getValue(_this.props.suggestionsVisible)); } else { _this.props.onSuggestionsVisibleChanged(!ObservableLike.getValue(_this.props.suggestionsVisible)); } _this.inputElement.current.select(); }; _this.onFocus = function (e) { var onFocus = _this.props.onFocus; if (ObservableLike.getValue(_this.props.textValue) === "" && _this.props.pickerProvider.onEmptyInputFocus && !ObservableLike.getValue(_this.props.suggestionsVisible)) { _this.updateSuggestionsList(_this.props.pickerProvider.onEmptyInputFocus()); } if (onFocus) { onFocus(e); } }; _this.onKeyDown = function (ev) { if (ev.isDefaultPrevented()) { return; } var keyCode = ev.which; var suggestionsVisible = ObservableLike.getValue(_this.props.suggestionsVisible); var input = _this.inputElement.current && _this.inputElement.current.inputElement.current; switch (keyCode) { case KeyCode.escape: if (_this.openedIdentityCard.value) { !(_this.suggestions.value && _this.suggestions.value[_this.selectedIndex.value]) && _this.selectedPersonaChanged(); } if (suggestionsVisible) { _this.props.onSuggestionsVisibleChanged(false); _this.openedIdentityCard.value = undefined; ev.stopPropagation(); } break; case KeyCode.tab: case KeyCode.enter: if (!ev.shiftKey && suggestionsVisible) { if (_this.completeSuggestion()) { ev.preventDefault(); ev.stopPropagation(); } } else if (suggestionsVisible) { _this.completeSuggestion(); } else if (keyCode === KeyCode.enter && input && !input.value) { // Enter on an empty input element should behave the same as the clear button _this.selectedPersonaChanged(undefined); } break; case KeyCode.rightArrow: if (_this.suggestions.value && _this.suggestions.value[_this.selectedIndex.value] && input && input.value.length === input.selectionEnd) { _this.focusContactCardButton(_this.suggestions.value[_this.selectedIndex.value]); ev.preventDefault(); } break; case KeyCode.upArrow: if (suggestionsVisible && _this.suggestions.value) { _this.selectedIndex.value = Math.max(0, _this.selectedIndex.value - 1); _this.forceUpdate(); ev.preventDefault(); ev.stopPropagation(); } break; case KeyCode.downArrow: if (suggestionsVisible && _this.suggestions.value) { _this.selectedIndex.value = Math.min(_this.suggestions.value.length - 1, _this.selectedIndex.value + 1); _this.forceUpdate(); ev.preventDefault(); ev.stopPropagation(); } else { _this.props.onSuggestionsVisibleChanged(true); } break; } }; _this.onKeyDownSuggestionItem = function (event) { if (!event.defaultPrevented) { if (event.which === KeyCode.leftArrow || event.which === KeyCode.escape || event.which === KeyCode.tab) { if (_this.inputElement.current) { _this.inputElement.current.focus(); event.preventDefault(); } } } }; _this.focusContactCardButton = function (tag) { if (_this.itemRefs[tag.entityId]) { _this.itemRefs[tag.entityId].focus(); } }; _this.onResolveSuggestions = function (updatedValue) { var suggestions = _this.props.pickerProvider.onFilterIdentities(updatedValue.toLocaleLowerCase(), []); if (suggestions !== null) { _this.updateSuggestionsList(suggestions, updatedValue); } }; _this.onSearchChange = function (event, value) { if (value.length == 0) { var onClear = _this.props.onClear; onClear && onClear(); } _this.selectedIndex.value = -1; _this.isEditing.value = true; _this.props.onInputChange(value); _this.updateValue(value); }; _this.onSuggestionClick = function (suggestion) { _this.selectedIndex.value = suggestion.index; _this.selectedPersonaChanged(suggestion.item); _this.props.onSuggestionsVisibleChanged(false); }; _this.onTextFieldChanged = function (newWidth, newHeight) { _this.setState({ width: Math.max(newWidth, 296) }); }; _this.selectedPersonaChanged = function (persona) { if (_this.lastPickedIdentity !== persona) { !!persona && !!_this.props.pickerProvider.addIdentitiesToMRU && _this.props.pickerProvider.addIdentitiesToMRU([persona]); } if (_this.props.onChange(persona) !== false) { _this.props.onInputChange(persona ? persona.displayName : ""); _this.lastPickedIdentity = persona; } else { _this.props.onInputChange(""); _this.props.onChange(undefined); _this.lastPickedIdentity = undefined; } _this.isEditing.value = false; }; _this.updateValue = function (updatedValue) { _this.props.onSuggestionsVisibleChanged(!!updatedValue); if (_this.cachedResults[updatedValue]) { _this.updateSuggestionsList(_this.cachedResults[updatedValue], updatedValue); } else { _this.onResolveSuggestions(updatedValue); } }; _this.cachedResults = {}; _this.timerManagement = new TimerManagement(); _this.lastPickedIdentity = ObservableLike.getValue(props.value); _this.state = { width: 296 }; return _this; } CustomIdentityPickerDropdown.prototype.render = function () { var _this = this; var _a = this.props, ariaLabel = _a.ariaLabel, ariaLabelledBy = _a.ariaLabelledBy, autoFocus = _a.autoFocus, disabled = _a.disabled, _b = _a.editPlaceholder, editPlaceholder = _b === void 0 ? Resources.IdentityPickerPlaceholderFocusText : _b, _c = _a.placeholder, placeholder = _c === void 0 ? Resources.IdentityPickerPlaceholderText : _c, required = _a.required; return (React.createElement(Observer, { suggestionsVisible: this.props.suggestionsVisible }, function (topProps) { return (React.createElement(FocusWithin, { onBlur: _this.onBlur, onFocus: _this.onFocus, ref: _this.focusWithin }, function (focusStatus) { return (React.createElement(React.Fragment, null, React.createElement(Observer, { selectedIdentity: _this.props.value, selectedIndex: _this.selectedIndex, suggestionsLoading: _this.suggestionsLoading, textValue: _this.props.textValue }, function (props) { var _a; var ariaActiveDescendantId; if (topProps.suggestionsVisible) { if (props.selectedIndex === -1 || props.suggestionsLoading) { ariaActiveDescendantId = "sug-list-transition"; } else if (!_this.suggestions || !_this.suggestions.length) { ariaActiveDescendantId = "sug-list-no-results"; } else { ariaActiveDescendantId = "sug-row-".concat(props.selectedIndex); } } var ariaControlsId = topProps.suggestionsVisible ? "tag-picker-callout" : undefined; var placeholderText = !focusStatus.hasFocus && !props.selectedIdentity ? placeholder : editPlaceholder; return (React.createElement(Measure, { onMeasure: _this.onTextFieldChanged }, React.createElement("div", { className: "bolt-identitypickerdropdown flex-row flex-grow", ref: _this.outerElement, onKeyDown: _this.onKeyDown }, React.createElement(TextField, { ariaExpanded: topProps.suggestionsVisible, ariaActiveDescendant: ariaActiveDescendantId, ariaAutoComplete: "list", ariaControls: ariaControlsId, autoFocus: autoFocus, ariaHasPopup: "listbox", ariaLabel: ariaLabel ? ariaLabel : props.textValue === "" ? placeholderText : props.textValue, ariaLabelledBy: ariaLabelledBy, className: css(_this.props.className, "bolt-identitypickerdropdown-textField flex-row flex-center", focusStatus.hasFocus && "bolt-identitypickerdropdown-open"), containerClassName: "bolt-identitypickerdropdown-container flex-column flex-grow", inputId: (_a = _this.props.inputId) !== null && _a !== void 0 ? _a : _this.textFieldId, onBlur: focusStatus.onBlur, onChange: _this.onSearchChange, onClick: _this.onClick, onFocus: focusStatus.onFocus, prefixIconProps: { render: _this.renderPersonaCoin }, placeholder: placeholderText, ref: _this.inputElement, required: required, role: "combobox", suffixIconProps: props.textValue && !disabled ? { ariaHidden: "false", iconName: "Clear", className: "bolt-identity-picker-clearButton fontSize", role: "button", ariaLabel: format(Resources.Remove, props.textValue), onClick: _this.onClearClicked, onKeyDown: _this.onClearKeyDown, tabIndex: 0 } : undefined, value: props.textValue, disabled: disabled })))); }), React.createElement(Observer, { openedIdentityCard: _this.openedIdentityCard, selectedIndex: _this.selectedIndex }, function (props) { return (React.createElement(IdentityPickerSuggestionsList, { calloutProps: { anchorElement: _this.outerElement.current, anchorOrigin: { horizontal: Location.start, vertical: Location.end }, calloutOrigin: { horizontal: Location.start, vertical: Location.start }, contentShadow: true, id: "tag-picker-callout", onDismiss: _this.onPickerDismiss, role: "presentation" }, suggestionsVisible: topProps.suggestionsVisible || !!props.openedIdentityCard, isLoading: _this.suggestionsLoading, onBlur: focusStatus.onBlur, onFocus: focusStatus.onFocus, onSuggestionClicked: _this.onSuggestionClick, onClosePersonaCard: _this.closePersonaCard, onDismiss: _this.onPickerDismiss, onOpenPersonaCard: _this.openPersonaCard, openedIdentityCard: props.openedIdentityCard, pickerProvider: _this.props.pickerProvider, renderSuggestion: _this.renderSuggestionItem, suggestions: _this.suggestions, suggestionTarget: _this.outerElement.current, selectedIndex: props.selectedIndex, width: _this.state.width, resultsMaximumNumber: _this.props.suggestionItemsMaximumCount, suggestionsContainerAriaLabel: _this.props.suggestionsContainerAriaLabel })); }))); })); })); }; CustomIdentityPickerDropdown.prototype.componentDidMount = function () { this.updateValue = this.timerManagement.debounce(this.updateValue, 250); if (this.props.autoFocus) { var textValue = ObservableLike.getValue(this.props.textValue); if (this.props.pickerProvider.onEmptyInputFocus) { this.updateSuggestionsList(this.props.pickerProvider.onEmptyInputFocus()); } else { this.updateSuggestionsList(this.props.pickerProvider.onEmptyInputFocus()); } this.inputElement.current && this.inputElement.current.select(); this.props.onSuggestionsVisibleChanged(true); } this.setState({ width: this.outerElement.current.clientWidth }); }; CustomIdentityPickerDropdown.prototype.componentWillUnmount = function () { this.currentPromise && this.currentPromise.cancel(); }; CustomIdentityPickerDropdown.prototype.updateSuggestionsList = function (suggestions, initialSearchValue) { var _this = this; var suggestionsArray = suggestions; var suggestionsPromiseLike = suggestions; var filteredNonIdentitySuggestions = []; if (this.props.pickerProvider.getAdditionalEntries) { this.nonIdentitySuggestions = this.props.pickerProvider.getAdditionalEntries().map(function (value) { return { displayName: value, entityType: IdentityType.Custom, entityId: value, originDirectory: "", originId: "" }; }); filteredNonIdentitySuggestions = this.nonIdentitySuggestions && initialSearchValue ? this.nonIdentitySuggestions.filter(function (s) { return startsWith(s.displayName, initialSearchValue); }) : this.nonIdentitySuggestions; } // Check to see if the returned value is an array, if it is then just pass it into the next function. // If the returned value is not an array then check to see if it's a promise or PromiseLike. If it is then resolve it asynchronously. if (Array.isArray(suggestionsArray)) { var resultSuggestions = filteredNonIdentitySuggestions.length ? __spreadArray(__spreadArray([], suggestionsArray, true), filteredNonIdentitySuggestions, true) : suggestionsArray; this.updateSuggestions(resultSuggestions, initialSearchValue); } else if (suggestionsPromiseLike && suggestionsPromiseLike.then) { this.suggestionsLoading.value = true; // Ensure that the promise will only use the callback if it was the most recent one. var promise_1 = (this.currentPromise = makeCancelable(suggestionsPromiseLike)); promise_1.promise.then(function (newSuggestions) { if (promise_1 === _this.currentPromise) { var resultSuggestions = filteredNonIdentitySuggestions.length ? __spreadArray(__spreadArray([], newSuggestions, true), filteredNonIdentitySuggestions, true) : newSuggestions; _this.updateSuggestions(resultSuggestions, initialSearchValue); if (!!initialSearchValue && initialSearchValue !== "" && _this.suggestions.value && _this.suggestions.value.length > 0) { _this.cachedResults[initialSearchValue] = resultSuggestions; } _this.suggestionsLoading.value = false; } }); } }; CustomIdentityPickerDropdown.prototype.setSuggestions = function (suggestions, selectedIndex) { var _this = this; this.suggestions.value = suggestions; // Selected index set after list is updated for screen readers. if (this.updateIndexTimer) { window.cancelAnimationFrame(this.updateIndexTimer); } this.updateIndexTimer = window.requestAnimationFrame(function () { _this.selectedIndex.value = selectedIndex; }); }; CustomIdentityPickerDropdown.prototype.updateSuggestions = function (suggestions, initialSearchValue) { // Only update the suggestions if the initial search value is the same as the current input if (initialSearchValue === undefined || (this.inputElement.current && ObservableLike.getValue(this.props.textValue) === initialSearchValue)) { this.setSuggestions(suggestions, ObservableLike.getValue(this.props.textValue) === "" ? -1 : 0); } }; return CustomIdentityPickerDropdown; }(React.Component)); export { CustomIdentityPickerDropdown };