All files / lib/util dropdown.ts

100% Statements 68/68
100% Branches 10/10
100% Functions 3/3
100% Lines 68/68

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 691x 1x 1x 1x 1x 1x 1x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 1x 19x 19x 19x 19x 19x 19x 19x 19x 3x 19x 2x 2x 2x 2x 19x 17x 17x 17x 17x 17x 17x 17x 17x 17x 15x 15x 15x 15x 15x 15x 15x 15x 15x 19x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 19x  
import { DropdownPosition } from '../types';
import { CaretMetrics } from './layout';
import { DROPDOWN_MARGIN, DROPDOWN_WIDTH } from '../constants';
 
/**
 * Keeps a value within bounds. `min` wins when the bounds cross, ie when there is not enough room at all.
 */
function clamp(value: number, min: number, max: number): number {
    return Math.max(min, Math.min(value, max));
}
 
/**
 * Decides where the dropdown goes from the measurements taken by `getCaretMetrics`.
 *
 * @note Metrics come in viewport coordinates, the result is relative to the container the dropdown lives in.
 * @returns null when the caret has been scrolled out of view, ie there is nothing left to anchor the dropdown to.
 */
export function getDropdownPosition(
    metrics: CaretMetrics,
    dropdownHeight: number
): DropdownPosition | null {
    const { caret, box, origin, viewport, scrollable } = metrics;
 
    // The whole caret line has to be visible, otherwise the dropdown would point at a sliver of clipped text
    if (
        scrollable &&
        (caret.top < box.top || caret.top + caret.height > box.bottom)
    ) {
        return null;
    }
 
    // Keep the dropdown on the input, then inside the viewport, then move it into container coordinates
    const left =
        clamp(
            clamp(caret.left, box.left, box.right) + DROPDOWN_MARGIN,
            DROPDOWN_MARGIN,
            viewport.width - DROPDOWN_WIDTH - DROPDOWN_MARGIN
        ) - origin.left;
    const base = { left, width: DROPDOWN_WIDTH };
 
    // Is there place for the dropdown below the caret?
    if (caret.top + dropdownHeight + 2 * DROPDOWN_MARGIN <= viewport.height) {
        return {
            ...base,
            toTop: false,
            top: caret.top + DROPDOWN_MARGIN - origin.top,
            height: dropdownHeight,
        };
    }
 
    // If there is place for the dropdown above the caret, show it there
    if (caret.top - dropdownHeight - DROPDOWN_MARGIN > 0) {
        return {
            ...base,
            toTop: true,
            top: caret.top - dropdownHeight - DROPDOWN_MARGIN - origin.top,
            height: dropdownHeight,
        };
    }
 
    // It fits nowhere: fill the viewport
    return {
        ...base,
        toTop: true,
        top: DROPDOWN_MARGIN - origin.top,
        height: viewport.height - 2 * DROPDOWN_MARGIN,
    };
}