1 | import * as React from "react";
|
2 | import { createComponent } from "reakit-system/createComponent";
|
3 | import { createHook } from "reakit-system/createHook";
|
4 | import { isButton } from "reakit-utils/isButton";
|
5 | import { useLiveRef } from "reakit-utils/useLiveRef";
|
6 | import { isSelfTarget } from "reakit-utils/isSelfTarget";
|
7 | import {
|
8 | TabbableOptions,
|
9 | TabbableHTMLProps,
|
10 | useTabbable,
|
11 | } from "../Tabbable/Tabbable";
|
12 | import { CLICKABLE_KEYS } from "./__keys";
|
13 |
|
14 | export type ClickableOptions = TabbableOptions & {
|
15 | |
16 |
|
17 |
|
18 |
|
19 | unstable_clickOnEnter?: boolean;
|
20 | |
21 |
|
22 |
|
23 |
|
24 | unstable_clickOnSpace?: boolean;
|
25 | };
|
26 |
|
27 | export type ClickableHTMLProps = TabbableHTMLProps;
|
28 |
|
29 | export type ClickableProps = ClickableOptions & ClickableHTMLProps;
|
30 |
|
31 | function isNativeClick(event: React.KeyboardEvent) {
|
32 | const element = event.currentTarget;
|
33 | if (!event.isTrusted) return false;
|
34 |
|
35 | return (
|
36 | isButton(element) ||
|
37 | element.tagName === "INPUT" ||
|
38 | element.tagName === "TEXTAREA" ||
|
39 | element.tagName === "A" ||
|
40 | element.tagName === "SELECT"
|
41 | );
|
42 | }
|
43 |
|
44 | export 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 |
|
125 | export const Clickable = createComponent({
|
126 | as: "button",
|
127 | memo: true,
|
128 | useHook: useClickable,
|
129 | });
|