@instructure/ui-position 11.7.4-snapshot-185 → 11.7.4-snapshot-10

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.
package/CHANGELOG.md CHANGED
@@ -3,17 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [11.7.4-snapshot-185](https://github.com/instructure/instructure-ui/compare/v11.6.0...v11.7.4-snapshot-185) (2026-05-15)
7
-
8
-
9
- ### Bug Fixes
10
-
11
- * **many:** update dependencies, remove lots of Babel plugins, remove Webpack 4 support ([98ff8e8](https://github.com/instructure/instructure-ui/commit/98ff8e8126a70d8496d6967795a8fbb2779c6fd9))
6
+ ## [11.7.4-snapshot-10](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-snapshot-10) (2026-05-19)
12
7
 
13
8
 
14
9
  ### Features
15
10
 
16
- * **many:** add solution for using both old and new token system in the same app ([688a713](https://github.com/instructure/instructure-ui/commit/688a713ff715433bb085323dbad61285387c5141))
11
+ * **ui-position:** add available space to position ([2ce7236](https://github.com/instructure/instructure-ui/commit/2ce7236db58f6678aabd544823c65dda4a51082c))
12
+ * **ui-position:** use border and padding in available space calculation ([1967e20](https://github.com/instructure/instructure-ui/commit/1967e20a214df364ead442b4f236b5e7c6019c66))
17
13
 
18
14
 
19
15
 
@@ -60,9 +60,13 @@ let Position = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, g
60
60
  static contentLocatorAttribute = 'data-position-content';
61
61
  constructor(props) {
62
62
  super(props);
63
+ const initial = this.calculatePosition(props);
64
+ this._availableHeight = initial.availableHeight;
65
+ this._availableWidth = initial.availableWidth;
63
66
  this.state = {
64
67
  positioned: false,
65
- ...this.calculatePosition(props)
68
+ placement: initial.placement,
69
+ style: initial.style
66
70
  };
67
71
  this.position = debounce(this.position, 0, {
68
72
  leading: false,
@@ -76,6 +80,8 @@ let Position = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, g
76
80
  _listener = null;
77
81
  _content;
78
82
  _target;
83
+ _availableHeight;
84
+ _availableWidth;
79
85
  handleRef = el => {
80
86
  const {
81
87
  elementRef
@@ -152,6 +158,22 @@ let Position = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, g
152
158
  }
153
159
  }, 0));
154
160
  };
161
+
162
+ // Write `--ui-position-available-{height,width}` directly on the content
163
+ // node's inline style
164
+ applyAvailableSpaceCustomProperties() {
165
+ const node = findDOMNode(this._content);
166
+ if (!node?.style) return;
167
+ const set = (name, value) => {
168
+ if (typeof value === 'number' && Number.isFinite(value)) {
169
+ node.style.setProperty(name, `${value}px`);
170
+ } else {
171
+ node.style.removeProperty(name);
172
+ }
173
+ };
174
+ set('--ui-position-available-height', this._availableHeight);
175
+ set('--ui-position-available-width', this._availableWidth);
176
+ }
155
177
  calculatePosition(props) {
156
178
  return calculateElementPosition(this._content, this._target, {
157
179
  placement: props.placement,
@@ -163,9 +185,19 @@ let Position = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, g
163
185
  });
164
186
  }
165
187
  position = () => {
188
+ const {
189
+ placement,
190
+ style,
191
+ availableHeight,
192
+ availableWidth
193
+ } = this.calculatePosition(this.props);
194
+ this._availableHeight = availableHeight;
195
+ this._availableWidth = availableWidth;
196
+ this.applyAvailableSpaceCustomProperties();
166
197
  this.setState({
167
198
  positioned: true,
168
- ...this.calculatePosition(this.props)
199
+ placement,
200
+ style
169
201
  });
170
202
  };
171
203
  startTracking() {
@@ -49,4 +49,12 @@ const mirrorMap = {
49
49
  stretch: 'stretch',
50
50
  offscreen: 'offscreen'
51
51
  };
52
+
53
+ /**
54
+ * The full output of `calculateElementPosition`, including the available
55
+ * space numbers driving `--ui-position-available-{height,width}`. Kept
56
+ * separate from `ElementPosition` so `PositionState` (which extends
57
+ * `ElementPosition`) doesn't falsely advertise these fields
58
+ */
59
+
52
60
  export { placementPropValues, mirrorMap };
@@ -23,7 +23,8 @@
23
23
  */
24
24
 
25
25
  import { getBoundingClientRect, getScrollParents, getOffsetParents, canUseDOM, findDOMNode, ownerDocument, ownerWindow } from '@instructure/ui-dom-utils';
26
- import { mirrorPlacement } from "./mirrorPlacement.js"; // @ts-expect-error will be needed for fix in the `offsetToPx` method
26
+ import { mirrorPlacement } from "./mirrorPlacement.js";
27
+ import { px } from '@instructure/ui-utils';
27
28
  /**
28
29
  * ---
29
30
  * category: utilities/position
@@ -63,9 +64,15 @@ function calculateElementPosition(element, target, options = {}) {
63
64
  };
64
65
  }
65
66
  const pos = new PositionData(element, target, options);
67
+ const {
68
+ height: availableHeight,
69
+ width: availableWidth
70
+ } = pos.availableSpace;
66
71
  return {
67
72
  placement: pos.placement,
68
- style: pos.style
73
+ style: pos.style,
74
+ availableHeight,
75
+ availableWidth
69
76
  };
70
77
  }
71
78
  class PositionedElement {
@@ -211,7 +218,6 @@ class PositionData {
211
218
  this.options = options || {};
212
219
  const {
213
220
  container,
214
- constrain,
215
221
  placement,
216
222
  over
217
223
  } = this.options;
@@ -222,16 +228,9 @@ class PositionData {
222
228
  left: this.options.offsetX
223
229
  });
224
230
  this.target = new PositionedElement(target || this.container, over ? this.element.placement : this.element.mirroredPlacement);
225
- if (constrain === 'window') {
226
- this.constrainTo(ownerWindow(element));
227
- } else if (constrain === 'scroll-parent') {
228
- this.constrainTo(getScrollParents(this.target.node)[0]);
229
- } else if (constrain === 'parent') {
230
- this.constrainTo(this.container);
231
- } else if (typeof constrain === 'function') {
232
- this.constrainTo(findDOMNode(constrain.call(null)));
233
- } else if (typeof constrain === 'object') {
234
- this.constrainTo(findDOMNode(constrain));
231
+ const constraintNode = this.resolveConstraintNode();
232
+ if (constraintNode) {
233
+ this.constrainTo(constraintNode);
235
234
  }
236
235
  }
237
236
  options;
@@ -418,6 +417,102 @@ class PositionData {
418
417
  }
419
418
  }
420
419
  }
420
+
421
+ // Resolves the `constrain` option (`'window'`, `'scroll-parent'`,
422
+ // `'parent'`, a function, or an element) to the DOM node it points at.
423
+ resolveConstraintNode() {
424
+ const {
425
+ constrain
426
+ } = this.options;
427
+ const elementNode = this.element?.node;
428
+ if (!elementNode) return null;
429
+ if (constrain === 'window') return ownerWindow(elementNode) ?? null;
430
+ if (constrain === 'scroll-parent') {
431
+ return getScrollParents(this.target?.node)[0] ?? null;
432
+ }
433
+ if (constrain === 'parent') return findDOMNode(this.container) ?? null;
434
+ if (typeof constrain === 'function') {
435
+ return findDOMNode(constrain.call(null)) ?? null;
436
+ }
437
+ if (typeof constrain === 'object' && constrain) {
438
+ return findDOMNode(constrain) ?? null;
439
+ }
440
+ return null;
441
+ }
442
+
443
+ /**
444
+ * Maximum height/width (in CSS px) an *inner* box inside the positioned
445
+ * element can occupy before the wrapper crosses the constraint edge.
446
+ * Drives the `--ui-position-available-{height,width}` CSS variables.
447
+ */
448
+ get availableSpace() {
449
+ const targetNode = this.target?.node;
450
+ const constraintNode = this.resolveConstraintNode();
451
+ if (!this.element || !targetNode?.getBoundingClientRect || !constraintNode) {
452
+ return {
453
+ height: Infinity,
454
+ width: Infinity
455
+ };
456
+ }
457
+ const targetRect = targetNode.getBoundingClientRect();
458
+ let constraintRect;
459
+ if ('getBoundingClientRect' in constraintNode) {
460
+ constraintRect = constraintNode.getBoundingClientRect();
461
+ } else {
462
+ const win = 'defaultView' in constraintNode ? constraintNode.defaultView : constraintNode;
463
+ if (!win) return {
464
+ height: Infinity,
465
+ width: Infinity
466
+ };
467
+ constraintRect = {
468
+ top: 0,
469
+ left: 0,
470
+ right: win.innerWidth,
471
+ bottom: win.innerHeight,
472
+ width: win.innerWidth,
473
+ height: win.innerHeight
474
+ };
475
+ }
476
+
477
+ // `offsetX` / `offsetY` always push the popover *away* from the trigger
478
+ // so on the primary axis they consume available space.
479
+ const elementNode = this.element.node;
480
+ const offsetY = px(this.element._offset.top, elementNode);
481
+ const offsetX = px(this.element._offset.left, elementNode);
482
+ const [primary] = this.element.placement;
483
+ let height;
484
+ if (primary === 'bottom') {
485
+ height = constraintRect.bottom - targetRect.bottom - offsetY;
486
+ } else if (primary === 'top') {
487
+ height = targetRect.top - constraintRect.top - offsetY;
488
+ } else {
489
+ height = constraintRect.height;
490
+ }
491
+ let width;
492
+ if (primary === 'end') {
493
+ width = constraintRect.right - targetRect.right - offsetX;
494
+ } else if (primary === 'start') {
495
+ width = targetRect.left - constraintRect.left - offsetX;
496
+ } else {
497
+ width = constraintRect.width;
498
+ }
499
+
500
+ // Measure the wrapper's frame
501
+ let vFrame = 0;
502
+ let hFrame = 0;
503
+ if (elementNode && 'ownerDocument' in elementNode) {
504
+ const view = elementNode.ownerDocument?.defaultView;
505
+ if (view) {
506
+ const cs = view.getComputedStyle(elementNode);
507
+ vFrame = (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0) + (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
508
+ hFrame = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0) + (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
509
+ }
510
+ }
511
+ return {
512
+ height: Math.max(0, Math.floor(height - vFrame - 1)),
513
+ width: Math.max(0, Math.floor(width - hFrame - 1))
514
+ };
515
+ }
421
516
  }
422
517
  function addOffsets(offsets) {
423
518
  return offsets.reduce((sum, offset) => {
@@ -72,9 +72,13 @@ let Position = exports.Position = (_dec = (0, _withDeterministicId.withDetermini
72
72
  static contentLocatorAttribute = 'data-position-content';
73
73
  constructor(props) {
74
74
  super(props);
75
+ const initial = this.calculatePosition(props);
76
+ this._availableHeight = initial.availableHeight;
77
+ this._availableWidth = initial.availableWidth;
75
78
  this.state = {
76
79
  positioned: false,
77
- ...this.calculatePosition(props)
80
+ placement: initial.placement,
81
+ style: initial.style
78
82
  };
79
83
  this.position = (0, _debounce.debounce)(this.position, 0, {
80
84
  leading: false,
@@ -88,6 +92,8 @@ let Position = exports.Position = (_dec = (0, _withDeterministicId.withDetermini
88
92
  _listener = null;
89
93
  _content;
90
94
  _target;
95
+ _availableHeight;
96
+ _availableWidth;
91
97
  handleRef = el => {
92
98
  const {
93
99
  elementRef
@@ -164,6 +170,22 @@ let Position = exports.Position = (_dec = (0, _withDeterministicId.withDetermini
164
170
  }
165
171
  }, 0));
166
172
  };
173
+
174
+ // Write `--ui-position-available-{height,width}` directly on the content
175
+ // node's inline style
176
+ applyAvailableSpaceCustomProperties() {
177
+ const node = (0, _findDOMNode.findDOMNode)(this._content);
178
+ if (!node?.style) return;
179
+ const set = (name, value) => {
180
+ if (typeof value === 'number' && Number.isFinite(value)) {
181
+ node.style.setProperty(name, `${value}px`);
182
+ } else {
183
+ node.style.removeProperty(name);
184
+ }
185
+ };
186
+ set('--ui-position-available-height', this._availableHeight);
187
+ set('--ui-position-available-width', this._availableWidth);
188
+ }
167
189
  calculatePosition(props) {
168
190
  return (0, _calculateElementPosition.calculateElementPosition)(this._content, this._target, {
169
191
  placement: props.placement,
@@ -175,9 +197,19 @@ let Position = exports.Position = (_dec = (0, _withDeterministicId.withDetermini
175
197
  });
176
198
  }
177
199
  position = () => {
200
+ const {
201
+ placement,
202
+ style,
203
+ availableHeight,
204
+ availableWidth
205
+ } = this.calculatePosition(this.props);
206
+ this._availableHeight = availableHeight;
207
+ this._availableWidth = availableWidth;
208
+ this.applyAvailableSpaceCustomProperties();
178
209
  this.setState({
179
210
  positioned: true,
180
- ...this.calculatePosition(this.props)
211
+ placement,
212
+ style
181
213
  });
182
214
  };
183
215
  startTracking() {
@@ -54,4 +54,11 @@ const mirrorMap = exports.mirrorMap = {
54
54
  bottom: 'top',
55
55
  stretch: 'stretch',
56
56
  offscreen: 'offscreen'
57
- };
57
+ };
58
+
59
+ /**
60
+ * The full output of `calculateElementPosition`, including the available
61
+ * space numbers driving `--ui-position-available-{height,width}`. Kept
62
+ * separate from `ElementPosition` so `PositionState` (which extends
63
+ * `ElementPosition`) doesn't falsely advertise these fields
64
+ */
@@ -14,6 +14,7 @@ var _findDOMNode = require("@instructure/ui-dom-utils/lib/findDOMNode.js");
14
14
  var _ownerDocument = require("@instructure/ui-dom-utils/lib/ownerDocument.js");
15
15
  var _ownerWindow = require("@instructure/ui-dom-utils/lib/ownerWindow.js");
16
16
  var _mirrorPlacement = require("./mirrorPlacement");
17
+ var _px = require("@instructure/ui-utils/lib/px.js");
17
18
  /*
18
19
  * The MIT License (MIT)
19
20
  *
@@ -38,8 +39,6 @@ var _mirrorPlacement = require("./mirrorPlacement");
38
39
  * SOFTWARE.
39
40
  */
40
41
 
41
- // @ts-expect-error will be needed for fix in the `offsetToPx` method
42
-
43
42
  /**
44
43
  * ---
45
44
  * category: utilities/position
@@ -79,9 +78,15 @@ function calculateElementPosition(element, target, options = {}) {
79
78
  };
80
79
  }
81
80
  const pos = new PositionData(element, target, options);
81
+ const {
82
+ height: availableHeight,
83
+ width: availableWidth
84
+ } = pos.availableSpace;
82
85
  return {
83
86
  placement: pos.placement,
84
- style: pos.style
87
+ style: pos.style,
88
+ availableHeight,
89
+ availableWidth
85
90
  };
86
91
  }
87
92
  class PositionedElement {
@@ -227,7 +232,6 @@ class PositionData {
227
232
  this.options = options || {};
228
233
  const {
229
234
  container,
230
- constrain,
231
235
  placement,
232
236
  over
233
237
  } = this.options;
@@ -238,16 +242,9 @@ class PositionData {
238
242
  left: this.options.offsetX
239
243
  });
240
244
  this.target = new PositionedElement(target || this.container, over ? this.element.placement : this.element.mirroredPlacement);
241
- if (constrain === 'window') {
242
- this.constrainTo((0, _ownerWindow.ownerWindow)(element));
243
- } else if (constrain === 'scroll-parent') {
244
- this.constrainTo((0, _getScrollParents.getScrollParents)(this.target.node)[0]);
245
- } else if (constrain === 'parent') {
246
- this.constrainTo(this.container);
247
- } else if (typeof constrain === 'function') {
248
- this.constrainTo((0, _findDOMNode.findDOMNode)(constrain.call(null)));
249
- } else if (typeof constrain === 'object') {
250
- this.constrainTo((0, _findDOMNode.findDOMNode)(constrain));
245
+ const constraintNode = this.resolveConstraintNode();
246
+ if (constraintNode) {
247
+ this.constrainTo(constraintNode);
251
248
  }
252
249
  }
253
250
  options;
@@ -434,6 +431,102 @@ class PositionData {
434
431
  }
435
432
  }
436
433
  }
434
+
435
+ // Resolves the `constrain` option (`'window'`, `'scroll-parent'`,
436
+ // `'parent'`, a function, or an element) to the DOM node it points at.
437
+ resolveConstraintNode() {
438
+ const {
439
+ constrain
440
+ } = this.options;
441
+ const elementNode = this.element?.node;
442
+ if (!elementNode) return null;
443
+ if (constrain === 'window') return (0, _ownerWindow.ownerWindow)(elementNode) ?? null;
444
+ if (constrain === 'scroll-parent') {
445
+ return (0, _getScrollParents.getScrollParents)(this.target?.node)[0] ?? null;
446
+ }
447
+ if (constrain === 'parent') return (0, _findDOMNode.findDOMNode)(this.container) ?? null;
448
+ if (typeof constrain === 'function') {
449
+ return (0, _findDOMNode.findDOMNode)(constrain.call(null)) ?? null;
450
+ }
451
+ if (typeof constrain === 'object' && constrain) {
452
+ return (0, _findDOMNode.findDOMNode)(constrain) ?? null;
453
+ }
454
+ return null;
455
+ }
456
+
457
+ /**
458
+ * Maximum height/width (in CSS px) an *inner* box inside the positioned
459
+ * element can occupy before the wrapper crosses the constraint edge.
460
+ * Drives the `--ui-position-available-{height,width}` CSS variables.
461
+ */
462
+ get availableSpace() {
463
+ const targetNode = this.target?.node;
464
+ const constraintNode = this.resolveConstraintNode();
465
+ if (!this.element || !targetNode?.getBoundingClientRect || !constraintNode) {
466
+ return {
467
+ height: Infinity,
468
+ width: Infinity
469
+ };
470
+ }
471
+ const targetRect = targetNode.getBoundingClientRect();
472
+ let constraintRect;
473
+ if ('getBoundingClientRect' in constraintNode) {
474
+ constraintRect = constraintNode.getBoundingClientRect();
475
+ } else {
476
+ const win = 'defaultView' in constraintNode ? constraintNode.defaultView : constraintNode;
477
+ if (!win) return {
478
+ height: Infinity,
479
+ width: Infinity
480
+ };
481
+ constraintRect = {
482
+ top: 0,
483
+ left: 0,
484
+ right: win.innerWidth,
485
+ bottom: win.innerHeight,
486
+ width: win.innerWidth,
487
+ height: win.innerHeight
488
+ };
489
+ }
490
+
491
+ // `offsetX` / `offsetY` always push the popover *away* from the trigger
492
+ // so on the primary axis they consume available space.
493
+ const elementNode = this.element.node;
494
+ const offsetY = (0, _px.px)(this.element._offset.top, elementNode);
495
+ const offsetX = (0, _px.px)(this.element._offset.left, elementNode);
496
+ const [primary] = this.element.placement;
497
+ let height;
498
+ if (primary === 'bottom') {
499
+ height = constraintRect.bottom - targetRect.bottom - offsetY;
500
+ } else if (primary === 'top') {
501
+ height = targetRect.top - constraintRect.top - offsetY;
502
+ } else {
503
+ height = constraintRect.height;
504
+ }
505
+ let width;
506
+ if (primary === 'end') {
507
+ width = constraintRect.right - targetRect.right - offsetX;
508
+ } else if (primary === 'start') {
509
+ width = targetRect.left - constraintRect.left - offsetX;
510
+ } else {
511
+ width = constraintRect.width;
512
+ }
513
+
514
+ // Measure the wrapper's frame
515
+ let vFrame = 0;
516
+ let hFrame = 0;
517
+ if (elementNode && 'ownerDocument' in elementNode) {
518
+ const view = elementNode.ownerDocument?.defaultView;
519
+ if (view) {
520
+ const cs = view.getComputedStyle(elementNode);
521
+ vFrame = (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0) + (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
522
+ hFrame = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0) + (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
523
+ }
524
+ }
525
+ return {
526
+ height: Math.max(0, Math.floor(height - vFrame - 1)),
527
+ width: Math.max(0, Math.floor(width - hFrame - 1))
528
+ };
529
+ }
437
530
  }
438
531
  function addOffsets(offsets) {
439
532
  return offsets.reduce((sum, offset) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-position",
3
- "version": "11.7.4-snapshot-185",
3
+ "version": "11.7.4-snapshot-10",
4
4
  "description": "A component for positioning content with respect to a designated target.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -15,22 +15,22 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@babel/runtime": "^7.29.2",
18
- "@instructure/debounce": "11.7.4-snapshot-185",
19
- "@instructure/emotion": "11.7.4-snapshot-185",
20
- "@instructure/shared-types": "11.7.4-snapshot-185",
21
- "@instructure/ui-dom-utils": "11.7.4-snapshot-185",
22
- "@instructure/ui-portal": "11.7.4-snapshot-185",
23
- "@instructure/ui-react-utils": "11.7.4-snapshot-185",
24
- "@instructure/ui-themes": "11.7.4-snapshot-185",
25
- "@instructure/uid": "11.7.4-snapshot-185",
26
- "@instructure/ui-utils": "11.7.4-snapshot-185"
18
+ "@instructure/debounce": "11.7.4-snapshot-10",
19
+ "@instructure/emotion": "11.7.4-snapshot-10",
20
+ "@instructure/ui-dom-utils": "11.7.4-snapshot-10",
21
+ "@instructure/ui-portal": "11.7.4-snapshot-10",
22
+ "@instructure/shared-types": "11.7.4-snapshot-10",
23
+ "@instructure/ui-react-utils": "11.7.4-snapshot-10",
24
+ "@instructure/ui-themes": "11.7.4-snapshot-10",
25
+ "@instructure/ui-utils": "11.7.4-snapshot-10",
26
+ "@instructure/uid": "11.7.4-snapshot-10"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@testing-library/jest-dom": "^6.6.3",
30
30
  "@testing-library/react": "15.0.7",
31
31
  "vitest": "^3.2.2",
32
- "@instructure/ui-babel-preset": "11.7.4-snapshot-185",
33
- "@instructure/ui-color-utils": "11.7.4-snapshot-185"
32
+ "@instructure/ui-color-utils": "11.7.4-snapshot-10",
33
+ "@instructure/ui-babel-preset": "11.7.4-snapshot-10"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=18 <=19"
@@ -80,9 +80,13 @@ class Position extends Component<PositionProps, PositionState> {
80
80
  constructor(props: PositionProps) {
81
81
  super(props)
82
82
 
83
+ const initial = this.calculatePosition(props)
84
+ this._availableHeight = initial.availableHeight
85
+ this._availableWidth = initial.availableWidth
83
86
  this.state = {
84
87
  positioned: false,
85
- ...this.calculatePosition(props)
88
+ placement: initial.placement,
89
+ style: initial.style
86
90
  }
87
91
  this.position = debounce(this.position, 0, {
88
92
  leading: false,
@@ -98,6 +102,8 @@ class Position extends Component<PositionProps, PositionState> {
98
102
  _listener: PositionChangeListenerType | null = null
99
103
  _content?: PositionElement
100
104
  _target?: PositionElement
105
+ _availableHeight?: number
106
+ _availableWidth?: number
101
107
 
102
108
  handleRef = (el: Element | null) => {
103
109
  const { elementRef } = this.props
@@ -220,6 +226,22 @@ class Position extends Component<PositionProps, PositionState> {
220
226
  )
221
227
  }
222
228
 
229
+ // Write `--ui-position-available-{height,width}` directly on the content
230
+ // node's inline style
231
+ applyAvailableSpaceCustomProperties() {
232
+ const node = findDOMNode(this._content) as HTMLElement | null
233
+ if (!node?.style) return
234
+ const set = (name: string, value: number | undefined) => {
235
+ if (typeof value === 'number' && Number.isFinite(value)) {
236
+ node.style.setProperty(name, `${value}px`)
237
+ } else {
238
+ node.style.removeProperty(name)
239
+ }
240
+ }
241
+ set('--ui-position-available-height', this._availableHeight)
242
+ set('--ui-position-available-width', this._availableWidth)
243
+ }
244
+
223
245
  calculatePosition(props: PositionProps) {
224
246
  return calculateElementPosition(this._content, this._target, {
225
247
  placement: props.placement,
@@ -232,10 +254,12 @@ class Position extends Component<PositionProps, PositionState> {
232
254
  }
233
255
 
234
256
  position = () => {
235
- this.setState({
236
- positioned: true,
237
- ...this.calculatePosition(this.props)
238
- })
257
+ const { placement, style, availableHeight, availableWidth } =
258
+ this.calculatePosition(this.props)
259
+ this._availableHeight = availableHeight
260
+ this._availableWidth = availableWidth
261
+ this.applyAvailableSpaceCustomProperties()
262
+ this.setState({ positioned: true, placement, style })
239
263
  }
240
264
 
241
265
  startTracking() {
@@ -135,6 +135,17 @@ export type ElementPosition = {
135
135
  }
136
136
  }
137
137
 
138
+ /**
139
+ * The full output of `calculateElementPosition`, including the available
140
+ * space numbers driving `--ui-position-available-{height,width}`. Kept
141
+ * separate from `ElementPosition` so `PositionState` (which extends
142
+ * `ElementPosition`) doesn't falsely advertise these fields
143
+ */
144
+ export type ElementPositionWithAvailableSpace = ElementPosition & {
145
+ availableHeight?: number
146
+ availableWidth?: number
147
+ }
148
+
138
149
  export type PositionElement = UIElement
139
150
 
140
151
  export type Offset<Type extends number | string | undefined = number> = {