timc1 / kbar

fast, portable, and extensible cmd+k interface for your site

Home Page:https://kbar.vercel.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

rowVirtualiser is setting the height a little bit short

rajatkulkarni95 opened this issue · comments

So I traced the issue back to the rowVirtualiser.totalSize being set on KBAR_LISTBOX that causes even if the result has a single item to have a scrollbar attached to it.

My div is 2 lines of content, causing this issue.

Any way we can tweak this value? I see a maxHeight prop, but this doesn't override it.
Screenshot 2023-07-13 at 10 05 33 PM
Screenshot 2023-07-13 at 10 05 42 PM

Cool library none theless, it's just a bit jarring to the eye to see the cutoff

Easiest way I found is to make your own KBarResults.

Here's what I used, adapted from the default Kbar code to have a estimateSize function and passing in 150px into the cell.

import * as React from 'react';
import { useVirtual } from 'react-virtual';

import { ActionImpl, KBAR_LISTBOX, getListboxItemId, useKBar } from 'kbar';
import { usePointerMovedSinceMount } from 'kbar/lib/utils';

const START_INDEX = 0;

interface RenderParams<T = ActionImpl | string> {
  item: T;
  active: boolean;
}

interface KBarResultsProps {
  items: any[];
  onRender: (params: RenderParams) => React.ReactElement;
  maxHeight?: number;
}

const estimateSize = () => 150;

export const KBarResults: React.FC<KBarResultsProps> = props => {
  const activeRef = React.useRef<HTMLDivElement>(null);
  const parentRef = React.useRef(null);

  // store a ref to all items so we do not have to pass
  // them as a dependency when setting up event listeners.
  const itemsRef = React.useRef(props.items);
  itemsRef.current = props.items;

  const rowVirtualizer = useVirtual({
    size: itemsRef.current.length,
    parentRef,
    estimateSize,
  });

  const { query, search, currentRootActionId, activeIndex, options } = useKBar(
    state => ({
      search: state.searchQuery,
      currentRootActionId: state.currentRootActionId,
      activeIndex: state.activeIndex,
    }),
  );

  React.useEffect(() => {
    const handler = event => {
      if (event.isComposing) {
        return;
      }

      if (event.key === 'ArrowUp' || (event.ctrlKey && event.key === 'p')) {
        event.preventDefault();
        query.setActiveIndex(index => {
          let nextIndex = index > START_INDEX ? index - 1 : index;
          // avoid setting active index on a group
          if (typeof itemsRef.current[nextIndex] === 'string') {
            if (nextIndex === 0) return index;
            nextIndex -= 1;
          }
          return nextIndex;
        });
      } else if (
        event.key === 'ArrowDown' ||
        (event.ctrlKey && event.key === 'n')
      ) {
        event.preventDefault();
        query.setActiveIndex(index => {
          let nextIndex =
            index < itemsRef.current.length - 1 ? index + 1 : index;
          // avoid setting active index on a group
          if (typeof itemsRef.current[nextIndex] === 'string') {
            if (nextIndex === itemsRef.current.length - 1) return index;
            nextIndex += 1;
          }
          return nextIndex;
        });
      } else if (event.key === 'Enter') {
        event.preventDefault();
        // storing the active dom element in a ref prevents us from
        // having to calculate the current action to perform based
        // on the `activeIndex`, which we would have needed to add
        // as part of the dependencies array.
        activeRef.current?.click();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [query]);

  // destructuring here to prevent linter warning to pass
  // entire rowVirtualizer in the dependencies array.
  const { scrollToIndex } = rowVirtualizer;
  React.useEffect(() => {
    scrollToIndex(activeIndex, {
      // ensure that if the first item in the list is a group
      // name and we are focused on the second item, to not
      // scroll past that group, hiding it.
      align: activeIndex <= 1 ? 'end' : 'auto',
    });
  }, [activeIndex, scrollToIndex]);

  React.useEffect(() => {
    // TODO(tim): fix scenario where async actions load in
    // and active index is reset to the first item. i.e. when
    // users register actions and bust the `useRegisterActions`
    // cache, we won't want to reset their active index as they
    // are navigating the list.
    query.setActiveIndex(
      // avoid setting active index on a group
      typeof props.items[START_INDEX] === 'string'
        ? START_INDEX + 1
        : START_INDEX,
    );
  }, [search, currentRootActionId, props.items, query]);

  const execute = React.useCallback(
    (item: RenderParams['item']) => {
      if (typeof item === 'string') return;
      if (item.command) {
        item.command.perform(item);
        query.toggle();
      } else {
        query.setSearch('');
        query.setCurrentRootAction(item.id);
      }
      options.callbacks?.onSelectAction?.(item);
    },
    [query, options],
  );

  const pointerMoved = usePointerMovedSinceMount();

  return (
    <div
      ref={parentRef}
      style={{
        maxHeight: props.maxHeight || 400,
        position: 'relative',
        overflow: 'auto',
      }}
    >
      <div
        role="listbox"
        id={KBAR_LISTBOX}
        style={{
          height: `${rowVirtualizer.totalSize}px`,
          width: '100%',
        }}
      >
        {rowVirtualizer.virtualItems.map(virtualRow => {
          const item = itemsRef.current[virtualRow.index];
          const handlers = typeof item !== 'string' && {
            onPointerMove: () =>
              pointerMoved &&
              activeIndex !== virtualRow.index &&
              query.setActiveIndex(virtualRow.index),
            onPointerDown: () => query.setActiveIndex(virtualRow.index),
            onClick: () => execute(item),
          };
          const active = virtualRow.index === activeIndex;

          return (
            <div
              ref={active ? activeRef : null}
              id={getListboxItemId(virtualRow.index)}
              role="option"
              aria-selected={active}
              key={virtualRow.index}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: '150px',
                transform: `translateY(${virtualRow.start}px)`,
              }}
              {...handlers}
            >
              {React.cloneElement(
                props.onRender({
                  item,
                  active,
                }),
                {
                  ref: virtualRow.measureRef,
                },
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
};

Hey! This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.