@jsenv/dom 0.3.0 → 0.5.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.
package/dist/jsenv_dom.js CHANGED
@@ -1,4 +1,3 @@
1
- import StyleObserver from "style-observer";
2
1
  import { signal, effect } from "@preact/signals";
3
2
  import { useState, useLayoutEffect } from "preact/hooks";
4
3
 
@@ -101,6 +100,38 @@ const createPubSub = (clearOnPublish = false) => {
101
100
  return [publish, subscribe, clear];
102
101
  };
103
102
 
103
+ const createValueEffect = (value) => {
104
+ const callbackSet = new Set();
105
+ const previousValueCleanupSet = new Set();
106
+
107
+ const updateValue = (newValue) => {
108
+ if (newValue === value) {
109
+ return;
110
+ }
111
+ for (const cleanup of previousValueCleanupSet) {
112
+ cleanup();
113
+ }
114
+ previousValueCleanupSet.clear();
115
+ const oldValue = value;
116
+ value = newValue;
117
+ for (const callback of callbackSet) {
118
+ const returnValue = callback(newValue, oldValue);
119
+ if (typeof returnValue === "function") {
120
+ previousValueCleanupSet.add(returnValue);
121
+ }
122
+ }
123
+ };
124
+
125
+ const addEffect = (callback) => {
126
+ callbackSet.add(callback);
127
+ return () => {
128
+ callbackSet.delete(callback);
129
+ };
130
+ };
131
+
132
+ return [updateValue, addEffect];
133
+ };
134
+
104
135
  // https://github.com/davidtheclark/tabbable/blob/master/index.js
105
136
  const isDocumentElement = (node) =>
106
137
  node === node.ownerDocument.documentElement;
@@ -1141,30 +1172,6 @@ const createElementFromSelector = (selector) => {
1141
1172
  return element;
1142
1173
  };
1143
1174
 
1144
- const styleEffect = (element, callback, properties = []) => {
1145
- const check = () => {
1146
- const values = {};
1147
- const computedStyle = getComputedStyle(element);
1148
- for (const property of properties) {
1149
- values[property] = normalizeStyle(
1150
- computedStyle.getPropertyValue(property),
1151
- property,
1152
- );
1153
- }
1154
- callback(values);
1155
- };
1156
-
1157
- check();
1158
- const observer = new StyleObserver(() => {
1159
- check();
1160
- });
1161
- observer.observe(element, properties);
1162
-
1163
- return () => {
1164
- observer.unobserve();
1165
- };
1166
- };
1167
-
1168
1175
  const addAttributeEffect = (attributeName, effect) => {
1169
1176
  const cleanupWeakMap = new WeakMap();
1170
1177
  const applyEffect = (element) => {
@@ -2082,12 +2089,21 @@ const addActiveElementEffect = (callback) => {
2082
2089
  return remove;
2083
2090
  };
2084
2091
 
2085
- const elementIsVisible = (node) => {
2092
+ const elementIsVisibleForFocus = (node) => {
2093
+ return getFocusVisibilityInfo(node).visible;
2094
+ };
2095
+ const getFocusVisibilityInfo = (node) => {
2086
2096
  if (isDocumentElement(node)) {
2087
- return true;
2097
+ return { visible: true, reason: "is document" };
2098
+ }
2099
+ if (node.hasAttribute("hidden")) {
2100
+ return { visible: false, reason: "has hidden attribute" };
2088
2101
  }
2089
2102
  if (getStyle(node, "visibility") === "hidden") {
2090
- return false;
2103
+ return { visible: false, reason: "uses visiblity: hidden" };
2104
+ }
2105
+ if (node.tagName === "INPUT" && node.type === "hidden") {
2106
+ return { visible: false, reason: "input type hidden" };
2091
2107
  }
2092
2108
  let nodeOrAncestor = node;
2093
2109
  while (nodeOrAncestor) {
@@ -2095,19 +2111,87 @@ const elementIsVisible = (node) => {
2095
2111
  break;
2096
2112
  }
2097
2113
  if (getStyle(nodeOrAncestor, "display") === "none") {
2098
- return false;
2114
+ return { visible: false, reason: "ancestor uses display: none" };
2099
2115
  }
2100
2116
  // Check if element is inside a closed details element
2101
2117
  if (elementIsDetails(nodeOrAncestor) && !nodeOrAncestor.open) {
2102
2118
  // Special case: summary elements are visible even when their parent details is closed
2103
2119
  // But only if this details element is the direct parent of the summary
2104
- if (elementIsSummary(node) && node.parentElement === nodeOrAncestor) ; else {
2105
- return false;
2120
+ if (!elementIsSummary(node) || node.parentElement !== nodeOrAncestor) {
2121
+ return { visible: false, reason: "inside closed details element" };
2106
2122
  }
2123
+ // Continue checking ancestors
2107
2124
  }
2108
2125
  nodeOrAncestor = nodeOrAncestor.parentNode;
2109
2126
  }
2110
- return true;
2127
+ return { visible: true, reason: "no reason to be hidden" };
2128
+ };
2129
+
2130
+ const elementIsVisuallyVisible = (node, options = {}) => {
2131
+ return getVisuallyVisibleInfo(node, options).visible;
2132
+ };
2133
+ const getVisuallyVisibleInfo = (
2134
+ node,
2135
+ { countOffscreenAsVisible = false } = {},
2136
+ ) => {
2137
+ // First check all the focusable visibility conditions
2138
+ const focusVisibilityInfo = getFocusVisibilityInfo(node);
2139
+ if (!focusVisibilityInfo.visible) {
2140
+ return focusVisibilityInfo;
2141
+ }
2142
+
2143
+ // Additional visual visibility checks
2144
+ if (getStyle(node, "opacity") === "0") {
2145
+ return { visible: false, reason: "uses opacity: 0" };
2146
+ }
2147
+
2148
+ const rect = node.getBoundingClientRect();
2149
+ if (rect.width === 0 && rect.height === 0) {
2150
+ return { visible: false, reason: "has zero dimensions" };
2151
+ }
2152
+
2153
+ // Check for clipping
2154
+ const clipStyle = getStyle(node, "clip");
2155
+ if (clipStyle && clipStyle !== "auto" && clipStyle.includes("rect(0")) {
2156
+ return { visible: false, reason: "clipped with clip property" };
2157
+ }
2158
+
2159
+ const clipPathStyle = getStyle(node, "clip-path");
2160
+ if (clipPathStyle && clipPathStyle.includes("inset(100%")) {
2161
+ return { visible: false, reason: "clipped with clip-path" };
2162
+ }
2163
+
2164
+ // Check if positioned off-screen (unless option says to count as visible)
2165
+ if (!countOffscreenAsVisible) {
2166
+ if (
2167
+ rect.right < 0 ||
2168
+ rect.bottom < 0 ||
2169
+ rect.left > window.innerWidth ||
2170
+ rect.top > window.innerHeight
2171
+ ) {
2172
+ return { visible: false, reason: "positioned off-screen" };
2173
+ }
2174
+ }
2175
+
2176
+ // Check for transform scale(0)
2177
+ const transformStyle = getStyle(node, "transform");
2178
+ if (transformStyle && transformStyle.includes("scale(0")) {
2179
+ return { visible: false, reason: "scaled to zero with transform" };
2180
+ }
2181
+
2182
+ return { visible: true, reason: "visually visible" };
2183
+ };
2184
+ const getFirstVisuallyVisibleAncestor = (node, options = {}) => {
2185
+ let ancestorCandidate = node.parentNode;
2186
+ while (ancestorCandidate) {
2187
+ const visibilityInfo = getVisuallyVisibleInfo(ancestorCandidate, options);
2188
+ if (visibilityInfo.visible) {
2189
+ return ancestorCandidate;
2190
+ }
2191
+ ancestorCandidate = ancestorCandidate.parentElement;
2192
+ }
2193
+ // This shouldn't happen in normal cases since document element is always visible
2194
+ return null;
2111
2195
  };
2112
2196
 
2113
2197
  const elementIsFocusable = (node) => {
@@ -2123,34 +2207,34 @@ const elementIsFocusable = (node) => {
2123
2207
  if (node.type === "hidden") {
2124
2208
  return false;
2125
2209
  }
2126
- return elementIsVisible(node);
2210
+ return elementIsVisibleForFocus(node);
2127
2211
  }
2128
2212
  if (
2129
2213
  ["button", "select", "datalist", "iframe", "textarea"].indexOf(nodeName) >
2130
2214
  -1
2131
2215
  ) {
2132
- return elementIsVisible(node);
2216
+ return elementIsVisibleForFocus(node);
2133
2217
  }
2134
2218
  if (["a", "area"].indexOf(nodeName) > -1) {
2135
2219
  if (node.hasAttribute("href") === false) {
2136
2220
  return false;
2137
2221
  }
2138
- return elementIsVisible(node);
2222
+ return elementIsVisibleForFocus(node);
2139
2223
  }
2140
2224
  if (["audio", "video"].indexOf(nodeName) > -1) {
2141
2225
  if (node.hasAttribute("controls") === false) {
2142
2226
  return false;
2143
2227
  }
2144
- return elementIsVisible(node);
2228
+ return elementIsVisibleForFocus(node);
2145
2229
  }
2146
2230
  if (nodeName === "summary") {
2147
- return elementIsVisible(node);
2231
+ return elementIsVisibleForFocus(node);
2148
2232
  }
2149
2233
  if (node.hasAttribute("tabindex") || node.hasAttribute("tabIndex")) {
2150
- return elementIsVisible(node);
2234
+ return elementIsVisibleForFocus(node);
2151
2235
  }
2152
2236
  if (node.hasAttribute("draggable")) {
2153
- return elementIsVisible(node);
2237
+ return elementIsVisibleForFocus(node);
2154
2238
  }
2155
2239
  return false;
2156
2240
  };
@@ -3583,11 +3667,12 @@ const allowWheelThrough = (element, connectedElement) => {
3583
3667
  wheelEvent.clientY,
3584
3668
  );
3585
3669
  for (const elementBehindMouse of elementsBehindMouse) {
3586
- const belongsToElement = isElementOrDescendant(elementBehindMouse);
3587
3670
  // try to scroll element itself
3588
3671
  if (tryToScrollOne(elementBehindMouse, wheelEvent)) {
3589
3672
  return;
3590
3673
  }
3674
+ const belongsToElement = isElementOrDescendant(elementBehindMouse);
3675
+ // try to scroll what is behind
3591
3676
  if (!belongsToElement) {
3592
3677
  break;
3593
3678
  }
@@ -3606,17 +3691,16 @@ const allowWheelThrough = (element, connectedElement) => {
3606
3691
  wheelEvent.clientY,
3607
3692
  );
3608
3693
  for (const elementBehindMouse of elementsBehindMouse) {
3609
- const belongsToElement = isElementOrDescendant(elementBehindMouse);
3610
3694
  // try to scroll element itself
3611
3695
  if (tryToScrollOne(elementBehindMouse, wheelEvent)) {
3612
3696
  return;
3613
3697
  }
3698
+ const belongsToElement = isElementOrDescendant(elementBehindMouse);
3614
3699
  if (belongsToElement) {
3615
- // the element is not scrollable and we don't care about his
3616
- // scrollable parent (because we know it's going to be the document)
3617
- // we search for scrollable container that might be behind it
3700
+ // keep searching if something in our element is scrollable
3618
3701
  continue;
3619
3702
  }
3703
+ // our element is not scrollable, try to scroll the container behind the mouse
3620
3704
  const scrollContainer = getScrollContainer(elementBehindMouse);
3621
3705
  if (tryToScrollOne(scrollContainer, wheelEvent)) {
3622
3706
  return;
@@ -11827,4 +11911,4 @@ const crossFade = {
11827
11911
  },
11828
11912
  };
11829
11913
 
11830
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getHeight, getInnerHeight, getInnerWidth, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, styleEffect, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
11914
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getInnerHeight, getInnerWidth, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getVisuallyVisibleInfo, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
package/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // state management
2
2
  export { createIterableWeakSet } from "./src/iterable_weak_set.js";
3
3
  export { createPubSub } from "./src/pub_sub.js";
4
+ export { createValueEffect } from "./src/value_effect.js";
4
5
 
5
6
  // style
6
7
  export { addWillChange, getStyle, setStyles } from "./src/style/dom_styles.js";
7
8
  export { createStyleController } from "./src/style/style_controller.js";
8
9
  export { getDefaultStyles } from "./src/style/style_default.js";
9
- export { styleEffect } from "./src/style/style_effect.js";
10
10
 
11
11
  // attributes
12
12
  export { addAttributeEffect } from "./src/attr/add_attribute_effect.js";
@@ -38,7 +38,13 @@ export {
38
38
  useActiveElement,
39
39
  } from "./src/interaction/focus/active_element.js";
40
40
  export { elementIsFocusable } from "./src/interaction/focus/element_is_focusable.js";
41
- export { elementIsVisible } from "./src/interaction/focus/element_is_visible.js";
41
+ export {
42
+ elementIsVisibleForFocus,
43
+ elementIsVisuallyVisible,
44
+ getFirstVisuallyVisibleAncestor,
45
+ getFocusVisibilityInfo,
46
+ getVisuallyVisibleInfo,
47
+ } from "./src/interaction/focus/element_visibility.js";
42
48
  export { findFocusable } from "./src/interaction/focus/find_focusable.js";
43
49
  export { initFocusGroup } from "./src/interaction/focus/focus_group.js";
44
50
  export { preventFocusNavViaKeyboard } from "./src/interaction/focus/focus_nav.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "DOM utilities for writing frontend code",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,9 +34,7 @@
34
34
  "build": "node ./scripts/build.mjs",
35
35
  "prepublishOnly": "npm run build"
36
36
  },
37
- "dependencies": {
38
- "style-observer": "0.1.2"
39
- },
37
+ "dependencies": {},
40
38
  "devDependencies": {
41
39
  "@jsenv/core": "../../../",
42
40
  "@jsenv/navi": "../navi",
@@ -1,4 +1,4 @@
1
- import { elementIsVisible } from "./element_is_visible.js";
1
+ import { elementIsVisibleForFocus } from "./element_visibility.js";
2
2
 
3
3
  export const elementIsFocusable = (node) => {
4
4
  // only element node can be focused, document, textNodes etc cannot
@@ -13,34 +13,34 @@ export const elementIsFocusable = (node) => {
13
13
  if (node.type === "hidden") {
14
14
  return false;
15
15
  }
16
- return elementIsVisible(node);
16
+ return elementIsVisibleForFocus(node);
17
17
  }
18
18
  if (
19
19
  ["button", "select", "datalist", "iframe", "textarea"].indexOf(nodeName) >
20
20
  -1
21
21
  ) {
22
- return elementIsVisible(node);
22
+ return elementIsVisibleForFocus(node);
23
23
  }
24
24
  if (["a", "area"].indexOf(nodeName) > -1) {
25
25
  if (node.hasAttribute("href") === false) {
26
26
  return false;
27
27
  }
28
- return elementIsVisible(node);
28
+ return elementIsVisibleForFocus(node);
29
29
  }
30
30
  if (["audio", "video"].indexOf(nodeName) > -1) {
31
31
  if (node.hasAttribute("controls") === false) {
32
32
  return false;
33
33
  }
34
- return elementIsVisible(node);
34
+ return elementIsVisibleForFocus(node);
35
35
  }
36
36
  if (nodeName === "summary") {
37
- return elementIsVisible(node);
37
+ return elementIsVisibleForFocus(node);
38
38
  }
39
39
  if (node.hasAttribute("tabindex") || node.hasAttribute("tabIndex")) {
40
- return elementIsVisible(node);
40
+ return elementIsVisibleForFocus(node);
41
41
  }
42
42
  if (node.hasAttribute("draggable")) {
43
- return elementIsVisible(node);
43
+ return elementIsVisibleForFocus(node);
44
44
  }
45
45
  return false;
46
46
  };
@@ -0,0 +1,111 @@
1
+ import { getStyle } from "../../style/dom_styles.js";
2
+ import {
3
+ elementIsDetails,
4
+ elementIsSummary,
5
+ isDocumentElement,
6
+ } from "../../utils.js";
7
+
8
+ export const elementIsVisibleForFocus = (node) => {
9
+ return getFocusVisibilityInfo(node).visible;
10
+ };
11
+ export const getFocusVisibilityInfo = (node) => {
12
+ if (isDocumentElement(node)) {
13
+ return { visible: true, reason: "is document" };
14
+ }
15
+ if (node.hasAttribute("hidden")) {
16
+ return { visible: false, reason: "has hidden attribute" };
17
+ }
18
+ if (getStyle(node, "visibility") === "hidden") {
19
+ return { visible: false, reason: "uses visiblity: hidden" };
20
+ }
21
+ if (node.tagName === "INPUT" && node.type === "hidden") {
22
+ return { visible: false, reason: "input type hidden" };
23
+ }
24
+ let nodeOrAncestor = node;
25
+ while (nodeOrAncestor) {
26
+ if (isDocumentElement(nodeOrAncestor)) {
27
+ break;
28
+ }
29
+ if (getStyle(nodeOrAncestor, "display") === "none") {
30
+ return { visible: false, reason: "ancestor uses display: none" };
31
+ }
32
+ // Check if element is inside a closed details element
33
+ if (elementIsDetails(nodeOrAncestor) && !nodeOrAncestor.open) {
34
+ // Special case: summary elements are visible even when their parent details is closed
35
+ // But only if this details element is the direct parent of the summary
36
+ if (!elementIsSummary(node) || node.parentElement !== nodeOrAncestor) {
37
+ return { visible: false, reason: "inside closed details element" };
38
+ }
39
+ // Continue checking ancestors
40
+ }
41
+ nodeOrAncestor = nodeOrAncestor.parentNode;
42
+ }
43
+ return { visible: true, reason: "no reason to be hidden" };
44
+ };
45
+
46
+ export const elementIsVisuallyVisible = (node, options = {}) => {
47
+ return getVisuallyVisibleInfo(node, options).visible;
48
+ };
49
+ export const getVisuallyVisibleInfo = (
50
+ node,
51
+ { countOffscreenAsVisible = false } = {},
52
+ ) => {
53
+ // First check all the focusable visibility conditions
54
+ const focusVisibilityInfo = getFocusVisibilityInfo(node);
55
+ if (!focusVisibilityInfo.visible) {
56
+ return focusVisibilityInfo;
57
+ }
58
+
59
+ // Additional visual visibility checks
60
+ if (getStyle(node, "opacity") === "0") {
61
+ return { visible: false, reason: "uses opacity: 0" };
62
+ }
63
+
64
+ const rect = node.getBoundingClientRect();
65
+ if (rect.width === 0 && rect.height === 0) {
66
+ return { visible: false, reason: "has zero dimensions" };
67
+ }
68
+
69
+ // Check for clipping
70
+ const clipStyle = getStyle(node, "clip");
71
+ if (clipStyle && clipStyle !== "auto" && clipStyle.includes("rect(0")) {
72
+ return { visible: false, reason: "clipped with clip property" };
73
+ }
74
+
75
+ const clipPathStyle = getStyle(node, "clip-path");
76
+ if (clipPathStyle && clipPathStyle.includes("inset(100%")) {
77
+ return { visible: false, reason: "clipped with clip-path" };
78
+ }
79
+
80
+ // Check if positioned off-screen (unless option says to count as visible)
81
+ if (!countOffscreenAsVisible) {
82
+ if (
83
+ rect.right < 0 ||
84
+ rect.bottom < 0 ||
85
+ rect.left > window.innerWidth ||
86
+ rect.top > window.innerHeight
87
+ ) {
88
+ return { visible: false, reason: "positioned off-screen" };
89
+ }
90
+ }
91
+
92
+ // Check for transform scale(0)
93
+ const transformStyle = getStyle(node, "transform");
94
+ if (transformStyle && transformStyle.includes("scale(0")) {
95
+ return { visible: false, reason: "scaled to zero with transform" };
96
+ }
97
+
98
+ return { visible: true, reason: "visually visible" };
99
+ };
100
+ export const getFirstVisuallyVisibleAncestor = (node, options = {}) => {
101
+ let ancestorCandidate = node.parentNode;
102
+ while (ancestorCandidate) {
103
+ const visibilityInfo = getVisuallyVisibleInfo(ancestorCandidate, options);
104
+ if (visibilityInfo.visible) {
105
+ return ancestorCandidate;
106
+ }
107
+ ancestorCandidate = ancestorCandidate.parentElement;
108
+ }
109
+ // This shouldn't happen in normal cases since document element is always visible
110
+ return null;
111
+ };
@@ -56,11 +56,12 @@ export const allowWheelThrough = (element, connectedElement) => {
56
56
  wheelEvent.clientY,
57
57
  );
58
58
  for (const elementBehindMouse of elementsBehindMouse) {
59
- const belongsToElement = isElementOrDescendant(elementBehindMouse);
60
59
  // try to scroll element itself
61
60
  if (tryToScrollOne(elementBehindMouse, wheelEvent)) {
62
61
  return;
63
62
  }
63
+ const belongsToElement = isElementOrDescendant(elementBehindMouse);
64
+ // try to scroll what is behind
64
65
  if (!belongsToElement) {
65
66
  break;
66
67
  }
@@ -79,17 +80,16 @@ export const allowWheelThrough = (element, connectedElement) => {
79
80
  wheelEvent.clientY,
80
81
  );
81
82
  for (const elementBehindMouse of elementsBehindMouse) {
82
- const belongsToElement = isElementOrDescendant(elementBehindMouse);
83
83
  // try to scroll element itself
84
84
  if (tryToScrollOne(elementBehindMouse, wheelEvent)) {
85
85
  return;
86
86
  }
87
+ const belongsToElement = isElementOrDescendant(elementBehindMouse);
87
88
  if (belongsToElement) {
88
- // the element is not scrollable and we don't care about his
89
- // scrollable parent (because we know it's going to be the document)
90
- // we search for scrollable container that might be behind it
89
+ // keep searching if something in our element is scrollable
91
90
  continue;
92
91
  }
92
+ // our element is not scrollable, try to scroll the container behind the mouse
93
93
  const scrollContainer = getScrollContainer(elementBehindMouse);
94
94
  if (tryToScrollOne(scrollContainer, wheelEvent)) {
95
95
  return;
@@ -0,0 +1,31 @@
1
+ export const createValueEffect = (value) => {
2
+ const callbackSet = new Set();
3
+ const previousValueCleanupSet = new Set();
4
+
5
+ const updateValue = (newValue) => {
6
+ if (newValue === value) {
7
+ return;
8
+ }
9
+ for (const cleanup of previousValueCleanupSet) {
10
+ cleanup();
11
+ }
12
+ previousValueCleanupSet.clear();
13
+ const oldValue = value;
14
+ value = newValue;
15
+ for (const callback of callbackSet) {
16
+ const returnValue = callback(newValue, oldValue);
17
+ if (typeof returnValue === "function") {
18
+ previousValueCleanupSet.add(returnValue);
19
+ }
20
+ }
21
+ };
22
+
23
+ const addEffect = (callback) => {
24
+ callbackSet.add(callback);
25
+ return () => {
26
+ callbackSet.delete(callback);
27
+ };
28
+ };
29
+
30
+ return [updateValue, addEffect];
31
+ };
@@ -1,36 +0,0 @@
1
- import { getStyle } from "../../style/dom_styles.js";
2
- import {
3
- elementIsDetails,
4
- elementIsSummary,
5
- isDocumentElement,
6
- } from "../../utils.js";
7
-
8
- export const elementIsVisible = (node) => {
9
- if (isDocumentElement(node)) {
10
- return true;
11
- }
12
- if (getStyle(node, "visibility") === "hidden") {
13
- return false;
14
- }
15
- let nodeOrAncestor = node;
16
- while (nodeOrAncestor) {
17
- if (isDocumentElement(nodeOrAncestor)) {
18
- break;
19
- }
20
- if (getStyle(nodeOrAncestor, "display") === "none") {
21
- return false;
22
- }
23
- // Check if element is inside a closed details element
24
- if (elementIsDetails(nodeOrAncestor) && !nodeOrAncestor.open) {
25
- // Special case: summary elements are visible even when their parent details is closed
26
- // But only if this details element is the direct parent of the summary
27
- if (elementIsSummary(node) && node.parentElement === nodeOrAncestor) {
28
- // Continue checking ancestors, don't return false yet
29
- } else {
30
- return false;
31
- }
32
- }
33
- nodeOrAncestor = nodeOrAncestor.parentNode;
34
- }
35
- return true;
36
- };
@@ -1,27 +0,0 @@
1
- import StyleObserver from "style-observer";
2
-
3
- import { normalizeStyle } from "./style_parsing.js";
4
-
5
- export const styleEffect = (element, callback, properties = []) => {
6
- const check = () => {
7
- const values = {};
8
- const computedStyle = getComputedStyle(element);
9
- for (const property of properties) {
10
- values[property] = normalizeStyle(
11
- computedStyle.getPropertyValue(property),
12
- property,
13
- );
14
- }
15
- callback(values);
16
- };
17
-
18
- check();
19
- const observer = new StyleObserver(() => {
20
- check();
21
- });
22
- observer.observe(element, properties);
23
-
24
- return () => {
25
- observer.unobserve();
26
- };
27
- };