Skip to content

Commit

Permalink
fix: focus visible polyfill should be initialised/disposed correctly (#…
Browse files Browse the repository at this point in the history
…29564)

* fix: focus visible polyfill should be initialised/disposed correctly

* Disposing focus visible polyfill should clear state and remove the
  focus visible attribute.
* Initializing focus visible polyfill should try to apply the focus
  visible attribute

Fixes #29402

* changefile

* refactor
  • Loading branch information
ling1726 authored Oct 19, 2023
1 parent 35d56d5 commit 0c71d4a
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: focus visible polyfill should be initialised/disposed correctly",
"packageName": "@fluentui/react-tabster",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createKeyborg, disposeKeyborg, Keyborg } from 'keyborg';
import { FOCUS_VISIBLE_ATTR } from './constants';
import { applyFocusVisiblePolyfill } from './focusVisiblePolyfill';
import { fireEvent } from '@testing-library/dom';

describe('focus visible polyfill', () => {
let keyborg: Keyborg;
beforeEach(() => {
keyborg = createKeyborg(window);
document.body.innerHTML = '';
});

afterEach(() => {
if (keyborg) {
disposeKeyborg(keyborg);
}
});

it('should set focus visible attribute on initialization if in keyboard navigation mode', () => {
const scope = document.createElement('div');
const button = document.createElement('button');
scope.append(button);
document.body.append(scope);

button.focus();
fireEvent.keyDown(window);
const dispose = applyFocusVisiblePolyfill(scope, window);

expect(button.hasAttribute(FOCUS_VISIBLE_ATTR)).toBe(true);

dispose();
});

it('should not set focus visible attribute on initialization if not in keyboard navigation mode', () => {
const scope = document.createElement('div');
const button = document.createElement('button');
scope.append(button);
document.body.append(scope);

button.focus();
const dispose = applyFocusVisiblePolyfill(scope, window);

expect(button.hasAttribute(FOCUS_VISIBLE_ATTR)).toBe(false);

dispose();
});

it('should remove focus visible attribute on dispose', () => {
const scope = document.createElement('div');
const button = document.createElement('button');
scope.append(button);
document.body.append(scope);

button.focus();
fireEvent.keyDown(window);
const dispose = applyFocusVisiblePolyfill(scope, window);

expect(button.hasAttribute(FOCUS_VISIBLE_ATTR)).toBe(true);
dispose();
expect(button.hasAttribute(FOCUS_VISIBLE_ATTR)).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,60 +38,59 @@ export function applyFocusVisiblePolyfill(scope: HTMLElement, targetWindow: Wind

const keyborg = createKeyborg(targetWindow);

function registerElementIfNavigating(el: EventTarget | HTMLElement | null) {
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 && state.current) {
removeFocusVisibleClass(state.current);
state.current = undefined;
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: KeyborgFocusInEvent) => {
if (state.current) {
removeFocusVisibleClass(state.current);
state.current = undefined;
}

if (keyborg.isNavigatingWithKeyboard() && isHTMLElement(e.target) && e.target) {
// Griffel can't create chained global styles so use the parent element for now
state.current = e.target;
applyFocusVisibleClass(state.current);
}
disposeCurrentElement();
registerElementIfNavigating(e.target);
};

// Make sure that when focus leaves the scope, the focus visible class is removed
const blurListener = (e: FocusEvent) => {
if (!e.relatedTarget || (isHTMLElement(e.relatedTarget) && !scope.contains(e.relatedTarget))) {
if (state.current) {
removeFocusVisibleClass(state.current);
state.current = undefined;
}
disposeCurrentElement();
}
};

scope.addEventListener(KEYBORG_FOCUSIN, keyborgListener as ListenerOverride);
scope.addEventListener('focusout', blurListener);
(scope as HTMLElementWithFocusVisibleScope).focusVisible = true;

registerElementIfNavigating(targetWindow.document.activeElement);

// Return disposer
return () => {
disposeCurrentElement();

scope.removeEventListener(KEYBORG_FOCUSIN, keyborgListener as ListenerOverride);
scope.removeEventListener('focusout', blurListener);
delete (scope as HTMLElementWithFocusVisibleScope).focusVisible;

disposeKeyborg(keyborg);
};
}

function applyFocusVisibleClass(el: HTMLElement) {
el.setAttribute(FOCUS_VISIBLE_ATTR, '');
}

function removeFocusVisibleClass(el: HTMLElement) {
el.removeAttribute(FOCUS_VISIBLE_ATTR);
}

function alreadyInScope(el: HTMLElement | null | undefined): boolean {
if (!el) {
return false;
Expand Down

0 comments on commit 0c71d4a

Please sign in to comment.