219 lines
8.6 KiB
JavaScript
219 lines
8.6 KiB
JavaScript
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
|
|
};
|
|
}
|