72 lines
2.5 KiB
JavaScript
72 lines
2.5 KiB
JavaScript
import { isHTMLElement } from '@fluentui/react-utilities';
|
|
import { KEYBORG_FOCUSIN, createKeyborg, disposeKeyborg } from 'keyborg';
|
|
import { FOCUS_VISIBLE_ATTR } from './constants';
|
|
/**
|
|
* @internal
|
|
* @param scope - Applies the ponyfill to all DOM children
|
|
* @param targetWindow - window
|
|
*/ export function applyFocusVisiblePolyfill(scope, targetWindow) {
|
|
if (alreadyInScope(scope)) {
|
|
// Focus visible polyfill already applied at this scope
|
|
return ()=>undefined;
|
|
}
|
|
const state = {
|
|
current: undefined
|
|
};
|
|
const keyborg = createKeyborg(targetWindow);
|
|
function registerElementIfNavigating(el) {
|
|
if (keyborg.isNavigatingWithKeyboard() && isHTMLElement(el)) {
|
|
state.current = el;
|
|
el.setAttribute(FOCUS_VISIBLE_ATTR, '');
|
|
}
|
|
}
|
|
function disposeCurrentElement() {
|
|
if (state.current) {
|
|
state.current.removeAttribute(FOCUS_VISIBLE_ATTR);
|
|
state.current = undefined;
|
|
}
|
|
}
|
|
// When navigation mode changes remove the focus-visible selector
|
|
keyborg.subscribe((isNavigatingWithKeyboard)=>{
|
|
if (!isNavigatingWithKeyboard) {
|
|
disposeCurrentElement();
|
|
}
|
|
});
|
|
// Keyborg's focusin event is delegated so it's only registered once on the window
|
|
// and contains metadata about the focus event
|
|
const keyborgListener = (e)=>{
|
|
disposeCurrentElement();
|
|
const target = e.composedPath()[0];
|
|
registerElementIfNavigating(target);
|
|
};
|
|
// Make sure that when focus leaves the scope, the focus visible class is removed
|
|
const blurListener = (e)=>{
|
|
if (!e.relatedTarget || isHTMLElement(e.relatedTarget) && !scope.contains(e.relatedTarget)) {
|
|
disposeCurrentElement();
|
|
}
|
|
};
|
|
scope.addEventListener(KEYBORG_FOCUSIN, keyborgListener);
|
|
scope.addEventListener('focusout', blurListener);
|
|
scope.focusVisible = true;
|
|
if (scope.contains(targetWindow.document.activeElement)) {
|
|
registerElementIfNavigating(targetWindow.document.activeElement);
|
|
}
|
|
// Return disposer
|
|
return ()=>{
|
|
disposeCurrentElement();
|
|
scope.removeEventListener(KEYBORG_FOCUSIN, keyborgListener);
|
|
scope.removeEventListener('focusout', blurListener);
|
|
delete scope.focusVisible;
|
|
disposeKeyborg(keyborg);
|
|
};
|
|
}
|
|
function alreadyInScope(el) {
|
|
if (!el) {
|
|
return false;
|
|
}
|
|
if (el.focusVisible) {
|
|
return true;
|
|
}
|
|
return alreadyInScope(el === null || el === void 0 ? void 0 : el.parentElement);
|
|
}
|