Filemedium importancesource

horizontalScroll.ts

utils/horizontalScroll.ts

No strong subsystem tag
138
Lines
4302
Bytes
2
Exports
0
Imports
10
Keywords

What this is

This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.

Beginner explanation

This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.

How it is used

Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.

Expert explanation

Architecturally, this file intersects with general runtime concerns. It contains 138 lines, 0 detected imports, and 2 detected exports.

Important relationships

Detected exports

  • HorizontalScrollWindow
  • calculateHorizontalScrollWindow

Keywords

endindexstartindexselectedwidthtotalitemsvisibleitemclampedselectedstartrange

Detected imports

  • No import paths detected.

Source notes

This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.

Open parent directory

Full source

export type HorizontalScrollWindow = {
  startIndex: number
  endIndex: number
  showLeftArrow: boolean
  showRightArrow: boolean
}

/**
 * Calculate the visible window of items that fit within available width,
 * ensuring the selected item is always visible. Uses edge-based scrolling:
 * the window only scrolls when the selected item would be outside the visible
 * range, and positions the selected item at the edge (not centered).
 *
 * @param itemWidths - Array of item widths (each width should include separator if applicable)
 * @param availableWidth - Total available width for items
 * @param arrowWidth - Width of scroll indicator arrow (including space)
 * @param selectedIdx - Index of selected item (must stay visible)
 * @param firstItemHasSeparator - Whether first item's width includes a separator that should be ignored
 * @returns Visible window bounds and whether to show scroll arrows
 */
export function calculateHorizontalScrollWindow(
  itemWidths: number[],
  availableWidth: number,
  arrowWidth: number,
  selectedIdx: number,
  firstItemHasSeparator = true,
): HorizontalScrollWindow {
  const totalItems = itemWidths.length

  if (totalItems === 0) {
    return {
      startIndex: 0,
      endIndex: 0,
      showLeftArrow: false,
      showRightArrow: false,
    }
  }

  // Clamp selectedIdx to valid range
  const clampedSelected = Math.max(0, Math.min(selectedIdx, totalItems - 1))

  // If all items fit, show them all
  const totalWidth = itemWidths.reduce((sum, w) => sum + w, 0)
  if (totalWidth <= availableWidth) {
    return {
      startIndex: 0,
      endIndex: totalItems,
      showLeftArrow: false,
      showRightArrow: false,
    }
  }

  // Calculate cumulative widths for efficient range calculations
  const cumulativeWidths: number[] = [0]
  for (let i = 0; i < totalItems; i++) {
    cumulativeWidths.push(cumulativeWidths[i]! + itemWidths[i]!)
  }

  // Helper to get width of range [start, end)
  function rangeWidth(start: number, end: number): number {
    const baseWidth = cumulativeWidths[end]! - cumulativeWidths[start]!
    // When starting after index 0 and first item has separator baked in,
    // subtract 1 because we don't render leading separator on first visible item
    if (firstItemHasSeparator && start > 0) {
      return baseWidth - 1
    }
    return baseWidth
  }

  // Calculate effective available width based on whether we'll show arrows
  function getEffectiveWidth(start: number, end: number): number {
    let width = availableWidth
    if (start > 0) width -= arrowWidth // left arrow
    if (end < totalItems) width -= arrowWidth // right arrow
    return width
  }

  // Edge-based scrolling: Start from the beginning and only scroll when necessary
  // First, calculate how many items fit starting from index 0
  let startIndex = 0
  let endIndex = 1

  // Expand from start as much as possible
  while (
    endIndex < totalItems &&
    rangeWidth(startIndex, endIndex + 1) <=
      getEffectiveWidth(startIndex, endIndex + 1)
  ) {
    endIndex++
  }

  // If selected is within visible range, we're done
  if (clampedSelected >= startIndex && clampedSelected < endIndex) {
    return {
      startIndex,
      endIndex,
      showLeftArrow: startIndex > 0,
      showRightArrow: endIndex < totalItems,
    }
  }

  // Selected is outside visible range - need to scroll
  if (clampedSelected >= endIndex) {
    // Selected is to the right - scroll so selected is at the right edge
    endIndex = clampedSelected + 1
    startIndex = clampedSelected

    // Expand left as much as possible (selected stays at right edge)
    while (
      startIndex > 0 &&
      rangeWidth(startIndex - 1, endIndex) <=
        getEffectiveWidth(startIndex - 1, endIndex)
    ) {
      startIndex--
    }
  } else {
    // Selected is to the left - scroll so selected is at the left edge
    startIndex = clampedSelected
    endIndex = clampedSelected + 1

    // Expand right as much as possible (selected stays at left edge)
    while (
      endIndex < totalItems &&
      rangeWidth(startIndex, endIndex + 1) <=
        getEffectiveWidth(startIndex, endIndex + 1)
    ) {
      endIndex++
    }
  }

  return {
    startIndex,
    endIndex,
    showLeftArrow: startIndex > 0,
    showRightArrow: endIndex < totalItems,
  }
}