@hakuna-matata-ui/utils
Version:
Common utilties and types for Chakra UI
152 lines (131 loc) • 4.15 kB
text/typescript
export function getFirstItem<T>(array: T[]): T | undefined {
return array != null && array.length ? array[0] : undefined
}
export function getLastItem<T>(array: T[]): T | undefined {
const length = array == null ? 0 : array.length
return length ? array[length - 1] : undefined
}
export function getPrevItem<T>(index: number, array: T[], loop = true): T {
const prevIndex = getPrevIndex(index, array.length, loop)
return array[prevIndex]
}
export function getNextItem<T>(index: number, array: T[], loop = true): T {
const nextIndex = getNextIndex(index, array.length, 1, loop)
return array[nextIndex]
}
export function removeIndex<T>(array: T[], index: number): T[] {
return array.filter((_, idx) => idx !== index)
}
export function addItem<T>(array: T[], item: T): T[] {
return [...array, item]
}
export function removeItem<T>(array: T[], item: T): T[] {
return array.filter((eachItem) => eachItem !== item)
}
/**
* Get the next index based on the current index and step.
*
* @param currentIndex the current index
* @param length the total length or count of items
* @param step the number of steps
* @param loop whether to circle back once `currentIndex` is at the start/end
*/
export function getNextIndex(
currentIndex: number,
length: number,
step = 1,
loop = true,
): number {
const lastIndex = length - 1
if (currentIndex === -1) {
return step > 0 ? 0 : lastIndex
}
const nextIndex = currentIndex + step
if (nextIndex < 0) {
return loop ? lastIndex : 0
}
if (nextIndex >= length) {
if (loop) return 0
return currentIndex > length ? length : currentIndex
}
return nextIndex
}
/**
* Get's the previous index based on the current index.
* Mostly used for keyboard navigation.
*
* @param index - the current index
* @param count - the length or total count of items in the array
* @param loop - whether we should circle back to the
* first/last once `currentIndex` is at the start/end
*/
export function getPrevIndex(
index: number,
count: number,
loop = true,
): number {
return getNextIndex(index, count, -1, loop)
}
/**
* Converts an array into smaller chunks or groups.
*
* @param array the array to chunk into group
* @param size the length of each chunk
*/
export function chunk<T>(array: T[], size: number): T[][] {
return array.reduce((rows: T[][], currentValue: T, index: number) => {
if (index % size === 0) {
rows.push([currentValue])
} else {
rows[rows.length - 1].push(currentValue)
}
return rows
}, [] as T[][])
}
/**
* Gets the next item based on a search string
*
* @param items array of items
* @param searchString the search string
* @param itemToString resolves an item to string
* @param currentItem the current selected item
*/
export function getNextItemFromSearch<T>(
items: T[],
searchString: string,
itemToString: (item: T) => string,
currentItem: T,
): T | undefined {
if (searchString == null) {
return currentItem
}
// If current item doesn't exist, find the item that matches the search string
if (!currentItem) {
const foundItem = items.find((item) =>
itemToString(item).toLowerCase().startsWith(searchString.toLowerCase()),
)
return foundItem
}
// Filter items for ones that match the search string (case insensitive)
const matchingItems = items.filter((item) =>
itemToString(item).toLowerCase().startsWith(searchString.toLowerCase()),
)
// If there's a match, let's get the next item to select
if (matchingItems.length > 0) {
let nextIndex: number
// If the currentItem is in the available items, we move to the next available option
if (matchingItems.includes(currentItem)) {
const currentIndex = matchingItems.indexOf(currentItem)
nextIndex = currentIndex + 1
if (nextIndex === matchingItems.length) {
nextIndex = 0
}
return matchingItems[nextIndex]
}
// Else, we pick the first item in the available items
nextIndex = items.indexOf(matchingItems[0])
return items[nextIndex]
}
// a decent fallback to the currentItem
return currentItem
}