@loke/ui
Version:
2 lines (1 loc) • 6.77 kB
JavaScript
import{createCollection}from"@loke/ui/collection";import{composeEventHandlers}from"@loke/ui/compose-events";import{useComposedRefs}from"@loke/ui/compose-refs";import{createContextScope}from"@loke/ui/context";import{Primitive}from"@loke/ui/primitive";import{useCallbackRef}from"@loke/ui/use-callback-ref";import{useControllableState}from"@loke/ui/use-controllable-state";import{useDirection}from"@loke/ui/use-direction";import{useId}from"@loke/ui/use-id";import{forwardRef,useCallback,useEffect,useRef,useState}from"react";import{jsx}from"react/jsx-runtime";var ENTRY_FOCUS="rovingFocusGroup.onEntryFocus",EVENT_OPTIONS={bubbles:!1,cancelable:!0},GROUP_NAME="RovingFocusGroup",[Collection,useCollection,createCollectionScope]=createCollection(GROUP_NAME),[createRovingFocusGroupContext,createRovingFocusGroupScope]=createContextScope(GROUP_NAME,[createCollectionScope]),[RovingFocusProvider,useRovingFocusContext]=createRovingFocusGroupContext(GROUP_NAME),RovingFocusGroup=forwardRef((props,forwardedRef)=>{return jsx(Collection.Provider,{scope:props.__scopeRovingFocusGroup,children:jsx(Collection.Slot,{scope:props.__scopeRovingFocusGroup,children:jsx(RovingFocusGroupImpl,{...props,ref:forwardedRef})})})});RovingFocusGroup.displayName=GROUP_NAME;var RovingFocusGroupImpl=forwardRef((props,forwardedRef)=>{let{__scopeRovingFocusGroup,orientation,loop=!1,dir,currentTabStopId:currentTabStopIdProp,defaultCurrentTabStopId,onCurrentTabStopIdChange,onEntryFocus,preventScrollOnEntryFocus=!1,...groupProps}=props,ref=useRef(null),composedRefs=useComposedRefs(forwardedRef,ref),direction=useDirection(dir),[currentTabStopId,setCurrentTabStopId]=useControllableState({caller:GROUP_NAME,defaultProp:defaultCurrentTabStopId??null,onChange:onCurrentTabStopIdChange,prop:currentTabStopIdProp}),[isTabbingBackOut,setIsTabbingBackOut]=useState(!1),handleEntryFocus=useCallbackRef(onEntryFocus),getItems=useCollection(__scopeRovingFocusGroup),isClickFocusRef=useRef(!1),[focusableItemsCount,setFocusableItemsCount]=useState(0);return useEffect(()=>{let node=ref.current;if(node)return node.addEventListener(ENTRY_FOCUS,handleEntryFocus),()=>node.removeEventListener(ENTRY_FOCUS,handleEntryFocus)},[handleEntryFocus]),jsx(RovingFocusProvider,{currentTabStopId,dir:direction,loop,onFocusableItemAdd:useCallback(()=>setFocusableItemsCount((prevCount)=>prevCount+1),[]),onFocusableItemRemove:useCallback(()=>setFocusableItemsCount((prevCount)=>prevCount-1),[]),onItemFocus:useCallback((tabStopId)=>setCurrentTabStopId(tabStopId),[setCurrentTabStopId]),onItemShiftTab:useCallback(()=>setIsTabbingBackOut(!0),[]),orientation,scope:__scopeRovingFocusGroup,children:jsx(Primitive.div,{"data-orientation":orientation,tabIndex:isTabbingBackOut||focusableItemsCount===0?-1:0,...groupProps,onBlur:composeEventHandlers(props.onBlur,()=>setIsTabbingBackOut(!1)),onFocus:composeEventHandlers(props.onFocus,(event)=>{let isKeyboardFocus=!isClickFocusRef.current;if(event.target===event.currentTarget&&isKeyboardFocus&&!isTabbingBackOut){let entryFocusEvent=new CustomEvent(ENTRY_FOCUS,EVENT_OPTIONS);if(event.currentTarget.dispatchEvent(entryFocusEvent),!entryFocusEvent.defaultPrevented){let items=getItems().filter((item)=>item.focusable),activeItem=items.find((item)=>item.active),currentItem=items.find((item)=>item.id===currentTabStopId),candidateNodes=[activeItem,currentItem,...items].filter(Boolean).map((item)=>item.ref.current).filter((node)=>node!==null);focusFirst(candidateNodes,preventScrollOnEntryFocus)}}isClickFocusRef.current=!1}),onMouseDown:composeEventHandlers(props.onMouseDown,()=>{isClickFocusRef.current=!0}),ref:composedRefs,style:{outline:"none",...props.style}})})}),ITEM_NAME="RovingFocusGroupItem",RovingFocusGroupItem=forwardRef((props,forwardedRef)=>{let{__scopeRovingFocusGroup,focusable=!0,active=!1,tabStopId,children,...itemProps}=props,autoId=useId(),id=tabStopId||autoId,context=useRovingFocusContext(ITEM_NAME,__scopeRovingFocusGroup),isCurrentTabStop=context.currentTabStopId===id,getItems=useCollection(__scopeRovingFocusGroup),{onFocusableItemAdd,onFocusableItemRemove,currentTabStopId}=context;return useEffect(()=>{if(focusable)return onFocusableItemAdd(),()=>onFocusableItemRemove()},[focusable,onFocusableItemAdd,onFocusableItemRemove]),jsx(Collection.ItemSlot,{active,focusable,id,scope:__scopeRovingFocusGroup,children:jsx(Primitive.span,{"data-orientation":context.orientation,tabIndex:isCurrentTabStop?0:-1,...itemProps,onFocus:composeEventHandlers(props.onFocus,()=>context.onItemFocus(id)),onKeyDown:composeEventHandlers(props.onKeyDown,(event)=>{if(event.key==="Tab"&&event.shiftKey){context.onItemShiftTab();return}if(event.target!==event.currentTarget)return;let focusIntent=getFocusIntent(event,context.orientation,context.dir);if(focusIntent!==void 0){if(event.metaKey||event.ctrlKey||event.altKey||event.shiftKey)return;event.preventDefault();let candidateNodes=getItems().filter((item)=>item.focusable).map((item)=>item.ref.current).filter((node)=>node!==null);if(focusIntent==="last")candidateNodes.reverse();else if(focusIntent==="prev"||focusIntent==="next"){if(focusIntent==="prev")candidateNodes.reverse();let currentIndex=candidateNodes.indexOf(event.currentTarget);candidateNodes=context.loop?wrapArray(candidateNodes,currentIndex+1):candidateNodes.slice(currentIndex+1)}setTimeout(()=>focusFirst(candidateNodes))}}),onMouseDown:composeEventHandlers(props.onMouseDown,(event)=>{if(focusable)context.onItemFocus(id);else event.preventDefault()}),ref:forwardedRef,children:typeof children==="function"?children({hasTabStop:currentTabStopId!=null,isCurrentTabStop}):children})})});RovingFocusGroupItem.displayName=ITEM_NAME;var MAP_KEY_TO_FOCUS_INTENT={ArrowDown:"next",ArrowLeft:"prev",ArrowRight:"next",ArrowUp:"prev",End:"last",Home:"first",PageDown:"last",PageUp:"first"};function getDirectionAwareKey(key,dir){if(dir!=="rtl")return key;if(key==="ArrowLeft")return"ArrowRight";if(key==="ArrowRight")return"ArrowLeft";return key}function getFocusIntent(event,orientation,dir){let key=getDirectionAwareKey(event.key,dir);if(orientation==="vertical"&&["ArrowLeft","ArrowRight"].includes(key))return;if(orientation==="horizontal"&&["ArrowUp","ArrowDown"].includes(key))return;return MAP_KEY_TO_FOCUS_INTENT[key]}function focusFirst(candidates,preventScroll=!1){let PREVIOUSLY_FOCUSED_ELEMENT=document.activeElement;for(let candidate of candidates){if(candidate===PREVIOUSLY_FOCUSED_ELEMENT)return;if(candidate.focus({preventScroll}),document.activeElement!==PREVIOUSLY_FOCUSED_ELEMENT)return}}function wrapArray(array,startIndex){return array.map((_,index)=>array[(startIndex+index)%array.length])}var Root=RovingFocusGroup,Item=RovingFocusGroupItem;export{createRovingFocusGroupScope,RovingFocusGroupItem,RovingFocusGroup,Root,Item};