Skip to content

Commit

Permalink
Remove utils dependency
Browse files Browse the repository at this point in the history
anubra266 committed Oct 6, 2024
1 parent b0c52a6 commit b0feb24
Showing 13 changed files with 308 additions and 116 deletions.
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -30,8 +30,6 @@
},
"peerDependencies": {
"@chakra-ui/react": "^2.5.5",
"@chakra-ui/react-utils": "^2.0.5",
"@chakra-ui/utils": "^2.0.8",
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"framer-motion": ">7.6.14",
@@ -92,4 +90,4 @@
"vite-plugin-linter": "^3.0.0",
"vite-tsconfig-paths": "^5.0.1"
}
}
}
35 changes: 29 additions & 6 deletions src/autocomplete-context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { createContext } from "@chakra-ui/react-utils";
import * as React from "react";
import { UseAutoCompleteReturn } from "./types";

export const [AutoCompleteProvider, useAutoCompleteContext] = createContext<
UseAutoCompleteReturn
>({
name: "AutoCompleteContext",
errorMessage:
"useAutoCompleteContext: `context` is undefined. Seems you forgot to wrap all autoomplete components within `<AutoComplete />`",
});
>();

type CreateContextReturn<T> = [React.Provider<T>, () => T, React.Context<T>];

export function createContext<ContextType>() {
const Context = React.createContext<ContextType | undefined>(undefined);

Context.displayName = "AutoCompleteContext";

function useContext() {
const context = React.useContext(Context);
const errorMessage =
"useAutoCompleteContext: `context` is undefined. Seems you forgot to wrap all autoomplete components within `<AutoComplete />`";

if (!context) {
const error = new Error(errorMessage);
error.name = "ContextError";
Error.captureStackTrace?.(error, useContext);
throw error;
}

return context;
}

return [Context.Provider, useContext, Context] as CreateContextReturn<
ContextType
>;
}
4 changes: 2 additions & 2 deletions src/autocomplete-creatable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Flex, FlexProps } from "@chakra-ui/react";
import { MaybeRenderProp } from "@chakra-ui/react-utils";
import { isEmpty, runIfFn } from "@chakra-ui/utils";
import { MaybeRenderProp } from "./types";
import { isEmpty, runIfFn } from "./utils";

import React from "react";

2 changes: 1 addition & 1 deletion src/autocomplete-group.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import {
FlexProps,
forwardRef,
} from "@chakra-ui/react";
import { omit } from "@chakra-ui/utils";
import { omit } from "./utils";
import React from "react";
import { useAutoCompleteContext } from "./autocomplete-context";

75 changes: 46 additions & 29 deletions src/autocomplete-input.tsx
Original file line number Diff line number Diff line change
@@ -2,43 +2,42 @@ import {
forwardRef,
Input,
InputProps,
InputGroup,
InputRightElement,
InputGroup,
InputRightElement,
Spinner,
SystemStyleObject,
useMergeRefs,
useMultiStyleConfig,
Wrap,
WrapItem,
PopoverAnchor
WrapItem,
PopoverAnchor,
} from "@chakra-ui/react";
import { runIfFn } from "@chakra-ui/utils";
import { MaybeRenderProp } from "@chakra-ui/react-utils";
import { runIfFn } from "./utils";
import React, { useEffect } from "react";

import { useAutoCompleteContext } from "./autocomplete-context";
import { UseAutoCompleteReturn } from "./types";
import { MaybeRenderProp, UseAutoCompleteReturn } from "./types";

export interface AutoCompleteInputProps extends Omit<InputProps, "children"> {
children?: MaybeRenderProp<{ tags: UseAutoCompleteReturn["tags"] }>;
wrapStyles?: SystemStyleObject;
hidePlaceholder?: boolean;
loadingIcon?: React.ReactNode
loadingIcon?: React.ReactNode;
}

const AutoCompleteInputComponent = forwardRef(
(props, forwardedRef) => {
const { isLoading } = useAutoCompleteContext();
const { loadingIcon, ...inputProps } = props;

return <InputGroup>
<Input {...inputProps} ref={forwardedRef} />
{ isLoading && <InputRightElement>
{ loadingIcon || <Spinner /> }
</InputRightElement> }
</InputGroup>;
}
)
const AutoCompleteInputComponent = forwardRef((props, forwardedRef) => {
const { isLoading } = useAutoCompleteContext();
const { loadingIcon, ...inputProps } = props;

return (
<InputGroup>
<Input {...inputProps} ref={forwardedRef} />
{isLoading && (
<InputRightElement>{loadingIcon || <Spinner />}</InputRightElement>
)}
</InputGroup>
);
});

export const AutoCompleteInput = forwardRef<AutoCompleteInputProps, "input">(
(props, forwardedRef) => {
@@ -47,9 +46,9 @@ export const AutoCompleteInput = forwardRef<AutoCompleteInputProps, "input">(
inputRef,
getInputProps,
tags,
setQuery,
value,
itemList
setQuery,
value,
itemList,
} = useAutoCompleteContext();

// const ref = useMergeRefs(forwardedRef, inputRef);
@@ -64,7 +63,10 @@ export const AutoCompleteInput = forwardRef<AutoCompleteInputProps, "input">(
const { value: inputValue } = rest;

useEffect(() => {
if(value !== undefined && (typeof value === 'string' || value instanceof String)) {
if (
value !== undefined &&
(typeof value === "string" || value instanceof String)
) {
const item = itemList.find(l => l.value === value);

const newQuery = item === undefined ? value : item.label;
@@ -74,7 +76,10 @@ export const AutoCompleteInput = forwardRef<AutoCompleteInputProps, "input">(
}, [value]);

useEffect(() => {
if(inputValue !== undefined && (typeof inputValue === 'string' || inputValue instanceof String)) {
if (
inputValue !== undefined &&
(typeof inputValue === "string" || inputValue instanceof String)
) {
setQuery(inputValue);
}
}, [inputValue]);
@@ -97,17 +102,29 @@ export const AutoCompleteInput = forwardRef<AutoCompleteInputProps, "input">(
}

const simpleInput = (
<AutoCompleteInputComponent isInvalid={isInvalid} {...(inputProps as any)} ref={ref} />
<AutoCompleteInputComponent
isInvalid={isInvalid}
{...(inputProps as any)}
ref={ref}
/>
);

const multipleInput = (
<Wrap aria-invalid={isInvalid} {...wrapperProps} ref={wrapperRef}>
{children}
<WrapItem as={AutoCompleteInputComponent} {...(inputProps as any)} ref={ref} />
<WrapItem
as={AutoCompleteInputComponent}
{...(inputProps as any)}
ref={ref}
/>
</Wrap>
);

return <PopoverAnchor>{autoCompleteProps.multiple ? multipleInput : simpleInput}</PopoverAnchor>;
return (
<PopoverAnchor>
{autoCompleteProps.multiple ? multipleInput : simpleInput}
</PopoverAnchor>
);
}
);

2 changes: 1 addition & 1 deletion src/autocomplete-item.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import {
forwardRef,
useMergeRefs,
} from "@chakra-ui/react";
import { isUndefined, omit } from "@chakra-ui/utils";
import { isUndefined, omit } from "./utils";
import React, { useEffect, useRef } from "react";

import { useAutoCompleteContext } from "./autocomplete-context";
2 changes: 1 addition & 1 deletion src/autocomplete-tag.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { WrapItem } from "@chakra-ui/layout";
import { Tag, TagCloseButton, TagLabel, TagProps } from "@chakra-ui/tag";
import { runIfFn } from "@chakra-ui/utils";
import { runIfFn } from "./utils";
import React, { memo } from "react";

type AutoCompleteTagProps = {
14 changes: 7 additions & 7 deletions src/autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { useImperativeHandle } from "react";
import { MaybeRenderProp } from "@chakra-ui/react-utils";

import { AutoCompleteProvider } from "./autocomplete-context";
import { useAutoComplete } from "./use-autocomplete";
import { chakra, forwardRef, Popover } from "@chakra-ui/react";
import { AutoCompleteRefMethods, UseAutoCompleteProps } from "./types";
import {
AutoCompleteRefMethods,
UseAutoCompleteProps,
MaybeRenderProp,
} from "./types";

export type AutoCompleteChildProps = {
isOpen: boolean;
@@ -24,7 +27,7 @@ export const AutoComplete = forwardRef<AutoCompleteProps, "div">(
isOpen,
onClose,
onOpen,
placement,
placement,
resetItems,
removeItem,
} = context;
@@ -48,10 +51,7 @@ export const AutoComplete = forwardRef<AutoCompleteProps, "div">(
closeOnBlur={true}
matchWidth={matchWidth}
>
<chakra.div
w="full"
ref={ref}
>
<chakra.div w="full" ref={ref}>
{children}
</chakra.div>
</Popover>
2 changes: 1 addition & 1 deletion src/helpers/group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isDefined, runIfFn } from "@chakra-ui/utils";
import { isDefined, runIfFn } from "../utils";
import { getChildDeep } from "react-nanny";
import { ReactNode } from "react";
import { getDefItemValue } from "./items";
7 changes: 5 additions & 2 deletions src/helpers/items.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { getChildrenDeep } from "react-nanny";
import { pick, isDefined, isEmpty } from "@chakra-ui/utils";
import { pick, isDefined, isEmpty } from "../utils";
import { ReactNode } from "react";
import { FlexProps } from "@chakra-ui/react";
import { fuzzyScore } from "./fuzzySearch";
import { Item } from "../types";
import { AutoCompleteItemProps } from "../autocomplete-item";

export const getDefItemValue = (item: AutoCompleteItemProps["value"]) =>
(typeof item === "string" || typeof item === "number" ? item : item[Object.keys(item)[0]])?.toString();
(typeof item === "string" || typeof item === "number"
? item
: item[Object.keys(item)[0]]
)?.toString();

export const setEmphasis = (children: any, query: string) => {
if (typeof children !== "string" || isEmpty(query)) {
15 changes: 10 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -4,16 +4,21 @@ import {
InputProps,
PlacementWithLogical,
SystemStyleObject,
WrapProps
WrapProps,
} from "@chakra-ui/react";
import { MaybeRenderProp } from "@chakra-ui/react-utils";
import React, { Dispatch, SetStateAction } from "react";

import { AutoCompleteProps } from "./autocomplete";
import { AutoCompleteGroupProps } from "./autocomplete-group";
import { AutoCompleteInputProps } from "./autocomplete-input";
import { AutoCompleteItemProps } from "./autocomplete-item";

export type Dict<T = any> = Record<string, T>;

export type MaybeRenderProp<P> =
| React.ReactNode
| ((props: P) => React.ReactNode);

export interface Item {
value: any;
label?: any;
@@ -28,7 +33,7 @@ export interface Item {
export type UseAutoCompleteProps = Partial<{
closeOnBlur: boolean;
closeOnSelect: boolean;
prefocusFirstItem: boolean
prefocusFirstItem: boolean;
creatable: boolean;
defaultEmptyStateProps: FlexProps;
defaultIsOpen: boolean;
@@ -44,7 +49,7 @@ export type UseAutoCompleteProps = Partial<{
) => boolean;
focusInputOnSelect: boolean;
freeSolo: boolean;
isLoading: boolean,
isLoading: boolean;
isReadOnly: boolean;
listAllValuesOnFocus: boolean;
matchWidth: boolean;
@@ -124,7 +129,7 @@ export type GroupReturnProps = {
export type UseAutoCompleteReturn = {
autoCompleteProps: AutoCompleteProps;
children: React.ReactNode;
defaultEmptyStateProps: FlexProps,
defaultEmptyStateProps: FlexProps;
filteredList: Item[];
filteredResults: Item[];
focusedValue: Item["value"];
113 changes: 55 additions & 58 deletions src/use-autocomplete.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import {
isUndefined,
omit,
runIfFn,
} from "@chakra-ui/utils";
} from "./utils";

import { useEffect, useMemo, useRef, useState } from "react";

@@ -44,7 +44,7 @@ export function useAutoComplete(
creatable,
emphasize,
emptyState = true,
defaultEmptyStateProps = {},
defaultEmptyStateProps = {},
freeSolo,
isReadOnly,
listAllValuesOnFocus,
@@ -74,11 +74,15 @@ export function useAutoComplete(

const { isOpen, onClose, onOpen } = useDisclosure({ defaultIsOpen });

const children = useMemo(() => runIfFn(autoCompleteProps.children, {
isOpen,
onClose,
onOpen,
}), [autoCompleteProps.children, isOpen]);
const children = useMemo(
() =>
runIfFn(autoCompleteProps.children, {
isOpen,
onClose,
onOpen,
}),
[autoCompleteProps.children, isOpen]
);
const itemList: Item[] = useMemo(() => getItemList(children), [children]);

const inputRef = useRef<HTMLInputElement>(null);
@@ -94,41 +98,45 @@ export function useAutoComplete(
else if (!isUndefined(valuesProp)) defaultQuery = valuesProp[0];

const [query, setQuery] = useState<string>(defaultQuery ?? "");
const filteredResults = useMemo(() =>
disableFilter
? itemList
: itemList
.filter(
i =>
i.fixed ||
runIfFn(
autoCompleteProps.filter || defaultFilterMethod,
query,
i.value,
i.label
) ||
listAll
)
.filter((i, index) =>
maxSuggestions ? i.fixed || index < maxSuggestions : true
), [query, itemList, listAll, maxSuggestions, disableFilter]);

// Add Creatable to Filtered List
const filteredResults = useMemo(
() =>
disableFilter
? itemList
: itemList
.filter(
i =>
i.fixed ||
runIfFn(
autoCompleteProps.filter || defaultFilterMethod,
query,
i.value,
i.label
) ||
listAll
)
.filter((i, index) =>
maxSuggestions ? i.fixed || index < maxSuggestions : true
),
[query, itemList, listAll, maxSuggestions, disableFilter]
);

// Add Creatable to Filtered List
const creatableArr: Item[] = creatable
? [{ value: query, noFilter: true, creatable: true }]
: [];

const filteredList = useMemo(() => {
return [...filteredResults, ...creatableArr]
return [...filteredResults, ...creatableArr];
}, [filteredResults, creatableArr]);
const [values, setValues] = useControllableState({
defaultValue: defaultValues.map(v => v?.toString()),
value: valuesProp,
onChange: (newValues: any[]) => {
const item = filteredList.find(opt => opt.value === newValues[0]);
if (!item) return;
const items = newValues.map(val =>
filteredList.find(opt => opt.value === val)
);
) as Item[];
runIfFn(
autoCompleteProps.onChange,
multiple ? newValues : newValues[0],
@@ -138,15 +146,13 @@ export function useAutoComplete(
});

useEffect(() => {
if(filteredList.length === 0 && !emptyState && isOpen) {
if (filteredList.length === 0 && !emptyState && isOpen) {
onClose();
}
}, [filteredList.length, emptyState, isOpen]);

const [focusedValue, setFocusedValue] = useState<Item["value"]>(
prefocusFirstItem
? itemList[0]?.value
: null
prefocusFirstItem ? itemList[0]?.value : null
);

const maxSelections = autoCompleteProps.maxSelections || values.length + 1;
@@ -171,25 +177,19 @@ export function useAutoComplete(

useEffect(() => {
if (isFocusedValueNotInList)
setFocusedValue(
prefocusFirstItem
? itemList[0]?.value
: null
);
}, [isFocusedValueNotInList])
setFocusedValue(prefocusFirstItem ? itemList[0]?.value : null);
}, [isFocusedValueNotInList]);

useUpdateEffect(() => {
if (prefocusFirstItem)
setFocusedValue(firstItem?.value);
if (prefocusFirstItem) setFocusedValue(firstItem?.value);
}, [query, firstItem?.value]);

useEffect(() => {
if (!isOpen && prefocusFirstItem)
setFocusedValue(itemList[0]?.value);
if (!isOpen && prefocusFirstItem) setFocusedValue(itemList[0]?.value);
}, [isOpen]);

useEffect(() => {
if(isOpen && listAllValuesOnFocus) {
if (isOpen && listAllValuesOnFocus) {
setListAll(true);
}
}, [isOpen, listAllValuesOnFocus, setListAll]);
@@ -238,6 +238,8 @@ export function useAutoComplete(
) => {
setValues(prevValues => {
const item = itemList.find(opt => opt.value === itemValue);
if (!item) return prevValues;

runIfFn(autoCompleteProps.onTagRemoved, itemValue, item, prevValues);
return prevValues.filter(i => i !== itemValue);
});
@@ -256,7 +258,8 @@ export function useAutoComplete(

const tags = multiple
? values.map(tag => ({
label: itemList.find(item => item.value === tag?.toString())?.label || tag,
label:
itemList.find(item => item.value === tag?.toString())?.label || tag,
onRemove: () => removeItem(tag),
}))
: [];
@@ -339,19 +342,15 @@ export function useAutoComplete(
}

if (key === "ArrowDown") {
if(!isOpen)
onOpen();
else
setFocusedValue(nextItem?.value);
if (!isOpen) onOpen();
else setFocusedValue(nextItem?.value);
e.preventDefault();
return;
}

if (key === "ArrowUp") {
if(!isOpen)
onOpen();
else
setFocusedValue(prevItem?.value);
if (!isOpen) onOpen();
else setFocusedValue(prevItem?.value);

e.preventDefault();
return;
@@ -360,8 +359,7 @@ export function useAutoComplete(
if (key === "Tab") {
if (isOpen && focusedItem && !focusedItem?.disabled)
selectItem(focusedItem?.value);
else
onClose();
else onClose();

return;
}
@@ -483,14 +481,13 @@ export function useAutoComplete(
}
};


return {
autoCompleteProps,
children,
filteredList,
filteredResults,
focusedValue,
defaultEmptyStateProps,
defaultEmptyStateProps,
getEmptyStateProps,
getGroupProps,
getInputProps,
@@ -509,7 +506,7 @@ export function useAutoComplete(
resetItems,
setQuery,
tags,
value,
value,
values,
};
}
149 changes: 149 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Dict } from "src/types";

export function getFirstItem<T>(array: T[]): T | undefined {
return array?.[0];
}

export function getLastItem<T>(array: T[]): T | undefined {
return array?.length ? array[array.length - 1] : undefined;
}

/**
* 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 to move (positive or negative).
* @param loop - Whether to loop around when the index exceeds boundaries.
*/
export function getNextIndex(
currentIndex: number,
length: number,
step = 1,
loop = true
): number {
if (length === 0) return -1;

let nextIndex = currentIndex + step;

if (currentIndex === -1) {
nextIndex = step > 0 ? 0 : length - 1;
}

if (loop) {
nextIndex = ((nextIndex % length) + length) % length;
} else {
nextIndex = Math.max(0, Math.min(nextIndex, length - 1));
}

return nextIndex;
}

/**
* Gets the previous index based on the current index.
* Mostly used for keyboard navigation.
*
* @param currentIndex - The current index.
* @param length - The total length or count of items.
* @param loop - Whether to loop around when the index exceeds boundaries.
*/
export function getPrevIndex(
currentIndex: number,
length: number,
loop = true
): number {
return getNextIndex(currentIndex, length, -1, loop);
}

export function getNextItem<T>(
currentIndex: number,
array: T[],
loop = true
): T | undefined {
const nextIndex = getNextIndex(currentIndex, array.length, 1, loop);
return array[nextIndex];
}

export function getPrevItem<T>(
currentIndex: number,
array: T[],
loop = true
): T | undefined {
const prevIndex = getPrevIndex(currentIndex, array.length, loop);
return array[prevIndex];
}

export function isArray(value: any): value is Array<any> {
return Array.isArray(value);
}

export function isEmptyArray(value: any): boolean {
return isArray(value) && value.length === 0;
}

export function isObject(value: any): value is Dict {
return value !== null && typeof value === "object" && !isArray(value);
}

export function isEmptyObject(value: any): boolean {
return isObject(value) && Object.keys(value).length === 0;
}

export function isEmpty(value: any): boolean {
if (isArray(value)) return isEmptyArray(value);
if (isObject(value)) return isEmptyObject(value);
return value == null || value === "";
}

export function isUndefined(value: any): value is undefined {
return typeof value === "undefined";
}

export function isDefined<T>(value: T): value is Exclude<T, undefined> {
return typeof value !== "undefined";
}

export function pick<T extends Dict, K extends keyof T>(
object: T,
keys: K[]
): { [P in K]: T[P] } {
const result = {} as { [P in K]: T[P] };

keys.forEach(key => {
if (key in object) {
result[key] = object[key];
}
});

return result;
}

export function omit<T extends Dict, K extends keyof T>(
object: T,
keys: K[]
): Omit<T, K> {
const result = { ...object };

keys.forEach(key => {
delete result[key];
});

return result;
}

export function isFunction<T extends Function = Function>(
value: any
): value is T {
return typeof value === "function";
}

export type MaybeFunction<T, Args extends unknown[] = []> =
| T
| ((...args: Args) => T);

export function runIfFn<T, Args extends unknown[]>(
valueOrFn: MaybeFunction<T, Args>,
...args: Args
): T {
return isFunction(valueOrFn) ? valueOrFn(...args) : valueOrFn;
}

0 comments on commit b0feb24

Please sign in to comment.