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.
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.