pour-engine 1.26.0 → 1.27.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.
@@ -6,6 +6,6 @@
6
6
  export default {
7
7
  engine: {
8
8
  name: 'pour engine',
9
- version: '1.26.0',
9
+ version: '1.27.0',
10
10
  },
11
11
  };
@@ -938,17 +938,60 @@ export function textShadowNegligible(cssText, fontSize) {
938
938
  * hit-test can't reach. Open shadow roots aren't walked here; a known
939
939
  * approximation that errs toward failing (never toward a false pass).
940
940
  */
941
+ /**
942
+ * The part of an element's border box its ancestors allow it to PAINT.
943
+ * An object-fit: cover video is routinely taller than its overflow-hidden
944
+ * crop, and the spilled region is guaranteed blank — treating the raw box
945
+ * as a possible backdrop sent captions sitting 16px past a cropped video
946
+ * to review. The walk respects positioning: an absolutely positioned
947
+ * element escapes the clips of ancestors below its containing block
948
+ * (position or transform/filter establish one), and a fixed element's
949
+ * clippers are rare enough that the walk stops rather than guess — every
950
+ * uncertain path keeps the FULL box, erring toward review, never past a
951
+ * real overlap. Returns null when the clip leaves nothing paintable.
952
+ */
953
+ export function paintableRect(element, rect) {
954
+ let { left, top, right, bottom } = rect;
955
+ let mode = getComputedStyle(element).position;
956
+ for (let a = element.parentElement; a; a = a.parentElement) {
957
+ if (mode === 'fixed') break; // conservative: no clip assumed above
958
+ const s = getComputedStyle(a);
959
+ const containingBlock = s.position !== 'static' || s.transform !== 'none' || (s.filter && s.filter !== 'none');
960
+ if (mode === 'absolute' && !containingBlock) continue; // escapes this ancestor's clip
961
+ // Only overflow: hidden/clip cut paint scroll-independently. A
962
+ // scrollable ancestor (auto/scroll) clips the CURRENT scroll state —
963
+ // a carousel slide parked outside the strip swings back under the
964
+ // user's finger, image and its overlaid caption together, so its
965
+ // media must keep counting as a possible backdrop (BBC's promo
966
+ // carousel sent two white-on-photo headlines to a false pass here).
967
+ const clips = (o) => o === 'hidden' || o === 'clip';
968
+ if (clips(s.overflowX) || clips(s.overflowY)) {
969
+ const b = a.getBoundingClientRect();
970
+ if (clips(s.overflowX)) { left = Math.max(left, b.left); right = Math.min(right, b.right); }
971
+ if (clips(s.overflowY)) { top = Math.max(top, b.top); bottom = Math.min(bottom, b.bottom); }
972
+ if (right <= left || bottom <= top) return null;
973
+ }
974
+ // Above a consumed ancestor the element is clipped exactly as that
975
+ // ancestor is: continue the walk in its shoes.
976
+ mode = s.position;
977
+ }
978
+ return { left, top, right, bottom, width: right - left, height: bottom - top,
979
+ x: left, y: top };
980
+ }
981
+
941
982
  export function mediaRects(doc) {
942
983
  if (!mediaRectsCache) {
943
984
  mediaRectsCache = [];
944
985
  for (const el of doc.querySelectorAll('img, video, canvas, svg')) {
945
- const rect = el.getBoundingClientRect();
986
+ // The box an ancestor clips away can never paint, so it is not a
987
+ // backdrop hazard however far it reaches.
988
+ const rect = paintableRect(el, el.getBoundingClientRect());
946
989
  // Sub-icon-sized media can't plausibly serve as a text backdrop.
947
990
  // Decorative media with pointer-events: none (animated gradient
948
991
  // canvases, ambient video) paints under text yet never appears in
949
992
  // elementsFromPoint stacks — flagged so callers can distrust even a
950
993
  // hit-test-verified backdrop where such media overlaps.
951
- if (rect.width >= 8 && rect.height >= 8) {
994
+ if (rect && rect.width >= 8 && rect.height >= 8) {
952
995
  mediaRectsCache.push({ element: el, rect, hitTestBlind: getComputedStyle(el).pointerEvents === 'none' });
953
996
  }
954
997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",