@jsenv/dom 0.10.3 → 0.10.4

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.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +100 -53
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -5419,6 +5419,31 @@ const getScrollContainerSet = (element) => {
5419
5419
  return scrollContainerSet;
5420
5420
  };
5421
5421
 
5422
+ /**
5423
+ * Rounds a CSS pixel value to the nearest physical pixel boundary for the current display.
5424
+ *
5425
+ * At zoom levels other than 100%, `devicePixelRatio` is not an integer (e.g. 1.25, 1.5),
5426
+ * so fractional CSS pixel values from `getBoundingClientRect()` may not align to the physical
5427
+ * pixel grid. Setting `top`/`left` to such values causes the browser to interpolate across
5428
+ * pixels, resulting in blurry rendering or misalignment with adjacent elements.
5429
+ *
5430
+ * Snapping to the physical grid ensures the value falls exactly on a pixel boundary.
5431
+ *
5432
+ * @param {number} value - A CSS pixel value (e.g. from getBoundingClientRect or scroll offset).
5433
+ * @returns {number} The nearest physical-pixel-aligned CSS pixel value.
5434
+ * @example
5435
+ * // At devicePixelRatio 1.25, snapToPixel(154.4) → 154.4 (already on grid)
5436
+ * // At devicePixelRatio 1.25, snapToPixel(154.3) → 154.4
5437
+ */
5438
+ const snapToPixel = (value) => {
5439
+ return Math.round(value * devicePixelRatio) / devicePixelRatio;
5440
+ };
5441
+
5442
+ // Round a CSS-pixel value to the nearest physical pixel boundary.
5443
+ // At zoom levels other than 100%, devicePixelRatio is not an integer (e.g. 1.25, 1.5),
5444
+ // so CSS pixels don't align 1:1 with physical pixels. Rounding to the physical grid
5445
+ // ensures the browser can render the element without sub-pixel blurring.
5446
+
5422
5447
  const getBorderSizes = (element) => {
5423
5448
  const {
5424
5449
  borderLeftWidth,
@@ -5426,11 +5451,12 @@ const getBorderSizes = (element) => {
5426
5451
  borderTopWidth,
5427
5452
  borderBottomWidth,
5428
5453
  } = window.getComputedStyle(element, null);
5454
+
5429
5455
  return {
5430
- left: parseFloat(borderLeftWidth),
5431
- right: parseFloat(borderRightWidth),
5432
- top: parseFloat(borderTopWidth),
5433
- bottom: parseFloat(borderBottomWidth),
5456
+ left: snapToPixel(parseFloat(borderLeftWidth)),
5457
+ right: snapToPixel(parseFloat(borderRightWidth)),
5458
+ top: snapToPixel(parseFloat(borderTopWidth)),
5459
+ bottom: snapToPixel(parseFloat(borderBottomWidth)),
5434
5460
  };
5435
5461
  };
5436
5462
 
@@ -5782,8 +5808,8 @@ const measureScrollbar = (scrollableElement) => {
5782
5808
  const scrollbarHeight = scrollDiv.offsetHeight - scrollDiv.clientHeight;
5783
5809
  scrollableElement.removeChild(scrollDiv);
5784
5810
  return [
5785
- hasXScrollbar ? scrollbarWidth : 0,
5786
- hasYScrollbar ? scrollbarHeight : 0,
5811
+ hasXScrollbar ? snapToPixel(scrollbarWidth) : 0,
5812
+ hasYScrollbar ? snapToPixel(scrollbarHeight) : 0,
5787
5813
  ];
5788
5814
  };
5789
5815
 
@@ -6096,6 +6122,18 @@ const scrollIntoViewWithStickyAwareness = (
6096
6122
  }
6097
6123
  };
6098
6124
 
6125
+ const getPaddingSizes = (element) => {
6126
+ const { paddingLeft, paddingRight, paddingTop, paddingBottom } =
6127
+ window.getComputedStyle(element, null);
6128
+
6129
+ return {
6130
+ left: snapToPixel(parseFloat(paddingLeft)),
6131
+ right: snapToPixel(parseFloat(paddingRight)),
6132
+ top: snapToPixel(parseFloat(paddingTop)),
6133
+ bottom: snapToPixel(parseFloat(paddingBottom)),
6134
+ };
6135
+ };
6136
+
6099
6137
  /**
6100
6138
  * Prevents scrolling on all scrollable containers that are ancestors of (or
6101
6139
  * siblings preceding) `element`. Used when an overlay (popover, dialog) is
@@ -6132,11 +6170,10 @@ const trapScrollInside = (element) => {
6132
6170
  return;
6133
6171
  }
6134
6172
  const [scrollbarWidth, scrollbarHeight] = measureScrollbar(el);
6135
- const paddingRight = parseInt(getStyle(el, "padding-right"), 0);
6136
- const paddingBottom = parseInt(getStyle(el, "padding-bottom"), 0);
6173
+ const { right, bottom } = getPaddingSizes(el);
6137
6174
  const removeScrollLockStyles = setStyles(el, {
6138
- "padding-right": `${paddingRight + scrollbarWidth}px`,
6139
- "padding-bottom": `${paddingBottom + scrollbarHeight}px`,
6175
+ "padding-right": `${right + scrollbarWidth}px`,
6176
+ "padding-bottom": `${bottom + scrollbarHeight}px`,
6140
6177
  "overflow": "hidden",
6141
6178
  });
6142
6179
  cleanupCallbackSet.add(removeScrollLockStyles);
@@ -9930,6 +9967,8 @@ const pickPositionRelativeTo = (
9930
9967
  positionYFixed,
9931
9968
  alignToViewportEdgeWhenAnchorNearEdge = 0,
9932
9969
  minLeft = 0,
9970
+ spacing = 0,
9971
+ viewportSpacing = 0,
9933
9972
  } = {},
9934
9973
  ) => {
9935
9974
 
@@ -9944,12 +9983,10 @@ const pickPositionRelativeTo = (
9944
9983
  top: elementTop,
9945
9984
  bottom: elementBottom,
9946
9985
  } = elementRect;
9947
- const {
9948
- left: anchorLeft,
9949
- right: anchorRight,
9950
- top: anchorTop,
9951
- bottom: anchorBottom,
9952
- } = anchorRect;
9986
+ const anchorLeft = snapToPixel(anchorRect.left);
9987
+ const anchorTop = snapToPixel(anchorRect.top);
9988
+ const anchorRight = snapToPixel(anchorRect.right);
9989
+ const anchorBottom = snapToPixel(anchorRect.bottom);
9953
9990
  const elementWidth = elementRight - elementLeft;
9954
9991
  const elementHeight = elementBottom - elementTop;
9955
9992
  const anchorWidth = anchorRight - anchorLeft;
@@ -9992,16 +10029,16 @@ const pickPositionRelativeTo = (
9992
10029
  // Compute effective space for a given Y value
9993
10030
  const spaceFor = (y) => {
9994
10031
  if (y === "above") {
9995
- return spaceAbove;
10032
+ return spaceAbove - spacing - viewportSpacing;
9996
10033
  }
9997
10034
  if (y === "above-overlap") {
9998
- return spaceAbove + anchorHeight;
10035
+ return spaceAbove + anchorHeight - viewportSpacing;
9999
10036
  }
10000
10037
  if (y === "below") {
10001
- return spaceBelow;
10038
+ return spaceBelow - spacing - viewportSpacing;
10002
10039
  }
10003
10040
  if (y === "below-overlap") {
10004
- return spaceBelow + anchorHeight;
10041
+ return spaceBelow + anchorHeight - viewportSpacing;
10005
10042
  }
10006
10043
  return Infinity; // center
10007
10044
  };
@@ -10049,16 +10086,16 @@ const pickPositionRelativeTo = (
10049
10086
  // Compute effective space for a given X value
10050
10087
  const spaceFor = (x) => {
10051
10088
  if (x === "to-the-left") {
10052
- return spaceLeft;
10089
+ return spaceLeft - spacing - viewportSpacing;
10053
10090
  }
10054
10091
  if (x === "left-aligned") {
10055
- return viewportWidth - anchorLeft;
10092
+ return viewportWidth - anchorLeft - viewportSpacing;
10056
10093
  }
10057
10094
  if (x === "right-aligned") {
10058
- return anchorRight;
10095
+ return anchorRight - viewportSpacing;
10059
10096
  }
10060
10097
  if (x === "to-the-right") {
10061
- return spaceRight;
10098
+ return spaceRight - spacing - viewportSpacing;
10062
10099
  }
10063
10100
  return Infinity; // center
10064
10101
  };
@@ -10095,7 +10132,7 @@ const pickPositionRelativeTo = (
10095
10132
  let elementPositionLeft;
10096
10133
  {
10097
10134
  if (finalX === "to-the-left") {
10098
- elementPositionLeft = anchorLeft - elementWidth;
10135
+ elementPositionLeft = anchorLeft - elementWidth - spacing;
10099
10136
  } else if (finalX === "left-aligned") {
10100
10137
  elementPositionLeft = anchorLeft;
10101
10138
  } else if (finalX === "center") {
@@ -10132,13 +10169,16 @@ const pickPositionRelativeTo = (
10132
10169
  elementPositionLeft = anchorRight - elementWidth;
10133
10170
  } else {
10134
10171
  // "to-the-right"
10135
- elementPositionLeft = anchorRight;
10172
+ elementPositionLeft = anchorRight + spacing;
10136
10173
  }
10137
- // Constrain horizontal position to viewport boundaries
10138
- if (elementPositionLeft < 0) {
10139
- elementPositionLeft = 0;
10140
- } else if (elementPositionLeft + elementWidth > viewportWidth) {
10141
- elementPositionLeft = viewportWidth - elementWidth;
10174
+ // Constrain horizontal position to viewport boundaries (with viewportSpacing margin)
10175
+ if (elementPositionLeft < viewportSpacing) {
10176
+ elementPositionLeft = viewportSpacing;
10177
+ } else if (
10178
+ elementPositionLeft + elementWidth >
10179
+ viewportWidth - viewportSpacing
10180
+ ) {
10181
+ elementPositionLeft = viewportWidth - viewportSpacing - elementWidth;
10142
10182
  }
10143
10183
  }
10144
10184
 
@@ -10146,11 +10186,14 @@ const pickPositionRelativeTo = (
10146
10186
  let elementPositionTop;
10147
10187
  {
10148
10188
  if (finalY === "above") {
10149
- const idealTop = anchorTop - elementHeight;
10150
- elementPositionTop = idealTop < 0 ? 0 : idealTop;
10189
+ // top is always anchorTop - elementHeight - spacing — max-height truncates if needed.
10190
+ const idealTop = anchorTop - elementHeight - spacing;
10191
+ elementPositionTop =
10192
+ idealTop < viewportSpacing ? viewportSpacing : idealTop;
10151
10193
  } else if (finalY === "above-overlap") {
10152
10194
  const idealTop = anchorBottom - elementHeight;
10153
- elementPositionTop = idealTop < 0 ? 0 : idealTop;
10195
+ elementPositionTop =
10196
+ idealTop < viewportSpacing ? viewportSpacing : idealTop;
10154
10197
  } else if (finalY === "center") {
10155
10198
  elementPositionTop = anchorTop + anchorHeight / 2 - elementHeight / 2;
10156
10199
  } else if (finalY === "below-overlap") {
@@ -10159,7 +10202,9 @@ const pickPositionRelativeTo = (
10159
10202
  idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
10160
10203
  } else {
10161
10204
  // "below"
10162
- const idealTop = anchorBottom;
10205
+ // top is always anchorBottom + spacing — max-height (via --space-available) truncates
10206
+ // the element height so it doesn't overflow the viewport bottom.
10207
+ const idealTop = anchorBottom + spacing;
10163
10208
  elementPositionTop =
10164
10209
  idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
10165
10210
  }
@@ -10176,13 +10221,26 @@ const pickPositionRelativeTo = (
10176
10221
 
10177
10222
  // Get document scroll for final coordinate conversion
10178
10223
  const { scrollLeft, scrollTop } = document.documentElement;
10179
- const elementDocumentLeft = elementPositionLeft + scrollLeft;
10180
- const elementDocumentTop = elementPositionTop + scrollTop;
10224
+ const elementDocumentLeft = snapToPixel(elementPositionLeft + scrollLeft);
10225
+ const elementDocumentTop = snapToPixel(elementPositionTop + scrollTop);
10181
10226
  const anchorDocumentLeft = anchorLeft + scrollLeft;
10182
10227
  const anchorDocumentTop = anchorTop + scrollTop;
10183
10228
  const anchorDocumentRight = anchorRight + scrollLeft;
10184
10229
  const anchorDocumentBottom = anchorBottom + scrollTop;
10185
10230
 
10231
+ // For overlap variants the element starts at the anchor edge (not past it),
10232
+ // so the usable space includes the anchor dimension.
10233
+ // spacing (gap between anchor and element) and viewportSpacing are subtracted
10234
+ // so callers get the net usable space directly.
10235
+ const effectiveSpaceAbove =
10236
+ (finalY === "above-overlap" ? spaceAbove + anchorHeight : spaceAbove) -
10237
+ (finalY === "above" ? spacing : 0) -
10238
+ viewportSpacing;
10239
+ const effectiveSpaceBelow =
10240
+ (finalY === "below-overlap" ? spaceBelow + anchorHeight : spaceBelow) -
10241
+ (finalY === "below" ? spacing : 0) -
10242
+ viewportSpacing;
10243
+
10186
10244
  return {
10187
10245
  positionX: finalX,
10188
10246
  positionY: finalY,
@@ -10194,10 +10252,10 @@ const pickPositionRelativeTo = (
10194
10252
  anchorTop: anchorDocumentTop,
10195
10253
  anchorRight: anchorDocumentRight,
10196
10254
  anchorBottom: anchorDocumentBottom,
10197
- spaceLeft,
10198
- spaceRight,
10199
- spaceAbove,
10200
- spaceBelow,
10255
+ spaceLeft: spaceLeft - viewportSpacing,
10256
+ spaceRight: spaceRight - viewportSpacing,
10257
+ spaceAbove: effectiveSpaceAbove,
10258
+ spaceBelow: effectiveSpaceBelow,
10201
10259
  };
10202
10260
  };
10203
10261
 
@@ -11973,17 +12031,6 @@ const getWidthWithoutTransition = (element) =>
11973
12031
  const getHeightWithoutTransition = (element) =>
11974
12032
  getHeight$1(element, transitionStyleController);
11975
12033
 
11976
- const getPaddingSizes = (element) => {
11977
- const { paddingLeft, paddingRight, paddingTop, paddingBottom } =
11978
- window.getComputedStyle(element, null);
11979
- return {
11980
- left: parseFloat(paddingLeft),
11981
- right: parseFloat(paddingRight),
11982
- top: parseFloat(paddingTop),
11983
- bottom: parseFloat(paddingBottom),
11984
- };
11985
- };
11986
-
11987
12034
  const getInnerHeight = (element) => {
11988
12035
  // Always subtract paddings and borders to get the content height
11989
12036
  const paddingSizes = getPaddingSizes(element);
@@ -13324,4 +13371,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
13324
13371
  };
13325
13372
  };
13326
13373
 
13327
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
13374
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyle, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {