@atlaskit/popper 8.0.1 → 8.2.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.
- package/CHANGELOG.md +31 -1
- package/dist/cjs/index.js +4 -3
- package/dist/cjs/internal/anchor-context.js +17 -0
- package/dist/cjs/internal/anchor-setter-context.js +17 -0
- package/dist/cjs/internal/read-viewport.js +31 -0
- package/dist/cjs/internal/rect-point-for-placement.js +93 -0
- package/dist/cjs/internal/set-style.js +43 -0
- package/dist/cjs/internal/use-anchor-state.js +23 -0
- package/dist/cjs/internal/use-fit-viewport-max-size.js +155 -0
- package/dist/cjs/internal/use-manager-anchor-setter.js +16 -0
- package/dist/cjs/internal/use-manager-anchor.js +16 -0
- package/dist/cjs/internal/use-reference-visibility.js +164 -0
- package/dist/cjs/manager.js +37 -0
- package/dist/cjs/popper-top-layer.js +286 -0
- package/dist/cjs/popper.js +48 -12
- package/dist/cjs/reference.js +44 -0
- package/dist/es2019/index.js +6 -1
- package/dist/es2019/internal/anchor-context.js +12 -0
- package/dist/es2019/internal/anchor-setter-context.js +9 -0
- package/dist/es2019/internal/read-viewport.js +25 -0
- package/dist/es2019/internal/rect-point-for-placement.js +91 -0
- package/dist/es2019/internal/set-style.js +39 -0
- package/dist/es2019/internal/use-anchor-state.js +12 -0
- package/dist/es2019/internal/use-fit-viewport-max-size.js +151 -0
- package/dist/es2019/internal/use-manager-anchor-setter.js +11 -0
- package/dist/es2019/internal/use-manager-anchor.js +11 -0
- package/dist/es2019/internal/use-reference-visibility.js +146 -0
- package/dist/es2019/manager.js +33 -0
- package/dist/es2019/popper-top-layer.js +267 -0
- package/dist/es2019/popper.js +37 -0
- package/dist/es2019/reference.js +35 -0
- package/dist/esm/index.js +6 -1
- package/dist/esm/internal/anchor-context.js +12 -0
- package/dist/esm/internal/anchor-setter-context.js +11 -0
- package/dist/esm/internal/read-viewport.js +25 -0
- package/dist/esm/internal/rect-point-for-placement.js +87 -0
- package/dist/esm/internal/set-style.js +37 -0
- package/dist/esm/internal/use-anchor-state.js +16 -0
- package/dist/esm/internal/use-fit-viewport-max-size.js +149 -0
- package/dist/esm/internal/use-manager-anchor-setter.js +11 -0
- package/dist/esm/internal/use-manager-anchor.js +11 -0
- package/dist/esm/internal/use-reference-visibility.js +157 -0
- package/dist/esm/manager.js +31 -0
- package/dist/esm/popper-top-layer.js +277 -0
- package/dist/esm/popper.js +48 -12
- package/dist/esm/reference.js +36 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/internal/anchor-context.d.ts +11 -0
- package/dist/types/internal/anchor-setter-context.d.ts +11 -0
- package/dist/types/internal/read-viewport.d.ts +12 -0
- package/dist/types/internal/rect-point-for-placement.d.ts +19 -0
- package/dist/types/internal/set-style.d.ts +14 -0
- package/dist/types/internal/use-anchor-state.d.ts +10 -0
- package/dist/types/internal/use-fit-viewport-max-size.d.ts +53 -0
- package/dist/types/internal/use-manager-anchor-setter.d.ts +7 -0
- package/dist/types/internal/use-manager-anchor.d.ts +6 -0
- package/dist/types/internal/use-reference-visibility.d.ts +32 -0
- package/dist/types/manager.d.ts +13 -0
- package/dist/types/popper-top-layer.d.ts +20 -0
- package/dist/types/reference.d.ts +25 -0
- package/package.json +20 -9
- package/popper.docs.tsx +1 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useLayoutEffect } from 'react';
|
|
2
|
+
import { setStyle } from './set-style';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies the `shouldFitViewport` size caps directly to the
|
|
6
|
+
* `position-area`-anchored popover host. Pure CSS — the browser owns every
|
|
7
|
+
* value, so there is no measurement, no scroll/resize listeners, and no
|
|
8
|
+
* `--ds-popper-anchor-*` custom properties.
|
|
9
|
+
*
|
|
10
|
+
* **Primary (placement) axis** is capped to `calc(100% - 5px - gap)`. Because
|
|
11
|
+
* the host carries `position-area`, its containing block *is* the position-area
|
|
12
|
+
* cell — the region between the anchor edge and the viewport edge — so `100%`
|
|
13
|
+
* resolves to that distance. The popover is pushed `gap` into the cell by its
|
|
14
|
+
* offset margin (`margin-*`, set by `useAnchorPosition`), so the gap is
|
|
15
|
+
* subtracted as well to keep the legacy `viewportPadding = 5` on the far edge.
|
|
16
|
+
* The cap is also more correct than measuring the requested placement: the cell
|
|
17
|
+
* follows whichever side `position-try-fallbacks` actually flips to, so it
|
|
18
|
+
* tracks the flip automatically.
|
|
19
|
+
*
|
|
20
|
+
* **Cross axis** is capped to `calc(100dvw|dvh - 10px)` (viewport, legacy
|
|
21
|
+
* `2 * viewportPadding`). `display: flex` (plus a `min-*-size: 0` reset on the
|
|
22
|
+
* child) lets an oversized child shrink and reflow to the cap. The host stays
|
|
23
|
+
* `overflow: visible` so it never clips the consumer surface's `box-shadow`;
|
|
24
|
+
* scrolling content that cannot reflow is the consumer surface's own
|
|
25
|
+
* responsibility (it owns `overflow`), matching legacy `react-popper`, which
|
|
26
|
+
* applied the cap to the consumer's own element.
|
|
27
|
+
*
|
|
28
|
+
* `min-inline-size` is reset to `0`. `useWidthFromAnchor({ mode: 'none' })`
|
|
29
|
+
* floors the host at `max-content` so a too-narrow span overflows and drives
|
|
30
|
+
* `position-try-fallbacks` rather than wrapping. That floor is the opposite of
|
|
31
|
+
* fitting: when it exceeds a cap, CSS min/max resolution lets the min win and
|
|
32
|
+
* the host overflows the viewport. In fit mode the caps must win, so the floor
|
|
33
|
+
* is neutralised here; `setStyle` restores the prior inline value on cleanup.
|
|
34
|
+
*
|
|
35
|
+
* Verified in Chromium: `max-block-size: calc(100% - …)` on a `position: fixed`
|
|
36
|
+
* + `position-area` element resolves `100%` to the cell, not the viewport.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const CROSS_INLINE_CAP = 'calc(100dvw - 10px)';
|
|
40
|
+
const CROSS_BLOCK_CAP = 'calc(100dvh - 10px)';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Legacy `viewportPadding` (`preventOverflow` padding) the popover kept from
|
|
44
|
+
* every viewport edge under `react-popper`. The caps above already *reserve*
|
|
45
|
+
* this space (`- 5px` on the primary far edge, `- 10px` = `2 * 5px` on the
|
|
46
|
+
* cross axis), but nothing makes the reservation land on the viewport side:
|
|
47
|
+
* the anchor-side gap is a real margin (`useAnchorPosition`'s `edgeMargin`),
|
|
48
|
+
* the viewport side has none, so a capped popover packs flush against the
|
|
49
|
+
* viewport edge it slid/flipped to. Re-applying the padding as a margin on the
|
|
50
|
+
* three non-anchor sides puts the reserved space back on the viewport side.
|
|
51
|
+
* `position-try` flips mirror margins and slides keep them per-side, so the
|
|
52
|
+
* gap tracks whichever edge the browser settles on.
|
|
53
|
+
*/
|
|
54
|
+
const VIEWPORT_PADDING = '5px';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The popover's three non-anchor margin sides for a given placement axis — the
|
|
58
|
+
* primary *far* side plus both cross sides. The remaining (anchor-facing) side
|
|
59
|
+
* already carries the offset gap from `useAnchorPosition` and is left alone.
|
|
60
|
+
*/
|
|
61
|
+
const NON_ANCHOR_MARGIN_SIDES = {
|
|
62
|
+
top: ['margin-block-start', 'margin-inline-start', 'margin-inline-end'],
|
|
63
|
+
bottom: ['margin-block-end', 'margin-inline-start', 'margin-inline-end'],
|
|
64
|
+
left: ['margin-inline-start', 'margin-block-start', 'margin-block-end'],
|
|
65
|
+
right: ['margin-inline-end', 'margin-block-start', 'margin-block-end']
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param target the `position-area`-anchored popover host
|
|
70
|
+
* @param placementAxis the primary axis derived from the popper placement
|
|
71
|
+
* @param gap the offset distance the popover is pushed from the
|
|
72
|
+
* anchor (CSS length), subtracted from the primary cap
|
|
73
|
+
* @param isEnabled mirrors the consumer's `shouldFitViewport` prop
|
|
74
|
+
* @param isOpen re-applies the caps after the host unmounts/remounts
|
|
75
|
+
* across open cycles (the host is torn down on exit)
|
|
76
|
+
*/
|
|
77
|
+
export function useFitViewportMaxSize({
|
|
78
|
+
target,
|
|
79
|
+
placementAxis,
|
|
80
|
+
gap,
|
|
81
|
+
isEnabled,
|
|
82
|
+
isOpen
|
|
83
|
+
}) {
|
|
84
|
+
useLayoutEffect(() => {
|
|
85
|
+
const element = target.current;
|
|
86
|
+
if (!element || !isEnabled) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const isBlockAxis = placementAxis === 'top' || placementAxis === 'bottom';
|
|
90
|
+
const primaryAxisCap = `calc(100% - 5px - ${gap})`;
|
|
91
|
+
const cleanupHost = setStyle({
|
|
92
|
+
element,
|
|
93
|
+
styles: [{
|
|
94
|
+
property: 'display',
|
|
95
|
+
value: 'flex'
|
|
96
|
+
},
|
|
97
|
+
// Neutralise `useWidthFromAnchor`'s `min-inline-size: max-content`
|
|
98
|
+
// floor so the caps below win and content reflows. `setStyle`
|
|
99
|
+
// restores the floor on cleanup.
|
|
100
|
+
{
|
|
101
|
+
property: 'min-inline-size',
|
|
102
|
+
value: '0'
|
|
103
|
+
}, isBlockAxis ? {
|
|
104
|
+
property: 'max-block-size',
|
|
105
|
+
value: primaryAxisCap
|
|
106
|
+
} : {
|
|
107
|
+
property: 'max-inline-size',
|
|
108
|
+
value: primaryAxisCap
|
|
109
|
+
}, isBlockAxis ? {
|
|
110
|
+
property: 'max-inline-size',
|
|
111
|
+
value: CROSS_INLINE_CAP
|
|
112
|
+
} : {
|
|
113
|
+
property: 'max-block-size',
|
|
114
|
+
value: CROSS_BLOCK_CAP
|
|
115
|
+
},
|
|
116
|
+
// Park the reserved viewport padding on the non-anchor sides so a
|
|
117
|
+
// capped popover keeps the legacy `viewportPadding` gap from the
|
|
118
|
+
// viewport edge instead of sitting flush against it.
|
|
119
|
+
...NON_ANCHOR_MARGIN_SIDES[placementAxis].map(property => ({
|
|
120
|
+
property,
|
|
121
|
+
value: VIEWPORT_PADDING
|
|
122
|
+
}))]
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// The host deliberately stays `overflow: visible`. An ancestor with
|
|
126
|
+
// `overflow: auto` clips a descendant's `box-shadow`, so capping and
|
|
127
|
+
// scrolling on the host would strip the elevation shadow from the
|
|
128
|
+
// consumer surface. Instead the host only constrains size; scrolling
|
|
129
|
+
// oversized content is the consumer surface's own responsibility,
|
|
130
|
+
// matching the contract `@atlaskit/top-layer`'s `PopoverSurface` follows
|
|
131
|
+
// (shadow and `overflow` on one element). For that surface (the host's
|
|
132
|
+
// flex child) to be clamped to the cap and scroll its own content, it
|
|
133
|
+
// must be allowed to shrink below its intrinsic size, so reset its
|
|
134
|
+
// min-size floor.
|
|
135
|
+
const child = element.firstElementChild;
|
|
136
|
+
const cleanupChild = child instanceof HTMLElement ? setStyle({
|
|
137
|
+
element: child,
|
|
138
|
+
styles: [{
|
|
139
|
+
property: 'min-block-size',
|
|
140
|
+
value: '0'
|
|
141
|
+
}, {
|
|
142
|
+
property: 'min-inline-size',
|
|
143
|
+
value: '0'
|
|
144
|
+
}]
|
|
145
|
+
}) : undefined;
|
|
146
|
+
return () => {
|
|
147
|
+
cleanupHost();
|
|
148
|
+
cleanupChild === null || cleanupChild === void 0 ? void 0 : cleanupChild();
|
|
149
|
+
};
|
|
150
|
+
}, [target, placementAxis, gap, isEnabled, isOpen]);
|
|
151
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { AnchorSetterContext } from './anchor-setter-context';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the setter that an ancestor `<Manager>` exposes to publish
|
|
6
|
+
* the anchor element from `<Reference>`. Returns a no-op when there is
|
|
7
|
+
* no surrounding `<Manager>`.
|
|
8
|
+
*/
|
|
9
|
+
export function useManagerAnchorSetter() {
|
|
10
|
+
return useContext(AnchorSetterContext);
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { AnchorContext } from './anchor-context';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the current anchor element published by an ancestor
|
|
6
|
+
* `<Reference>` via the shared `<Manager>` provider. Returns `null`
|
|
7
|
+
* when there is no surrounding `<Manager>` / `<Reference>` pair.
|
|
8
|
+
*/
|
|
9
|
+
export function useManagerAnchor() {
|
|
10
|
+
return useContext(AnchorContext);
|
|
11
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useState } from 'react';
|
|
2
|
+
import { bind, bindAll } from 'bind-event-listener';
|
|
3
|
+
import { readViewport } from './read-viewport';
|
|
4
|
+
const INITIAL = {
|
|
5
|
+
isReferenceHidden: false,
|
|
6
|
+
hasPopperEscaped: false
|
|
7
|
+
};
|
|
8
|
+
function isFullyOutsideViewport(rect, viewport) {
|
|
9
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (rect.bottom <= 0) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (rect.right <= 0) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (rect.top >= viewport.height) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (rect.left >= viewport.width) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function isClippingOverflow(value) {
|
|
27
|
+
return value === 'auto' || value === 'scroll' || value === 'hidden' || value === 'clip';
|
|
28
|
+
}
|
|
29
|
+
function isClippedByAncestors(element) {
|
|
30
|
+
const rect = element.getBoundingClientRect();
|
|
31
|
+
let parent = element.parentElement;
|
|
32
|
+
while (parent) {
|
|
33
|
+
// Read the longhand axis properties directly. The `overflow` shorthand
|
|
34
|
+
// is only populated when both axes share the same value, so reading it
|
|
35
|
+
// alone misses ancestors that set only `overflow-x` or `overflow-y`.
|
|
36
|
+
const style = window.getComputedStyle(parent);
|
|
37
|
+
const clipsHorizontally = isClippingOverflow(style.overflowX);
|
|
38
|
+
const clipsVertically = isClippingOverflow(style.overflowY);
|
|
39
|
+
if (clipsHorizontally || clipsVertically) {
|
|
40
|
+
const parentRect = parent.getBoundingClientRect();
|
|
41
|
+
const horizontallyOutside = rect.right <= parentRect.left || rect.left >= parentRect.right;
|
|
42
|
+
const verticallyOutside = rect.bottom <= parentRect.top || rect.top >= parentRect.bottom;
|
|
43
|
+
if (clipsHorizontally && horizontallyOutside) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (clipsVertically && verticallyOutside) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
parent = parent.parentElement;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Exists **only** to preserve API parity with the legacy popper.js
|
|
57
|
+
* implementation. The top-layer popper does not natively expose
|
|
58
|
+
* popper.js's `isReferenceHidden` / `hasPopperEscaped` render-prop
|
|
59
|
+
* modifiers, but existing consumers still branch on those values, so
|
|
60
|
+
* this hook reconstructs them from live DOM measurement.
|
|
61
|
+
*
|
|
62
|
+
* Do not reach for this hook in new code. If you find yourself wanting
|
|
63
|
+
* these signals for a new feature, prefer a first-class top-layer or
|
|
64
|
+
* anchor-positioning primitive instead.
|
|
65
|
+
*
|
|
66
|
+
* - `isReferenceHidden`: the anchor's bounding rect is fully outside the
|
|
67
|
+
* visual viewport OR is clipped to zero area by any scrollable
|
|
68
|
+
* ancestor.
|
|
69
|
+
* - `hasPopperEscaped`: the popover surface's rect is fully outside the
|
|
70
|
+
* visual viewport.
|
|
71
|
+
*
|
|
72
|
+
* Measurement runs in a `useLayoutEffect` so the first paint already
|
|
73
|
+
* reflects the correct values, preventing consumers from animating
|
|
74
|
+
* from `opacity: 1` to `opacity: 0` on mount.
|
|
75
|
+
*/
|
|
76
|
+
export function useReferenceVisibility({
|
|
77
|
+
anchor,
|
|
78
|
+
popoverRef
|
|
79
|
+
}) {
|
|
80
|
+
const [visibility, setVisibility] = useState(INITIAL);
|
|
81
|
+
const measure = useCallback(() => {
|
|
82
|
+
if (!anchor) {
|
|
83
|
+
setVisibility(previous => previous.isReferenceHidden === false && previous.hasPopperEscaped === false ? previous : INITIAL);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const viewport = readViewport();
|
|
87
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
88
|
+
const isReferenceHidden = isFullyOutsideViewport(anchorRect, viewport) || isClippedByAncestors(anchor);
|
|
89
|
+
const popoverElement = popoverRef.current;
|
|
90
|
+
const hasPopperEscaped = popoverElement ? isFullyOutsideViewport(popoverElement.getBoundingClientRect(), viewport) : false;
|
|
91
|
+
setVisibility(previous => {
|
|
92
|
+
if (previous.isReferenceHidden === isReferenceHidden && previous.hasPopperEscaped === hasPopperEscaped) {
|
|
93
|
+
return previous;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
isReferenceHidden,
|
|
97
|
+
hasPopperEscaped
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}, [anchor, popoverRef]);
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
measure();
|
|
103
|
+
if (typeof window === 'undefined') {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const cleanups = [];
|
|
107
|
+
|
|
108
|
+
// Resize observer on the anchor so layout changes update visibility.
|
|
109
|
+
if (anchor && typeof ResizeObserver !== 'undefined') {
|
|
110
|
+
const observer = new ResizeObserver(() => measure());
|
|
111
|
+
observer.observe(anchor);
|
|
112
|
+
cleanups.push(() => observer.disconnect());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Scroll + resize: any scroll event in the document can move the
|
|
116
|
+
// anchor in/out of any scrollable ancestor's viewport.
|
|
117
|
+
cleanups.push(bindAll(window, [{
|
|
118
|
+
type: 'scroll',
|
|
119
|
+
listener: measure,
|
|
120
|
+
options: {
|
|
121
|
+
capture: true,
|
|
122
|
+
passive: true
|
|
123
|
+
}
|
|
124
|
+
}, {
|
|
125
|
+
type: 'resize',
|
|
126
|
+
listener: measure,
|
|
127
|
+
options: {
|
|
128
|
+
passive: true
|
|
129
|
+
}
|
|
130
|
+
}]));
|
|
131
|
+
const visual = window.visualViewport;
|
|
132
|
+
if (visual) {
|
|
133
|
+
cleanups.push(bind(visual, {
|
|
134
|
+
type: 'resize',
|
|
135
|
+
listener: measure,
|
|
136
|
+
options: {
|
|
137
|
+
passive: true
|
|
138
|
+
}
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
return () => {
|
|
142
|
+
cleanups.forEach(cleanup => cleanup());
|
|
143
|
+
};
|
|
144
|
+
}, [anchor, measure]);
|
|
145
|
+
return visibility;
|
|
146
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Manager as ReactPopperManager } from 'react-popper';
|
|
3
|
+
import { AnchorContext } from './internal/anchor-context';
|
|
4
|
+
import { AnchorSetterContext } from './internal/anchor-setter-context';
|
|
5
|
+
import { useAnchorState } from './internal/use-anchor-state';
|
|
6
|
+
|
|
7
|
+
// Derive the wrapper's props from `react-popper`'s own `ManagerProps` so the
|
|
8
|
+
// public surface provably accepts exactly what the underlying `<Manager>`
|
|
9
|
+
// accepts (today just `children`) and stays in sync if that contract changes.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wraps `react-popper`'s `<Manager>` so the anchor that `<Reference>`
|
|
13
|
+
* captures is published through `@atlaskit/popper`'s own bridge context.
|
|
14
|
+
* Doing so insulates `<Popper>` consumers from `react-popper`'s dual
|
|
15
|
+
* CJS / ESM builds, which otherwise create two unrelated context
|
|
16
|
+
* instances that prevent the FF-on top-layer adapter from discovering
|
|
17
|
+
* the anchor.
|
|
18
|
+
*/
|
|
19
|
+
export function Manager({
|
|
20
|
+
children
|
|
21
|
+
}) {
|
|
22
|
+
// `setAnchor` is the `useState` setter, which React guarantees is stable
|
|
23
|
+
// across renders, so it can be passed straight to the context provider.
|
|
24
|
+
const {
|
|
25
|
+
anchor,
|
|
26
|
+
setAnchor
|
|
27
|
+
} = useAnchorState();
|
|
28
|
+
return /*#__PURE__*/React.createElement(ReactPopperManager, null, /*#__PURE__*/React.createElement(AnchorSetterContext.Provider, {
|
|
29
|
+
value: setAnchor
|
|
30
|
+
}, /*#__PURE__*/React.createElement(AnchorContext.Provider, {
|
|
31
|
+
value: anchor
|
|
32
|
+
}, children)));
|
|
33
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/* popper-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
|
|
2
|
+
import { ax, ix } from "@compiled/react/runtime";
|
|
3
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { getDocument } from '@atlaskit/browser-apis';
|
|
5
|
+
import noop from '@atlaskit/ds-lib/noop';
|
|
6
|
+
import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
|
|
7
|
+
import { Popover } from '@atlaskit/top-layer/popover';
|
|
8
|
+
import { useAnchorPosition } from '@atlaskit/top-layer/use-anchor-position';
|
|
9
|
+
import { useAnchorPositionAtPoint } from '@atlaskit/top-layer/use-anchor-position-at-point';
|
|
10
|
+
import { usePopoverId } from '@atlaskit/top-layer/use-popover-id';
|
|
11
|
+
import { useWidthFromAnchor } from '@atlaskit/top-layer/use-width-from-anchor';
|
|
12
|
+
import { rectPointForPlacement } from './internal/rect-point-for-placement';
|
|
13
|
+
import { useFitViewportMaxSize } from './internal/use-fit-viewport-max-size';
|
|
14
|
+
import { useManagerAnchor } from './internal/use-manager-anchor';
|
|
15
|
+
import { useReferenceVisibility } from './internal/use-reference-visibility';
|
|
16
|
+
/**
|
|
17
|
+
* Inert render-prop values. The browser owns positioning under CSS Anchor
|
|
18
|
+
* Positioning, so `style` / `ref` / `arrowProps` / `update` / `forceUpdate`
|
|
19
|
+
* are no-ops that consumers can safely spread. Module scope keeps identities
|
|
20
|
+
* stable for effect dep arrays.
|
|
21
|
+
*/
|
|
22
|
+
const noopStyle = {};
|
|
23
|
+
const noopSetRef = noop;
|
|
24
|
+
const noopUpdate = () => Promise.resolve(null);
|
|
25
|
+
const noopForceUpdate = () => ({});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `react-popper` stamps `data-popper-arrow` on the arrow element at runtime
|
|
29
|
+
* but does not declare it on `arrowProps`. Widen the type so consumers that
|
|
30
|
+
* rely on the attribute (CSS selectors, snapshots, tests) keep working.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const noopArrowProps = {
|
|
34
|
+
ref: noopSetRef,
|
|
35
|
+
style: noopStyle,
|
|
36
|
+
'data-popper-arrow': true
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns whether the current page is laid out right-to-left.
|
|
41
|
+
*/
|
|
42
|
+
function isPageRtl() {
|
|
43
|
+
const document = getDocument();
|
|
44
|
+
if (!document) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return document.dir === 'rtl' || document.body.dir === 'rtl' || document.documentElement.dir === 'rtl';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `@popperjs/core`'s `Placement` union is a strict subset of
|
|
52
|
+
* `TLegacyPlacement` (the placement-map adds `top-center` /
|
|
53
|
+
* `bottom-center` on top of popper's enum), so every value popper hands
|
|
54
|
+
* us is a valid legacy placement. The cast keeps the runtime path free
|
|
55
|
+
* of an extra module-level lookup that bundlers can occasionally fail
|
|
56
|
+
* to wire up (observed as `Cannot read properties of undefined (reading
|
|
57
|
+
* 'includes')` in component-test bundles).
|
|
58
|
+
*/
|
|
59
|
+
function toLegacyPlacement(placement) {
|
|
60
|
+
return placement;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the primary axis (`top` / `bottom` / `left` / `right`) of a popper
|
|
65
|
+
* placement, used to pick which axis `useFitViewportMaxSize` caps to the
|
|
66
|
+
* anchor edge.
|
|
67
|
+
*/
|
|
68
|
+
function getPlacementAxis(placement) {
|
|
69
|
+
if (placement.startsWith('top')) {
|
|
70
|
+
return 'top';
|
|
71
|
+
}
|
|
72
|
+
if (placement.startsWith('bottom')) {
|
|
73
|
+
return 'bottom';
|
|
74
|
+
}
|
|
75
|
+
if (placement.startsWith('left')) {
|
|
76
|
+
return 'left';
|
|
77
|
+
}
|
|
78
|
+
if (placement.startsWith('right')) {
|
|
79
|
+
return 'right';
|
|
80
|
+
}
|
|
81
|
+
// `auto*` placements have no fixed axis. Default to `bottom` to match the
|
|
82
|
+
// `auto -> block-end` mapping in `fromLegacyPlacement`.
|
|
83
|
+
return 'bottom';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Normalises popper's `[along, away]` offset (which may include `null` or
|
|
88
|
+
* `undefined` entries) into the `[along, away]` number tuple
|
|
89
|
+
* `fromLegacyPlacement` expects.
|
|
90
|
+
*/
|
|
91
|
+
function popperToTopLayerOffset(offset) {
|
|
92
|
+
if (!offset) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const [along, away] = offset;
|
|
96
|
+
if (along == null && away == null) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return [along !== null && along !== void 0 ? along : 0, away !== null && away !== void 0 ? away : 0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* FF-on implementation of `@atlaskit/popper`'s `<Popper>` primitive.
|
|
104
|
+
*
|
|
105
|
+
* Renders the consumer's render-prop output into a `<Popover>` from
|
|
106
|
+
* `@atlaskit/top-layer`, which lifts the element into the browser top
|
|
107
|
+
* layer and positions it via CSS Anchor Positioning. The render-prop
|
|
108
|
+
* contract (`PopperChildrenProps`) is preserved at the type level;
|
|
109
|
+
* `style` and `arrowProps.style` are inert at runtime because the
|
|
110
|
+
* browser owns positioning.
|
|
111
|
+
*
|
|
112
|
+
* Gated behind the `platform-dst-top-layer` feature flag from
|
|
113
|
+
* `popper.tsx`.
|
|
114
|
+
*/
|
|
115
|
+
export function PopperTopLayer({
|
|
116
|
+
children,
|
|
117
|
+
offset,
|
|
118
|
+
placement = 'bottom-start',
|
|
119
|
+
referenceElement,
|
|
120
|
+
shouldFitViewport = false
|
|
121
|
+
}) {
|
|
122
|
+
var _ref;
|
|
123
|
+
// `modifiers` and `strategy` are accepted for source compatibility but have
|
|
124
|
+
// no runtime effect; CSS Anchor Positioning + top-layer rendering replaces
|
|
125
|
+
// them. See `top-layer/notes/migrations/popper-migration.md`.
|
|
126
|
+
|
|
127
|
+
// Anchor resolution: `referenceElement` prop, then `<Manager>` context.
|
|
128
|
+
const managerAnchor = useManagerAnchor();
|
|
129
|
+
const effectiveReference = (_ref = referenceElement !== null && referenceElement !== void 0 ? referenceElement : managerAnchor) !== null && _ref !== void 0 ? _ref : undefined;
|
|
130
|
+
|
|
131
|
+
// Real DOM nodes go to `useAnchorPosition`; popper `VirtualElement`s are
|
|
132
|
+
// bridged through `useAnchorPositionAtPoint`, which owns its own synthetic
|
|
133
|
+
// anchor.
|
|
134
|
+
const htmlAnchor = effectiveReference instanceof HTMLElement ? effectiveReference : null;
|
|
135
|
+
const virtualReference = effectiveReference != null && !(effectiveReference instanceof HTMLElement) ? effectiveReference : null;
|
|
136
|
+
const htmlAnchorRef = useRef(htmlAnchor);
|
|
137
|
+
htmlAnchorRef.current = htmlAnchor;
|
|
138
|
+
const popoverRef = useRef(null);
|
|
139
|
+
const popoverId = usePopoverId();
|
|
140
|
+
|
|
141
|
+
// Track the resolved DOM anchor in state so visibility / max-size hooks
|
|
142
|
+
// re-run when its identity changes. Virtual anchors do not feed these
|
|
143
|
+
// hooks because their probe is outside the consumer's DOM.
|
|
144
|
+
//
|
|
145
|
+
// Adjust state during render by comparing against the state itself: the
|
|
146
|
+
// conditional guard means `setResolvedAnchor` is skipped once they match,
|
|
147
|
+
// so it converges in one extra render. This is the React-documented pattern
|
|
148
|
+
// and is side-effect-free — no ref mutation during render.
|
|
149
|
+
const [resolvedAnchor, setResolvedAnchor] = useState(htmlAnchor);
|
|
150
|
+
if (resolvedAnchor !== htmlAnchor) {
|
|
151
|
+
setResolvedAnchor(htmlAnchor);
|
|
152
|
+
}
|
|
153
|
+
const topLayerPlacement = useMemo(() => {
|
|
154
|
+
return fromLegacyPlacement({
|
|
155
|
+
legacy: toLegacyPlacement(placement),
|
|
156
|
+
offset: popperToTopLayerOffset(offset)
|
|
157
|
+
});
|
|
158
|
+
}, [placement, offset]);
|
|
159
|
+
const isOpen = effectiveReference != null;
|
|
160
|
+
|
|
161
|
+
// HTML-element path. No-op when the reference is virtual or absent.
|
|
162
|
+
useAnchorPosition({
|
|
163
|
+
anchorRef: htmlAnchorRef,
|
|
164
|
+
popoverRef,
|
|
165
|
+
placement: topLayerPlacement,
|
|
166
|
+
isEnabled: htmlAnchor != null,
|
|
167
|
+
isOpen
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Virtual-element path. `useAnchorPositionAtPoint` owns a synthetic
|
|
171
|
+
// anchor in `document.body` and latches `getPoint` once per
|
|
172
|
+
// `isEnabled` activation. Reading the latest `virtualReference` and
|
|
173
|
+
// `topLayerPlacement` via refs ensures the latched closure always
|
|
174
|
+
// sees the current values rather than the ones captured at first
|
|
175
|
+
// activation, which would otherwise go stale if either prop changes
|
|
176
|
+
// while the popper stays open.
|
|
177
|
+
const virtualReferenceRef = useRef(virtualReference);
|
|
178
|
+
virtualReferenceRef.current = virtualReference;
|
|
179
|
+
const topLayerPlacementRef = useRef(topLayerPlacement);
|
|
180
|
+
topLayerPlacementRef.current = topLayerPlacement;
|
|
181
|
+
const isVirtualEnabled = virtualReference != null;
|
|
182
|
+
useAnchorPositionAtPoint({
|
|
183
|
+
popoverRef,
|
|
184
|
+
placement: topLayerPlacement,
|
|
185
|
+
isEnabled: isVirtualEnabled,
|
|
186
|
+
isOpen,
|
|
187
|
+
getPoint: () => {
|
|
188
|
+
const current = virtualReferenceRef.current;
|
|
189
|
+
if (!current) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return rectPointForPlacement({
|
|
193
|
+
rect: current.getBoundingClientRect(),
|
|
194
|
+
placement: topLayerPlacementRef.current,
|
|
195
|
+
isRtl: isPageRtl()
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const {
|
|
200
|
+
isReferenceHidden,
|
|
201
|
+
hasPopperEscaped
|
|
202
|
+
} = useReferenceVisibility({
|
|
203
|
+
anchor: resolvedAnchor,
|
|
204
|
+
popoverRef
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Restore legacy `react-popper`'s natural-width behaviour. Under CSS
|
|
208
|
+
// Anchor Positioning the `position-area` grid cell becomes the popover
|
|
209
|
+
// host's containing block, so an auto-width host shrinks to that cell.
|
|
210
|
+
// When the anchor sits near a viewport edge the cell is narrow, so the
|
|
211
|
+
// content wraps far more than it did under `react-popper` (which kept the
|
|
212
|
+
// content's natural width and shifted/flipped to stay on screen).
|
|
213
|
+
// `min-inline-size: max-content` (mode `'none'`) floors the host at its
|
|
214
|
+
// content's intrinsic width, so a too-narrow span overflows the viewport
|
|
215
|
+
// (driving `position-try-fallbacks`) instead of wrapping. This is safe
|
|
216
|
+
// alongside `shouldFitViewport`: the fit caps clamp the host on both axes,
|
|
217
|
+
// so the host never exceeds the viewport / anchor-edge cap.
|
|
218
|
+
useWidthFromAnchor({
|
|
219
|
+
mode: 'none',
|
|
220
|
+
popoverRef,
|
|
221
|
+
anchorRef: htmlAnchorRef,
|
|
222
|
+
isOpen
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// `shouldFitViewport` caps are applied directly to the `position-area`
|
|
226
|
+
// host, whose containing block is the cell between the anchor edge and the
|
|
227
|
+
// viewport edge — so a pure-CSS `calc(100% - 5px - gap)` reproduces the
|
|
228
|
+
// legacy per-placement anchor-edge cap with no measurement. The gap mirrors
|
|
229
|
+
// `getPlacement`: an omitted offset resolves to `space.100`, otherwise the
|
|
230
|
+
// consumer's `away` value.
|
|
231
|
+
const resolvedOffset = popperToTopLayerOffset(offset);
|
|
232
|
+
const fitGap = resolvedOffset ? `${resolvedOffset[1]}px` : "var(--ds-space-100, 8px)";
|
|
233
|
+
const placementAxis = getPlacementAxis(placement);
|
|
234
|
+
useFitViewportMaxSize({
|
|
235
|
+
target: popoverRef,
|
|
236
|
+
placementAxis,
|
|
237
|
+
gap: fitGap,
|
|
238
|
+
isEnabled: shouldFitViewport,
|
|
239
|
+
isOpen
|
|
240
|
+
});
|
|
241
|
+
const renderChildren = children;
|
|
242
|
+
if (typeof renderChildren !== 'function') {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const renderPropArg = {
|
|
246
|
+
ref: noopSetRef,
|
|
247
|
+
style: noopStyle,
|
|
248
|
+
placement,
|
|
249
|
+
isReferenceHidden,
|
|
250
|
+
hasPopperEscaped,
|
|
251
|
+
update: noopUpdate,
|
|
252
|
+
forceUpdate: noopForceUpdate,
|
|
253
|
+
arrowProps: noopArrowProps
|
|
254
|
+
};
|
|
255
|
+
const content = renderChildren(renderPropArg);
|
|
256
|
+
|
|
257
|
+
// The `shouldFitViewport` size caps live on the `<Popover>` host itself
|
|
258
|
+
// (applied by `useFitViewportMaxSize` above), so the consumer's content is
|
|
259
|
+
// rendered directly with no intermediate wrapper.
|
|
260
|
+
return /*#__PURE__*/React.createElement(Popover, {
|
|
261
|
+
ref: popoverRef,
|
|
262
|
+
id: popoverId,
|
|
263
|
+
isOpen: isOpen,
|
|
264
|
+
mode: "manual",
|
|
265
|
+
animate: false
|
|
266
|
+
}, content);
|
|
267
|
+
}
|
package/dist/es2019/popper.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import { Popper as ReactPopper } from 'react-popper';
|
|
3
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
3
4
|
import { getMaxSizeModifiers } from './max-size';
|
|
5
|
+
import { PopperTopLayer } from './popper-top-layer';
|
|
4
6
|
export { placements } from '@popperjs/core';
|
|
5
7
|
// Export types from PopperJS / React Popper
|
|
6
8
|
|
|
@@ -19,6 +21,41 @@ function defaultChildrenFn() {
|
|
|
19
21
|
}
|
|
20
22
|
const defaultOffset = [0, 8];
|
|
21
23
|
export function Popper({
|
|
24
|
+
children,
|
|
25
|
+
offset,
|
|
26
|
+
placement,
|
|
27
|
+
referenceElement,
|
|
28
|
+
modifiers,
|
|
29
|
+
strategy,
|
|
30
|
+
shouldFitViewport
|
|
31
|
+
}) {
|
|
32
|
+
// The FF check sits at the very top of the public Popper so the
|
|
33
|
+
// rest of the function (which has its own hooks) does not violate
|
|
34
|
+
// the rules of hooks. Each branch is its own component with its
|
|
35
|
+
// own complete hook order. Props are forwarded explicitly to
|
|
36
|
+
// satisfy `no-unsafe-spread-props`.
|
|
37
|
+
if (fg('platform-dst-top-layer')) {
|
|
38
|
+
return /*#__PURE__*/React.createElement(PopperTopLayer, {
|
|
39
|
+
children: children,
|
|
40
|
+
offset: offset,
|
|
41
|
+
placement: placement,
|
|
42
|
+
referenceElement: referenceElement,
|
|
43
|
+
modifiers: modifiers,
|
|
44
|
+
strategy: strategy,
|
|
45
|
+
shouldFitViewport: shouldFitViewport
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return /*#__PURE__*/React.createElement(LegacyPopper, {
|
|
49
|
+
children: children,
|
|
50
|
+
offset: offset,
|
|
51
|
+
placement: placement,
|
|
52
|
+
referenceElement: referenceElement,
|
|
53
|
+
modifiers: modifiers,
|
|
54
|
+
strategy: strategy,
|
|
55
|
+
shouldFitViewport: shouldFitViewport
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function LegacyPopper({
|
|
22
59
|
children = defaultChildrenFn,
|
|
23
60
|
offset = defaultOffset,
|
|
24
61
|
placement = 'bottom-start',
|