@instructure/ui-position 11.7.3 → 11.7.4-pr-snapshot-1781695314229

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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE.md +1 -0
  3. package/{lib/Position/props.js → babel.config.cjs} +12 -7
  4. package/es/Position/index.js +40 -8
  5. package/es/PositionPropTypes.js +8 -0
  6. package/es/calculateElementPosition.js +108 -13
  7. package/es/index.js +6 -6
  8. package/es/mirrorHorizontalPlacement.js +4 -2
  9. package/es/mirrorPlacement.js +4 -2
  10. package/es/parsePlacement.js +1 -1
  11. package/package.json +14 -16
  12. package/src/Position/index.tsx +36 -11
  13. package/src/PositionPropTypes.ts +11 -0
  14. package/src/calculateElementPosition.ts +119 -15
  15. package/src/executeMirrorFunction.ts +1 -1
  16. package/src/index.ts +6 -6
  17. package/src/mirrorHorizontalPlacement.ts +2 -2
  18. package/src/mirrorPlacement.ts +2 -2
  19. package/src/parsePlacement.ts +1 -1
  20. package/tsconfig.build.tsbuildinfo +1 -1
  21. package/types/Position/index.d.ts +11 -7
  22. package/types/Position/index.d.ts.map +1 -1
  23. package/types/PositionPropTypes.d.ts +10 -0
  24. package/types/PositionPropTypes.d.ts.map +1 -1
  25. package/types/calculateElementPosition.d.ts +2 -2
  26. package/types/calculateElementPosition.d.ts.map +1 -1
  27. package/types/executeMirrorFunction.d.ts +1 -1
  28. package/types/executeMirrorFunction.d.ts.map +1 -1
  29. package/types/index.d.ts +6 -6
  30. package/types/index.d.ts.map +1 -1
  31. package/types/mirrorHorizontalPlacement.d.ts +1 -1
  32. package/types/mirrorHorizontalPlacement.d.ts.map +1 -1
  33. package/types/mirrorPlacement.d.ts +1 -1
  34. package/types/mirrorPlacement.d.ts.map +1 -1
  35. package/types/parsePlacement.d.ts +1 -1
  36. package/types/parsePlacement.d.ts.map +1 -1
  37. package/lib/Position/index.js +0 -249
  38. package/lib/Position/styles.js +0 -50
  39. package/lib/Position/theme.js +0 -47
  40. package/lib/PositionPropTypes.js +0 -57
  41. package/lib/calculateElementPosition.js +0 -524
  42. package/lib/executeMirrorFunction.js +0 -37
  43. package/lib/index.js +0 -47
  44. package/lib/mirrorHorizontalPlacement.js +0 -60
  45. package/lib/mirrorPlacement.js +0 -63
  46. package/lib/package.json +0 -1
  47. package/lib/parsePlacement.js +0 -12
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
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-pr-snapshot-1781695314229](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-pr-snapshot-1781695314229) (2026-06-17)
7
+
8
+
9
+ ### Features
10
+
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))
13
+
14
+
15
+
16
+
17
+
6
18
  ## [11.7.3](https://github.com/instructure/instructure-ui/compare/v11.7.2...v11.7.3) (2026-05-07)
7
19
 
8
20
 
package/LICENSE.md CHANGED
@@ -2,6 +2,7 @@
2
2
  title: The MIT License (MIT)
3
3
  category: Getting Started
4
4
  order: 9
5
+ isWIP: true
5
6
  ---
6
7
 
7
8
  # The MIT License (MIT)
@@ -1,9 +1,3 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.allowedProps = void 0;
7
1
  /*
8
2
  * The MIT License (MIT)
9
3
  *
@@ -28,4 +22,15 @@ exports.allowedProps = void 0;
28
22
  * SOFTWARE.
29
23
  */
30
24
 
31
- const allowedProps = exports.allowedProps = ['renderTarget', 'target', 'placement', 'mountNode', 'insertAt', 'constrain', 'offsetX', 'offsetY', 'id', 'shouldTrackPosition', 'shouldPositionOverTarget', 'onPositionChanged', 'onPositioned', 'children', 'containerDisplay', 'elementRef'];
25
+ module.exports = {
26
+ presets: [
27
+ [
28
+ require('@instructure/ui-babel-preset'),
29
+ {
30
+ esModules: Boolean(process.env.ES_MODULES),
31
+ removeConsole: process.env.NODE_ENV === 'production',
32
+ transformImports: Boolean(process.env.TRANSFORM_IMPORTS)
33
+ }
34
+ ]
35
+ ]
36
+ }
@@ -30,18 +30,18 @@ import { deepEqual, shallowEqual, combineDataCid } from '@instructure/ui-utils';
30
30
  import { debounce } from '@instructure/debounce';
31
31
  import { Portal } from '@instructure/ui-portal';
32
32
  import { withStyle } from '@instructure/emotion';
33
- import generateStyle from "./styles.js";
34
- import generateComponentTheme from "./theme.js";
35
- import { allowedProps } from "./props.js";
36
- import { calculateElementPosition } from "../calculateElementPosition.js";
33
+ import generateStyle from './styles.js';
34
+ import generateComponentTheme from './theme.js';
35
+ import { allowedProps } from './props.js';
36
+ import { calculateElementPosition } from '../calculateElementPosition.js';
37
37
  import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
38
38
  /**
39
39
  ---
40
- category: components/utilities
40
+ category: components/Util Components
41
41
  ---
42
42
  **/
43
43
  let Position = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, generateComponentTheme), _dec(_class = _dec2(_class = class Position extends Component {
44
- static displayName = "Position";
44
+ static displayName = 'Position';
45
45
  static componentId = 'Position';
46
46
  static allowedProps = allowedProps;
47
47
  static defaultProps = {
@@ -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) => {
package/es/index.js CHANGED
@@ -21,9 +21,9 @@
21
21
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  * SOFTWARE.
23
23
  */
24
- export { Position } from "./Position/index.js";
25
- export { calculateElementPosition } from "./calculateElementPosition.js";
26
- export { executeMirrorFunction } from "./executeMirrorFunction.js";
27
- export { mirrorHorizontalPlacement } from "./mirrorHorizontalPlacement.js";
28
- export { mirrorPlacement } from "./mirrorPlacement.js";
29
- export { parsePlacement } from "./parsePlacement.js";
24
+ export { Position } from './Position/index.js';
25
+ export { calculateElementPosition } from './calculateElementPosition.js';
26
+ export { executeMirrorFunction } from './executeMirrorFunction.js';
27
+ export { mirrorHorizontalPlacement } from './mirrorHorizontalPlacement.js';
28
+ export { mirrorPlacement } from './mirrorPlacement.js';
29
+ export { parsePlacement } from './parsePlacement.js';
@@ -21,8 +21,10 @@
21
21
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  * SOFTWARE.
23
23
  */
24
- import { mirrorMap } from "./PositionPropTypes.js";
25
- import executeMirrorFunction from "./executeMirrorFunction.js";
24
+
25
+ import { mirrorMap } from './PositionPropTypes.js';
26
+ import executeMirrorFunction from './executeMirrorFunction.js';
27
+
26
28
  /**
27
29
  * Given a string or array of one or two placement values, mirrors the placement
28
30
  * horizontally.
@@ -21,8 +21,10 @@
21
21
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  * SOFTWARE.
23
23
  */
24
- import { mirrorMap } from "./PositionPropTypes.js";
25
- import executeMirrorFunction from "./executeMirrorFunction.js";
24
+
25
+ import { mirrorMap } from './PositionPropTypes.js';
26
+ import executeMirrorFunction from './executeMirrorFunction.js';
27
+
26
28
  /**
27
29
  * ---
28
30
  * category: utilities/position
@@ -21,4 +21,4 @@
21
21
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  * SOFTWARE.
23
23
  */
24
- export { parsePlacement } from "./calculateElementPosition.js";
24
+ export { parsePlacement } from './calculateElementPosition.js';
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@instructure/ui-position",
3
- "version": "11.7.3",
3
+ "version": "11.7.4-pr-snapshot-1781695314229",
4
+ "type": "module",
4
5
  "description": "A component for positioning content with respect to a designated target.",
5
6
  "author": "Instructure, Inc. Engineering and Product Design",
6
7
  "module": "./es/index.js",
7
- "main": "./lib/index.js",
8
8
  "types": "./types/index.d.ts",
9
9
  "repository": {
10
10
  "type": "git",
@@ -15,22 +15,22 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@babel/runtime": "^7.29.2",
18
- "@instructure/ui-dom-utils": "11.7.3",
19
- "@instructure/emotion": "11.7.3",
20
- "@instructure/ui-portal": "11.7.3",
21
- "@instructure/shared-types": "11.7.3",
22
- "@instructure/debounce": "11.7.3",
23
- "@instructure/ui-react-utils": "11.7.3",
24
- "@instructure/ui-themes": "11.7.3",
25
- "@instructure/ui-utils": "11.7.3",
26
- "@instructure/uid": "11.7.3"
18
+ "@instructure/debounce": "11.7.4-pr-snapshot-1781695314229",
19
+ "@instructure/shared-types": "11.7.4-pr-snapshot-1781695314229",
20
+ "@instructure/emotion": "11.7.4-pr-snapshot-1781695314229",
21
+ "@instructure/ui-portal": "11.7.4-pr-snapshot-1781695314229",
22
+ "@instructure/ui-dom-utils": "11.7.4-pr-snapshot-1781695314229",
23
+ "@instructure/ui-react-utils": "11.7.4-pr-snapshot-1781695314229",
24
+ "@instructure/ui-themes": "11.7.4-pr-snapshot-1781695314229",
25
+ "@instructure/ui-utils": "11.7.4-pr-snapshot-1781695314229",
26
+ "@instructure/uid": "11.7.4-pr-snapshot-1781695314229"
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.3",
33
- "@instructure/ui-color-utils": "11.7.3"
32
+ "@instructure/ui-babel-preset": "11.7.4-pr-snapshot-1781695314229",
33
+ "@instructure/ui-color-utils": "11.7.4-pr-snapshot-1781695314229"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=18 <=19"
@@ -44,10 +44,8 @@
44
44
  "src": "./src/index.ts",
45
45
  "types": "./types/index.d.ts",
46
46
  "import": "./es/index.js",
47
- "require": "./lib/index.js",
48
47
  "default": "./es/index.js"
49
48
  },
50
- "./lib/*": "./lib/*",
51
49
  "./es/*": "./es/*",
52
50
  "./types/*": "./types/*",
53
51
  "./package.json": "./package.json",
@@ -57,7 +55,7 @@
57
55
  "lint": "ui-scripts lint",
58
56
  "lint:fix": "ui-scripts lint --fix",
59
57
  "clean": "ui-scripts clean",
60
- "build": "ui-scripts build --modules es,cjs",
58
+ "build": "ui-scripts build",
61
59
  "build:watch": "pnpm run ts:check -- --watch & ui-scripts build --watch",
62
60
  "build:types": "tsc -p tsconfig.build.json",
63
61
  "ts:check": "tsc -p tsconfig.build.json --noEmit --emitDeclarationOnly false"
@@ -41,22 +41,23 @@ import { debounce } from '@instructure/debounce'
41
41
  import { Portal } from '@instructure/ui-portal'
42
42
  import { withStyle } from '@instructure/emotion'
43
43
 
44
- import generateStyle from './styles'
45
- import generateComponentTheme from './theme'
44
+ import generateStyle from './styles.js'
45
+ import generateComponentTheme from './theme.js'
46
46
  import type { PositionProps, PositionState } from './props'
47
- import { allowedProps } from './props'
47
+ import { allowedProps } from './props.js'
48
48
 
49
- import { calculateElementPosition } from '../calculateElementPosition'
50
- import { PositionElement } from '../PositionPropTypes'
49
+ import { calculateElementPosition } from '../calculateElementPosition.js'
50
+ import { PositionElement } from '../PositionPropTypes.js'
51
51
 
52
52
  /**
53
53
  ---
54
- category: components/utilities
54
+ category: components/Util Components
55
55
  ---
56
56
  **/
57
57
  @withDeterministicId()
58
58
  @withStyle(generateStyle, generateComponentTheme)
59
59
  class Position extends Component<PositionProps, PositionState> {
60
+ static displayName = 'Position'
60
61
  static readonly componentId = 'Position'
61
62
 
62
63
  static allowedProps = allowedProps
@@ -80,9 +81,13 @@ class Position extends Component<PositionProps, PositionState> {
80
81
  constructor(props: PositionProps) {
81
82
  super(props)
82
83
 
84
+ const initial = this.calculatePosition(props)
85
+ this._availableHeight = initial.availableHeight
86
+ this._availableWidth = initial.availableWidth
83
87
  this.state = {
84
88
  positioned: false,
85
- ...this.calculatePosition(props)
89
+ placement: initial.placement,
90
+ style: initial.style
86
91
  }
87
92
  this.position = debounce(this.position, 0, {
88
93
  leading: false,
@@ -98,6 +103,8 @@ class Position extends Component<PositionProps, PositionState> {
98
103
  _listener: PositionChangeListenerType | null = null
99
104
  _content?: PositionElement
100
105
  _target?: PositionElement
106
+ _availableHeight?: number
107
+ _availableWidth?: number
101
108
 
102
109
  handleRef = (el: Element | null) => {
103
110
  const { elementRef } = this.props
@@ -220,6 +227,22 @@ class Position extends Component<PositionProps, PositionState> {
220
227
  )
221
228
  }
222
229
 
230
+ // Write `--ui-position-available-{height,width}` directly on the content
231
+ // node's inline style
232
+ applyAvailableSpaceCustomProperties() {
233
+ const node = findDOMNode(this._content) as HTMLElement | null
234
+ if (!node?.style) return
235
+ const set = (name: string, value: number | undefined) => {
236
+ if (typeof value === 'number' && Number.isFinite(value)) {
237
+ node.style.setProperty(name, `${value}px`)
238
+ } else {
239
+ node.style.removeProperty(name)
240
+ }
241
+ }
242
+ set('--ui-position-available-height', this._availableHeight)
243
+ set('--ui-position-available-width', this._availableWidth)
244
+ }
245
+
223
246
  calculatePosition(props: PositionProps) {
224
247
  return calculateElementPosition(this._content, this._target, {
225
248
  placement: props.placement,
@@ -232,10 +255,12 @@ class Position extends Component<PositionProps, PositionState> {
232
255
  }
233
256
 
234
257
  position = () => {
235
- this.setState({
236
- positioned: true,
237
- ...this.calculatePosition(this.props)
238
- })
258
+ const { placement, style, availableHeight, availableWidth } =
259
+ this.calculatePosition(this.props)
260
+ this._availableHeight = availableHeight
261
+ this._availableWidth = availableWidth
262
+ this.applyAvailableSpaceCustomProperties()
263
+ this.setState({ positioned: true, placement, style })
239
264
  }
240
265
 
241
266
  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> = {