Skip to content

Commit

Permalink
fix(Thumbnail): focus now appears on arrow nav as well as tabbing
Browse files Browse the repository at this point in the history
  • Loading branch information
liamross committed Apr 15, 2020
1 parent b8e7775 commit 70efd90
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 10 deletions.
5 changes: 3 additions & 2 deletions src/components/Thumbnail/Thumbnail.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import classnames from 'classnames';
import React, { MouseEvent, ReactNode, ReactText, useRef } from 'react';
import { FileLike } from '../../data';
import { useFile, useFocus, useAccessibleFocus } from '../../hooks';
import { useAccessibleFocus, useFile, useFocus } from '../../hooks';
import { ClickableDiv, ClickableDivProps } from '../ClickableDiv';
import { EditableText } from '../EditableText';
import { FileSkeleton } from '../FileSkeleton';
import { Image } from '../Image';
import { ToolButton } from '../ToolButton';
import { thumbnailFocusObservable } from './thumbnailFocusObservable';

export interface ThumbnailButtonProps<F> {
key: ReactText;
Expand Down Expand Up @@ -91,7 +92,7 @@ export function Thumbnail<F extends FileLike>({
onBlur,
...divProps
}: ThumbnailProps<F>) {
const isUserTabbing = useAccessibleFocus();
const isUserTabbing = useAccessibleFocus(thumbnailFocusObservable);

const thumbnailRef = useRef<HTMLDivElement>(null);

Expand Down
80 changes: 80 additions & 0 deletions src/components/Thumbnail/thumbnailFocusObservable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { FocusObservable } from '../../hooks';

const validKeys = [
// Valid keys for thumbnail accessibility.
'Tab',
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown',
];

class ThumbnailFocusObservable implements FocusObservable {
private _subscribers: Function[];
private _isUserTabbing: boolean;

constructor() {
this._subscribers = [];
this._isUserTabbing = false;
}

get value() {
return this._isUserTabbing;
}

subscribe(subscriber: Function) {
// If adding first subscriber, begin listening to document.
if (this._subscribers.length === 0) {
if (this._isUserTabbing) {
this._tabToMouseListener();
} else {
this._mouseToTabListener();
}
}
const exists = this._subscribers.includes(subscriber);
if (!exists) this._subscribers.push(subscriber);
return this._unsubscribe(subscriber);
}

private _unsubscribe(subscriber: Function) {
return () => {
this._subscribers = this._subscribers.filter(s => s !== subscriber);
// If no subscribers, stop listening to document.
if (this._subscribers.length === 0) this._removeAllListeners();
};
}

private _setIsUserTabbing(isUserTabbing: boolean) {
this._isUserTabbing = isUserTabbing;
this._subscribers.forEach(subscriber => subscriber());
}

private _handleFirstTab = (event: KeyboardEvent) => {
if (validKeys.includes(event.key)) {
this._setIsUserTabbing(true);
this._tabToMouseListener();
}
};

private _handleFirstMouse = () => {
this._setIsUserTabbing(false);
this._mouseToTabListener();
};

private _tabToMouseListener() {
window.removeEventListener('keydown', this._handleFirstTab);
window.addEventListener('mousedown', this._handleFirstMouse);
}

private _mouseToTabListener() {
window.removeEventListener('mousedown', this._handleFirstMouse);
window.addEventListener('keydown', this._handleFirstTab);
}

private _removeAllListeners() {
window.removeEventListener('mousedown', this._handleFirstMouse);
window.removeEventListener('keydown', this._handleFirstTab);
}
}

export const thumbnailFocusObservable = new ThumbnailFocusObservable();
23 changes: 15 additions & 8 deletions src/hooks/useAccessibleFocus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@ import { useEffect, useState } from 'react';
/**
* Will return true if the user is using keyboard navigation, or false if they
* are using their mouse. The returned value will be true if the Tab key was
* used more recently than mouse click, and false if not.
* used more recently than mouse click, and false if not. You can also provide
* custom behavior by passing your own observable.
* @param observable Optional custom observable.
*/
export function useAccessibleFocus() {
const [isUserTabbing, setIsUserTabbing] = useState(observable.isUserTabbing);
export function useAccessibleFocus(observable: FocusObservable = tabObservable) {
const [isUserTabbing, setIsUserTabbing] = useState(observable.value);

useEffect(() => {
return observable.subscribe(() => setIsUserTabbing(observable.isUserTabbing));
}, []);
return observable.subscribe(() => setIsUserTabbing(observable.value));
}, [observable]);

return isUserTabbing;
}

class AccessibleFocusObservable {
export interface FocusObservable {
value: boolean;
subscribe(subscriber: Function): void;
}

class AccessibleFocusObservable implements FocusObservable {
private _subscribers: Function[];
private _isUserTabbing: boolean;

Expand All @@ -24,7 +31,7 @@ class AccessibleFocusObservable {
this._isUserTabbing = false;
}

get isUserTabbing() {
get value() {
return this._isUserTabbing;
}

Expand Down Expand Up @@ -83,4 +90,4 @@ class AccessibleFocusObservable {
}
}

const observable = new AccessibleFocusObservable();
const tabObservable = new AccessibleFocusObservable();

0 comments on commit 70efd90

Please sign in to comment.