@jsenv/dom 0.17.2 → 0.17.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 +297 -255
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -2051,16 +2051,12 @@ const parseCSSTransform = (transformString, normalize) => {
2051
2051
 
2052
2052
  const transformObj = {};
2053
2053
 
2054
- // Parse transform functions
2055
- const transformPattern = /(\w+)\(([^)]+)\)/g;
2056
- let match;
2057
-
2058
- while ((match = transformPattern.exec(transformString)) !== null) {
2059
- const [, functionName, value] = match;
2060
-
2054
+ for (const { functionName, value, source } of readTransformFunctions(
2055
+ transformString,
2056
+ )) {
2061
2057
  // Handle matrix functions specially
2062
2058
  if (functionName === "matrix" || functionName === "matrix3d") {
2063
- const matrixComponents = parseMatrixTransform(match[0]);
2059
+ const matrixComponents = parseMatrixTransform(source);
2064
2060
  if (matrixComponents) {
2065
2061
  // Only add non-default values to preserve original information
2066
2062
  Object.assign(transformObj, matrixComponents);
@@ -2079,6 +2075,42 @@ const parseCSSTransform = (transformString, normalize) => {
2079
2075
  // Return undefined if no properties were extracted (preserves original information)
2080
2076
  return Object.keys(transformObj).length > 0 ? transformObj : undefined;
2081
2077
  };
2078
+
2079
+ // Cuts "translateX(10px) translateY(env(safe-area-inset-top))" into its
2080
+ // functions. Parentheses are counted rather than matched with a regex: a
2081
+ // transform value may hold calc(), env(), min()… and stopping at the first ")"
2082
+ // truncates them.
2083
+ const TRANSFORM_FUNCTION_START_REGEX = /(\w+)\(/g;
2084
+ const readTransformFunctions = (transformString) => {
2085
+ const transformFunctions = [];
2086
+ TRANSFORM_FUNCTION_START_REGEX.lastIndex = 0;
2087
+ let match;
2088
+ while ((match = TRANSFORM_FUNCTION_START_REGEX.exec(transformString))) {
2089
+ const valueStart = match.index + match[0].length;
2090
+ let depth = 1;
2091
+ let index = valueStart;
2092
+ while (index < transformString.length && depth > 0) {
2093
+ const char = transformString[index];
2094
+ if (char === "(") {
2095
+ depth++;
2096
+ } else if (char === ")") {
2097
+ depth--;
2098
+ }
2099
+ index++;
2100
+ }
2101
+ if (depth > 0) {
2102
+ // Unbalanced: nothing reliable left to read after this point.
2103
+ break;
2104
+ }
2105
+ transformFunctions.push({
2106
+ functionName: match[1],
2107
+ value: transformString.slice(valueStart, index - 1),
2108
+ source: transformString.slice(match.index, index),
2109
+ });
2110
+ TRANSFORM_FUNCTION_START_REGEX.lastIndex = index;
2111
+ }
2112
+ return transformFunctions;
2113
+ };
2082
2114
  // Parse a matrix transform and extract simple transform components when possible
2083
2115
  const parseMatrixTransform = (matrixString) => {
2084
2116
  // Match matrix() or matrix3d() functions
@@ -2324,33 +2356,6 @@ const globalCSSKeywordSet = new Set([
2324
2356
  "unset",
2325
2357
  "revert",
2326
2358
  ]);
2327
- // Keywords that should NOT get automatic units when used with properties from:
2328
- // - pxPropertySet (width, height, fontSize, etc.)
2329
- // - degPropertySet (rotate, skew, etc.)
2330
- // - unitlessPropertySet (opacity, zIndex, etc.)
2331
- // This prevents auto-unit addition: e.g., width: "auto" stays "auto", not "autopx"
2332
- const unitlessKeywordSet = new Set([
2333
- ...globalCSSKeywordSet,
2334
- // Size/dimension keywords for pxPropertySet properties
2335
- "fit-content",
2336
- "min-content",
2337
- "max-content",
2338
- // Font size keywords for fontSize
2339
- "medium",
2340
- "small",
2341
- "large",
2342
- "x-small",
2343
- "x-large",
2344
- "xx-small",
2345
- "xx-large",
2346
- "smaller",
2347
- "larger",
2348
- // Border width keywords for borderWidth properties
2349
- "thin",
2350
- "thick",
2351
- // Line height keyword (though lineHeight is handled specially)
2352
- "normal",
2353
- ]);
2354
2359
  // Keywords for backgroundImage property that should NOT be wrapped in url()
2355
2360
  // Used to prevent: background: "none" becoming background: "url(none)"
2356
2361
  const backgroundKeywordSet = new Set([
@@ -2387,10 +2392,28 @@ const getUnit = (value) => {
2387
2392
  }
2388
2393
  return "";
2389
2394
  };
2390
- // Check if value already has a unit
2391
- const isUnitless = (value) => getUnit(value) === "";
2392
2395
  const hasCSSSizeUnit = (value) => cssSizeUnitSet.has(getUnit(value));
2393
2396
 
2397
+ // A single number and nothing else — the only shape a unit may be appended to.
2398
+ // Everything else already says what it is: a unit ("2em"), a keyword ("auto"),
2399
+ // a CSS expression ("env(safe-area-inset-top)", "calc(…)") or a list of those
2400
+ // ("0 auto", "10px env(safe-area-inset-right)"). Asking "is this one number"
2401
+ // covers them all at once; listing what must be left alone (keywords, then
2402
+ // functions, then lists…) only ever covers what someone thought of.
2403
+ const isBareNumber = (value) => {
2404
+ if (value === "") {
2405
+ return false;
2406
+ }
2407
+ return !isNaN(Number(value));
2408
+ };
2409
+ // The same number, carrying exactly the unit asked for, and nothing else.
2410
+ const isBareNumberWithUnit = (value, unit) => {
2411
+ if (!value.endsWith(unit)) {
2412
+ return false;
2413
+ }
2414
+ return isBareNumber(value.slice(0, -unit.length));
2415
+ };
2416
+
2394
2417
  // url(
2395
2418
  // linear-gradient(
2396
2419
  // radial-gradient(
@@ -2650,23 +2673,21 @@ const isCSSKeyword = (value) => {
2650
2673
  };
2651
2674
  const normalizeNumber = (value, { unit, propertyName, preferedType }) => {
2652
2675
  if (typeof value === "string") {
2653
- // CSS variables and CSS functions like calc() must be passed through as-is
2654
- if (isCSSFunction(value)) {
2655
- return value;
2656
- }
2657
- // Keep strings as-is (including %, em, rem, auto, none, etc.)
2676
+ value = value.trim();
2658
2677
  if (preferedType === "string") {
2659
- if (unit && isUnitless(value) && !unitlessKeywordSet.has(value)) {
2678
+ // Everything that is not a lone number already carries its own meaning
2679
+ // and goes to the DOM untouched: "2em", "auto", "env(safe-area-inset-top)",
2680
+ // "calc(…)", "0 auto", "10px env(safe-area-inset-right)".
2681
+ if (unit && isBareNumber(value)) {
2660
2682
  return `${value}${unit}`;
2661
2683
  }
2662
2684
  return value;
2663
2685
  }
2664
- // convert to number if possible (font-size: "12px" -> fontSize:12, opacity: "0.5" -> opacity: 0.5)
2665
- if (!unit || value.endsWith(unit)) {
2666
- const numericValue = parseFloat(value);
2667
- if (!isNaN(numericValue)) {
2668
- return numericValue;
2669
- }
2686
+ // A number to work with, only when the value is exactly one:
2687
+ // "12px" -> 12, "0.5" -> 0.5. "10px 20px" is a list and "calc()" is an
2688
+ // expression parseFloat would silently keep their first number.
2689
+ if (unit ? isBareNumberWithUnit(value, unit) : isBareNumber(value)) {
2690
+ return parseFloat(value);
2670
2691
  }
2671
2692
  return value;
2672
2693
  }
@@ -4587,6 +4608,185 @@ const findFocusable = (element, { exclude } = {}) => {
4587
4608
  return focusableDescendant;
4588
4609
  };
4589
4610
 
4611
+ // note: keep in mind that an element with overflow: 'hidden' is scrollable
4612
+ // it can be scrolled using keyboard arrows or JavaScript properties such as scrollTop, scrollLeft
4613
+ // the only overflow that prevents scroll is "visible"
4614
+ const isScrollable = (element, { includeHidden } = {}) => {
4615
+ if (canHaveVerticalScroll(element, { includeHidden })) {
4616
+ return true;
4617
+ }
4618
+ if (canHaveHorizontalScroll(element, { includeHidden })) {
4619
+ return true;
4620
+ }
4621
+ return false;
4622
+ };
4623
+
4624
+ // Whether this element is what scrolls on that axis: it says it may (overflow)
4625
+ // and it has somewhere to go (it overflows). Both are needed — an "auto" box
4626
+ // whose content fits scrolls nothing, and `overflow-x: auto` alone makes the
4627
+ // COMPUTED overflow-y auto too (CSS does not let one axis stay visible next to
4628
+ // a scrolling one), so a box scrolling sideways declares a vertical scroll it
4629
+ // will never do.
4630
+ const canScroll = (element, axis) => {
4631
+ if (!element || element.nodeType !== 1) {
4632
+ return false;
4633
+ }
4634
+ const style = getComputedStyle(element);
4635
+ const overflow = axis === "x" ? style.overflowX : style.overflowY;
4636
+ if (overflow !== "auto" && overflow !== "scroll") {
4637
+ return false;
4638
+ }
4639
+ const scrollSize = axis === "x" ? element.scrollWidth : element.scrollHeight;
4640
+ const clientSize = axis === "x" ? element.clientWidth : element.clientHeight;
4641
+ // A pixel of slack: subpixel content rounds scrollSize up on boxes that have
4642
+ // nowhere to scroll to.
4643
+ return scrollSize - clientSize > 1;
4644
+ };
4645
+
4646
+ const canHaveVerticalScroll = (element, { includeHidden }) => {
4647
+ const verticalOverflow = getStyle(element, "overflow-y");
4648
+ if (verticalOverflow === "visible") {
4649
+ // browser returns "visible" on documentElement even if it is scrollable
4650
+ if (isDocumentElement(element)) {
4651
+ return true;
4652
+ }
4653
+ return false;
4654
+ }
4655
+ if (verticalOverflow === "hidden" || verticalOverflow === "clip") {
4656
+ return includeHidden;
4657
+ }
4658
+ const overflow = getStyle(element, "overflow");
4659
+ if (overflow === "visible") {
4660
+ // browser returns "visible" on documentElement even if it is scrollable
4661
+ if (isDocumentElement(element)) {
4662
+ return true;
4663
+ }
4664
+ return false;
4665
+ }
4666
+ if (overflow === "hidden" || overflow === "clip") {
4667
+ return includeHidden;
4668
+ }
4669
+ return true; // "auto", "scroll"
4670
+ };
4671
+ const canHaveHorizontalScroll = (element, { includeHidden }) => {
4672
+ const horizontalOverflow = getStyle(element, "overflow-x");
4673
+ if (horizontalOverflow === "visible") {
4674
+ // browser returns "visible" on documentElement even if it is scrollable
4675
+ if (isDocumentElement(element)) {
4676
+ return true;
4677
+ }
4678
+ return false;
4679
+ }
4680
+ if (horizontalOverflow === "hidden" || horizontalOverflow === "clip") {
4681
+ return includeHidden;
4682
+ }
4683
+ const overflow = getStyle(element, "overflow");
4684
+ if (overflow === "visible") {
4685
+ if (isDocumentElement(element)) {
4686
+ // browser returns "visible" on documentElement even if it is scrollable
4687
+ return true;
4688
+ }
4689
+ return false;
4690
+ }
4691
+ if (overflow === "hidden" || overflow === "clip") {
4692
+ return includeHidden;
4693
+ }
4694
+ return true; // "auto", "scroll"
4695
+ };
4696
+
4697
+ const getScrollingElement = (document) => {
4698
+ const { scrollingElement } = document;
4699
+ if (scrollingElement) {
4700
+ return scrollingElement;
4701
+ }
4702
+
4703
+ if (isCompliant(document)) {
4704
+ return document.documentElement;
4705
+ }
4706
+
4707
+ const body = document.body;
4708
+ const isFrameset = body && !/body/i.test(body.tagName);
4709
+ const possiblyScrollingElement = isFrameset ? getNextBodyElement(body) : body;
4710
+
4711
+ // If `body` is itself scrollable, it is not the `scrollingElement`.
4712
+ return possiblyScrollingElement && bodyIsScrollable(possiblyScrollingElement)
4713
+ ? null
4714
+ : possiblyScrollingElement;
4715
+ };
4716
+
4717
+ const isHidden = (element) => {
4718
+ const display = getStyle(element, "display");
4719
+ if (display === "none") {
4720
+ return false;
4721
+ }
4722
+
4723
+ if (
4724
+ display === "table-row" ||
4725
+ display === "table-group" ||
4726
+ display === "table-column"
4727
+ ) {
4728
+ return getStyle(element, "visibility") !== "collapsed";
4729
+ }
4730
+
4731
+ return true;
4732
+ };
4733
+ const isCompliant = (document) => {
4734
+ // Note: document.compatMode can be toggle at runtime by document.write
4735
+ const isStandardsMode = /^CSS1/.test(document.compatMode);
4736
+ if (isStandardsMode) {
4737
+ return testScrollCompliance(document);
4738
+ }
4739
+ return false;
4740
+ };
4741
+ const testScrollCompliance = (document) => {
4742
+ const iframe = document.createElement("iframe");
4743
+ iframe.style.height = "1px";
4744
+ const parentNode = document.body || document.documentElement || document;
4745
+ parentNode.appendChild(iframe);
4746
+ const iframeDocument = iframe.contentWindow.document;
4747
+ iframeDocument.write('<!DOCTYPE html><div style="height:9999em">x</div>');
4748
+ iframeDocument.close();
4749
+ const scrollComplianceResult =
4750
+ iframeDocument.documentElement.scrollHeight >
4751
+ iframeDocument.body.scrollHeight;
4752
+ iframe.parentNode.removeChild(iframe);
4753
+ return scrollComplianceResult;
4754
+ };
4755
+ const getNextBodyElement = (frameset) => {
4756
+ // We use this function to be correct per spec in case `document.body` is
4757
+ // a `frameset` but there exists a later `body`. Since `document.body` is
4758
+ // a `frameset`, we know the root is an `html`, and there was no `body`
4759
+ // before the `frameset`, so we just need to look at siblings after the
4760
+ // `frameset`.
4761
+ let current = frameset;
4762
+ while ((current = current.nextSibling)) {
4763
+ if (current.nodeType === 1 && isBodyElement(current)) {
4764
+ return current;
4765
+ }
4766
+ }
4767
+ return null;
4768
+ };
4769
+ const isBodyElement = (element) => element.ownerDocument.body === element;
4770
+ const bodyIsScrollable = (body) => {
4771
+ // a body element is scrollable if body and html are scrollable and rendered
4772
+ if (!isScrollable(body)) {
4773
+ return false;
4774
+ }
4775
+ if (isHidden(body)) {
4776
+ return false;
4777
+ }
4778
+
4779
+ const documentElement = body.ownerDocument.documentElement;
4780
+ if (!isScrollable(documentElement)) {
4781
+ return false;
4782
+ }
4783
+ if (isHidden(documentElement)) {
4784
+ return false;
4785
+ }
4786
+
4787
+ return true;
4788
+ };
4789
+
4590
4790
  /**
4591
4791
  * Returns the browser's default action for a keyboard event on its target element.
4592
4792
  *
@@ -4677,23 +4877,6 @@ const isTypingIntent = (e) => {
4677
4877
  return false;
4678
4878
  };
4679
4879
 
4680
- // Whether this element is what scrolls on that axis: it says it may (overflow)
4681
- // and it has somewhere to go (it overflows). Both are needed — an "auto" box
4682
- // whose content fits scrolls nothing, and the keys are then free.
4683
- const canScrollSelf = (element, axis) => {
4684
- if (!element || element.nodeType !== 1) {
4685
- return false;
4686
- }
4687
- const style = getComputedStyle(element);
4688
- const overflow = axis === "x" ? style.overflowX : style.overflowY;
4689
- if (overflow !== "auto" && overflow !== "scroll") {
4690
- return false;
4691
- }
4692
- return axis === "x"
4693
- ? element.scrollWidth > element.clientWidth
4694
- : element.scrollHeight > element.clientHeight;
4695
- };
4696
-
4697
4880
  const DEFAULT_BEHAVIORS = [
4698
4881
  {
4699
4882
  test: () => true,
@@ -4870,22 +5053,17 @@ const DEFAULT_BEHAVIORS = [
4870
5053
  // be read from the keyboard, and that is not a key to take. Asked per axis
4871
5054
  // and per element, not from a class or an attribute: what makes it true is
4872
5055
  // that it overflows right now.
4873
- test: (el) => canScrollSelf(el, "y") || canScrollSelf(el, "x"),
5056
+ test: (el) => canScroll(el, "y") || canScroll(el, "x"),
4874
5057
  keys: {
4875
- arrowup: (e) =>
4876
- canScrollSelf(e.target, "y") ? "scroll_self" : undefined,
4877
- arrowdown: (e) =>
4878
- canScrollSelf(e.target, "y") ? "scroll_self" : undefined,
4879
- arrowleft: (e) =>
4880
- canScrollSelf(e.target, "x") ? "scroll_self" : undefined,
4881
- arrowright: (e) =>
4882
- canScrollSelf(e.target, "x") ? "scroll_self" : undefined,
4883
- pageup: (e) => (canScrollSelf(e.target, "y") ? "scroll_self" : undefined),
4884
- pagedown: (e) =>
4885
- canScrollSelf(e.target, "y") ? "scroll_self" : undefined,
4886
- home: (e) => (canScrollSelf(e.target, "y") ? "scroll_self" : undefined),
4887
- end: (e) => (canScrollSelf(e.target, "y") ? "scroll_self" : undefined),
4888
- space: (e) => (canScrollSelf(e.target, "y") ? "scroll_self" : undefined),
5058
+ arrowup: (e) => (canScroll(e.target, "y") ? "scroll_self" : undefined),
5059
+ arrowdown: (e) => (canScroll(e.target, "y") ? "scroll_self" : undefined),
5060
+ arrowleft: (e) => (canScroll(e.target, "x") ? "scroll_self" : undefined),
5061
+ arrowright: (e) => (canScroll(e.target, "x") ? "scroll_self" : undefined),
5062
+ pageup: (e) => (canScroll(e.target, "y") ? "scroll_self" : undefined),
5063
+ pagedown: (e) => (canScroll(e.target, "y") ? "scroll_self" : undefined),
5064
+ home: (e) => (canScroll(e.target, "y") ? "scroll_self" : undefined),
5065
+ end: (e) => (canScroll(e.target, "y") ? "scroll_self" : undefined),
5066
+ space: (e) => (canScroll(e.target, "y") ? "scroll_self" : undefined),
4889
5067
  },
4890
5068
  // no fallback: only these keys are claimed, everything else keeps looking
4891
5069
  },
@@ -6345,163 +6523,6 @@ const captureScrollState = (element) => {
6345
6523
  };
6346
6524
  };
6347
6525
 
6348
- // note: keep in mind that an element with overflow: 'hidden' is scrollable
6349
- // it can be scrolled using keyboard arrows or JavaScript properties such as scrollTop, scrollLeft
6350
- // the only overflow that prevents scroll is "visible"
6351
- const isScrollable = (element, { includeHidden } = {}) => {
6352
- if (canHaveVerticalScroll(element, { includeHidden })) {
6353
- return true;
6354
- }
6355
- if (canHaveHorizontalScroll(element, { includeHidden })) {
6356
- return true;
6357
- }
6358
- return false;
6359
- };
6360
-
6361
- const canHaveVerticalScroll = (element, { includeHidden }) => {
6362
- const verticalOverflow = getStyle(element, "overflow-y");
6363
- if (verticalOverflow === "visible") {
6364
- // browser returns "visible" on documentElement even if it is scrollable
6365
- if (isDocumentElement(element)) {
6366
- return true;
6367
- }
6368
- return false;
6369
- }
6370
- if (verticalOverflow === "hidden" || verticalOverflow === "clip") {
6371
- return includeHidden;
6372
- }
6373
- const overflow = getStyle(element, "overflow");
6374
- if (overflow === "visible") {
6375
- // browser returns "visible" on documentElement even if it is scrollable
6376
- if (isDocumentElement(element)) {
6377
- return true;
6378
- }
6379
- return false;
6380
- }
6381
- if (overflow === "hidden" || overflow === "clip") {
6382
- return includeHidden;
6383
- }
6384
- return true; // "auto", "scroll"
6385
- };
6386
- const canHaveHorizontalScroll = (element, { includeHidden }) => {
6387
- const horizontalOverflow = getStyle(element, "overflow-x");
6388
- if (horizontalOverflow === "visible") {
6389
- // browser returns "visible" on documentElement even if it is scrollable
6390
- if (isDocumentElement(element)) {
6391
- return true;
6392
- }
6393
- return false;
6394
- }
6395
- if (horizontalOverflow === "hidden" || horizontalOverflow === "clip") {
6396
- return includeHidden;
6397
- }
6398
- const overflow = getStyle(element, "overflow");
6399
- if (overflow === "visible") {
6400
- if (isDocumentElement(element)) {
6401
- // browser returns "visible" on documentElement even if it is scrollable
6402
- return true;
6403
- }
6404
- return false;
6405
- }
6406
- if (overflow === "hidden" || overflow === "clip") {
6407
- return includeHidden;
6408
- }
6409
- return true; // "auto", "scroll"
6410
- };
6411
-
6412
- const getScrollingElement = (document) => {
6413
- const { scrollingElement } = document;
6414
- if (scrollingElement) {
6415
- return scrollingElement;
6416
- }
6417
-
6418
- if (isCompliant(document)) {
6419
- return document.documentElement;
6420
- }
6421
-
6422
- const body = document.body;
6423
- const isFrameset = body && !/body/i.test(body.tagName);
6424
- const possiblyScrollingElement = isFrameset ? getNextBodyElement(body) : body;
6425
-
6426
- // If `body` is itself scrollable, it is not the `scrollingElement`.
6427
- return possiblyScrollingElement && bodyIsScrollable(possiblyScrollingElement)
6428
- ? null
6429
- : possiblyScrollingElement;
6430
- };
6431
-
6432
- const isHidden = (element) => {
6433
- const display = getStyle(element, "display");
6434
- if (display === "none") {
6435
- return false;
6436
- }
6437
-
6438
- if (
6439
- display === "table-row" ||
6440
- display === "table-group" ||
6441
- display === "table-column"
6442
- ) {
6443
- return getStyle(element, "visibility") !== "collapsed";
6444
- }
6445
-
6446
- return true;
6447
- };
6448
- const isCompliant = (document) => {
6449
- // Note: document.compatMode can be toggle at runtime by document.write
6450
- const isStandardsMode = /^CSS1/.test(document.compatMode);
6451
- if (isStandardsMode) {
6452
- return testScrollCompliance(document);
6453
- }
6454
- return false;
6455
- };
6456
- const testScrollCompliance = (document) => {
6457
- const iframe = document.createElement("iframe");
6458
- iframe.style.height = "1px";
6459
- const parentNode = document.body || document.documentElement || document;
6460
- parentNode.appendChild(iframe);
6461
- const iframeDocument = iframe.contentWindow.document;
6462
- iframeDocument.write('<!DOCTYPE html><div style="height:9999em">x</div>');
6463
- iframeDocument.close();
6464
- const scrollComplianceResult =
6465
- iframeDocument.documentElement.scrollHeight >
6466
- iframeDocument.body.scrollHeight;
6467
- iframe.parentNode.removeChild(iframe);
6468
- return scrollComplianceResult;
6469
- };
6470
- const getNextBodyElement = (frameset) => {
6471
- // We use this function to be correct per spec in case `document.body` is
6472
- // a `frameset` but there exists a later `body`. Since `document.body` is
6473
- // a `frameset`, we know the root is an `html`, and there was no `body`
6474
- // before the `frameset`, so we just need to look at siblings after the
6475
- // `frameset`.
6476
- let current = frameset;
6477
- while ((current = current.nextSibling)) {
6478
- if (current.nodeType === 1 && isBodyElement(current)) {
6479
- return current;
6480
- }
6481
- }
6482
- return null;
6483
- };
6484
- const isBodyElement = (element) => element.ownerDocument.body === element;
6485
- const bodyIsScrollable = (body) => {
6486
- // a body element is scrollable if body and html are scrollable and rendered
6487
- if (!isScrollable(body)) {
6488
- return false;
6489
- }
6490
- if (isHidden(body)) {
6491
- return false;
6492
- }
6493
-
6494
- const documentElement = body.ownerDocument.documentElement;
6495
- if (!isScrollable(documentElement)) {
6496
- return false;
6497
- }
6498
- if (isHidden(documentElement)) {
6499
- return false;
6500
- }
6501
-
6502
- return true;
6503
- };
6504
-
6505
6526
  // https://developer.mozilla.org/en-US/docs/Glossary/Scroll_container
6506
6527
 
6507
6528
 
@@ -7008,24 +7029,45 @@ const getScrollport = (scrollBox, scrollContainer) => {
7008
7029
  };
7009
7030
  };
7010
7031
 
7011
- // https://davidwalsh.name/detect-scrollbar-width
7032
+ /**
7033
+ * Returns [verticalScrollbarWidth, horizontalScrollbarHeight] as currently
7034
+ * rendered by `scrollableElement`, in px. Returns zeros when the element has no
7035
+ * classic scrollbar: no overflow, overlay scrollbars, `scrollbar-width: none`,
7036
+ * or a `::-webkit-scrollbar { display: none }` rule.
7037
+ *
7038
+ * The measurement is taken on the element itself (its own border box versus its
7039
+ * own content box) rather than on a probe node appended inside it. A probe
7040
+ * cannot answer this question: `scrollbar-width` and `::-webkit-scrollbar` are
7041
+ * not inherited, so a probe reports the platform default scrollbar size even
7042
+ * when the element renders no scrollbar at all.
7043
+ */
7012
7044
  const measureScrollbar = (scrollableElement) => {
7013
- const hasXScrollbar =
7014
- scrollableElement.scrollHeight > scrollableElement.clientHeight;
7015
- const hasYScrollbar =
7016
- scrollableElement.scrollWidth > scrollableElement.clientWidth;
7017
- if (!hasXScrollbar && !hasYScrollbar) {
7018
- return [0, 0];
7019
- }
7020
- const scrollDiv = document.createElement("div");
7021
- scrollDiv.style.cssText = `position: absolute; width: 100px; height: 100px; overflow: scroll; pointer-events: none; visibility: hidden;`;
7022
- scrollableElement.appendChild(scrollDiv);
7023
- const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
7024
- const scrollbarHeight = scrollDiv.offsetHeight - scrollDiv.clientHeight;
7025
- scrollableElement.removeChild(scrollDiv);
7045
+ if (
7046
+ scrollableElement === document.documentElement ||
7047
+ scrollableElement === document.scrollingElement
7048
+ ) {
7049
+ // documentElement.clientWidth/Height report the viewport minus its
7050
+ // scrollbars, not this element's own box (which `max-width` can shrink), so
7051
+ // the border box to compare against is the window itself.
7052
+ return [
7053
+ snapToPixel(window.innerWidth - document.documentElement.clientWidth),
7054
+ snapToPixel(window.innerHeight - document.documentElement.clientHeight),
7055
+ ];
7056
+ }
7057
+ const { left, right, top, bottom } = getBorderSizes(scrollableElement);
7058
+ const scrollbarWidth =
7059
+ scrollableElement.offsetWidth -
7060
+ scrollableElement.clientWidth -
7061
+ left -
7062
+ right;
7063
+ const scrollbarHeight =
7064
+ scrollableElement.offsetHeight -
7065
+ scrollableElement.clientHeight -
7066
+ top -
7067
+ bottom;
7026
7068
  return [
7027
- hasXScrollbar ? snapToPixel(scrollbarWidth) : 0,
7028
- hasYScrollbar ? snapToPixel(scrollbarHeight) : 0,
7069
+ scrollbarWidth > 0 ? snapToPixel(scrollbarWidth) : 0,
7070
+ scrollbarHeight > 0 ? snapToPixel(scrollbarHeight) : 0,
7029
7071
  ];
7030
7072
  };
7031
7073
 
@@ -16813,4 +16855,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
16813
16855
  };
16814
16856
  };
16815
16857
 
16816
- export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, captureScrollState, chainEvent, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
16858
+ export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, 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.17.2",
3
+ "version": "0.17.4",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {