1 | import * as React from "react";
|
2 | import { createComponent } from "reakit-system/createComponent";
|
3 | import { createHook } from "reakit-system/createHook";
|
4 | import { isTextField } from "reakit-utils/isTextField";
|
5 | import { getDocument } from "reakit-utils/getDocument";
|
6 | import { isSelfTarget } from "reakit-utils/isSelfTarget";
|
7 | import { useLiveRef } from "reakit-utils/useLiveRef";
|
8 | import { useRole, RoleOptions, RoleHTMLProps } from "../Role/Role";
|
9 | import { CompositeStateReturn } from "./CompositeState";
|
10 | import { setTextFieldValue } from "./__utils/setTextFieldValue";
|
11 | import { COMPOSITE_ITEM_WIDGET_KEYS } from "./__keys";
|
12 |
|
13 | export type unstable_CompositeItemWidgetOptions = RoleOptions &
|
14 | Pick<Partial<CompositeStateReturn>, "wrap"> &
|
15 | Pick<
|
16 | CompositeStateReturn,
|
17 | "unstable_hasActiveWidget" | "unstable_setHasActiveWidget" | "currentId"
|
18 | >;
|
19 |
|
20 | export type unstable_CompositeItemWidgetHTMLProps = RoleHTMLProps;
|
21 |
|
22 | export type unstable_CompositeItemWidgetProps = unstable_CompositeItemWidgetOptions &
|
23 | unstable_CompositeItemWidgetHTMLProps;
|
24 |
|
25 | function focusCurrentItem(widget: Element, currentId?: string | null) {
|
26 | if (currentId) {
|
27 | getDocument(widget).getElementById(currentId)?.focus();
|
28 | }
|
29 | }
|
30 |
|
31 | function getTextFieldValue(element: HTMLElement) {
|
32 | return (element as HTMLInputElement).value;
|
33 | }
|
34 |
|
35 | export const unstable_useCompositeItemWidget = createHook<
|
36 | unstable_CompositeItemWidgetOptions,
|
37 | unstable_CompositeItemWidgetHTMLProps
|
38 | >({
|
39 | name: "CompositeItemWidget",
|
40 | compose: useRole,
|
41 | keys: COMPOSITE_ITEM_WIDGET_KEYS,
|
42 |
|
43 | useProps(
|
44 | options,
|
45 | {
|
46 | onFocus: htmlOnFocus,
|
47 | onBlur: htmlOnBlur,
|
48 | onKeyDown: htmlOnKeyDown,
|
49 | ...htmlProps
|
50 | }
|
51 | ) {
|
52 | const initialValue = React.useRef("");
|
53 | const onFocusRef = useLiveRef(htmlOnFocus);
|
54 | const onBlurRef = useLiveRef(htmlOnBlur);
|
55 | const onKeyDownRef = useLiveRef(htmlOnKeyDown);
|
56 |
|
57 | const onFocus = React.useCallback(
|
58 | (event: React.FocusEvent<HTMLElement>) => {
|
59 | onFocusRef.current?.(event);
|
60 | options.unstable_setHasActiveWidget?.(true);
|
61 | if (isTextField(event.currentTarget)) {
|
62 | initialValue.current = getTextFieldValue(event.currentTarget);
|
63 | }
|
64 | },
|
65 | [options.unstable_setHasActiveWidget]
|
66 | );
|
67 |
|
68 | const onBlur = React.useCallback(
|
69 | (event: React.FocusEvent) => {
|
70 | onBlurRef.current?.(event);
|
71 | options.unstable_setHasActiveWidget?.(false);
|
72 | },
|
73 | [options.unstable_setHasActiveWidget]
|
74 | );
|
75 |
|
76 | const onKeyDown = React.useCallback(
|
77 | (event: React.KeyboardEvent<HTMLElement>) => {
|
78 | onKeyDownRef.current?.(event);
|
79 | if (event.defaultPrevented) return;
|
80 | if (!isSelfTarget(event)) return;
|
81 | if (event.nativeEvent.isComposing) return;
|
82 | const element = event.currentTarget;
|
83 | if (event.key === "Enter") {
|
84 | if (isTextField(element)) {
|
85 | const isMultilineTextField = element.tagName === "TEXTAREA";
|
86 |
|
87 | if (isMultilineTextField && event.shiftKey) return;
|
88 |
|
89 | event.preventDefault();
|
90 | focusCurrentItem(element, options.currentId);
|
91 | }
|
92 | } else if (event.key === "Escape") {
|
93 | focusCurrentItem(element, options.currentId);
|
94 | if (isTextField(element)) {
|
95 | setTextFieldValue(element, initialValue.current);
|
96 | }
|
97 | }
|
98 | },
|
99 | [options.currentId]
|
100 | );
|
101 |
|
102 | return {
|
103 | tabIndex: options.unstable_hasActiveWidget ? 0 : -1,
|
104 | "data-composite-item-widget": true,
|
105 | onFocus,
|
106 | onBlur,
|
107 | onKeyDown,
|
108 | ...htmlProps,
|
109 | };
|
110 | },
|
111 | });
|
112 |
|
113 | export const unstable_CompositeItemWidget = createComponent({
|
114 | as: "div",
|
115 | useHook: unstable_useCompositeItemWidget,
|
116 | });
|