@loke/ui
Version:
2 lines (1 loc) • 6.03 kB
JavaScript
import{useComposedRefs}from"@loke/ui/compose-refs";import{Primitive}from"@loke/ui/primitive";import{useCallbackRef}from"@loke/ui/use-callback-ref";import{forwardRef,useCallback,useEffect,useRef,useState}from"react";import{jsx}from"react/jsx-runtime";var AUTOFOCUS_ON_MOUNT="focusScope.autoFocusOnMount",AUTOFOCUS_ON_UNMOUNT="focusScope.autoFocusOnUnmount",EVENT_OPTIONS={bubbles:!1,cancelable:!0},FOCUS_SCOPE_NAME="FocusScope",FocusScope=forwardRef((props,forwardedRef)=>{let{loop=!1,trapped=!1,onMountAutoFocus:onMountAutoFocusProp,onUnmountAutoFocus:onUnmountAutoFocusProp,...scopeProps}=props,[container,setContainer]=useState(null),onMountAutoFocus=useCallbackRef(onMountAutoFocusProp),onUnmountAutoFocus=useCallbackRef(onUnmountAutoFocusProp),lastFocusedElementRef=useRef(null),composedRefs=useComposedRefs(forwardedRef,(node)=>setContainer(node)),focusScope=useRef({pause(){this.paused=!0},paused:!1,resume(){this.paused=!1}}).current;useEffect(()=>{if(trapped){let handleFocusIn=function(event){if(focusScope.paused||!container)return;let target=event.target;if(container.contains(target))lastFocusedElementRef.current=target;else focus(lastFocusedElementRef.current,{select:!0})},handleFocusOut=function(event){if(focusScope.paused||!container)return;let relatedTarget=event.relatedTarget;if(relatedTarget===null)return;if(!container.contains(relatedTarget))focus(lastFocusedElementRef.current,{select:!0})},handleMutations=function(mutations){if(document.activeElement!==document.body)return;for(let mutation of mutations)if(mutation.removedNodes.length>0)focus(container)};document.addEventListener("focusin",handleFocusIn),document.addEventListener("focusout",handleFocusOut);let mutationObserver=new MutationObserver(handleMutations);if(container)mutationObserver.observe(container,{childList:!0,subtree:!0});return()=>{document.removeEventListener("focusin",handleFocusIn),document.removeEventListener("focusout",handleFocusOut),mutationObserver.disconnect()}}},[trapped,container,focusScope.paused]),useEffect(()=>{if(container){focusScopesStack.add(focusScope);let previouslyFocusedElement=document.activeElement;if(!container.contains(previouslyFocusedElement)){let mountEvent=new CustomEvent(AUTOFOCUS_ON_MOUNT,EVENT_OPTIONS);if(container.addEventListener(AUTOFOCUS_ON_MOUNT,onMountAutoFocus),container.dispatchEvent(mountEvent),!mountEvent.defaultPrevented){if(focusFirst(removeLinks(getTabbableCandidates(container)),{select:!0}),document.activeElement===previouslyFocusedElement)focus(container)}}return()=>{container.removeEventListener(AUTOFOCUS_ON_MOUNT,onMountAutoFocus),setTimeout(()=>{let unmountEvent=new CustomEvent(AUTOFOCUS_ON_UNMOUNT,EVENT_OPTIONS);if(container.addEventListener(AUTOFOCUS_ON_UNMOUNT,onUnmountAutoFocus),container.dispatchEvent(unmountEvent),!unmountEvent.defaultPrevented)focus(previouslyFocusedElement??document.body,{select:!0});container.removeEventListener(AUTOFOCUS_ON_UNMOUNT,onUnmountAutoFocus),focusScopesStack.remove(focusScope)},0)}}},[container,onMountAutoFocus,onUnmountAutoFocus,focusScope]);let handleKeyDown=useCallback((event)=>{if(!(loop||trapped))return;if(focusScope.paused)return;let isTabKey=event.key==="Tab"&&!event.altKey&&!event.ctrlKey&&!event.metaKey,focusedElement=document.activeElement;if(isTabKey&&focusedElement){let currentContainer=event.currentTarget,[first,last]=getTabbableEdges(currentContainer);if(first&&last){if(!event.shiftKey&&focusedElement===last){if(event.preventDefault(),loop)focus(first,{select:!0})}else if(event.shiftKey&&focusedElement===first){if(event.preventDefault(),loop)focus(last,{select:!0})}}else if(focusedElement===container)event.preventDefault()}},[loop,trapped,focusScope.paused]);return jsx(Primitive.div,{tabIndex:-1,...scopeProps,onKeyDown:handleKeyDown,ref:composedRefs})});FocusScope.displayName=FOCUS_SCOPE_NAME;function focusFirst(candidates,{select=!1}={}){let previouslyFocusedElement=document.activeElement;for(let candidate of candidates)if(focus(candidate,{select}),document.activeElement!==previouslyFocusedElement)return}function getTabbableEdges(container){let candidates=getTabbableCandidates(container),first=findVisible(candidates,container),last=findVisible(candidates.reverse(),container);return[first,last]}function getTabbableCandidates(container){let nodes=[],walker=document.createTreeWalker(container,NodeFilter.SHOW_ELEMENT,{acceptNode:(node)=>{if(!(node instanceof HTMLElement))return NodeFilter.FILTER_SKIP;let isHiddenInput=node.tagName==="INPUT"&&node.type==="hidden";if(node.disabled||node.hidden||isHiddenInput)return NodeFilter.FILTER_SKIP;return node.tabIndex>=0?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});while(walker.nextNode())nodes.push(walker.currentNode);return nodes}function findVisible(elements,container){for(let element of elements)if(!isHidden(element,{upTo:container}))return element}function isHidden(node,{upTo}){if(getComputedStyle(node).visibility==="hidden")return!0;while(node){if(upTo!==void 0&&node===upTo)return!1;if(getComputedStyle(node).display==="none")return!0;node=node.parentElement}return!1}function isSelectableInput(element){return element instanceof HTMLInputElement&&"select"in element}function focus(element,{select=!1}={}){if(element?.focus){let previouslyFocusedElement=document.activeElement;if(element.focus({preventScroll:!0}),element!==previouslyFocusedElement&&isSelectableInput(element)&&select)element.select()}}var focusScopesStack=createFocusScopesStack();function createFocusScopesStack(){let stack=[];return{add(focusScope){let activeFocusScope=stack[0];if(focusScope!==activeFocusScope)activeFocusScope?.pause();stack=arrayRemove(stack,focusScope),stack.unshift(focusScope)},remove(focusScope){stack=arrayRemove(stack,focusScope),stack[0]?.resume()}}}function arrayRemove(array,item){let updatedArray=[...array],index=updatedArray.indexOf(item);if(index!==-1)updatedArray.splice(index,1);return updatedArray}function removeLinks(items){return items.filter((item)=>item.tagName!=="A")}var Root=FocusScope;export{Root,FocusScope};