UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

180 lines (179 loc) 12.4 kB
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 };