azure-devops-ui
Version:
React components for building web UI in Azure DevOps
278 lines (277 loc) • 15.4 kB
JavaScript
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;