azure-devops-ui
Version:
React components for building web UI in Azure DevOps
180 lines (179 loc) • 12.4 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import "./Dropdown.css";
import * as React from "react";
import { ObservableLike } from '../../Core/Observable';
import { format } from '../../Core/Util/String';
import { Button } from '../../Button';
import { Callout, ContentSize } from '../../Callout';
import { FocusZone, FocusZoneDirection, FocusZoneKeyStroke } from '../../FocusZone';
import { IconSize } from '../../Icon';
import { ListBox } from '../../ListBox';
import { Observer } from '../../Observer';
import * as Resources from '../../Resources.Dropdown';
import { TextField } from '../../TextField';
import { css, getSafeIdSelector, KeyCode, preventDefault } from '../../Util';
const ItemsForFilter = 10;
const DefaultWidth = 256;
// This should match the total horizontal padding on bolt-dropdown-filter-container
const FilterBarPadding = 16;
// This should match the width + margin of the textfield search icon
const FilterBarIconWidth = 27;
export function DropdownCallout(props) {
return React.createElement(DropdownCalloutComponent, Object.assign({}, props));
}
export class DropdownCalloutComponent extends React.Component {
constructor(props) {
super(props);
this.callout = React.createRef();
this.calloutContent = React.createRef();
this.filterBox = React.createRef();
this.initFocusElement = React.createRef();
this.hasFocused = false;
this.updateLayout = () => {
// Allow the new items to draw before updating the layout.
setTimeout(() => {
if (this.callout.current) {
this.callout.current.updateLayout();
}
}, 0);
return true;
};
this.onMouseDown = (event) => {
if (this.props.ignoreMouseDown) {
if (event.target.tagName !== "INPUT") {
preventDefault(event);
}
}
};
this.listBoxDidUpdate = () => {
this.getScrollWidth();
};
this.getScrollWidth = () => {
window.requestAnimationFrame(() => {
if (this.calloutContent.current && this.props.width >= 0) {
const widthChange = this.calloutContent.current.offsetWidth - this.props.width;
// A 1 pixel change in the width may change the total width by 2 pixels if there are two scroll bars.
// Only rerender when a scrollbar is appearing or being removed which should be > 1 pixel change.
if (Math.abs(widthChange) > 1) {
this.setState({ scrollBarWidth: widthChange + this.state.scrollBarWidth });
}
}
});
};
this.state = { scrollBarWidth: 0 };
}
componentDidMount() {
this.getScrollWidth();
}
componentDidUpdate(prevProps) {
if (this.shouldShowFilterBox() &&
prevProps.loading !== this.props.loading &&
!!!this.props.loading &&
!this.hasFocused) {
this.focus();
this.hasFocused = true;
}
}
shouldShowFilterBox() {
var _a;
return (_a = this.props.showFilterBox) !== null && _a !== void 0 ? _a : this.props.items.length > ItemsForFilter;
}
focus() {
if (this.filterBox.current) {
this.filterBox.current.focus();
}
else if (this.initFocusElement.current) {
this.initFocusElement.current.focus();
}
}
render() {
const { actions, anchorElement, anchorOffset, anchorOrigin, anchorPoint, ariaLabel, blurDismiss = true, calloutContentClassName, columns, containerClassName, contentLocation, dropdownOrigin, enforceSingleSelect, excludeFocusZone, excludeTabStop, filteredItems, filteredNoResultsText, filteredResultsLoadingText, filterPlaceholderText, filterText, focusOnMount, getUnselectableRanges, id, items, lightDismiss, listBoxClassName, listBoxRef, loading, onActivate, onFilterKeyDown, onFilterTextChanged, onSelect, onToggle, portalProps, renderBeforeContent, renderItem, searching, selection, showCloseButton, showChecksColumn, showFilterBox, showItemsWhileSearching, showTree, title, updateFilteredItems, userFilteredItems } = this.props;
let { width = DefaultWidth } = this.props;
if (width > 0) {
width -= this.state.scrollBarWidth;
}
const textFieldId = `bolt-dropdown-textfield-${id}`;
const clearInput = () => {
filterText.value = "";
if (onFilterTextChanged) {
onFilterTextChanged(null, "");
}
if (updateFilteredItems) {
updateFilteredItems();
}
};
const onDismiss = () => {
if (this.props.onDismiss) {
this.props.onDismiss();
}
clearInput();
};
return (React.createElement(Callout, { anchorElement: anchorElement, anchorOffset: anchorOffset, anchorOrigin: anchorOrigin, anchorPoint: anchorPoint, role: "presentation", blurDismiss: blurDismiss, calloutOrigin: dropdownOrigin, contentClassName: css(calloutContentClassName, "bolt-dropdown flex-column custom-scrollbar v-scroll-auto h-scroll-hidden"), contentLocation: contentLocation, contentRef: this.calloutContent, contentShadow: true, contentSize: ContentSize.Auto, escDismiss: true, id: id, portalProps: portalProps, lightDismiss: lightDismiss, focuszoneProps: {
postprocessKeyStroke: event => {
// dismiss the callout on tab key instead of letting the
// browser handle the tab key, since with React.portals it
// will move to the body, instead of the next tabbable element after
// the dropdown.
if (event.which === KeyCode.tab && !event.defaultPrevented) {
event.preventDefault();
onDismiss();
return FocusZoneKeyStroke.IgnoreAll;
}
return FocusZoneKeyStroke.IgnoreNone;
}
}, onDismiss: onDismiss, ref: this.callout },
React.createElement(FocusZone, { circularNavigation: true, defaultActiveElement: this.shouldShowFilterBox()
? getSafeIdSelector(textFieldId)
: ".bolt-dropdown-init-focus", direction: FocusZoneDirection.Vertical, focusOnMount: focusOnMount !== undefined ? focusOnMount : true, preventScrollOnFocus: window.self !== window.top },
React.createElement("div", { className: "bolt-dropdown-container no-outline", onMouseDown: this.onMouseDown, onKeyDown: onFilterKeyDown, style: { width: width >= 0 ? width : undefined } },
React.createElement("div", { "aria-hidden": "true", "aria-roledescription": Resources.DropdownCalloutRoleDescription, className: "bolt-dropdown-init-focus no-outline", tabIndex: !excludeTabStop ? -1 : undefined, ref: this.initFocusElement, role: "menuitem" }),
React.createElement(Observer, { items: { observableValue: items, filter: this.updateLayout } }, () => {
const shouldShowFilterBox = this.shouldShowFilterBox();
return shouldShowFilterBox || title || showCloseButton ? (React.createElement("div", { className: "bolt-dropdown-header-container" },
(title || showCloseButton) && (React.createElement("div", { className: "bolt-dropdown-header flex-row flex-center" },
React.createElement("div", { className: "bolt-dropdown-header-text flex-grow font-weight-semibold" }, title),
showCloseButton && (React.createElement(Button, { className: "bolt-dropdown-header-button", ariaLabel: Resources.Close, iconProps: { iconName: "Cancel" }, onClick: onDismiss, subtle: true })))),
shouldShowFilterBox && (React.createElement("div", { key: "bolt-dropdown-filter-container", className: "bolt-dropdown-filter-container" },
React.createElement(Observer, { filterText: filterText }, (props) => {
return (React.createElement(TextField, { key: "bolt-dropdown-filter", ariaLabel: Resources.SearchAriaLabel, className: "bolt-dropdown-filter", excludeTabStop: true, inputId: textFieldId, onChange: onFilterTextChanged, placeholder: filterPlaceholderText, prefixIconProps: { iconName: "Search" }, ref: this.filterBox, value: filterText, maxWidth: this.props.width - FilterBarPadding - FilterBarIconWidth, suffixIconProps: props.filterText.length > 0
? {
ariaLabel: Resources.ClearText,
iconName: "ChromeClose",
onClick: clearInput,
size: IconSize.small,
tooltipProps: {
text: Resources.ClearText
},
role: "button"
}
: undefined }));
}))))) : null;
}),
renderBeforeContent && renderBeforeContent(),
React.createElement(Observer, { filteredItems: filteredItems, filteredNoResultsText: filteredNoResultsText, listBoxItems: { observableValue: items, filter: updateFilteredItems }, userFilteredItems: { observableValue: userFilteredItems, filter: updateFilteredItems } }, (props) => {
let noItemsElement = null;
let noItemsText = "";
if (((filteredItems && filteredItems.length === 0) || items.length === 0) && !searching) {
noItemsText =
filterText.value === ""
? this.props.noItemsText
: format(props.filteredNoResultsText || Resources.NoFilterResults, filterText.value);
if (noItemsText) {
noItemsElement = React.createElement("div", { className: "bolt-dropdown-no-items" }, noItemsText);
}
}
return (React.createElement(React.Fragment, null,
noItemsElement,
React.createElement(ListBox, { ariaLabel: ariaLabel, className: listBoxClassName, columns: columns, containerClassName: css("bolt-dropdown-list-box-container", containerClassName), didUpdate: this.listBoxDidUpdate, enforceSingleSelect: enforceSingleSelect, excludeFocusZone: excludeFocusZone, excludeTabStop: true, searchResultsLoadingText: filteredResultsLoadingText, focuszoneProps: null, getUnselectableRanges: getUnselectableRanges, items: filteredItems ? filteredItems.value : items, loading: loading, onActivate: onActivate, onSelect: onSelect, onToggle: onToggle, renderItem: renderItem, ref: listBoxRef, searching: searching, selection: selection, showChecksColumn: showChecksColumn === undefined ? true : showChecksColumn, showItemsWhileSearching: showItemsWhileSearching, showTree: showTree })));
}),
React.createElement(Observer, { actions: actions }, (props) => {
const actions = this.props.actions;
return actions && actions.length ? (React.createElement("div", { className: "bolt-actions-container flex-column" }, ObservableLike.getValue(actions).map((actionProps, index) => (React.createElement(Button, Object.assign({ key: actionProps.id || index, subtle: true, excludeTabStop: true }, actionProps)))))) : null;
})))));
}
}
DropdownCalloutComponent.defaultProps = {
width: DefaultWidth,
ignoreMouseDown: true
};