This repository has been archived by the owner on Feb 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
228 additions
and
17 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import React from 'react' | ||
import { FocusVisibleContext } from '_utils/focusVisible/FocusVisibleProvider' | ||
|
||
export const FocusVisibleConsumer = () => { | ||
const [isFocused, setIsFocused] = React.useState(false) | ||
const { hadKeyboardEvent, isInitialized } = React.useContext(FocusVisibleContext) | ||
|
||
const onFocus = React.useCallback(() => { | ||
if (!isFocused) setIsFocused(true) | ||
}, [isFocused]) | ||
|
||
const onBlur = React.useCallback(() => { | ||
if (isFocused) setIsFocused(false) | ||
}, [isFocused]) | ||
|
||
let focusVisible: boolean | ||
if (isInitialized) { | ||
focusVisible = hadKeyboardEvent && isFocused | ||
} else { | ||
// Fallback to focused when the `FocusVisibleProvider` is not used. | ||
focusVisible = isFocused | ||
} | ||
|
||
return React.useMemo( | ||
() => ({ | ||
focusVisible, | ||
onFocus, | ||
onBlur, | ||
}), | ||
[focusVisible, onBlur, onFocus], | ||
) | ||
} | ||
|
||
export default FocusVisibleConsumer |
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,131 @@ | ||
import React, { useEffect, useState, ReactNode } from 'react' | ||
|
||
// A React hook based on: https://github.com/WICG/focus-visible | ||
|
||
interface FocusVisibleProviderProps { | ||
children: ReactNode | ||
} | ||
|
||
export const FocusVisibleContext = React.createContext({ | ||
hadKeyboardEvent: true, | ||
isInitialized: false, | ||
}) | ||
|
||
export const FocusVisibleProvider = ({ children }: FocusVisibleProviderProps) => { | ||
const [hadKeyboardEvent, setHadKeyboardEvent] = useState(true) | ||
|
||
const onPointerDown = () => { | ||
setHadKeyboardEvent(false) | ||
} | ||
|
||
/** | ||
* When the polfyill first loads, assume the user is in keyboard modality. | ||
* If any event is received from a pointing device (e.g. mouse, pointer, | ||
* touch), turn off keyboard modality. | ||
* This accounts for situations where focus enters the page from the URL bar. | ||
* @param {PointerEvent} e | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const onInitialPointerMove = (e: any) => { | ||
// Work around a Safari quirk that fires a mousemove on <html> whenever the | ||
// window blurs, even if you're tabbing out of the page. ¯\_(ツ)_/¯ | ||
if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') { | ||
return | ||
} | ||
|
||
setHadKeyboardEvent(false) | ||
// eslint-disable-next-line no-use-before-define | ||
removeInitialPointerMoveListeners() | ||
} | ||
|
||
/** | ||
* Add a group of listeners to detect usage of any pointing devices. | ||
* These listeners will be added when the polyfill first loads, and anytime | ||
* the window is blurred, so that they are active when the window regains | ||
* focus. | ||
*/ | ||
const addInitialPointerMoveListeners = () => { | ||
document.addEventListener('mousemove', onInitialPointerMove) | ||
document.addEventListener('mousedown', onInitialPointerMove) | ||
document.addEventListener('mouseup', onInitialPointerMove) | ||
document.addEventListener('pointermove', onInitialPointerMove) | ||
document.addEventListener('pointerdown', onInitialPointerMove) | ||
document.addEventListener('pointerup', onInitialPointerMove) | ||
document.addEventListener('touchmove', onInitialPointerMove) | ||
document.addEventListener('touchstart', onInitialPointerMove) | ||
document.addEventListener('touchend', onInitialPointerMove) | ||
} | ||
|
||
const removeInitialPointerMoveListeners = () => { | ||
document.removeEventListener('mousemove', onInitialPointerMove) | ||
document.removeEventListener('mousedown', onInitialPointerMove) | ||
document.removeEventListener('mouseup', onInitialPointerMove) | ||
document.removeEventListener('pointermove', onInitialPointerMove) | ||
document.removeEventListener('pointerdown', onInitialPointerMove) | ||
document.removeEventListener('pointerup', onInitialPointerMove) | ||
document.removeEventListener('touchmove', onInitialPointerMove) | ||
document.removeEventListener('touchstart', onInitialPointerMove) | ||
document.removeEventListener('touchend', onInitialPointerMove) | ||
} | ||
|
||
/** | ||
* If the most recent user interaction was via the keyboard; | ||
* and the key press did not include a meta, alt/option, or control key; | ||
* then the modality is keyboard. Otherwise, the modality is not keyboard. | ||
* Apply `focus-visible` to any current active element and keep track | ||
* of our keyboard modality state with `hadKeyboardEvent`. | ||
* @param {KeyboardEvent} e | ||
*/ | ||
const onKeyDown = (e: KeyboardEvent) => { | ||
if (e.metaKey || e.altKey || e.ctrlKey) { | ||
return | ||
} | ||
|
||
setHadKeyboardEvent(true) | ||
} | ||
/** | ||
* If the user changes tabs, keep track of whether or not the previously | ||
* focused element had .focus-visible. | ||
*/ | ||
const onVisibilityChange = () => { | ||
if (document.visibilityState === 'hidden') { | ||
// If the tab becomes active again, the browser will handle calling focus | ||
// on the element (Safari actually calls it twice). | ||
// If this tab change caused a blur on an element with focus-visible, | ||
// re-apply the class when the user switches back to the tab. | ||
setHadKeyboardEvent(true) | ||
addInitialPointerMoveListeners() | ||
} | ||
} | ||
|
||
useEffect(() => { | ||
// For some kinds of state, we are interested in changes at the global scope | ||
// only. For example, global pointer input, global key presses and global | ||
// visibility change should affect the state at every scope: | ||
document.addEventListener('keydown', onKeyDown, true) | ||
document.addEventListener('mousedown', onPointerDown, true) | ||
document.addEventListener('pointerdown', onPointerDown, true) | ||
document.addEventListener('touchstart', onPointerDown, true) | ||
document.addEventListener('visibilitychange', onVisibilityChange, true) | ||
|
||
addInitialPointerMoveListeners() | ||
|
||
return () => { | ||
document.removeEventListener('keydown', onKeyDown, true) | ||
document.removeEventListener('mousedown', onPointerDown, true) | ||
document.removeEventListener('pointerdown', onPointerDown, true) | ||
document.removeEventListener('touchstart', onPointerDown, true) | ||
document.removeEventListener('visibilitychange', onVisibilityChange, true) | ||
|
||
removeInitialPointerMoveListeners() | ||
} | ||
}, [setHadKeyboardEvent]) | ||
|
||
return ( | ||
<FocusVisibleContext.Provider value={{ hadKeyboardEvent, isInitialized: true }}> | ||
{children} | ||
</FocusVisibleContext.Provider> | ||
) | ||
} | ||
|
||
export default FocusVisibleProvider |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,42 @@ | ||
import React from 'react' | ||
import React, { Component } from 'react' | ||
import { FocusVisibleProvider } from '_utils/focusVisible/FocusVisibleProvider' | ||
|
||
import { storiesOf } from '@storybook/react' | ||
import { withKnobs, text } from '@storybook/addon-knobs' | ||
import { action } from '@storybook/addon-actions' | ||
import Section from 'layout/section/baseSection' | ||
import Why from 'why' | ||
|
||
class LayoutExample extends Component { | ||
render() { | ||
return ( | ||
<div> | ||
<FocusVisibleProvider> | ||
<Section> | ||
<ul> | ||
<li> | ||
<Why | ||
onClick={action('clicked')} | ||
title={text('Title', 'Why this is a text that is so long ? (new window)')} | ||
> | ||
{text('Text', 'Why this is a text that is so long ?')} | ||
</Why> | ||
</li> | ||
<li> | ||
<button type="button">Focus provider - No consumer</button> | ||
</li> | ||
</ul> | ||
</Section> | ||
</FocusVisibleProvider> | ||
<Section> | ||
<button type="button">No focus provider</button> | ||
</Section> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
const stories = storiesOf('Widgets|Why', module) | ||
stories.addDecorator(withKnobs) | ||
|
||
stories.add('default', () => ( | ||
<Section> | ||
<Why | ||
onClick={action('clicked')} | ||
title={text('Title', 'Why this is a text that is so long ? (new window)')} | ||
> | ||
{text('Text', 'Why this is a text that is so long ?')} | ||
</Why> | ||
</Section> | ||
)) | ||
stories.add('default', () => <LayoutExample />) |