Skip to content

Instantly share code, notes, and snippets.

@vincentriemer
Created November 4, 2018 01:22
Show Gist options
  • Save vincentriemer/b1c05324be90b467f27f5d692ac00312 to your computer and use it in GitHub Desktop.
Save vincentriemer/b1c05324be90b467f27f5d692ac00312 to your computer and use it in GitHub Desktop.
A first attempt at an accessible "press handler" React hook
import React from "react";
import { usePress } from "./usePress";
export const MyJSButton = () => {
const [pressRef, isPressed, accessibilityProps] = usePress(
"button", // role [button | link]
() => console.log("Pressed!"), // callback
true // should preventDefault
);
return (
<div
{...accessibilityProps}
ref={pressRef}
style={{
opacity: isPressed ? 0.5 : 1,
transition: "opacity 0.2s",
}}
>
My Weird JS Button
</div>
);
};
import { useState, useRef, useEffect, useCallback } from "react";
import invariant from "invariant";
const keycodes = {
SPACE: 32,
ENTER: 13,
};
const usePress = (role, callback, preventDefault = false) => {
invariant(
["button", "link"].includes(role),
`usePress: Invalid role provided, must be one of: button, link`
);
const pressRef = useRef(null);
const pointerCleanupRef = useRef(null);
const [isPressed, setPressed] = useState(false);
const handleKeyUp = useCallback(() => {
setPressed(false);
callback();
const element = pressRef.current;
element.removeEventListener("keyup", handleKeyUp, false);
}, []);
const handleKeyDown = useCallback(event => {
const isValidEnterPress = event.keyCode === keycodes.ENTER;
const isValidSpacePress =
role === "button" && event.keyCode === keycodes.SPACE;
if (isValidEnterPress || isValidSpacePress) {
preventDefault && event.preventDefault();
setPressed(true);
const element = pressRef.current;
element.addEventListener("keyup", handleKeyUp, false);
}
}, []);
const handlePointerCancel = useCallback(() => {
setPressed(false);
pointerCleanupRef.current();
}, []);
const handlePointerUp = useCallback(() => {
setPressed(false);
callback();
pointerCleanupRef.current();
}, []);
pointerCleanupRef.current = useCallback(() => {
const element = pressRef.current;
document.removeEventListener("pointerup", handlePointerUp, false);
element.removeEventListener("pointerout", handlePointerCancel, false);
}, []);
const handlePointerDown = useCallback(event => {
setPressed(true);
const element = pressRef.current;
document.addEventListener("pointerup", handlePointerUp, false);
element.addEventListener("pointerout", handlePointerCancel, false);
}, []);
const clickPd = useCallback(event => {
event.preventDefault();
}, []);
useEffect(() => {
const element = pressRef.current;
invariant(
element instanceof HTMLElement,
`usePress: ref has not been attached to an html element`
);
// Necessary for pointer events polyfill
element.setAttribute("touch-action", "maniuplation");
element.addEventListener("pointerdown", handlePointerDown, false);
element.addEventListener("keydown", handleKeyDown, false);
preventDefault && element.addEventListener("click", clickPd, false);
return () => {
element.removeEventListener("pointerdown", handlePointerDown, false);
element.removeEventListener("keydown", handleKeyDown, false);
preventDefault && element.removeEventListener("click", clickPd, false);
};
}, []);
const props = {
role,
tabIndex: 0,
"aria-pressed": isPressed,
};
return [pressRef, isPressed, props];
};
export { usePress };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment