import * as React from 'react'; import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; import { useOnKeyboardNavigationChange } from '@fluentui/react-tabster'; import { useOptionWalker } from './useOptionWalker'; import { ACTIVEDESCENDANT_ATTRIBUTE, ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE } from './constants'; import { scrollIntoView } from './scrollIntoView'; export const createActiveDescendantChangeEvent = (detail)=>new CustomEvent('activedescendantchange', { bubbles: true, cancelable: false, composed: true, detail }); export function useActiveDescendant(options) { const { imperativeRef, matchOption: matchOptionUnstable } = options; const focusVisibleRef = React.useRef(false); const shouldShowFocusVisibleAttrRef = React.useRef(true); const activeIdRef = React.useRef(null); const lastActiveIdRef = React.useRef(null); const activeParentRef = React.useRef(null); const attributeVisibilityRef = React.useRef(true); const removeAttribute = React.useCallback(()=>{ var _activeParentRef_current; (_activeParentRef_current = activeParentRef.current) === null || _activeParentRef_current === void 0 ? void 0 : _activeParentRef_current.removeAttribute('aria-activedescendant'); }, []); const setAttribute = React.useCallback((id)=>{ if (id) { activeIdRef.current = id; } if (attributeVisibilityRef.current && activeIdRef.current) { var _activeParentRef_current; (_activeParentRef_current = activeParentRef.current) === null || _activeParentRef_current === void 0 ? void 0 : _activeParentRef_current.setAttribute('aria-activedescendant', activeIdRef.current); } }, []); useOnKeyboardNavigationChange((isNavigatingWithKeyboard)=>{ focusVisibleRef.current = isNavigatingWithKeyboard; const active = getActiveDescendant(); if (!active) { return; } if (isNavigatingWithKeyboard && shouldShowFocusVisibleAttrRef.current) { active.setAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, ''); } else { active.removeAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE); } }); const matchOption = useEventCallback(matchOptionUnstable); const listboxRef = React.useRef(null); const { optionWalker, listboxCallbackRef } = useOptionWalker({ matchOption }); const getActiveDescendant = React.useCallback(()=>{ var _listboxRef_current; return (_listboxRef_current = listboxRef.current) === null || _listboxRef_current === void 0 ? void 0 : _listboxRef_current.querySelector(`#${activeIdRef.current}`); }, [ listboxRef ]); const setShouldShowFocusVisibleAttribute = React.useCallback((shouldShow)=>{ shouldShowFocusVisibleAttrRef.current = shouldShow; const active = getActiveDescendant(); if (!active) { return; } if (shouldShow && focusVisibleRef.current) { active.setAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, ''); } else { active.removeAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE); } }, [ getActiveDescendant ]); const blurActiveDescendant = React.useCallback(()=>{ const active = getActiveDescendant(); if (active) { active.removeAttribute(ACTIVEDESCENDANT_ATTRIBUTE); active.removeAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE); } removeAttribute(); lastActiveIdRef.current = activeIdRef.current; activeIdRef.current = null; var _active_id; return (_active_id = active === null || active === void 0 ? void 0 : active.id) !== null && _active_id !== void 0 ? _active_id : null; }, [ getActiveDescendant, removeAttribute ]); const focusActiveDescendant = React.useCallback((nextActive)=>{ if (!nextActive) { return; } const previousActiveId = blurActiveDescendant(); scrollIntoView(nextActive); setAttribute(nextActive.id); nextActive.setAttribute(ACTIVEDESCENDANT_ATTRIBUTE, ''); if (focusVisibleRef.current && shouldShowFocusVisibleAttrRef.current) { nextActive.setAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, ''); } const event = createActiveDescendantChangeEvent({ id: nextActive.id, previousId: previousActiveId }); nextActive.dispatchEvent(event); }, [ blurActiveDescendant, setAttribute ]); const controller = React.useMemo(()=>({ first: ({ passive } = {})=>{ const first = optionWalker.first(); if (!passive) { focusActiveDescendant(first); } return first === null || first === void 0 ? void 0 : first.id; }, last: ({ passive } = {})=>{ const last = optionWalker.last(); if (!passive) { focusActiveDescendant(last); } return last === null || last === void 0 ? void 0 : last.id; }, next: ({ passive } = {})=>{ const active = getActiveDescendant(); if (!active) { return; } optionWalker.setCurrent(active); const next = optionWalker.next(); if (!passive) { focusActiveDescendant(next); } return next === null || next === void 0 ? void 0 : next.id; }, prev: ({ passive } = {})=>{ const active = getActiveDescendant(); if (!active) { return; } optionWalker.setCurrent(active); const next = optionWalker.prev(); if (!passive) { focusActiveDescendant(next); } return next === null || next === void 0 ? void 0 : next.id; }, blur: ()=>{ blurActiveDescendant(); }, active: ()=>{ var _getActiveDescendant; return (_getActiveDescendant = getActiveDescendant()) === null || _getActiveDescendant === void 0 ? void 0 : _getActiveDescendant.id; }, focus: (id)=>{ if (!listboxRef.current) { return; } const target = listboxRef.current.querySelector(`#${id}`); if (target) { focusActiveDescendant(target); } }, focusLastActive: ()=>{ if (!listboxRef.current || !lastActiveIdRef.current) { return; } const target = listboxRef.current.querySelector(`#${lastActiveIdRef.current}`); if (target) { focusActiveDescendant(target); return true; } }, find (predicate, { passive, startFrom } = {}) { const target = optionWalker.find(predicate, startFrom); if (!passive) { focusActiveDescendant(target); } return target === null || target === void 0 ? void 0 : target.id; }, scrollActiveIntoView: ()=>{ if (!listboxRef.current) { return; } const active = getActiveDescendant(); if (!active) { return; } scrollIntoView(active); }, showAttributes () { attributeVisibilityRef.current = true; setAttribute(); }, hideAttributes () { attributeVisibilityRef.current = false; removeAttribute(); }, showFocusVisibleAttributes () { setShouldShowFocusVisibleAttribute(true); }, hideFocusVisibleAttributes () { setShouldShowFocusVisibleAttribute(false); } }), [ optionWalker, listboxRef, setAttribute, removeAttribute, focusActiveDescendant, blurActiveDescendant, getActiveDescendant, setShouldShowFocusVisibleAttribute ]); React.useImperativeHandle(imperativeRef, ()=>controller); return { listboxRef: useMergedRefs(listboxRef, listboxCallbackRef), activeParentRef, controller }; }