@jsenv/dom 0.4.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 +129 -20
- package/index.js +8 -1
- package/package.json +1 -1
- package/src/interaction/focus/element_is_focusable.js +8 -8
- package/src/interaction/focus/element_visibility.js +111 -0
- package/src/interaction/scroll/wheel_through.js +5 -5
- package/src/value_effect.js +31 -0
- package/src/interaction/focus/element_is_visible.js +0 -36
package/dist/jsenv_dom.js
CHANGED
|
@@ -100,6 +100,38 @@ const createPubSub = (clearOnPublish = false) => {
|
|
|
100
100
|
return [publish, subscribe, clear];
|
|
101
101
|
};
|
|
102
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
|
+
|
|
103
135
|
// https://github.com/davidtheclark/tabbable/blob/master/index.js
|
|
104
136
|
const isDocumentElement = (node) =>
|
|
105
137
|
node === node.ownerDocument.documentElement;
|
|
@@ -2057,12 +2089,21 @@ const addActiveElementEffect = (callback) => {
|
|
|
2057
2089
|
return remove;
|
|
2058
2090
|
};
|
|
2059
2091
|
|
|
2060
|
-
const
|
|
2092
|
+
const elementIsVisibleForFocus = (node) => {
|
|
2093
|
+
return getFocusVisibilityInfo(node).visible;
|
|
2094
|
+
};
|
|
2095
|
+
const getFocusVisibilityInfo = (node) => {
|
|
2061
2096
|
if (isDocumentElement(node)) {
|
|
2062
|
-
return true;
|
|
2097
|
+
return { visible: true, reason: "is document" };
|
|
2098
|
+
}
|
|
2099
|
+
if (node.hasAttribute("hidden")) {
|
|
2100
|
+
return { visible: false, reason: "has hidden attribute" };
|
|
2063
2101
|
}
|
|
2064
2102
|
if (getStyle(node, "visibility") === "hidden") {
|
|
2065
|
-
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" };
|
|
2066
2107
|
}
|
|
2067
2108
|
let nodeOrAncestor = node;
|
|
2068
2109
|
while (nodeOrAncestor) {
|
|
@@ -2070,19 +2111,87 @@ const elementIsVisible = (node) => {
|
|
|
2070
2111
|
break;
|
|
2071
2112
|
}
|
|
2072
2113
|
if (getStyle(nodeOrAncestor, "display") === "none") {
|
|
2073
|
-
return false;
|
|
2114
|
+
return { visible: false, reason: "ancestor uses display: none" };
|
|
2074
2115
|
}
|
|
2075
2116
|
// Check if element is inside a closed details element
|
|
2076
2117
|
if (elementIsDetails(nodeOrAncestor) && !nodeOrAncestor.open) {
|
|
2077
2118
|
// Special case: summary elements are visible even when their parent details is closed
|
|
2078
2119
|
// But only if this details element is the direct parent of the summary
|
|
2079
|
-
if (elementIsSummary(node)
|
|
2080
|
-
return false;
|
|
2120
|
+
if (!elementIsSummary(node) || node.parentElement !== nodeOrAncestor) {
|
|
2121
|
+
return { visible: false, reason: "inside closed details element" };
|
|
2081
2122
|
}
|
|
2123
|
+
// Continue checking ancestors
|
|
2082
2124
|
}
|
|
2083
2125
|
nodeOrAncestor = nodeOrAncestor.parentNode;
|
|
2084
2126
|
}
|
|
2085
|
-
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;
|
|
2086
2195
|
};
|
|
2087
2196
|
|
|
2088
2197
|
const elementIsFocusable = (node) => {
|
|
@@ -2098,34 +2207,34 @@ const elementIsFocusable = (node) => {
|
|
|
2098
2207
|
if (node.type === "hidden") {
|
|
2099
2208
|
return false;
|
|
2100
2209
|
}
|
|
2101
|
-
return
|
|
2210
|
+
return elementIsVisibleForFocus(node);
|
|
2102
2211
|
}
|
|
2103
2212
|
if (
|
|
2104
2213
|
["button", "select", "datalist", "iframe", "textarea"].indexOf(nodeName) >
|
|
2105
2214
|
-1
|
|
2106
2215
|
) {
|
|
2107
|
-
return
|
|
2216
|
+
return elementIsVisibleForFocus(node);
|
|
2108
2217
|
}
|
|
2109
2218
|
if (["a", "area"].indexOf(nodeName) > -1) {
|
|
2110
2219
|
if (node.hasAttribute("href") === false) {
|
|
2111
2220
|
return false;
|
|
2112
2221
|
}
|
|
2113
|
-
return
|
|
2222
|
+
return elementIsVisibleForFocus(node);
|
|
2114
2223
|
}
|
|
2115
2224
|
if (["audio", "video"].indexOf(nodeName) > -1) {
|
|
2116
2225
|
if (node.hasAttribute("controls") === false) {
|
|
2117
2226
|
return false;
|
|
2118
2227
|
}
|
|
2119
|
-
return
|
|
2228
|
+
return elementIsVisibleForFocus(node);
|
|
2120
2229
|
}
|
|
2121
2230
|
if (nodeName === "summary") {
|
|
2122
|
-
return
|
|
2231
|
+
return elementIsVisibleForFocus(node);
|
|
2123
2232
|
}
|
|
2124
2233
|
if (node.hasAttribute("tabindex") || node.hasAttribute("tabIndex")) {
|
|
2125
|
-
return
|
|
2234
|
+
return elementIsVisibleForFocus(node);
|
|
2126
2235
|
}
|
|
2127
2236
|
if (node.hasAttribute("draggable")) {
|
|
2128
|
-
return
|
|
2237
|
+
return elementIsVisibleForFocus(node);
|
|
2129
2238
|
}
|
|
2130
2239
|
return false;
|
|
2131
2240
|
};
|
|
@@ -3558,11 +3667,12 @@ const allowWheelThrough = (element, connectedElement) => {
|
|
|
3558
3667
|
wheelEvent.clientY,
|
|
3559
3668
|
);
|
|
3560
3669
|
for (const elementBehindMouse of elementsBehindMouse) {
|
|
3561
|
-
const belongsToElement = isElementOrDescendant(elementBehindMouse);
|
|
3562
3670
|
// try to scroll element itself
|
|
3563
3671
|
if (tryToScrollOne(elementBehindMouse, wheelEvent)) {
|
|
3564
3672
|
return;
|
|
3565
3673
|
}
|
|
3674
|
+
const belongsToElement = isElementOrDescendant(elementBehindMouse);
|
|
3675
|
+
// try to scroll what is behind
|
|
3566
3676
|
if (!belongsToElement) {
|
|
3567
3677
|
break;
|
|
3568
3678
|
}
|
|
@@ -3581,17 +3691,16 @@ const allowWheelThrough = (element, connectedElement) => {
|
|
|
3581
3691
|
wheelEvent.clientY,
|
|
3582
3692
|
);
|
|
3583
3693
|
for (const elementBehindMouse of elementsBehindMouse) {
|
|
3584
|
-
const belongsToElement = isElementOrDescendant(elementBehindMouse);
|
|
3585
3694
|
// try to scroll element itself
|
|
3586
3695
|
if (tryToScrollOne(elementBehindMouse, wheelEvent)) {
|
|
3587
3696
|
return;
|
|
3588
3697
|
}
|
|
3698
|
+
const belongsToElement = isElementOrDescendant(elementBehindMouse);
|
|
3589
3699
|
if (belongsToElement) {
|
|
3590
|
-
//
|
|
3591
|
-
// scrollable parent (because we know it's going to be the document)
|
|
3592
|
-
// we search for scrollable container that might be behind it
|
|
3700
|
+
// keep searching if something in our element is scrollable
|
|
3593
3701
|
continue;
|
|
3594
3702
|
}
|
|
3703
|
+
// our element is not scrollable, try to scroll the container behind the mouse
|
|
3595
3704
|
const scrollContainer = getScrollContainer(elementBehindMouse);
|
|
3596
3705
|
if (tryToScrollOne(scrollContainer, wheelEvent)) {
|
|
3597
3706
|
return;
|
|
@@ -11802,4 +11911,4 @@ const crossFade = {
|
|
|
11802
11911
|
},
|
|
11803
11912
|
};
|
|
11804
11913
|
|
|
11805
|
-
export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable,
|
|
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,6 +1,7 @@
|
|
|
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";
|
|
@@ -37,7 +38,13 @@ export {
|
|
|
37
38
|
useActiveElement,
|
|
38
39
|
} from "./src/interaction/focus/active_element.js";
|
|
39
40
|
export { elementIsFocusable } from "./src/interaction/focus/element_is_focusable.js";
|
|
40
|
-
export {
|
|
41
|
+
export {
|
|
42
|
+
elementIsVisibleForFocus,
|
|
43
|
+
elementIsVisuallyVisible,
|
|
44
|
+
getFirstVisuallyVisibleAncestor,
|
|
45
|
+
getFocusVisibilityInfo,
|
|
46
|
+
getVisuallyVisibleInfo,
|
|
47
|
+
} from "./src/interaction/focus/element_visibility.js";
|
|
41
48
|
export { findFocusable } from "./src/interaction/focus/find_focusable.js";
|
|
42
49
|
export { initFocusGroup } from "./src/interaction/focus/focus_group.js";
|
|
43
50
|
export { preventFocusNavViaKeyboard } from "./src/interaction/focus/focus_nav.js";
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
34
|
+
return elementIsVisibleForFocus(node);
|
|
35
35
|
}
|
|
36
36
|
if (nodeName === "summary") {
|
|
37
|
-
return
|
|
37
|
+
return elementIsVisibleForFocus(node);
|
|
38
38
|
}
|
|
39
39
|
if (node.hasAttribute("tabindex") || node.hasAttribute("tabIndex")) {
|
|
40
|
-
return
|
|
40
|
+
return elementIsVisibleForFocus(node);
|
|
41
41
|
}
|
|
42
42
|
if (node.hasAttribute("draggable")) {
|
|
43
|
-
return
|
|
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
|
-
//
|
|
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
|
-
};
|