help-layer 1.0.1 → 1.1.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.
@@ -23,6 +23,18 @@ export function makeVirtualElement(getDocRect: () => {
23
23
  height: number;
24
24
  };
25
25
  };
26
+ /**
27
+ * Whether the reference lives in a `position: fixed` subtree. Such a reference stays put in the
28
+ * viewport while the page scrolls, so an absolutely-positioned floating element (which scrolls with
29
+ * the document) would have to be re-corrected every frame and visibly jitters. For these we switch the
30
+ * floating element to Floating UI's `fixed` strategy (and position:fixed) so both live in the same
31
+ * viewport space and stay glued without per-frame correction.
32
+ *
33
+ * Virtual elements (free placements) aren't in the DOM and already track scroll via their getRect, so
34
+ * they report false. Walks across shadow boundaries via the host so Shadow DOM targets are handled too.
35
+ * @param {Element|object} reference
36
+ */
37
+ export function isFixedReference(reference: Element | object): boolean;
26
38
  /**
27
39
  * Overlap the marker onto a corner of the target (element or virtual element), stick it there, and keep it following.
28
40
  * @param {Element|object} reference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "help-layer",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "engines": {
@@ -32,11 +32,14 @@
32
32
  "lint:fix": "eslint src tests --fix",
33
33
  "typecheck": "tsc -p jsconfig.json",
34
34
  "check": "npm run lint && npm run typecheck && npm test",
35
+ "build:site": "node demo/build-site.js",
36
+ "build:demos": "node demo/build-framework-demos.js",
35
37
  "build:bundle": "node scripts/build.js",
36
38
  "build:types": "tsc -p jsconfig.json --declaration --emitDeclarationOnly --noEmit false --rootDir src --outDir dist/types",
37
39
  "build": "npm run build:bundle && npm run build:types",
40
+ "record:gif": "npm run build:demos && node scripts/record-demo.js",
38
41
  "prepublishOnly": "npm run build",
39
- "demo": "node scripts/serve.js"
42
+ "demo": "npm run build:demos && node scripts/serve.js"
40
43
  },
41
44
  "devDependencies": {
42
45
  "@playwright/test": "^1.61.0",
@@ -45,8 +48,10 @@
45
48
  "eslint-plugin-import": "^2.32.0",
46
49
  "eslint-plugin-n": "^17.24.0",
47
50
  "eslint-plugin-promise": "^7.2.1",
51
+ "gifenc": "^1.0.3",
48
52
  "jest": "^29.7.0",
49
53
  "jest-environment-jsdom": "^29.7.0",
54
+ "pngjs": "^7.0.0",
50
55
  "react": "^19.2.7",
51
56
  "react-dom": "^19.2.7",
52
57
  "typescript": "^6.0.3",
package/src/floating.js CHANGED
@@ -40,6 +40,37 @@ function place(el, x, y) {
40
40
  el.style.top = `${y}px`;
41
41
  }
42
42
 
43
+ /**
44
+ * Whether the reference lives in a `position: fixed` subtree. Such a reference stays put in the
45
+ * viewport while the page scrolls, so an absolutely-positioned floating element (which scrolls with
46
+ * the document) would have to be re-corrected every frame and visibly jitters. For these we switch the
47
+ * floating element to Floating UI's `fixed` strategy (and position:fixed) so both live in the same
48
+ * viewport space and stay glued without per-frame correction.
49
+ *
50
+ * Virtual elements (free placements) aren't in the DOM and already track scroll via their getRect, so
51
+ * they report false. Walks across shadow boundaries via the host so Shadow DOM targets are handled too.
52
+ * @param {Element|object} reference
53
+ */
54
+ export function isFixedReference(reference) {
55
+ if (!(reference instanceof Element)) {
56
+ return false;
57
+ }
58
+ let node = reference;
59
+ while (node) {
60
+ if (getComputedStyle(node).position === 'fixed') {
61
+ return true;
62
+ }
63
+ const parent = node.parentElement;
64
+ if (parent) {
65
+ node = parent;
66
+ } else {
67
+ const root = node.getRootNode();
68
+ node = root instanceof ShadowRoot ? root.host : null;
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+
43
74
  // Half of the default marker size (22px). The amount used to overlap the marker onto the
44
75
  // target's corner with an "inset". (If the marker-size CSS variable is changed, the resulting
45
76
  // drift is left as existing behavior = not compensated for here.)
@@ -66,9 +97,17 @@ function markerOffset(placement) {
66
97
  * @returns {() => void} cleanup
67
98
  */
68
99
  export function anchorMarker(reference, markerEl, onPlaced, placement = 'top-end') {
100
+ // Match the floating element's strategy to the reference: a fixed reference needs a fixed marker, or
101
+ // it jitters while scrolling (see isFixedReference). Inline !important beats the stylesheet's
102
+ // `position: absolute !important`.
103
+ const strategy = isFixedReference(reference) ? 'fixed' : 'absolute';
104
+ if (strategy === 'fixed') {
105
+ markerEl.style.setProperty('position', 'fixed', 'important');
106
+ }
69
107
  const update = () => {
70
108
  computePosition(reference, markerEl, {
71
109
  placement,
110
+ strategy,
72
111
  middleware: [offset(markerOffset(placement))],
73
112
  }).then(({ x, y }) => {
74
113
  place(markerEl, x, y);
@@ -95,9 +134,15 @@ export function anchorMarker(reference, markerEl, onPlaced, placement = 'top-end
95
134
  * calling update repositions immediately (used for reference-side transform moves that autoUpdate doesn't pick up, etc.).
96
135
  */
97
136
  export function anchorPopup(reference, popupEl, placement = 'bottom-start') {
137
+ // The reference is the clicked marker. If it's fixed (anchored to a fixed target), the popup must be
138
+ // fixed too or it jitters on scroll. Set position every open so reopening on a normal marker restores
139
+ // absolute. Inline !important beats the stylesheet's `position: absolute !important`.
140
+ const strategy = isFixedReference(reference) ? 'fixed' : 'absolute';
141
+ popupEl.style.setProperty('position', strategy, 'important');
98
142
  const update = () => {
99
143
  computePosition(reference, popupEl, {
100
144
  placement,
145
+ strategy,
101
146
  middleware: [offset(8), flip({ padding: 8 }), shift({ padding: 8 })],
102
147
  }).then(({ x, y }) => {
103
148
  place(popupEl, x, y);
package/src/style.js CHANGED
@@ -48,7 +48,9 @@ const CSS = `
48
48
  border: none;
49
49
  /* Structural properties are !important so a host's broad rules (e.g. button { display:none }) can't
50
50
  hide or distort the marker. top/left stay non-important because place() writes them inline per
51
- frame; !important there would override that and pin the marker to 0,0. Theme stays var()-driven. */
51
+ frame; !important there would override that and pin the marker to 0,0. Theme stays var()-driven.
52
+ Note: for targets in a position:fixed subtree, floating.js overrides this with an inline
53
+ position:fixed !important (inline important beats this rule) so the marker doesn't jitter. */
52
54
  position: absolute !important;
53
55
  display: block !important;
54
56
  visibility: visible !important;