@gechiui/compose
Version:
GeChiUI higher-order components (HOCs).
175 lines (163 loc) • 5.13 kB
JavaScript
/**
* External dependencies
*/
import { debounce } from 'lodash';
/**
* GeChiUI dependencies
*/
import { useState, useLayoutEffect } from '@gechiui/element';
import { getScrollContainer } from '@gechiui/dom';
import { PAGEUP, PAGEDOWN, HOME, END } from '@gechiui/keycodes';
const DEFAULT_INIT_WINDOW_SIZE = 30;
/**
* @typedef {Object} GCFixedWindowList
*
* @property {number} visibleItems Items visible in the current viewport
* @property {number} start Start index of the window
* @property {number} end End index of the window
* @property {(index:number)=>boolean} itemInView Returns true if item is in the window
*/
/**
* @typedef {Object} GCFixedWindowListOptions
*
* @property {number} [windowOverscan] Renders windowOverscan number of items before and after the calculated visible window.
* @property {boolean} [useWindowing] When false avoids calculating the window size
* @property {number} [initWindowSize] Initial window size to use on first render before we can calculate the window size.
*/
/**
*
* @param {import('react').RefObject<HTMLElement>} elementRef Used to find the closest scroll container that contains element.
* @param { number } itemHeight Fixed item height in pixels
* @param { number } totalItems Total items in list
* @param { GCFixedWindowListOptions } [options] Options object
* @return {[ GCFixedWindowList, setFixedListWindow:(nextWindow:GCFixedWindowList)=>void]} Array with the fixed window list and setter
*/
export default function useFixedWindowList(
elementRef,
itemHeight,
totalItems,
options
) {
const initWindowSize = options?.initWindowSize ?? DEFAULT_INIT_WINDOW_SIZE;
const useWindowing = options?.useWindowing ?? true;
const [ fixedListWindow, setFixedListWindow ] = useState( {
visibleItems: initWindowSize,
start: 0,
end: initWindowSize,
itemInView: ( /** @type {number} */ index ) => {
return index >= 0 && index <= initWindowSize;
},
} );
useLayoutEffect( () => {
if ( ! useWindowing ) {
return;
}
const scrollContainer = getScrollContainer( elementRef.current );
const measureWindow = (
/** @type {boolean | undefined} */ initRender
) => {
if ( ! scrollContainer ) {
return;
}
const visibleItems = Math.ceil(
scrollContainer.clientHeight / itemHeight
);
// Aim to keep opening list view fast, afterward we can optimize for scrolling
const windowOverscan = initRender
? visibleItems
: options?.windowOverscan ?? visibleItems;
const firstViewableIndex = Math.floor(
scrollContainer.scrollTop / itemHeight
);
const start = Math.max( 0, firstViewableIndex - windowOverscan );
const end = Math.min(
totalItems - 1,
firstViewableIndex + visibleItems + windowOverscan
);
setFixedListWindow( ( lastWindow ) => {
const nextWindow = {
visibleItems,
start,
end,
itemInView: ( /** @type {number} */ index ) => {
return start <= index && index <= end;
},
};
if (
lastWindow.start !== nextWindow.start ||
lastWindow.end !== nextWindow.end ||
lastWindow.visibleItems !== nextWindow.visibleItems
) {
return nextWindow;
}
return lastWindow;
} );
};
measureWindow( true );
const debounceMeasureList = debounce( () => {
measureWindow();
}, 16 );
scrollContainer?.addEventListener( 'scroll', debounceMeasureList );
scrollContainer?.ownerDocument?.defaultView?.addEventListener(
'resize',
debounceMeasureList
);
scrollContainer?.ownerDocument?.defaultView?.addEventListener(
'resize',
debounceMeasureList
);
return () => {
scrollContainer?.removeEventListener(
'scroll',
debounceMeasureList
);
scrollContainer?.ownerDocument?.defaultView?.removeEventListener(
'resize',
debounceMeasureList
);
};
}, [ itemHeight, elementRef, totalItems ] );
useLayoutEffect( () => {
if ( ! useWindowing ) {
return;
}
const scrollContainer = getScrollContainer( elementRef.current );
const handleKeyDown = ( /** @type {KeyboardEvent} */ event ) => {
switch ( event.keyCode ) {
case HOME: {
return scrollContainer?.scrollTo( { top: 0 } );
}
case END: {
return scrollContainer?.scrollTo( {
top: totalItems * itemHeight,
} );
}
case PAGEUP: {
return scrollContainer?.scrollTo( {
top:
scrollContainer.scrollTop -
fixedListWindow.visibleItems * itemHeight,
} );
}
case PAGEDOWN: {
return scrollContainer?.scrollTo( {
top:
scrollContainer.scrollTop +
fixedListWindow.visibleItems * itemHeight,
} );
}
}
};
scrollContainer?.ownerDocument?.defaultView?.addEventListener(
'keydown',
handleKeyDown
);
return () => {
scrollContainer?.ownerDocument?.defaultView?.removeEventListener(
'keydown',
handleKeyDown
);
};
}, [ totalItems, itemHeight, elementRef, fixedListWindow.visibleItems ] );
return [ fixedListWindow, setFixedListWindow ];
}