UNPKG

3.45 kBPlain TextView Raw
1import * as React from "react";
2import { createComponent } from "reakit-system/createComponent";
3import { createHook } from "reakit-system/createHook";
4import { isButton } from "reakit-utils/isButton";
5import { useLiveRef } from "reakit-utils/useLiveRef";
6import { isSelfTarget } from "reakit-utils/isSelfTarget";
7import {
8 TabbableOptions,
9 TabbableHTMLProps,
10 useTabbable,
11} from "../Tabbable/Tabbable";
12import { CLICKABLE_KEYS } from "./__keys";
13
14export type ClickableOptions = TabbableOptions & {
15 /**
16 * Whether or not trigger click on pressing <kbd>Enter</kbd>.
17 * @private
18 */
19 unstable_clickOnEnter?: boolean;
20 /**
21 * Whether or not trigger click on pressing <kbd>Space</kbd>.
22 * @private
23 */
24 unstable_clickOnSpace?: boolean;
25};
26
27export type ClickableHTMLProps = TabbableHTMLProps;
28
29export type ClickableProps = ClickableOptions & ClickableHTMLProps;
30
31function isNativeClick(event: React.KeyboardEvent) {
32 const element = event.currentTarget;
33 if (!event.isTrusted) return false;
34 // istanbul ignore next: can't test trusted events yet
35 return (
36 isButton(element) ||
37 element.tagName === "INPUT" ||
38 element.tagName === "TEXTAREA" ||
39 element.tagName === "A" ||
40 element.tagName === "SELECT"
41 );
42}
43
44export const useClickable = createHook<ClickableOptions, ClickableHTMLProps>({
45 name: "Clickable",
46 compose: useTabbable,
47 keys: CLICKABLE_KEYS,
48
49 useOptions({
50 unstable_clickOnEnter = true,
51 unstable_clickOnSpace = true,
52 ...options
53 }) {
54 return {
55 unstable_clickOnEnter,
56 unstable_clickOnSpace,
57 ...options,
58 };
59 },
60
61 useProps(
62 options,
63 { onKeyDown: htmlOnKeyDown, onKeyUp: htmlOnKeyUp, ...htmlProps }
64 ) {
65 const [active, setActive] = React.useState(false);
66 const onKeyDownRef = useLiveRef(htmlOnKeyDown);
67 const onKeyUpRef = useLiveRef(htmlOnKeyUp);
68
69 const onKeyDown = React.useCallback(
70 (event: React.KeyboardEvent<HTMLElement>) => {
71 onKeyDownRef.current?.(event);
72
73 if (event.defaultPrevented) return;
74 if (options.disabled) return;
75 if (event.metaKey) return;
76 if (!isSelfTarget(event)) return;
77
78 const isEnter = options.unstable_clickOnEnter && event.key === "Enter";
79 const isSpace = options.unstable_clickOnSpace && event.key === " ";
80
81 if (isEnter || isSpace) {
82 if (isNativeClick(event)) return;
83 event.preventDefault();
84 if (isEnter) {
85 event.currentTarget.click();
86 } else if (isSpace) {
87 setActive(true);
88 }
89 }
90 },
91 [
92 options.disabled,
93 options.unstable_clickOnEnter,
94 options.unstable_clickOnSpace,
95 ]
96 );
97
98 const onKeyUp = React.useCallback(
99 (event: React.KeyboardEvent<HTMLElement>) => {
100 onKeyUpRef.current?.(event);
101
102 if (event.defaultPrevented) return;
103 if (options.disabled) return;
104 if (event.metaKey) return;
105
106 const isSpace = options.unstable_clickOnSpace && event.key === " ";
107
108 if (active && isSpace) {
109 setActive(false);
110 event.currentTarget.click();
111 }
112 },
113 [options.disabled, options.unstable_clickOnSpace, active]
114 );
115
116 return {
117 "data-active": active || undefined,
118 onKeyDown,
119 onKeyUp,
120 ...htmlProps,
121 };
122 },
123});
124
125export const Clickable = createComponent({
126 as: "button",
127 memo: true,
128 useHook: useClickable,
129});