import classNames from "classnames"; import * as React from "react"; import { Boundary } from "../../common/boundary"; import * as Classes from "../../common/classes"; import { OVERFLOW_LIST_OBSERVE_PARENTS_CHANGED } from "../../common/errors"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; import { IResizeEntry, ResizeSensor } from "../resize-sensor/resizeSensor"; export interface IOverflowListProps extends IProps { /** * Which direction the items should collapse from: start or end of the * children. This also determines whether `overflowRenderer` appears before * (`START`) or after (`END`) the visible items. * @default Boundary.START */ collapseFrom?: Boundary; /** * All items to display in the list. Items that do not fit in the container * will be rendered in the overflow instead. */ items: T[]; /** * The number of visible items will never be lower than the number passed to * this prop. * @default 0 */ minVisibleItems?: number; /** * If `true`, all parent DOM elements of the container will also be * observed. If changes to a parent's size is detected, the overflow will be * recalculated. * * Only enable this prop if the overflow should be recalculated when a * parent element resizes in a way that does not also cause the * `OverflowList` to resize. * @default false */ observeParents?: boolean; /** * Callback invoked to render the overflowed items. Unlike * `visibleItemRenderer`, this prop is invoked once with all items that do * not fit in the container. * * Typical use cases for this prop will put overflowed items in a dropdown * menu or display a "+X items" label. */ overflowRenderer: (overflowItems: T[]) => React.ReactNode; /** CSS properties to apply to the root element. */ style?: React.CSSProperties; /** * Callback invoked to render each visible item. * Remember to set a `key` on the rendered element! */ visibleItemRenderer: (item: T, index: number) => React.ReactChild; } export interface IOverflowListState { overflow: T[]; visible: T[]; } export class OverflowList extends React.PureComponent, IOverflowListState> { public static displayName = `${DISPLAYNAME_PREFIX}.OverflowList`; public static defaultProps: Partial> = { collapseFrom: Boundary.START, minVisibleItems: 0, }; public static ofType() { return OverflowList as new (props: IOverflowListProps) => OverflowList; } public state: IOverflowListState = { overflow: [], visible: this.props.items, }; /** A cache containing the widths of all elements being observed to detect growing/shrinking */ private previousWidths = new Map(); private spacer: Element | null = null; public componentDidMount() { this.repartition(false); } public componentWillReceiveProps(nextProps: IOverflowListProps) { const { collapseFrom, items, minVisibleItems, observeParents, overflowRenderer, visibleItemRenderer, } = this.props; if (observeParents !== nextProps.observeParents) { console.warn(OVERFLOW_LIST_OBSERVE_PARENTS_CHANGED); } if ( collapseFrom !== nextProps.collapseFrom || items !== nextProps.items || minVisibleItems !== nextProps.minVisibleItems || overflowRenderer !== nextProps.overflowRenderer || visibleItemRenderer !== nextProps.visibleItemRenderer ) { // reset visible state if the above props change. this.setState({ overflow: [], visible: nextProps.items, }); } } public componentDidUpdate() { this.repartition(false); } public render() { const { className, collapseFrom, observeParents, style, visibleItemRenderer } = this.props; const overflow = this.maybeRenderOverflow(); return (
{collapseFrom === Boundary.START ? overflow : null} {this.state.visible.map(visibleItemRenderer)} {collapseFrom === Boundary.END ? overflow : null}
(this.spacer = ref)} />
); } private maybeRenderOverflow() { const { overflow } = this.state; if (overflow.length === 0) { return null; } return this.props.overflowRenderer(overflow); } private resize = (entries: IResizeEntry[]) => { // if any parent is growing, assume we have more room than before const growing = entries.some(entry => { const previousWidth = this.previousWidths.get(entry.target) || 0; return entry.contentRect.width > previousWidth; }); this.repartition(growing); entries.forEach(entry => this.previousWidths.set(entry.target, entry.contentRect.width)); }; private repartition(growing: boolean) { if (this.spacer == null) { return; } if (growing) { this.setState({ overflow: [], visible: this.props.items, }); } else if (this.spacer.getBoundingClientRect().width < 1) { // spacer has flex-shrink and width 1px so if it's any smaller then we know to shrink this.setState(state => { if (state.visible.length <= this.props.minVisibleItems) { return null; } const collapseFromStart = this.props.collapseFrom === Boundary.START; const visible = state.visible.slice(); const next = collapseFromStart ? visible.shift() : visible.pop(); if (next === undefined) { return null; } const overflow = collapseFromStart ? [...state.overflow, next] : [next, ...state.overflow]; return { overflow, visible, }; }); } } }