import React, { createContext, ReactNode, useRef, useState } from 'react'; import { CHARACTER_MATCHING, STRING_MATCHING, __DEV__ } from '../const'; import { ReactSearchHighlightProvider, useReactSearchHighlight } from '../context'; import Text from '../elements/Text'; import { useDidMountEffect, useKeyDown, useOffScreen, useRefComposition } from '../hooks'; import { ContextType } from '../types'; /** * InternalContext manages internal states between the components * without exposing methods to global states */ interface InternalContextInitialStateType { isPopoverExpanded: boolean; listItemActiveIndex: number; updateInternalContext: any; /** * Need for popover list active index keydown */ dataLength: number; /** * Need while rendering highlighted words */ matchingAlgorithm?: typeof CHARACTER_MATCHING | typeof STRING_MATCHING; } const InternalContextInitialState: InternalContextInitialStateType = { isPopoverExpanded: false, listItemActiveIndex: 0, updateInternalContext: (key: string, value: any) => {}, dataLength: 0 }; export const InternalContext = createContext(InternalContextInitialState); type D = (values: ContextType) => JSX.Element; interface WrapperProps { children: D | any; isFunction: boolean; } const WrapperInner = React.forwardRef( ({children, isFunction}, forwardedRef) => { const [state, setState] = useState(InternalContextInitialState); const __state = useReactSearchHighlight(); const updateInternalContext = ( key: keyof InternalContextInitialStateType, value: any ) => { setState(prev => ({...prev, [key]: value})); }; const listRef = useRef(null); const composedRefs = useRefComposition([listRef, forwardedRef]); const [isListVisible, setIsListVisible] = useOffScreen(listRef); useDidMountEffect(() => { updateInternalContext('isPopoverExpanded', isListVisible); }, [isListVisible]); return (
setIsListVisible(true)} > {isFunction ? children(__state) : children}
); } ); export const Wrapper = React.forwardRef< HTMLDivElement, Pick >(({children}: Pick, forwardedRef) => { const isFunction = typeof children === 'function'; return ( <> {isFunction ? ( ) : ( )} ); }); export interface PopOverListProps extends React.OlHTMLAttributes { children: ReactNode; } export const PopOverList = React.forwardRef( ({children, ...any}, forwardedRef) => { const {suggestions} = useReactSearchHighlight(); const { isPopoverExpanded, listItemActiveIndex, updateInternalContext } = React.useContext(InternalContext); const listItemActiveIndexArrowDown = () => { if (listItemActiveIndex < suggestions.length - 1) updateInternalContext('listItemActiveIndex', listItemActiveIndex + 1); }; const listItemActiveIndexArrowUp = () => { if (listItemActiveIndex > 0) updateInternalContext('listItemActiveIndex', listItemActiveIndex - 1); }; useKeyDown(listItemActiveIndexArrowDown, false, 'arrowdown', [ listItemActiveIndex, suggestions.length ]); useKeyDown(listItemActiveIndexArrowUp, false, 'arrowup', [ listItemActiveIndex ]); return ( <> {isPopoverExpanded && (
    {children}
)} ); } ); export interface PopOverOptionProps extends React.LiHTMLAttributes { // React element node children: ReactNode; // Determines the navigation index used for internal list navigation optionIndex: number; } export const PopOverOption: React.FC = ({ children, optionIndex, ...any }) => { const {updateInternalContext, listItemActiveIndex} = React.useContext( InternalContext ); if (__DEV__ && typeof optionIndex !== 'number') throw new ReferenceError( `optionIndex type is invalid: expected a number but got: ${typeof optionIndex}. ` + `To avoid the error please provide optionIndex in PopOverOption component` + `\n\t{state.searchData?.map((item, index) => (` + `\n\t\t\t { updateInternalContext('listItemActiveIndex', optionIndex); }; return (
  • {children}
  • ); }; export interface PopOverOptionTextProps extends React.HTMLAttributes { className?: string; // Determines text value to render with highlight value: string; // Determine type of html element ex: p, h1, h2 as: string; } export const PopOverOptionText: React.FC = ({ className, value, as = 'h3', ...any }) => { const {input} = useReactSearchHighlight(); const {matchingAlgorithm} = React.useContext(InternalContext); const words = value?.split(//); const isHighlighted = (word: string) => { return matchingAlgorithm === STRING_MATCHING ? word.toLocaleLowerCase() === input : input.includes(word.toLocaleLowerCase()); }; return ( {words?.map((word, index) => isHighlighted(word) ? ( {word} ) : ( {word} ) )} ); };