@bemedev/mind-flow 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,31 @@
1
1
  import { onCleanup } from 'solid-js';
2
2
 
3
3
  /**
4
- * Callback function type invoked when the mouse leaves the bound element.
4
+ * Callback function type invoked when the mouse leaves or focus is lost from the
5
+ * bound element.
5
6
  *
6
- * @param event - Optional mouse event that triggered the leave.
7
+ * @param event - Optional mouse or focus event that triggered the leave.
7
8
  */
8
- export type MouseOutCallback = (event?: MouseEvent) => void;
9
+ export type MouseOutCallback = (event?: MouseEvent | FocusEvent) => void;
9
10
 
10
11
  /** Configuration options for the {@linkcode mouseOut} directive. */
11
12
  export type MouseOutOptions = {
12
- /** Callback function to trigger after the timer elapses. */
13
+ /**
14
+ * Callback function to trigger after the timer elapses of type
15
+ * {@linkcode MouseOutCallback}.
16
+ */
13
17
  callback?: MouseOutCallback;
14
18
 
15
- /** Alternative callback property name for triggering after the timer elapses. */
19
+ /**
20
+ * Alternative callback property name for triggering after the timer elapses of
21
+ * type {@linkcode MouseOutCallback}.
22
+ */
16
23
  onMouseOut?: MouseOutCallback;
17
24
 
18
25
  /**
19
- * Delay in milliseconds before triggering the callback after the mouse leaves. If
20
- * the cursor re-enters the element before this duration, the timer is reset.
26
+ * Delay in milliseconds before triggering the callback after the mouse leaves or
27
+ * focus is lost. If the cursor re-enters the element or focus is regained before
28
+ * this duration, the timer is reset.
21
29
  *
22
30
  * @defaultValue 0
23
31
  */
@@ -32,8 +40,9 @@ export type MouseOutParam =
32
40
 
33
41
  /**
34
42
  * Solid custom directive that triggers a callback when the mouse leaves the bound
35
- * element after a specified timer duration. If the user re-enters the component
36
- * before the timer expires, the timer is reset/cancelled.
43
+ * element and focus is not inside it, after a specified timer duration. If the user
44
+ * re-enters the component or gains focus before the timer expires, the timer is
45
+ * reset/cancelled.
37
46
  *
38
47
  * @param el - The bound DOM element.
39
48
  * @param accessor - Accessor returning a callback, tuple `[callback, delay]`, or
@@ -44,6 +53,7 @@ export const mouseOut = (
44
53
  accessor: () => MouseOutParam,
45
54
  ) => {
46
55
  let timer: ReturnType<typeof setTimeout> | undefined;
56
+ let isHovered = false;
47
57
 
48
58
  const clear = () => {
49
59
  if (timer !== undefined) {
@@ -52,11 +62,12 @@ export const mouseOut = (
52
62
  }
53
63
  };
54
64
 
55
- const onMouseEnter = () => {
56
- clear();
65
+ const isFocused = () => {
66
+ const active = el.ownerDocument?.activeElement ?? document.activeElement;
67
+ return !!active && el.contains(active);
57
68
  };
58
69
 
59
- const onMouseLeave = (event: MouseEvent) => {
70
+ const triggerLeave = (event?: MouseEvent | FocusEvent) => {
60
71
  clear();
61
72
 
62
73
  const param = accessor();
@@ -87,12 +98,40 @@ export const mouseOut = (
87
98
  }
88
99
  };
89
100
 
101
+ const onMouseEnter = () => {
102
+ isHovered = true;
103
+ clear();
104
+ };
105
+
106
+ const onMouseLeave = (event: MouseEvent) => {
107
+ isHovered = false;
108
+ if (isFocused()) return;
109
+ triggerLeave(event);
110
+ };
111
+
112
+ const onFocusIn = () => {
113
+ clear();
114
+ };
115
+
116
+ const onFocusOut = (event: FocusEvent) => {
117
+ const nextTarget = event.relatedTarget as Node | null;
118
+ const stillHasFocus = !!nextTarget && el.contains(nextTarget);
119
+
120
+ if (!stillHasFocus && !isHovered) {
121
+ triggerLeave(event);
122
+ }
123
+ };
124
+
90
125
  el.addEventListener('mouseleave', onMouseLeave as EventListener);
91
126
  el.addEventListener('mouseenter', onMouseEnter as EventListener);
127
+ el.addEventListener('focusin', onFocusIn as EventListener);
128
+ el.addEventListener('focusout', onFocusOut as EventListener);
92
129
 
93
130
  onCleanup(() => {
94
131
  clear();
95
132
  el.removeEventListener('mouseleave', onMouseLeave as EventListener);
96
133
  el.removeEventListener('mouseenter', onMouseEnter as EventListener);
134
+ el.removeEventListener('focusin', onFocusIn as EventListener);
135
+ el.removeEventListener('focusout', onFocusOut as EventListener);
97
136
  });
98
137
  };