{"version":3,"file":"KanbanBoard.mjs","names":[],"sources":["../../../../package/common/kanbanBoard/KanbanBoard.tsx"],"sourcesContent":["import type { KeyboardEvent } from 'react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n  DndContext,\n  DragOverlay,\n  KeyboardSensor,\n  MouseSensor,\n  TouchSensor,\n  useSensor,\n  useSensors,\n  type DragStartEvent,\n  type DragOverEvent,\n  type DragEndEvent,\n  type Active,\n  type ScreenReaderInstructions,\n} from '@dnd-kit/core';\nimport { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';\nimport cx from 'classnames';\n\nimport type { KanbanBoardProps } from './kanbanBoard.types';\nimport styles from './kanbanBoard.module.pcss';\nimport KanbanColumn, { ColumnHeaderContent } from './KanbanColumn';\nimport {\n  applyPlacement,\n  buildAnnouncements,\n  columnsEqual,\n  findArrowFocusTarget,\n  findPlacement,\n  isColumnDragData,\n  kanbanCollisionDetection,\n  kanbanKeyboardCoordinateGetter,\n  moveColumn,\n  moveItem,\n  scrollsTowardBoard,\n  type DragPlacement,\n} from './kanbanBoard.utils';\n\nconst defaultGetItemLabel = <T,>(_item: T, id: string): string => id;\n\n// Hoisted so these are referentially stable across renders — inline literals here would give\n// `useSensor` a new options object every render (including every dragOver during an active\n// drag), which defeats its own memoization and makes `sensors` a new array each time too.\n//\n// Mouse and touch are split (rather than one combined `PointerSensor`) because they need\n// different activation constraints: a small movement threshold for mouse starts a drag\n// immediately without interfering with clicks, but the same threshold on a touchscreen would\n// hijack an ordinary scroll gesture as soon as the finger moves a few pixels. Touch instead\n// requires a brief press-and-hold before a drag arms, so a normal swipe-to-scroll passes through\n// untouched — the same pattern `VirtualList`'s `longPressDelay` uses.\nconst MOUSE_SENSOR_OPTIONS = { activationConstraint: { distance: 8 } };\nconst TOUCH_SENSOR_OPTIONS = { activationConstraint: { delay: 250, tolerance: 5 } };\nconst KEYBOARD_SENSOR_OPTIONS = { coordinateGetter: kanbanKeyboardCoordinateGetter };\n\nconst SCREEN_READER_INSTRUCTIONS: ScreenReaderInstructions = {\n  draggable: `\n    To pick up a card or column, press space or enter.\n    Use the arrow keys to move between cards and columns.\n    While dragging, use the arrow keys to move it — for a card, between positions and columns;\n    for a column, left/right to reorder it.\n    Press space or enter again to drop it, or press escape to cancel.\n  `,\n};\n\nfunction KanbanBoard<T = unknown>({\n  columns,\n  items,\n  renderItem,\n  onChange,\n  dragHandle,\n  getItemLabel = defaultGetItemLabel,\n  getItemDisabled,\n  renderEmptyColumn,\n  renderColumnActions,\n  reorderableColumns,\n  collapseEmptyColumns = false,\n  columnMaxHeight,\n  onLoadMore,\n  loadMoreThreshold,\n  onKeyDown,\n  className,\n  style,\n  classNames,\n  styles: slotStyles,\n  ...rest\n}: KanbanBoardProps<T>) {\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const [activeIsColumn, setActiveIsColumn] = useState(false);\n  const [placement, setPlacement] = useState<DragPlacement | null>(null);\n\n  // Guards against a self-sustaining reorder loop: applying a card move reflows the columns\n  // involved, which can shift a *different* card under an otherwise-stationary pointer. dnd-kit\n  // then re-fires `dragOver` with that new `over`, even though the user hasn't moved anything —\n  // and if the geometry decision for that new target flips back once applied, the two states\n  // (or more) can bounce forever, purely from our own layout changes, eventually tripping React's\n  // \"Maximum update depth exceeded\" guard. `activeRect` only changes when the dragged card's own\n  // position actually moves (real pointer/keyboard input), so it's used here as a proxy for \"did\n  // anything the user did actually change\" — a reorder is only applied once per distinct\n  // `activeRect`, which lets a drag settle and then holds still instead of oscillating.\n  const lastAppliedActiveRectKeyRef = useRef<string | null>(null);\n\n  const boardRef = useRef<HTMLDivElement>(null);\n\n  // Lets an ancestor of the board (the page) auto-scroll only toward a part of the board out of\n  // view. Left to dnd-kit, a card held near the bottom of the viewport scrolled the page on until\n  // the board was gone; refused, dnd-kit moves on to the column's own list.\n  const draggedRectRef = useRef<Active['rect'] | null>(null);\n  const canScroll = useCallback((element: Element) => {\n    const board = boardRef.current;\n    const dragged = draggedRectRef.current?.current.translated;\n    if (!board || !dragged || board.contains(element)) return true;\n    const scroller =\n      element === document.scrollingElement ? { top: 0, bottom: window.innerHeight } : element.getBoundingClientRect();\n    return scrollsTowardBoard(scroller, board.getBoundingClientRect(), dragged);\n  }, []);\n  // dnd-kit asks `canScroll` only when it re-plans, which WebKit never prompts while the pointer\n  // holds still, so the page scrolled on past the board there. A render hands dnd-kit a new\n  // `canScroll`, which makes it re-plan: the scroll listener below asks for one when the page\n  // should stop.\n  const [, replanAutoScroll] = useState(0);\n\n  // A scroll is input too: it moves the cards under a pointer that holds still, which is what the\n  // auto-scroll at a column's edge does. Only a scroll that moves this board counts, so a scroller\n  // elsewhere on the page cannot switch the guard off for the whole drag.\n  const isDragging = activeId !== null;\n  useEffect(() => {\n    if (!isDragging) return;\n    const handleScroll = ({ target }: Event) => {\n      const board = boardRef.current;\n      if (!board || !(target instanceof Node) || !(target.contains(board) || board.contains(target))) return;\n      lastAppliedActiveRectKeyRef.current = null;\n      const scroller = target === document ? document.scrollingElement : target;\n      if (scroller instanceof Element && !canScroll(scroller)) replanAutoScroll((n) => n + 1);\n    };\n    // Capture, because a scroll event does not bubble, and the scroller can be any ancestor.\n    window.addEventListener('scroll', handleScroll, { capture: true, passive: true });\n    return () => window.removeEventListener('scroll', handleScroll, { capture: true });\n  }, [isDragging, canScroll]);\n\n  const sensors = useSensors(\n    useSensor(MouseSensor, MOUSE_SENSOR_OPTIONS),\n    useSensor(TouchSensor, TOUCH_SENSOR_OPTIONS),\n    useSensor(KeyboardSensor, KEYBOARD_SENSOR_OPTIONS),\n  );\n\n  // Rebuilt from the live `columns` on every change rather than snapshotted at drag start: a page\n  // `onLoadMore` appended mid-drag stayed invisible in the snapshot, so the column kept asking for\n  // more, and the drop then handed the snapshot to `onChange`, discarding every page loaded since.\n  const displayColumns = useMemo(() => applyPlacement(columns, placement), [columns, placement]);\n\n  // `SortableContext` memoizes its own derived `items`/context value keyed on this array's\n  // *reference*, not its contents — an inline `.map()` here would hand it a new array every\n  // render (including every `dragOver` while a drag is in progress, via `placement`), forcing\n  // every column's own `useSortable` to recompute and its whole subtree to re-render on every\n  // board render, not just when the column order actually changes. Same class of bug as the\n  // sensor options and the `data`/`disabled` objects in `KanbanColumn.tsx`/`KanbanItem.tsx`.\n  const columnIds = useMemo(() => displayColumns.map((col) => col.id), [displayColumns]);\n\n  // Read from `columns`, not `displayColumns`, so no column grows or shrinks during a drag: a\n  // width change under the pointer moves the drop targets, and a column that shrank mid-drag once\n  // fed a reorder loop. A column the drag empties or fills changes width once, after the drop.\n  const collapsedColumnIds = useMemo(\n    () => new Set(collapseEmptyColumns ? columns.filter((col) => col.itemIds.length === 0).map((col) => col.id) : []),\n    [columns, collapseEmptyColumns],\n  );\n\n  const announcements = useMemo(\n    () => buildAnnouncements(displayColumns, items, getItemLabel),\n    [displayColumns, items, getItemLabel],\n  );\n\n  const accessibility = useMemo(\n    () => ({ announcements, screenReaderInstructions: SCREEN_READER_INSTRUCTIONS }),\n    [announcements],\n  );\n\n  function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n    onKeyDown?.(event);\n    if (event.defaultPrevented || activeId !== null || !boardRef.current || !(event.target instanceof HTMLElement))\n      return;\n    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n    const target = findArrowFocusTarget(boardRef.current, event.target, event.key);\n    if (!target) return;\n    event.preventDefault();\n    target.focus();\n  }\n\n  function handleDragStart({ active }: DragStartEvent) {\n    setActiveId(String(active.id));\n    setActiveIsColumn(isColumnDragData(active.data.current));\n    draggedRectRef.current = active.rect;\n    lastAppliedActiveRectKeyRef.current = null;\n  }\n\n  function handleDragOver({ active, over }: DragOverEvent) {\n    if (!over) return;\n\n    if (isColumnDragData(active.data.current)) {\n      const activeColumnId = String(active.id);\n      const overColumnId = String(over.id);\n\n      setPlacement((prev) => {\n        const base = applyPlacement(columns, prev);\n        const next = moveColumn(base, activeColumnId, overColumnId);\n        return next === base ? prev : findPlacement(next, activeColumnId, true);\n      });\n      return;\n    }\n\n    const activeItemId = String(active.id);\n    const overId = String(over.id);\n    const activeRect = active.rect.current.translated ?? active.rect.current.initial;\n\n    const activeRectKey = activeRect ? `${activeRect.top}:${activeRect.left}` : null;\n    if (activeRectKey !== null && activeRectKey === lastAppliedActiveRectKeyRef.current) {\n      return;\n    }\n    lastAppliedActiveRectKeyRef.current = activeRectKey;\n\n    setPlacement((prev) => {\n      const base = applyPlacement(columns, prev);\n      const next = moveItem(base, activeItemId, overId, { activeRect, overRect: over.rect });\n      return next === base ? prev : findPlacement(next, activeItemId, false);\n    });\n  }\n\n  function handleDragEnd({ over }: DragEndEvent) {\n    handleDragCancel();\n    if (over && !columnsEqual(displayColumns, columns)) onChange(displayColumns);\n  }\n\n  function handleDragCancel() {\n    setActiveId(null);\n    setActiveIsColumn(false);\n    setPlacement(null);\n  }\n\n  const activeColumn = activeId && activeIsColumn ? displayColumns.find((col) => col.id === activeId) : undefined;\n\n  return (\n    <DndContext\n      sensors={sensors}\n      collisionDetection={kanbanCollisionDetection}\n      onDragStart={handleDragStart}\n      onDragOver={handleDragOver}\n      onDragEnd={handleDragEnd}\n      onDragCancel={handleDragCancel}\n      accessibility={accessibility}\n      // A new `canScroll` every render on purpose, see `replanAutoScroll`.\n      autoScroll={{ canScroll: (element) => canScroll(element) }}\n    >\n      <SortableContext id='kanban-columns' items={columnIds} strategy={horizontalListSortingStrategy}>\n        <div {...rest} ref={boardRef} className={cx(styles.board, className)} style={style} onKeyDown={handleKeyDown}>\n          {displayColumns.map((col) => (\n            <KanbanColumn\n              key={col.id}\n              id={col.id}\n              title={col.title}\n              itemIds={col.itemIds}\n              maxItems={col.maxItems}\n              disabled={col.disabled}\n              getItemDisabled={getItemDisabled}\n              getItemLabel={getItemLabel}\n              emptyPlaceholder={renderEmptyColumn?.(col)}\n              actions={renderColumnActions?.(col)}\n              reorderable={reorderableColumns}\n              activeIsColumn={activeId !== null && activeIsColumn}\n              collapsed={collapsedColumnIds.has(col.id)}\n              columnMaxHeight={columnMaxHeight}\n              hasMore={col.hasMore}\n              onLoadMore={onLoadMore ? () => onLoadMore(col.id) : undefined}\n              loadMoreThreshold={loadMoreThreshold}\n              items={items}\n              renderItem={renderItem}\n              dragHandle={dragHandle}\n            />\n          ))}\n        </div>\n      </SortableContext>\n      <DragOverlay>\n        {activeColumn ? (\n          <div className={cx(styles.column, styles.columnDragOverlay)} inert>\n            <div className={styles.columnHeader}>\n              <ColumnHeaderContent\n                title={activeColumn.title}\n                count={activeColumn.itemIds.length}\n                maxItems={activeColumn.maxItems}\n                actions={renderColumnActions?.(activeColumn)}\n              />\n            </div>\n          </div>\n        ) : activeId && !activeIsColumn && items[activeId] !== undefined ? (\n          <div className={cx(styles.dragOverlay, classNames?.dragOverlay)} style={slotStyles?.dragOverlay}>\n            {renderItem(items[activeId], activeId)}\n          </div>\n        ) : null}\n      </DragOverlay>\n    </DndContext>\n  );\n}\n\nexport default KanbanBoard;\n"],"mappings":"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"}