-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: improve focus selector perf with focus-visible and focus-within…
… polyfill (#24154) * feat: focus-visible polyfill Adds a polyfill for `:focus-visible` that is built on top of Keyborg that serves as a replacement for `useKeyboardNavAttribute` * focus-within polyfill * use focusout * changefiles * update md * remove unused * fix typings * fix link vr tests * fix link vr tests * PR suggestions * update changefile * assign ref in effect * add doics * update md * PR suggestions
- Loading branch information
Showing
23 changed files
with
310 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-checkbox-25fb0c36-d07f-4a39-bd82-831bb2b756b0.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "refactor: use `useFocusWithin` hook for :focus-within styles", | ||
"packageName": "@fluentui/react-checkbox", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-portal-2940da05-fb94-4cac-8763-6e0c4a14ee8f.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "fix: use `useFocusVisible` hook for :focus-visible styles", | ||
"packageName": "@fluentui/react-portal", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-provider-9ddc6123-46e0-4094-9670-659f00f241b8.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "fix: use `useFocusVisible` hook for :focus-visible styles", | ||
"packageName": "@fluentui/react-provider", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-radio-36321438-6ccb-4827-85da-098c7e6c76cf.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "refactor: use `useFocusWithin` hook for :focus-within styles", | ||
"packageName": "@fluentui/react-radio", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-slider-e562a2af-50fe-4b42-9eec-003613926997.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "refactor: use `useFocusWithin` hook for :focus-within styles", | ||
"packageName": "@fluentui/react-slider", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-switch-504a98f0-bdbd-4377-959c-1dcb2275040a.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "refactor: use `useFocusWithin` hook for :focus-within styles", | ||
"packageName": "@fluentui/react-switch", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-tabster-897f1851-6bb3-4082-8a54-ace4ca191faf.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "minor", | ||
"comment": "feat: create `:focus-visible` and `:focus-within` polyfills", | ||
"packageName": "@fluentui/react-tabster", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
packages/react-components/react-tabster/src/focus/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
packages/react-components/react-tabster/src/focus/focusVisiblePolyfill.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { KEYBORG_FOCUSIN, KeyborgFocusInEvent, createKeyborg, disposeKeyborg } from 'keyborg'; | ||
import { FOCUS_VISIBLE_CLASS } from './constants'; | ||
|
||
/** | ||
* Because `addEventListener` type override falls back to 2nd definition (evt name is unknown string literal) | ||
* evt is being typed as a base class of MouseEvent -> `Event`. | ||
* This type is used to override `listener` calls to make TS happy | ||
*/ | ||
type ListenerOverride = (evt: Event) => void; | ||
|
||
type FocusVisibleState = { | ||
/** | ||
* Current element with focus visible in state | ||
*/ | ||
current: HTMLElement | undefined; | ||
}; | ||
|
||
type HTMLElementWithFocusVisibleScope = { | ||
focusVisible: boolean | undefined; | ||
} & HTMLElement; | ||
|
||
export function applyFocusVisiblePolyfill(scope: HTMLElement, win: Window): () => void { | ||
if (alreadyInScope(scope)) { | ||
// Focus visible polyfill already applied at this scope | ||
return () => undefined; | ||
} | ||
|
||
const state: FocusVisibleState = { | ||
current: undefined, | ||
}; | ||
|
||
const keyborg = createKeyborg(win); | ||
|
||
// When navigation mode changes remove the focus-visible selector | ||
keyborg.subscribe(isNavigatingWithKeyboard => { | ||
if (!isNavigatingWithKeyboard && state.current) { | ||
removeFocusVisibleClass(state.current); | ||
state.current = undefined; | ||
} | ||
}); | ||
|
||
// 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); | ||
} | ||
}; | ||
|
||
// 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; | ||
} | ||
} | ||
}; | ||
|
||
scope.addEventListener(KEYBORG_FOCUSIN, keyborgListener as ListenerOverride); | ||
scope.addEventListener('focusout', blurListener); | ||
(scope as HTMLElementWithFocusVisibleScope).focusVisible = true; | ||
|
||
// Return disposer | ||
return () => { | ||
scope.removeEventListener(KEYBORG_FOCUSIN, keyborgListener as ListenerOverride); | ||
scope.removeEventListener('focusout', blurListener); | ||
delete (scope as HTMLElementWithFocusVisibleScope).focusVisible; | ||
disposeKeyborg(keyborg); | ||
}; | ||
} | ||
|
||
function applyFocusVisibleClass(el: HTMLElement) { | ||
el.classList.add(FOCUS_VISIBLE_CLASS); | ||
} | ||
|
||
function removeFocusVisibleClass(el: HTMLElement) { | ||
el.classList.remove(FOCUS_VISIBLE_CLASS); | ||
} | ||
|
||
function isHTMLElement(target: EventTarget | null): target is HTMLElement { | ||
if (!target) { | ||
return false; | ||
} | ||
return Boolean(target && typeof target === 'object' && 'classList' in target && 'contains' in target); | ||
} | ||
|
||
function alreadyInScope(el: HTMLElement | null | undefined): boolean { | ||
if (!el) { | ||
return false; | ||
} | ||
|
||
if ((el as HTMLElementWithFocusVisibleScope).focusVisible) { | ||
return true; | ||
} | ||
|
||
return alreadyInScope(el?.parentElement); | ||
} |
Oops, something went wrong.