UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

278 lines (277 loc) 15.4 kB
import "../../CommonImports"; import "../../Core/core.css"; import "./FilterBar.css"; import * as React from "react"; import * as Utils_Accessibility from '../../Core/Util/Accessibility'; import { format } from '../../Core/Util/String'; import { Button } from '../../Button'; import * as Resources from '../../Resources.FilterBar'; import { SurfaceBackground, SurfaceContext } from '../../Surface'; import { css, getSafeId } from '../../Util'; import { FILTER_APPLIED_EVENT, FILTER_CHANGE_EVENT } from '../../Utilities/Filter'; let idCount = 0; export class FilterBar extends React.Component { constructor(props) { super(props); this._firstChildIsKeywordItem = false; this._prevContainerWidth = 0; this._id = getSafeId("filter-bar-" + idCount++); this._onResize = () => { if (!this._resizeTimeout) { this._resizeTimeout = setTimeout(() => { this._resizeTimeout = null; if (this._isMounted) { const containerIsGrowing = this._prevContainerWidth < this._filterBarElement.clientWidth; const shouldHidePlaceholderLabels = this.state.shouldHidePlaceholderLabels && containerIsGrowing ? false : this.state.shouldHidePlaceholderLabels; const shouldHaveMaxItemWidth = this.state.shouldHaveMaxItemWidth && containerIsGrowing ? false : this.state.shouldHaveMaxItemWidth; this._prevContainerWidth = this._filterBarElement.clientWidth; this.setState({ filtersToShowStopIndex: FilterBar.RENDER_EVERYTHING, shouldHidePlaceholderLabels, shouldHaveMaxItemWidth }); } }, 100); } }; this._onPageLeft = () => { this._hasMadeVisibleFilterAnnouncement = false; const startIndex = this._startingFilterIndices.pop() || 0; this._hasPagedLeft = true; this.setState({ filtersToShowStartIndex: startIndex, filtersToShowStopIndex: FilterBar.RENDER_EVERYTHING }); }; this._onPageRight = () => { this._hasMadeVisibleFilterAnnouncement = false; this._startingFilterIndices.push(this.state.filtersToShowStartIndex); this._hasPagedRight = true; this.setState({ filtersToShowStartIndex: this.state.filtersToShowStopIndex, filtersToShowStopIndex: FilterBar.RENDER_EVERYTHING }); }; this._calculateFiltersToShowStopIndex = () => { let totalWidth = this._rightElement.clientWidth; for (let i = 0; i < this._childrenContainerElements.length; i++) { const elem = this._childrenContainerElements[i]; const elemWidth = elem.clientWidth + parseFloat(window.getComputedStyle(elem).marginRight); totalWidth += elemWidth; if (totalWidth > this._filterBarElement.clientWidth) { // Make sure we show at least one filter. const endIndex = this.state.filtersToShowStartIndex + i; return endIndex > this.state.filtersToShowStartIndex ? endIndex : this.state.filtersToShowStartIndex + 1; } } return this.state.filtersToShowStartIndex + this._childrenContainerElements.length; }; this._onFilterChanged = (changedState) => { this.setState({ hasChangesToApply: this.props.filter.hasChangesToApply(), hasChangesToReset: this.props.filter.hasChangesToReset(), filtersToShowStopIndex: FilterBar.RENDER_EVERYTHING, shouldHidePlaceholderLabels: false }); }; this._onFilterApplied = (changedState) => { this.setState({ hasChangesToApply: this.props.filter.hasChangesToApply() }); }; this._onClearAndDismiss = () => { if (this.props.filter.hasChangesToReset()) { this.props.filter.reset(); } if (this.props.onDismissClicked) { this.props.onDismissClicked(); } this.focus(); }; this._onApplyChanges = () => { this.props.filter.applyChanges(); this.focus(); }; if (!props.filter) { throw new Error("Cannot create a FilterBar without a filter prop."); } this._startingFilterIndices = []; this._hasMadeVisibleFilterAnnouncement = false; this._isMounted = false; this.state = { hasChangesToReset: props.filter.hasChangesToReset(), hasChangesToApply: props.filter.hasChangesToApply(), filtersToShowStartIndex: 0, filtersToShowStopIndex: FilterBar.RENDER_EVERYTHING, shouldHidePlaceholderLabels: false, shouldHaveMaxItemWidth: false }; } focus() { if (this._filterItemRefs && this._filterItemRefs.length > 0) { this._filterItemRefs[0].focus(); } } forceUpdate() { super.forceUpdate(); if (this._filterItemRefs) { this._filterItemRefs.forEach(filterItem => filterItem.forceUpdate()); } } componentDidMount() { this.props.filter && this.props.filter.subscribe(this._onFilterChanged, FILTER_CHANGE_EVENT); this.props.filter && this.props.filter.subscribe(this._onFilterApplied, FILTER_APPLIED_EVENT); window.addEventListener("resize", this._onResize); const stopIndex = this._calculateFiltersToShowStopIndex(); if (stopIndex < React.Children.toArray(this.props.children).length - 1) { this.setState({ shouldHidePlaceholderLabels: true }); } else { this.setState({ filtersToShowStopIndex: stopIndex }); } this._isMounted = true; if (this.props.onMounted) { this.props.onMounted(this); } } UNSAFE_componentWillReceiveProps(nextProps) { // Bug#1284606 Checking our child components to see if we actually changed // the contents of the filter bar so we can know if we want to reload tabbing // and adjust how we're drawing out child components const currentKeys = this.getChildKeysAsString(this.props); const newKeys = this.getChildKeysAsString(nextProps); if (currentKeys !== newKeys) { // Bug#1284606 Triggers a full re-render in the event we changed the child elements this.setState({ hasChangesToApply: nextProps.filter.hasChangesToApply(), hasChangesToReset: nextProps.filter.hasChangesToReset(), filtersToShowStartIndex: 0, filtersToShowStopIndex: FilterBar.RENDER_EVERYTHING }); } else { // Normal prop updates, triggered when child components update as well this.setState({ hasChangesToApply: nextProps.filter.hasChangesToApply(), hasChangesToReset: nextProps.filter.hasChangesToReset() }); } } componentWillUnmount() { this.props.filter && this.props.filter.unsubscribe(this._onFilterChanged, FILTER_CHANGE_EVENT); this.props.filter && this.props.filter.unsubscribe(this._onFilterApplied, FILTER_APPLIED_EVENT); window.removeEventListener("resize", this._onResize); this._isMounted = false; } componentDidUpdate() { if (this.props.onRenderComplete) { this.props.onRenderComplete(); } if (this._hasPagedLeft && this.state.filtersToShowStopIndex > 0) { if (this.state.filtersToShowStartIndex == 0) { this._nextButtonElem && this._nextButtonElem.focus(); } this._hasPagedLeft = false; } if (this.state.filtersToShowStopIndex < 0) { const filtersToShowStopIndex = this._calculateFiltersToShowStopIndex(); const allFiltersFit = filtersToShowStopIndex === React.Children.toArray(this.props.children).length; if (!allFiltersFit && !this.state.shouldHidePlaceholderLabels) { this.setState({ shouldHidePlaceholderLabels: true }); } else if (!allFiltersFit && !this.state.shouldHaveMaxItemWidth) { this.setState({ shouldHaveMaxItemWidth: true }); } else { if (this._hasPagedRight && filtersToShowStopIndex === this.state.filtersToShowStartIndex + this._childrenContainerElements.length) { this._prevButtonElem && this._prevButtonElem.focus(); } this.setState({ filtersToShowStopIndex }); this._hasPagedRight = false; } } else if (!this._hasMadeVisibleFilterAnnouncement) { Utils_Accessibility.announce(format(Resources.AnnonuceVisibleFilters, this.state.filtersToShowStartIndex + 1, this.state.filtersToShowStopIndex), false); this._hasMadeVisibleFilterAnnouncement = true; } if (this._prevContainerWidth != this._filterBarElement.clientWidth) { this._onResize(); } } render() { const { children, filter, className, onDismissClicked, searchRoleAriaLabel } = this.props; const { hasChangesToApply, hasChangesToReset, filtersToShowStopIndex, filtersToShowStartIndex, shouldHaveMaxItemWidth, shouldHidePlaceholderLabels } = this.state; this._filterItemRefs = []; this._childrenContainerElements = []; let isFirstChild = true; let isKeywordPresent = false; this._firstChildIsKeywordItem = false; let childrenWithProps = React.Children.map(children, child => { if (child === null) { return null; } let containerClassName = "vss-FilterBar--item"; const childElement = child; if (childElement.props.isTextItem && !isKeywordPresent) { this._firstChildIsKeywordItem = isFirstChild; isKeywordPresent = true; containerClassName = css(containerClassName, "vss-FilterBar--item-keyword-container"); } else if (shouldHaveMaxItemWidth) { containerClassName = css(containerClassName, "max-width-small"); } isFirstChild = false; const childWithProps = React.cloneElement(childElement, { filter: childElement.props.filter || filter, ref: (elem) => { if (elem) { this._filterItemRefs.push(elem); } }, hideSelectedItemIcon: true, showPlaceholderAsLabel: !shouldHidePlaceholderLabels && childElement.props.showPlaceholderAsLabel }); return (React.createElement("div", { className: containerClassName, ref: (elem) => { if (elem) { this._childrenContainerElements.push(elem); } } }, childWithProps)); }); const canPageRight = filtersToShowStopIndex < childrenWithProps.length; const canPageLeft = filtersToShowStartIndex > 0; if (canPageRight || canPageLeft) { const endIndex = filtersToShowStopIndex > 0 ? filtersToShowStopIndex : childrenWithProps.length; childrenWithProps = childrenWithProps.slice(filtersToShowStartIndex, endIndex); } const clearLabel = onDismissClicked ? Resources.ClearAndDismissFilterBarLinkLabel : Resources.ClearFilterBarLinkLabel; return (React.createElement(SurfaceContext.Consumer, null, surfaceContext => (React.createElement("div", { className: css(className, "vss-FilterBar", surfaceContext.background === SurfaceBackground.neutral && "bolt-filterbar-white depth-8 no-v-margin"), role: "toolbar", "aria-label": searchRoleAriaLabel || Resources.FilterBarAriaLabel, id: this._id }, React.createElement("div", { className: css("vss-FilterBar--list", (!this._firstChildIsKeywordItem || filtersToShowStartIndex > 0) && "justify-right"), ref: (elem) => { this._filterBarElement = elem; } }, childrenWithProps, React.createElement("div", { className: "vss-FilterBar--right-items", ref: (elem) => { this._rightElement = elem; } }, (canPageLeft || canPageRight) && (React.createElement("div", { className: "vss-FilterBar--page-button-container" }, React.createElement(Button, { className: "filter-bar-button vss-FilterBar-page-button", ref: (elem) => { this._prevButtonElem = elem; }, onClick: this._onPageLeft, disabled: !canPageLeft, ariaLabel: Resources.FilterPageLeftAriaLabel, iconProps: { iconName: "ChevronLeftMed" } }), React.createElement(Button, { className: "filter-bar-button vss-FilterBar-page-button", ref: (elem) => { this._nextButtonElem = elem; }, onClick: this._onPageRight, disabled: !canPageRight, ariaLabel: Resources.FilterPageRightAriaLabel, iconProps: { iconName: "ChevronRightMed" } }))), !this.props.hideClearAction && (React.createElement("div", { className: "vss-FilterBar--action vss-FilterBar--action-clear" }, React.createElement(Button, { ariaLabel: clearLabel, className: "filter-bar-button", disabled: !hasChangesToReset && !onDismissClicked, iconProps: { iconName: "Cancel" }, onClick: this._onClearAndDismiss, subtle: true, tooltipProps: { text: clearLabel } }))), filter.usesApplyMode() && (React.createElement("div", { className: "vss-FilterBar--action vss-FilterBar--action-apply" }, React.createElement(Button, { className: "filter-bar-button", disabled: !hasChangesToApply, onClick: this._onApplyChanges, iconProps: { iconName: "CheckMark" } }, Resources.ApplyChangesFilterBarText))))))))); } getChildKeysAsString(props) { const childKeys = (props.children && React.Children.map(props.children, child => { if (child === null) { return null; } const childAsFilterBarItem = child; // If the child isn't able to be cast properly, just note that it exists // it's infeasible to get a unique identifier from an arbitrary, possibly stateful component // That said, we DO want to at least count these elements instead of excluding them // so we aren't filtering them out if (childAsFilterBarItem === undefined) { return ""; } return childAsFilterBarItem.props.filterItemKey; })) || []; return JSON.stringify(childKeys); } } FilterBar.RENDER_EVERYTHING = -1;