@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 +129 -45
- package/index.js +8 -2
- package/package.json +2 -4
- 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/src/style/style_effect.js +0 -27
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2228
|
+
return elementIsVisibleForFocus(node);
|
|
2145
2229
|
}
|
|
2146
2230
|
if (nodeName === "summary") {
|
|
2147
|
-
return
|
|
2231
|
+
return elementIsVisibleForFocus(node);
|
|
2148
2232
|
}
|
|
2149
2233
|
if (node.hasAttribute("tabindex") || node.hasAttribute("tabIndex")) {
|
|
2150
|
-
return
|
|
2234
|
+
return elementIsVisibleForFocus(node);
|
|
2151
2235
|
}
|
|
2152
2236
|
if (node.hasAttribute("draggable")) {
|
|
2153
|
-
return
|
|
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
|
-
//
|
|
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,
|
|
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 {
|
|
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
|
+
"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 {
|
|
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
|
-
};
|
|
@@ -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
|
-
};
|