@jsenv/dom 0.9.1 → 0.9.2

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 +139 -59
  2. package/package.json +2 -2
package/dist/jsenv_dom.js CHANGED
@@ -385,7 +385,8 @@ const areSameRGBA = (first, second) => {
385
385
  };
386
386
  const resolveCSSColor = (color, element) => {
387
387
  const rgba = parseCSSColor(color, element);
388
- return stringifyCSSColor(rgba);
388
+ const colorString = stringifyCSSColor(rgba);
389
+ return colorString;
389
390
  };
390
391
 
391
392
  /**
@@ -440,12 +441,27 @@ const parseCSSColor = (color, element) => {
440
441
  return color;
441
442
  }
442
443
 
444
+ // oklab(L a b) and oklab(L a b / alpha)
445
+ if (color.startsWith("oklab(")) {
446
+ const oklabMatch = color.match(
447
+ /^oklab\(\s*([\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/,
448
+ );
449
+ if (oklabMatch) {
450
+ const L = parseFloat(oklabMatch[1]);
451
+ const a = parseFloat(oklabMatch[2]);
452
+ const b = parseFloat(oklabMatch[3]);
453
+ const alpha = oklabMatch[4] !== undefined ? parseFloat(oklabMatch[4]) : 1;
454
+ const [r, g, bChannel] = oklabToRgb(L, a, b);
455
+ return [r, g, bChannel, alpha];
456
+ }
457
+ return color;
458
+ }
459
+
443
460
  // Pass through CSS color functions we don't handle
444
461
  if (
445
462
  color.startsWith("lch(") ||
446
463
  color.startsWith("oklch(") ||
447
464
  color.startsWith("lab(") ||
448
- color.startsWith("oklab(") ||
449
465
  color.startsWith("hwb(") ||
450
466
  color.includes("color-contrast(")
451
467
  ) {
@@ -505,6 +521,27 @@ const parseCSSColor = (color, element) => {
505
521
  const rgba = convertColorToRgba(resolvedColor);
506
522
  return rgba;
507
523
  };
524
+ const oklabToRgb = (L, a, b) => {
525
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
526
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
527
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
528
+ const l = l_ * l_ * l_;
529
+ const m = m_ * m_ * m_;
530
+ const s = s_ * s_ * s_;
531
+ const rLinear = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
532
+ const gLinear = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
533
+ const bLinear = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
534
+ const toSrgb = (linear) => {
535
+ const clamped = linear < 0 ? 0 : linear > 1 ? 1 : linear;
536
+ const srgb =
537
+ clamped <= 0.0031308
538
+ ? 12.92 * clamped
539
+ : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
540
+ return Math.round(srgb * 255);
541
+ };
542
+ return [toSrgb(rLinear), toSrgb(gLinear), toSrgb(bLinear)];
543
+ };
544
+
508
545
  /**
509
546
  * Converts HSL color to RGB
510
547
  * @param {number} h - Hue (0-360)
@@ -637,12 +674,10 @@ const stringifyCSSColor = (value) => {
637
674
  }
638
675
  const rgba = value;
639
676
  const [r, g, b, a = 1] = rgba;
640
-
641
677
  // Validate RGB values
642
678
  if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
643
679
  return null;
644
680
  }
645
-
646
681
  // Validate alpha value
647
682
  if (a < 0 || a > 1) {
648
683
  return null;
@@ -660,10 +695,7 @@ const stringifyCSSColor = (value) => {
660
695
  return name;
661
696
  }
662
697
  }
663
- }
664
-
665
- // Use rgb() for opaque colors, rgba() for transparent
666
- if (a === 1) {
698
+ // Use rgb() for opaque colors, rgba() for transparent
667
699
  return `rgb(${rInt}, ${gInt}, ${bInt})`;
668
700
  }
669
701
  if (a === 0 && rInt === 0 && gInt === 0 && bInt === 0) {
@@ -4034,12 +4066,6 @@ const performArrowNavigation = (
4034
4066
  }
4035
4067
 
4036
4068
  const onTargetToFocus = (targetToFocus) => {
4037
- console.debug(
4038
- `Arrow navigation: ${event.key} from`,
4039
- activeElement,
4040
- "to",
4041
- targetToFocus,
4042
- );
4043
4069
  event.preventDefault();
4044
4070
  markFocusNav(event);
4045
4071
  targetToFocus.focus();
@@ -7041,6 +7067,8 @@ installImportMetaCss(import.meta);const setupConstraintFeedbackLine = () => {
7041
7067
  const { grabEvent, dragEvent } = gestureInfo;
7042
7068
  if (
7043
7069
  grabEvent.type === "programmatic" ||
7070
+ // dragEvent can be null when only mousedown without yet any mousemove
7071
+ !dragEvent ||
7044
7072
  dragEvent.type === "programmatic"
7045
7073
  ) {
7046
7074
  // programmatic drag
@@ -8913,31 +8941,48 @@ const initPositionSticky = (element) => {
8913
8941
  const computedStyle = getComputedStyle(element);
8914
8942
  const topCssValue = computedStyle.top;
8915
8943
  const top = parseFloat(topCssValue);
8916
- if (isNaN(top)) {
8917
- return () => {}; // Early return if no valid top value
8944
+ const leftCssValue = computedStyle.left;
8945
+ const left = parseFloat(leftCssValue);
8946
+ const hasTop = !isNaN(top);
8947
+ const hasLeft = !isNaN(left);
8948
+ if (!hasTop && !hasLeft) {
8949
+ return () => {}; // Early return if no valid top or left value
8918
8950
  }
8919
8951
 
8920
8952
  // Skip polyfill if native position:sticky would work (no overflow:auto/hidden parents)
8921
8953
  const scrollContainerSet = getScrollContainerSet(element);
8922
- {
8923
- let hasOverflowHiddenOrAuto = false;
8924
- for (const scrollContainer of scrollContainerSet) {
8925
- const scrollContainerComputedStyle = getComputedStyle(scrollContainer);
8926
- const overflowX = scrollContainerComputedStyle.overflowX;
8927
- if (overflowX === "auto" || overflowX === "hidden") {
8928
- hasOverflowHiddenOrAuto = true;
8929
- break;
8930
- }
8931
- const overflowY = scrollContainerComputedStyle.overflowY;
8932
- if (overflowY === "auto" || overflowY === "hidden") {
8933
- hasOverflowHiddenOrAuto = true;
8934
- break;
8935
- }
8954
+ // Determine per-axis whether an intermediate container blocks native sticky.
8955
+ // Native sticky fails only when there is a scroll container between the element
8956
+ // and the document with overflow set on that axis.
8957
+ let xScrollContainer = null; // first intermediate container blocking horizontal sticky
8958
+ let yScrollContainer = null; // first intermediate container blocking vertical sticky
8959
+ for (const scrollContainer of scrollContainerSet) {
8960
+ if (scrollContainer === document.documentElement) {
8961
+ break;
8936
8962
  }
8937
- if (!hasOverflowHiddenOrAuto) {
8938
- return () => {}; // Native sticky will work fine
8963
+ const style = getComputedStyle(scrollContainer);
8964
+ if (
8965
+ xScrollContainer === null &&
8966
+ (style.overflowX === "auto" ||
8967
+ style.overflowX === "hidden" ||
8968
+ style.overflowX === "scroll")
8969
+ ) {
8970
+ xScrollContainer = scrollContainer;
8971
+ }
8972
+ if (
8973
+ yScrollContainer === null &&
8974
+ (style.overflowY === "auto" ||
8975
+ style.overflowY === "hidden" ||
8976
+ style.overflowY === "scroll")
8977
+ ) {
8978
+ yScrollContainer = scrollContainer;
8939
8979
  }
8940
8980
  }
8981
+ const needsPolyfillX = hasLeft && xScrollContainer !== null;
8982
+ const needsPolyfillY = hasTop && yScrollContainer !== null;
8983
+ if (!needsPolyfillX && !needsPolyfillY) {
8984
+ return () => {}; // Native sticky will work fine on both axes
8985
+ }
8941
8986
 
8942
8987
  const cleanupCallbackSet = new Set();
8943
8988
  const cleanup = () => {
@@ -8983,39 +9028,72 @@ const initPositionSticky = (element) => {
8983
9028
  const placeholderRect = placeholder.getBoundingClientRect();
8984
9029
  const parentRect = parentElement.getBoundingClientRect();
8985
9030
 
8986
- // Calculate left position in viewport coordinates (fixed positioning)
8987
- const leftPosition = placeholderRect.left;
8988
- element.style.left = `${Math.round(leftPosition)}px`;
8989
-
8990
- // Determine if element should be sticky or at its natural position
9031
+ // The CSS `top`/`left` values are offsets from the scroll container's edge.
9032
+ // getBoundingClientRect() always returns viewport coordinates (already accounting
9033
+ // for scroll position of all ancestors), so to convert the CSS offset to a
9034
+ // viewport threshold we add the scroll container's own viewport position.
9035
+ //
9036
+ // Example: main starts at viewport x=250, left=0 → leftThreshold=250.
9037
+ // After scrolling main 670px: placeholderRect.left = 250-670 = -420.
9038
+ // -420 <= 250 → stuck → element.style.left = 250px (main's left edge). ✓
9039
+ //
9040
+ // If no intermediate scroll container exists, use 0 (document/viewport edge).
9041
+ const yContainerRect = yScrollContainer
9042
+ ? yScrollContainer.getBoundingClientRect()
9043
+ : { top: 0 };
9044
+ const xContainerRect = xScrollContainer
9045
+ ? xScrollContainer.getBoundingClientRect()
9046
+ : { left: 0 };
9047
+ const topThreshold = yContainerRect.top + top;
9048
+ const leftThreshold = xContainerRect.left + left;
9049
+
9050
+ // ── Vertical (top) ──────────────────────────────────────────────────────
8991
9051
  let topPosition;
8992
- let isStuck = false;
8993
-
8994
- // Check if we need to stick the element
8995
- if (placeholderRect.top <= top) {
8996
- // Element should be stuck at "top" position in the viewport
8997
- topPosition = top;
8998
- isStuck = true;
8999
-
9000
- // But make sure it doesn't go beyond parent's bottom boundary
9001
- const parentBottom = parentRect.bottom;
9002
- const elementBottom = top + height;
9003
-
9004
- if (elementBottom > parentBottom) {
9005
- // Adjust to stay within parent
9006
- topPosition = parentBottom - height;
9052
+ let isStuckVertically = false;
9053
+ if (hasTop) {
9054
+ if (placeholderRect.top <= topThreshold) {
9055
+ topPosition = topThreshold;
9056
+ isStuckVertically = true;
9057
+ // Don't go beyond parent's bottom boundary
9058
+ const parentBottom = parentRect.bottom;
9059
+ const elementBottom = topThreshold + height;
9060
+ if (elementBottom > parentBottom) {
9061
+ topPosition = parentBottom - height;
9062
+ }
9063
+ } else {
9064
+ topPosition = placeholderRect.top;
9007
9065
  }
9008
9066
  } else {
9009
- // Element should be at its natural position in the flow
9010
9067
  topPosition = placeholderRect.top;
9011
9068
  }
9012
9069
 
9070
+ // ── Horizontal (left) ───────────────────────────────────────────────────
9071
+ let leftPosition;
9072
+ let isStuckHorizontally = false;
9073
+ if (hasLeft) {
9074
+ if (placeholderRect.left <= leftThreshold) {
9075
+ leftPosition = leftThreshold;
9076
+ isStuckHorizontally = true;
9077
+ // Don't go beyond parent's right boundary
9078
+ const parentRight = parentRect.right;
9079
+ const elementRight = leftThreshold + width;
9080
+ if (elementRight > parentRight) {
9081
+ leftPosition = parentRight - width;
9082
+ }
9083
+ } else {
9084
+ leftPosition = placeholderRect.left;
9085
+ }
9086
+ } else {
9087
+ leftPosition = placeholderRect.left;
9088
+ }
9089
+
9013
9090
  element.style.top = `${topPosition}px`;
9091
+ element.style.left = `${Math.round(leftPosition)}px`;
9014
9092
  element.style.width = `${width}px`;
9015
9093
  element.style.height = `${height}px`;
9016
9094
 
9017
9095
  // Set attribute for potential styling
9018
- if (isStuck) {
9096
+ if (isStuckVertically || isStuckHorizontally) {
9019
9097
  element.setAttribute("data-sticky", "");
9020
9098
  } else {
9021
9099
  element.removeAttribute("data-sticky");
@@ -9038,12 +9116,14 @@ const initPositionSticky = (element) => {
9038
9116
  updatePosition();
9039
9117
  };
9040
9118
 
9041
- for (const scrollContainer of scrollContainerSet) {
9042
- scrollContainer.addEventListener("scroll", handleScroll, {
9043
- passive: true,
9044
- });
9119
+ // Listen on all scroll containers (including document) since the element
9120
+ // uses position:fixed and any ancestor scroll changes its apparent position.
9121
+ const listenTargets = new Set(scrollContainerSet);
9122
+ listenTargets.add(document.documentElement);
9123
+ for (const scrollTarget of listenTargets) {
9124
+ scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
9045
9125
  cleanupCallbackSet.add(() => {
9046
- scrollContainer.removeEventListener("scroll", handleScroll, {
9126
+ scrollTarget.removeEventListener("scroll", handleScroll, {
9047
9127
  passive: true,
9048
9128
  });
9049
9129
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "DOM utilities for writing frontend code",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,7 +42,7 @@
42
42
  "@jsenv/core": "../../../",
43
43
  "@jsenv/navi": "../navi",
44
44
  "@jsenv/snapshot": "../../tooling/snapshot",
45
- "@preact/signals": "2.8.1",
45
+ "@preact/signals": "2.9.0",
46
46
  "preact": "11.0.0-beta.0"
47
47
  },
48
48
  "publishConfig": {