Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(v2): remove focus outline from mouse users #3626

Merged
merged 4 commits into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/docusaurus-theme-classic/src/theme/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import Footer from '@theme/Footer';
import LayoutProviders from '@theme/LayoutProviders';
import LayoutHead from '@theme/LayoutHead';
import type {Props} from '@theme/Layout';
import useKeyboardNavigation from '@theme/hooks/useKeyboardNavigation';
import './styles.css';

function Layout(props: Props): JSX.Element {
const {children, noFooter, wrapperClassName} = props;

useKeyboardNavigation();

return (
<LayoutProviders>
<LayoutHead {...props} />
Expand Down
32 changes: 2 additions & 30 deletions packages/docusaurus-theme-classic/src/theme/Tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import React, {useState, Children, ReactElement, useEffect} from 'react';
import React, {useState, Children, ReactElement} from 'react';
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
import type {Props} from '@theme/Tabs';

Expand All @@ -16,14 +16,12 @@ import styles from './styles.module.css';
const keys = {
left: 37,
right: 39,
tab: 9,
};

function Tabs(props: Props): JSX.Element {
const {block, children, defaultValue, values, groupId, className} = props;
const {tabGroupChoices, setTabGroupChoices} = useUserPreferencesContext();
const [selectedValue, setSelectedValue] = useState(defaultValue);
const [keyboardPress, setKeyboardPress] = useState(false);

if (groupId != null) {
const relevantTabGroupChoice = tabGroupChoices[groupId];
Expand Down Expand Up @@ -78,28 +76,6 @@ function Tabs(props: Props): JSX.Element {
}
};

const handleKeyboardEvent = (event) => {
if (event.metaKey || event.altKey || event.ctrlKey) {
return;
}

setKeyboardPress(true);
};

const handleMouseEvent = () => {
setKeyboardPress(false);
};

useEffect(() => {
window.addEventListener('keydown', handleKeyboardEvent);
window.addEventListener('mousedown', handleMouseEvent);

return () => {
window.removeEventListener('keydown', handleKeyboardEvent);
window.removeEventListener('mousedown', handleMouseEvent);
};
}, []);

return (
<div>
<ul
Expand All @@ -120,19 +96,15 @@ function Tabs(props: Props): JSX.Element {
className={clsx('tabs__item', styles.tabItem, {
'tabs__item--active': selectedValue === value,
})}
style={keyboardPress ? {} : {outline: 'none'}}
key={value}
ref={(tabControl) => tabRefs.push(tabControl)}
onKeyDown={(event) => {
handleKeydown(tabRefs, event.target, event);
handleKeyboardEvent(event);
}}
onFocus={() => changeSelectedValue(value)}
onClick={() => {
changeSelectedValue(value);
setKeyboardPress(false);
}}
onPointerDown={() => setKeyboardPress(false)}>
}}>
{label}
</li>
))}
Expand Down
10 changes: 10 additions & 0 deletions packages/docusaurus-theme-classic/src/theme/hooks/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

body:not(.navigation-with-keyboard) *:focus {
outline: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {useEffect} from 'react';

import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

import './styles.css';

// This hook detect keyboard focus indicator to not show outline for mouse users
// Inspired by https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2
function useKeyboardNavigation(): void {
useEffect(() => {
if (!ExecutionEnvironment.canUseDOM) {
return undefined;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed, useEffect never runs in node


const keyboardFocusedClassName = 'navigation-with-keyboard';

function handleFirstTab(e: KeyboardEvent) {
if (e.key === 'Tab') {
document.body.classList.add(keyboardFocusedClassName);

document.removeEventListener('keydown', handleFirstTab);
Copy link
Contributor Author

@lex111 lex111 Oct 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For best performance, I add/remove event handlers depending on whether the focus indicator is enabled. Is it worth it? Or we'd better add event handlers once when the Layout component is mounted (as it was done before in Tabs component), and accordingly remove them only when the component is unmounted (from useEffect return).

Something like this:

  useEffect(() => {
    if (!ExecutionEnvironment.canUseDOM) {
      return undefined;
    }

    const keyboardFocusedClassName = 'navigation-with-keyboard';

    function handleTab(e: MouseEvent | KeyboardEvent) {
      document.body.classList.toggle(keyboardFocusedClassName, e.key === 'Tab');
    }


    document.addEventListener('mousedown', handleTab);
    document.addEventListener('keydown', handleTab);

    return () => {
      document.body.classList.remove(keyboardFocusedClassName);
      document.removeEventListener('keydown', handleTab);
      document.removeEventListener('mousedown', handleTab);
    };
  }, []);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we have a performance issue for tabs, which is unlikely, I'd rather keep the code simpler to read/understand.

But I'm not sure the classList.toggle would work great because it would remove the outline on non-tab key presses? A user might navigate to inputs with keyboard, and then type in the input, and I think the focus ring should remain in such case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the outline will be displayed, you can see this on the page https://deploy-preview-3626--docusaurus-2.netlify.app/classic/docs/styling-layout

image

document.addEventListener('mousedown', handleMouseDown);
}
}

function handleMouseDown() {
document.body.classList.remove(keyboardFocusedClassName);

document.removeEventListener('mousedown', handleMouseDown);
document.addEventListener('keydown', handleFirstTab);
}

document.addEventListener('keydown', handleFirstTab);

return () => {
document.body.classList.remove(keyboardFocusedClassName);

document.removeEventListener('keydown', handleFirstTab);
document.removeEventListener('mousedown', handleMouseDown);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use mousemove instead of mousedown event? Not sure about that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure either, maybe the user is tabbing and moving the mouse at the same time 🤪 probably a niche usecase not very important to consider

};
}, []);
}

export default useKeyboardNavigation;
6 changes: 6 additions & 0 deletions packages/docusaurus-theme-classic/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ declare module '@theme/hooks/useWindowSize' {
export default function useWindowSize(): WindowSize | undefined;
}

declare module '@theme/hooks/useKeyboardNavigation' {
const useKeyboardNavigation: () => void;

export default useKeyboardNavigation;
}

declare module '@theme/Layout' {
import type {ReactNode} from 'react';

Expand Down