All files / owid-grapher/grapher/controls/CollapsibleList CollapsibleList.tsx

97.83% Statements 135/138
72% Branches 18/25
100% Functions 12/12
97.83% Lines 135/138

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 1841x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 165x 165x 615x 615x 615x 615x 165x 165x 165x 11x 11x 33x 33x 33x 11x 11x 33x 33x 33x 33x 33x 11x 11x 33x 33x 33x 33x 33x 33x 33x     33x 33x 33x 33x 33x 11x 11x 33x 33x 11x 11x 99x 99x 11x 11x   11x 11x 11x 33x 33x 33x 11x 11x 22x 22x 22x 22x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 1x 1x 11x 11x 33x 33x 33x 33x 33x 123x 33x 33x 33x 33x 33x 33x 33x 33x 33x 22x 22x 33x 33x 33x 33x 33x 41x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 41x 33x 33x                                                                                            
import React, { ReactNode } from "react"
import { observable, action } from "mobx"
import { observer } from "mobx-react"
import { throttle } from "../../../clientUtils/Util"
import { Tippy } from "../../chart/Tippy"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog"
 
interface ListChild {
    index: number
    child: ReactNode
}
 
/** A UI component inspired by the "Priority+ Navbar" or "Progressively Collapsing Navbar"*/
@observer
export class CollapsibleList extends React.Component {
    private outerContainerRef: React.RefObject<HTMLDivElement> =
        React.createRef()
    private moreButtonRef: React.RefObject<HTMLLIElement> = React.createRef()
    private outerContainerWidth: number = 0
    private moreButtonWidth: number = 0
    private itemsWidths: number[] = []
 
    @observable private numItemsVisible?: number
 
    private get children(): ListChild[] {
        return (
            React.Children.map(this.props.children, (child, i) => {
                return {
                    index: i,
                    child,
                }
            }) ?? []
        )
    }
 
    private updateOuterContainerWidth(): void {
        this.outerContainerWidth =
            this.outerContainerRef.current?.clientWidth ?? 0
    }
 
    private calculateItemWidths(): void {
        this.itemsWidths = []
        this.outerContainerRef.current
            ?.querySelectorAll(".list-item.visible, .list-item.hidden")
            .forEach((item): number => this.itemsWidths.push(item.clientWidth))
    }
 
    @action private updateNumItemsVisible(): void {
        const numItemsVisibleWithoutMoreButton = numItemsVisible(
            this.itemsWidths,
            this.outerContainerWidth
        )
 
        this.numItemsVisible =
            numItemsVisibleWithoutMoreButton >= this.children.length
                ? numItemsVisibleWithoutMoreButton
                : numItemsVisible(
                      this.itemsWidths,
                      this.outerContainerWidth,
                      this.moreButtonWidth
                  )
    }
 
    private get visibleItems(): ListChild[] {
        return this.children.slice(0, this.numItemsVisible)
    }
 
    private get dropdownItems(): ListChild[] {
        return this.children.slice(this.numItemsVisible)
    }
 
    @action private onResize = throttle((): void => {
        this.updateItemVisibility()
    }, 100)
 
    @action private updateItemVisibility(): void {
        this.updateOuterContainerWidth()
        this.updateNumItemsVisible()
    }
 
    componentDidUpdate(): void {
        // react to children being added or removed, for example
        this.calculateItemWidths()
        this.updateItemVisibility()
    }
 
    componentDidMount(): void {
        window.addEventListener("resize", this.onResize)
 
        this.moreButtonWidth = this.moreButtonRef.current?.clientWidth ?? 0
        this.calculateItemWidths()
        this.updateItemVisibility()
    }
 
    componentWillUnmount(): void {
        window.removeEventListener("resize", this.onResize)
    }
 
    render(): JSX.Element {
        return (
            <div className="collapsibleList" ref={this.outerContainerRef}>
                <ul>
                    {this.visibleItems.map(
                        (item): JSX.Element => (
                            <li key={item.index} className="list-item visible">
                                {item.child}
                            </li>
                        )
                    )}
                    <li
                        className="list-item moreButton"
                        ref={this.moreButtonRef}
                        style={{
                            visibility: this.dropdownItems.length
                                ? "visible"
                                : "hidden",
                        }}
                    >
                        <MoreButton
                            options={this.dropdownItems.map(
                                (item): JSX.Element => (
                                    <li
                                        key={item.index}
                                        className="list-item dropdown"
                                    >
                                        {item.child}
                                    </li>
                                )
                            )}
                        />
                    </li>
                    {/* Invisibly render dropdown items such that we can measure their clientWidth, too */}
                    {this.dropdownItems.map(
                        (item): JSX.Element => (
                            <li key={item.index} className="list-item hidden">
                                {item.child}
                            </li>
                        )
                    )}
                </ul>
            </div>
        )
    }
}
 
export class MoreButton extends React.Component<{
    options: React.ReactElement[]
}> {
    render(): JSX.Element {
        const { options } = this.props
        return (
            <Tippy
                content={options}
                interactive={true}
                trigger={"click"}
                placement={"bottom"}
            >
                <span>
                    <FontAwesomeIcon icon={faCog} />
                    &nbsp;More
                </span>
            </Tippy>
        )
    }
}
 
/**
 * Given: an array of item widths, a container width, and a starting width
 * Returns the number of items that can fit in the container
 */
export function numItemsVisible(
    itemWidths: number[],
    containerWidth: number,
    startingWidth: number = 0
): number {
    let total = startingWidth
    for (let i = 0; i < itemWidths.length; i++) {
        if (total + itemWidths[i] > containerWidth) return i
        else total += itemWidths[i]
    }
    return itemWidths.length
}