@atlaskit/top-layer 0.11.0 → 0.12.1
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 +24 -0
- package/dist/cjs/animations/presets.js +5 -0
- package/dist/cjs/entry-points/use-arrow-navigation.js +3 -2
- package/dist/cjs/internal/placement-to-position-area.js +43 -0
- package/dist/cjs/internal/placement-to-try-fallbacks.js +82 -0
- package/dist/cjs/internal/use-anchor-position.js +18 -131
- package/dist/cjs/internal/use-animated-visibility.js +84 -7
- package/dist/cjs/popover/popover.js +2 -0
- package/dist/cjs/popup/popup-content.js +55 -15
- package/dist/cjs/popup/popup-context.js +14 -33
- package/dist/cjs/popup/popup-provider.js +15 -0
- package/dist/cjs/popup/popup-trigger-function.js +9 -6
- package/dist/cjs/popup/popup-trigger.js +14 -9
- package/dist/cjs/popup/popup.js +7 -7
- package/dist/cjs/popup/use-maybe-popup-context.js +17 -0
- package/dist/cjs/popup/use-popup-context.js +20 -0
- package/dist/cjs/use-arrow-navigation/index.js +3 -2
- package/dist/cjs/use-arrow-navigation/is-at-current-menu-level.js +87 -0
- package/dist/cjs/use-arrow-navigation/use-arrow-navigation.js +0 -82
- package/dist/es2019/animations/presets.js +5 -0
- package/dist/es2019/entry-points/use-arrow-navigation.js +2 -1
- package/dist/es2019/internal/placement-to-position-area.js +40 -0
- package/dist/es2019/internal/placement-to-try-fallbacks.js +77 -0
- package/dist/es2019/internal/use-anchor-position.js +2 -116
- package/dist/es2019/internal/use-animated-visibility.js +84 -7
- package/dist/es2019/popover/popover.js +2 -0
- package/dist/es2019/popup/popup-content.js +55 -14
- package/dist/es2019/popup/popup-context.js +14 -30
- package/dist/es2019/popup/popup-provider.js +10 -0
- package/dist/es2019/popup/popup-trigger-function.js +8 -5
- package/dist/es2019/popup/popup-trigger.js +14 -8
- package/dist/es2019/popup/popup.js +4 -4
- package/dist/es2019/popup/use-maybe-popup-context.js +12 -0
- package/dist/es2019/popup/use-popup-context.js +15 -0
- package/dist/es2019/use-arrow-navigation/index.js +2 -1
- package/dist/es2019/use-arrow-navigation/is-at-current-menu-level.js +81 -0
- package/dist/es2019/use-arrow-navigation/use-arrow-navigation.js +0 -81
- package/dist/esm/animations/presets.js +5 -0
- package/dist/esm/entry-points/use-arrow-navigation.js +2 -1
- package/dist/esm/internal/placement-to-position-area.js +38 -0
- package/dist/esm/internal/placement-to-try-fallbacks.js +76 -0
- package/dist/esm/internal/use-anchor-position.js +16 -127
- package/dist/esm/internal/use-animated-visibility.js +84 -7
- package/dist/esm/popover/popover.js +2 -0
- package/dist/esm/popup/popup-content.js +55 -14
- package/dist/esm/popup/popup-context.js +14 -30
- package/dist/esm/popup/popup-provider.js +10 -0
- package/dist/esm/popup/popup-trigger-function.js +8 -5
- package/dist/esm/popup/popup-trigger.js +14 -8
- package/dist/esm/popup/popup.js +6 -6
- package/dist/esm/popup/use-maybe-popup-context.js +12 -0
- package/dist/esm/popup/use-popup-context.js +15 -0
- package/dist/esm/use-arrow-navigation/index.js +2 -1
- package/dist/esm/use-arrow-navigation/is-at-current-menu-level.js +81 -0
- package/dist/esm/use-arrow-navigation/use-arrow-navigation.js +0 -81
- package/dist/types/animations/types.d.ts +5 -1
- package/dist/types/entry-points/use-arrow-navigation.d.ts +2 -1
- package/dist/types/internal/placement-to-position-area.d.ts +21 -0
- package/dist/types/internal/placement-to-try-fallbacks.d.ts +36 -0
- package/dist/types/internal/use-anchor-position.d.ts +0 -55
- package/dist/types/internal/use-animated-visibility.d.ts +8 -2
- package/dist/types/popover/types.d.ts +8 -0
- package/dist/types/popup/popup-context.d.ts +25 -25
- package/dist/types/popup/popup-provider.d.ts +10 -0
- package/dist/types/popup/types.d.ts +12 -0
- package/dist/types/popup/use-maybe-popup-context.d.ts +8 -0
- package/dist/types/popup/use-popup-context.d.ts +7 -0
- package/dist/types/use-arrow-navigation/index.d.ts +2 -1
- package/dist/types/use-arrow-navigation/is-at-current-menu-level.d.ts +48 -0
- package/dist/types/use-arrow-navigation/use-arrow-navigation.d.ts +0 -48
- package/dist/types-ts4.5/animations/types.d.ts +5 -1
- package/dist/types-ts4.5/entry-points/use-arrow-navigation.d.ts +2 -1
- package/dist/types-ts4.5/internal/placement-to-position-area.d.ts +21 -0
- package/dist/types-ts4.5/internal/placement-to-try-fallbacks.d.ts +36 -0
- package/dist/types-ts4.5/internal/use-anchor-position.d.ts +0 -55
- package/dist/types-ts4.5/internal/use-animated-visibility.d.ts +8 -2
- package/dist/types-ts4.5/popover/types.d.ts +8 -0
- package/dist/types-ts4.5/popup/popup-context.d.ts +25 -25
- package/dist/types-ts4.5/popup/popup-provider.d.ts +10 -0
- package/dist/types-ts4.5/popup/types.d.ts +12 -0
- package/dist/types-ts4.5/popup/use-maybe-popup-context.d.ts +8 -0
- package/dist/types-ts4.5/popup/use-popup-context.d.ts +7 -0
- package/dist/types-ts4.5/use-arrow-navigation/index.d.ts +2 -1
- package/dist/types-ts4.5/use-arrow-navigation/is-at-current-menu-level.d.ts +48 -0
- package/dist/types-ts4.5/use-arrow-navigation/use-arrow-navigation.d.ts +0 -48
- package/package.json +2 -2
- package/dist/cjs/arrow/index.js +0 -131
- package/dist/cjs/arrow/types.js +0 -5
- package/dist/es2019/arrow/index.js +0 -269
- package/dist/es2019/arrow/types.js +0 -1
- package/dist/esm/arrow/index.js +0 -124
- package/dist/esm/arrow/types.js +0 -1
- package/dist/types/arrow/index.d.ts +0 -24
- package/dist/types/arrow/types.d.ts +0 -29
- package/dist/types-ts4.5/arrow/index.d.ts +0 -24
- package/dist/types-ts4.5/arrow/types.d.ts +0 -29
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getPlacement } from './resolve-placement';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves placement options to CSS `position-area` syntax.
|
|
5
|
+
*
|
|
6
|
+
* The `align` field represents the *visual* alignment:
|
|
7
|
+
* - `align: 'start'` means "aligned to the trigger's start edge" (left in LTR for block axis)
|
|
8
|
+
* - `align: 'end'` means "aligned to the trigger's end edge"
|
|
9
|
+
* - `align: 'center'` means centered (single-axis position-area)
|
|
10
|
+
*
|
|
11
|
+
* CSS `position-area` uses `span-*` keywords which indicate the *expansion direction*.
|
|
12
|
+
* Visual start-alignment requires `span-{cross-axis}-end` (expand toward end = start-aligned).
|
|
13
|
+
* This function handles the inversion internally.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'center' } }) // → 'block-end'
|
|
17
|
+
* placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'start' } }) // → 'block-end span-inline-end'
|
|
18
|
+
* placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'end' } }) // → 'block-end span-inline-start'
|
|
19
|
+
*/
|
|
20
|
+
export function placementToPositionArea({
|
|
21
|
+
placement
|
|
22
|
+
}) {
|
|
23
|
+
const {
|
|
24
|
+
axis,
|
|
25
|
+
edge,
|
|
26
|
+
align
|
|
27
|
+
} = getPlacement({
|
|
28
|
+
placement
|
|
29
|
+
});
|
|
30
|
+
const edgeValue = `${axis}-${edge}`;
|
|
31
|
+
if (align === 'center') {
|
|
32
|
+
return edgeValue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Cross-axis name: block ↔ inline
|
|
36
|
+
const crossAxis = axis === 'block' ? 'inline' : 'block';
|
|
37
|
+
// Invert align → CSS span direction: visual 'start' → span toward 'end'
|
|
38
|
+
const spanDir = align === 'start' ? 'end' : 'start';
|
|
39
|
+
return `${edgeValue} span-${crossAxis}-${spanDir}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getPlacement } from './resolve-placement';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns `position-try-fallbacks` value based on placement.
|
|
5
|
+
*
|
|
6
|
+
* For centered placements, includes cross-axis aligned position-area
|
|
7
|
+
* values so the browser can shift the popover along the cross-axis
|
|
8
|
+
* when the centered position would overflow the viewport edge.
|
|
9
|
+
*
|
|
10
|
+
* Fallback order for centered placements (e.g. `block-end`):
|
|
11
|
+
* 1. Same edge, start-aligned (shift for near-edge overflow)
|
|
12
|
+
* 2. Same edge, end-aligned (shift for far-edge overflow)
|
|
13
|
+
* 3. Flipped edge, centered
|
|
14
|
+
* 4. Flipped edge, start-aligned
|
|
15
|
+
* 5. Flipped edge, end-aligned
|
|
16
|
+
*
|
|
17
|
+
* For aligned placements (start/end), includes the same-edge centered
|
|
18
|
+
* position as fallbacks so the browser can try centering when the aligned
|
|
19
|
+
* position would overflow, before flipping to the opposite edge.
|
|
20
|
+
*
|
|
21
|
+
* **Why `flip-inline flip-block` (or `flip-block flip-inline`) is included:**
|
|
22
|
+
*
|
|
23
|
+
* When a popup is in a viewport corner (e.g. inline-end align-end at the
|
|
24
|
+
* top-right), flipping only the primary axis (`flip-inline`) moves the popup
|
|
25
|
+
* to the left but keeps the same cross-axis span direction (e.g. upward),
|
|
26
|
+
* which is still off-screen. The combined keyword `flip-inline flip-block`
|
|
27
|
+
* mirrors the popup diagonally. It flips BOTH the position-area AND both
|
|
28
|
+
* inline and block margins simultaneously, placing the popup at the
|
|
29
|
+
* diagonally opposite corner where there is always space.
|
|
30
|
+
*
|
|
31
|
+
* Named `position-area` entries (e.g. `inline-start span-block-end`) in
|
|
32
|
+
* `position-try-fallbacks` do NOT update margins, so only the `flip-*`
|
|
33
|
+
* tactic keywords correctly flip the gap margin along with the position.
|
|
34
|
+
*/
|
|
35
|
+
export function placementToTryFallbacks({
|
|
36
|
+
placement
|
|
37
|
+
}) {
|
|
38
|
+
const {
|
|
39
|
+
axis,
|
|
40
|
+
edge,
|
|
41
|
+
align
|
|
42
|
+
} = getPlacement({
|
|
43
|
+
placement
|
|
44
|
+
});
|
|
45
|
+
const crossAxis = axis === 'block' ? 'inline' : 'block';
|
|
46
|
+
const flippedEdge = edge === 'start' ? 'end' : 'start';
|
|
47
|
+
const sameEdge = `${axis}-${edge}`;
|
|
48
|
+
const oppositeEdge = `${axis}-${flippedEdge}`;
|
|
49
|
+
const flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
|
|
50
|
+
const flipCrossKeyword = axis === 'block' ? 'flip-inline' : 'flip-block';
|
|
51
|
+
|
|
52
|
+
// Single algorithm: produce the shift list against any base alignment.
|
|
53
|
+
// For `align: 'start'` the "near" shift expands toward `end`; for
|
|
54
|
+
// `align: 'end'` it expands toward `start`; for `align: 'center'`
|
|
55
|
+
// either order is symmetric (we use end-then-start to match the
|
|
56
|
+
// historical fallback list).
|
|
57
|
+
function shifts(baseAlign, edgeKey) {
|
|
58
|
+
if (baseAlign === 'center') {
|
|
59
|
+
return [`${edgeKey} span-${crossAxis}-end`, `${edgeKey} span-${crossAxis}-start`];
|
|
60
|
+
}
|
|
61
|
+
const near = baseAlign === 'start' ? 'end' : 'start';
|
|
62
|
+
const far = baseAlign === 'start' ? 'start' : 'end';
|
|
63
|
+
return [`${edgeKey} span-${crossAxis}-${near}`, edgeKey, `${edgeKey} span-${crossAxis}-${far}`];
|
|
64
|
+
}
|
|
65
|
+
if (align !== 'center') {
|
|
66
|
+
// Aligned (start/end): try same-edge shifts, then single-axis flip,
|
|
67
|
+
// then diagonal flip for corner overflow, then opposite-edge shifts.
|
|
68
|
+
// Legacy Popper fallback ordering for e.g. bottom-start:
|
|
69
|
+
// [bottom, bottom-end, top-start, top, top-end, auto].
|
|
70
|
+
return [...shifts(align, sameEdge), flipKeyword, `${flipKeyword} ${flipCrossKeyword}`, ...shifts(align, oppositeEdge)].join(', ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Centered: same-edge shifts, single-axis flip, opposite-edge shifts.
|
|
74
|
+
// Diagonal flip is unnecessary because centered placements expand
|
|
75
|
+
// equally in both cross-axis directions.
|
|
76
|
+
return [...shifts('center', sameEdge), flipKeyword, ...shifts('center', oppositeEdge)].join(', ');
|
|
77
|
+
}
|
|
@@ -18,6 +18,8 @@ import rafSchedule from 'raf-schd';
|
|
|
18
18
|
import once from '@atlaskit/ds-lib/once';
|
|
19
19
|
import { computeFallbackPosition } from './anchor-positioning-fallback';
|
|
20
20
|
import { combine } from './combine';
|
|
21
|
+
import { placementToPositionArea } from './placement-to-position-area';
|
|
22
|
+
import { placementToTryFallbacks } from './placement-to-try-fallbacks';
|
|
21
23
|
import { resolveCssLengthToPixels } from './resolve-css-length-to-pixels';
|
|
22
24
|
import { getPlacement } from './resolve-placement';
|
|
23
25
|
import { setStyle } from './set-style';
|
|
@@ -36,122 +38,6 @@ const NoopResizeObserver = class {
|
|
|
36
38
|
disconnect() {}
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
/**
|
|
40
|
-
* Resolves placement options to CSS `position-area` syntax.
|
|
41
|
-
*
|
|
42
|
-
* The `align` field represents the *visual* alignment:
|
|
43
|
-
* - `align: 'start'` means "aligned to the trigger's start edge" (left in LTR for block axis)
|
|
44
|
-
* - `align: 'end'` means "aligned to the trigger's end edge"
|
|
45
|
-
* - `align: 'center'` means centered (single-axis position-area)
|
|
46
|
-
*
|
|
47
|
-
* CSS `position-area` uses `span-*` keywords which indicate the *expansion direction*.
|
|
48
|
-
* Visual start-alignment requires `span-{cross-axis}-end` (expand toward end = start-aligned).
|
|
49
|
-
* This function handles the inversion internally.
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'center' } }) // → 'block-end'
|
|
53
|
-
* placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'start' } }) // → 'block-end span-inline-end'
|
|
54
|
-
* placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'end' } }) // → 'block-end span-inline-start'
|
|
55
|
-
*/
|
|
56
|
-
export function placementToPositionArea({
|
|
57
|
-
placement
|
|
58
|
-
}) {
|
|
59
|
-
const {
|
|
60
|
-
axis,
|
|
61
|
-
edge,
|
|
62
|
-
align
|
|
63
|
-
} = getPlacement({
|
|
64
|
-
placement
|
|
65
|
-
});
|
|
66
|
-
const edgeValue = `${axis}-${edge}`;
|
|
67
|
-
if (align === 'center') {
|
|
68
|
-
return edgeValue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Cross-axis name: block ↔ inline
|
|
72
|
-
const crossAxis = axis === 'block' ? 'inline' : 'block';
|
|
73
|
-
// Invert align → CSS span direction: visual 'start' → span toward 'end'
|
|
74
|
-
const spanDir = align === 'start' ? 'end' : 'start';
|
|
75
|
-
return `${edgeValue} span-${crossAxis}-${spanDir}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Returns `position-try-fallbacks` value based on placement.
|
|
80
|
-
*
|
|
81
|
-
* For centered placements, includes cross-axis aligned position-area
|
|
82
|
-
* values so the browser can shift the popover along the cross-axis
|
|
83
|
-
* when the centered position would overflow the viewport edge.
|
|
84
|
-
*
|
|
85
|
-
* Fallback order for centered placements (e.g. `block-end`):
|
|
86
|
-
* 1. Same edge, start-aligned (shift for near-edge overflow)
|
|
87
|
-
* 2. Same edge, end-aligned (shift for far-edge overflow)
|
|
88
|
-
* 3. Flipped edge, centered
|
|
89
|
-
* 4. Flipped edge, start-aligned
|
|
90
|
-
* 5. Flipped edge, end-aligned
|
|
91
|
-
*
|
|
92
|
-
* For aligned placements (start/end), includes the same-edge centered
|
|
93
|
-
* position as fallbacks so the browser can try centering when the aligned
|
|
94
|
-
* position would overflow, before flipping to the opposite edge.
|
|
95
|
-
*
|
|
96
|
-
* **Why `flip-inline flip-block` (or `flip-block flip-inline`) is included:**
|
|
97
|
-
*
|
|
98
|
-
* When a popup is in a viewport corner (e.g. inline-end align-end at the
|
|
99
|
-
* top-right), flipping only the primary axis (`flip-inline`) moves the popup
|
|
100
|
-
* to the left but keeps the same cross-axis span direction (e.g. upward),
|
|
101
|
-
* which is still off-screen. The combined keyword `flip-inline flip-block`
|
|
102
|
-
* mirrors the popup diagonally. It flips BOTH the position-area AND both
|
|
103
|
-
* inline and block margins simultaneously, placing the popup at the
|
|
104
|
-
* diagonally opposite corner where there is always space.
|
|
105
|
-
*
|
|
106
|
-
* Named `position-area` entries (e.g. `inline-start span-block-end`) in
|
|
107
|
-
* `position-try-fallbacks` do NOT update margins, so only the `flip-*`
|
|
108
|
-
* tactic keywords correctly flip the gap margin along with the position.
|
|
109
|
-
*/
|
|
110
|
-
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
111
|
-
export function placementToTryFallbacks({
|
|
112
|
-
placement
|
|
113
|
-
}) {
|
|
114
|
-
const {
|
|
115
|
-
axis,
|
|
116
|
-
edge,
|
|
117
|
-
align
|
|
118
|
-
} = getPlacement({
|
|
119
|
-
placement
|
|
120
|
-
});
|
|
121
|
-
const crossAxis = axis === 'block' ? 'inline' : 'block';
|
|
122
|
-
const flippedEdge = edge === 'start' ? 'end' : 'start';
|
|
123
|
-
const sameEdge = `${axis}-${edge}`;
|
|
124
|
-
const oppositeEdge = `${axis}-${flippedEdge}`;
|
|
125
|
-
const flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
|
|
126
|
-
const flipCrossKeyword = axis === 'block' ? 'flip-inline' : 'flip-block';
|
|
127
|
-
|
|
128
|
-
// Single algorithm: produce the shift list against any base alignment.
|
|
129
|
-
// For `align: 'start'` the "near" shift expands toward `end`; for
|
|
130
|
-
// `align: 'end'` it expands toward `start`; for `align: 'center'`
|
|
131
|
-
// either order is symmetric (we use end-then-start to match the
|
|
132
|
-
// historical fallback list).
|
|
133
|
-
function shifts(baseAlign, edgeKey) {
|
|
134
|
-
if (baseAlign === 'center') {
|
|
135
|
-
return [`${edgeKey} span-${crossAxis}-end`, `${edgeKey} span-${crossAxis}-start`];
|
|
136
|
-
}
|
|
137
|
-
const near = baseAlign === 'start' ? 'end' : 'start';
|
|
138
|
-
const far = baseAlign === 'start' ? 'start' : 'end';
|
|
139
|
-
return [`${edgeKey} span-${crossAxis}-${near}`, edgeKey, `${edgeKey} span-${crossAxis}-${far}`];
|
|
140
|
-
}
|
|
141
|
-
if (align !== 'center') {
|
|
142
|
-
// Aligned (start/end): try same-edge shifts, then single-axis flip,
|
|
143
|
-
// then diagonal flip for corner overflow, then opposite-edge shifts.
|
|
144
|
-
// Legacy Popper fallback ordering for e.g. bottom-start:
|
|
145
|
-
// [bottom, bottom-end, top-start, top, top-end, auto].
|
|
146
|
-
return [...shifts(align, sameEdge), flipKeyword, `${flipKeyword} ${flipCrossKeyword}`, ...shifts(align, oppositeEdge)].join(', ');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Centered: same-edge shifts, single-axis flip, opposite-edge shifts.
|
|
150
|
-
// Diagonal flip is unnecessary because centered placements expand
|
|
151
|
-
// equally in both cross-axis directions.
|
|
152
|
-
return [...shifts('center', sameEdge), flipKeyword, ...shifts('center', oppositeEdge)].join(', ');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
41
|
/**
|
|
156
42
|
* Returns true when the popover is currently open. Wrapped because the
|
|
157
43
|
* `:popover-open` pseudo-class is not implemented in some test environments
|
|
@@ -43,6 +43,7 @@ export function useAnimatedVisibility({
|
|
|
43
43
|
isOpen,
|
|
44
44
|
animate,
|
|
45
45
|
elementRef,
|
|
46
|
+
onEnterFinish,
|
|
46
47
|
onExitFinish
|
|
47
48
|
}) {
|
|
48
49
|
// Resolve the animation preset and inject its CSS (idempotent).
|
|
@@ -77,13 +78,76 @@ export function useAnimatedVisibility({
|
|
|
77
78
|
setShowChildren(false);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
// Keep
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
// Keep stable refs to the latest callbacks so effects and timeout closures
|
|
82
|
+
// always call the current callback without needing them in dependency arrays.
|
|
83
|
+
const onEnterFinishRef = useRef(onEnterFinish);
|
|
84
|
+
onEnterFinishRef.current = onEnterFinish;
|
|
84
85
|
const onExitFinishRef = useRef(onExitFinish);
|
|
85
86
|
onExitFinishRef.current = onExitFinish;
|
|
86
87
|
|
|
88
|
+
// ── Animated entry effect ──
|
|
89
|
+
//
|
|
90
|
+
// Fires `onEnterFinish` after the entry transition completes.
|
|
91
|
+
// When there is no animation, fires it immediately via a separate effect.
|
|
92
|
+
//
|
|
93
|
+
// A fallback timeout (enterDurationMs + 50ms buffer) ensures it always
|
|
94
|
+
// fires even if `transitionend` is never emitted (e.g. zero-duration,
|
|
95
|
+
// browser quirk, or reduced motion).
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
var _preset$enterDuration;
|
|
98
|
+
if (!isOpen || !showChildren || !willAnimate) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const element = elementRef.current;
|
|
102
|
+
if (!element) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Both the transitionend listener and the fallback timeout are guarded by
|
|
107
|
+
// a shared `hasFired` flag to prevent double-firing. This handles the race
|
|
108
|
+
// where the 350ms transition finishes after the 400ms fallback fires
|
|
109
|
+
// (or vice versa): whichever fires first sets `hasFired = true`, and the
|
|
110
|
+
// second path becomes a no-op.
|
|
111
|
+
let hasFired = false;
|
|
112
|
+
function handleTransitionEnd() {
|
|
113
|
+
var _onEnterFinishRef$cur;
|
|
114
|
+
if (hasFired) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
hasFired = true;
|
|
118
|
+
clearTimeout(fallbackId);
|
|
119
|
+
unbind();
|
|
120
|
+
(_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 ? void 0 : _onEnterFinishRef$cur.call(onEnterFinishRef);
|
|
121
|
+
}
|
|
122
|
+
const fallbackId = setTimeout(handleTransitionEnd, ((_preset$enterDuration = preset === null || preset === void 0 ? void 0 : preset.enterDurationMs) !== null && _preset$enterDuration !== void 0 ? _preset$enterDuration : 0) + 50);
|
|
123
|
+
const unbind = bind(element, {
|
|
124
|
+
type: 'transitionend',
|
|
125
|
+
listener: handleTransitionEnd,
|
|
126
|
+
options: {
|
|
127
|
+
once: true
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
hasFired = true;
|
|
132
|
+
clearTimeout(fallbackId);
|
|
133
|
+
unbind();
|
|
134
|
+
};
|
|
135
|
+
}, [isOpen, showChildren, willAnimate, preset === null || preset === void 0 ? void 0 : preset.enterDurationMs, elementRef]);
|
|
136
|
+
|
|
137
|
+
// ── Non-animated entry callback ──
|
|
138
|
+
//
|
|
139
|
+
// When there is no animation, fire `onEnterFinish` immediately on mount and after `isOpen` goes false->true.
|
|
140
|
+
// The initial value is false to make sure it fires on mount if the current `isOpen` is true.
|
|
141
|
+
const prevIsOpenForEntryRef = useRef(false);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const prevIsOpen = prevIsOpenForEntryRef.current;
|
|
144
|
+
prevIsOpenForEntryRef.current = isOpen;
|
|
145
|
+
if (!prevIsOpen && isOpen && !willAnimate) {
|
|
146
|
+
var _onEnterFinishRef$cur2;
|
|
147
|
+
(_onEnterFinishRef$cur2 = onEnterFinishRef.current) === null || _onEnterFinishRef$cur2 === void 0 ? void 0 : _onEnterFinishRef$cur2.call(onEnterFinishRef);
|
|
148
|
+
}
|
|
149
|
+
}, [isOpen, willAnimate]);
|
|
150
|
+
|
|
87
151
|
// ── Animated exit effect ──
|
|
88
152
|
//
|
|
89
153
|
// Runs when all three conditions are true:
|
|
@@ -108,20 +172,32 @@ export function useAnimatedVisibility({
|
|
|
108
172
|
if (!element) {
|
|
109
173
|
return;
|
|
110
174
|
}
|
|
111
|
-
|
|
175
|
+
|
|
176
|
+
// Both the transitionend listener and the fallback timeout are guarded by
|
|
177
|
+
// a shared `hasFired` flag to prevent double-firing. This handles the race
|
|
178
|
+
// where the 350ms transition finishes after the 400ms fallback fires
|
|
179
|
+
// (or vice versa): whichever fires first sets `hasFired = true`, and the
|
|
180
|
+
// second path becomes a no-op.
|
|
181
|
+
let hasFired = false;
|
|
182
|
+
function handleTransitionEnd() {
|
|
112
183
|
var _onExitFinishRef$curr;
|
|
184
|
+
if (hasFired) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
hasFired = true;
|
|
113
188
|
clearTimeout(fallbackId);
|
|
189
|
+
unbind();
|
|
114
190
|
setShowChildren(false);
|
|
115
191
|
(_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 ? void 0 : _onExitFinishRef$curr.call(onExitFinishRef);
|
|
116
192
|
}
|
|
117
193
|
|
|
118
194
|
// Fallback: unmount even if transitionend never fires
|
|
119
|
-
const fallbackId = setTimeout(
|
|
195
|
+
const fallbackId = setTimeout(handleTransitionEnd, ((_preset$exitDurationM = preset === null || preset === void 0 ? void 0 : preset.exitDurationMs) !== null && _preset$exitDurationM !== void 0 ? _preset$exitDurationM : 0) + 50);
|
|
120
196
|
|
|
121
197
|
// Primary: listen for the CSS transition to complete
|
|
122
198
|
const unbind = bind(element, {
|
|
123
199
|
type: 'transitionend',
|
|
124
|
-
listener:
|
|
200
|
+
listener: handleTransitionEnd,
|
|
125
201
|
options: {
|
|
126
202
|
once: true
|
|
127
203
|
}
|
|
@@ -129,6 +205,7 @@ export function useAnimatedVisibility({
|
|
|
129
205
|
|
|
130
206
|
// Cleanup on re-render or unmount: cancel pending timeout and listener
|
|
131
207
|
return () => {
|
|
208
|
+
hasFired = true;
|
|
132
209
|
clearTimeout(fallbackId);
|
|
133
210
|
unbind();
|
|
134
211
|
};
|
|
@@ -63,6 +63,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
63
63
|
mode: modeProp = 'auto',
|
|
64
64
|
onClose,
|
|
65
65
|
onOpenChange,
|
|
66
|
+
onEnterFinish,
|
|
66
67
|
onExitFinish,
|
|
67
68
|
animate,
|
|
68
69
|
placement,
|
|
@@ -91,6 +92,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
91
92
|
isOpen,
|
|
92
93
|
animate,
|
|
93
94
|
elementRef: ownRef,
|
|
95
|
+
onEnterFinish,
|
|
94
96
|
onExitFinish
|
|
95
97
|
});
|
|
96
98
|
|
|
@@ -3,11 +3,13 @@ import _extends from "@babel/runtime/helpers/extends";
|
|
|
3
3
|
import { ax, ix } from "@compiled/react/runtime";
|
|
4
4
|
import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
|
|
5
5
|
import mergeRefs from '@atlaskit/ds-lib/merge-refs';
|
|
6
|
+
import { prefersReducedMotion } from '../internal/reduced-motion';
|
|
6
7
|
import { roleToAriaHasPopup, shouldFocusIntoPopover } from '../internal/role-types';
|
|
7
8
|
import { useAnchorPosition } from '../internal/use-anchor-position';
|
|
8
9
|
import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
|
|
9
10
|
import { Popover } from '../popover/popover';
|
|
10
|
-
import { useMaybePopupContext } from './popup-context';
|
|
11
|
+
import { useMaybePopupContext } from './use-maybe-popup-context';
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Thin context wrapper that composes `Popover` + `useAnchorPosition`.
|
|
13
15
|
*
|
|
@@ -30,9 +32,11 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
30
32
|
onClose: onCloseProp,
|
|
31
33
|
testId: testIdProp,
|
|
32
34
|
forceFallbackPositioning: forceFallbackPositioningProp,
|
|
33
|
-
id: idProp
|
|
35
|
+
id: idProp,
|
|
36
|
+
onEnterFinish,
|
|
37
|
+
onExitFinish
|
|
34
38
|
}, ref) {
|
|
35
|
-
var _ref,
|
|
39
|
+
var _ref, _ctx$popoverRef;
|
|
36
40
|
// When inside <Popup>, context provides defaults. Props always win.
|
|
37
41
|
const ctx = useMaybePopupContext();
|
|
38
42
|
const triggerRef = triggerRefProp !== null && triggerRefProp !== void 0 ? triggerRefProp : ctx === null || ctx === void 0 ? void 0 : ctx.triggerRef;
|
|
@@ -41,16 +45,47 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
41
45
|
const testId = testIdProp !== null && testIdProp !== void 0 ? testIdProp : ctx === null || ctx === void 0 ? void 0 : ctx.testId;
|
|
42
46
|
const forceFallbackPositioning = forceFallbackPositioningProp !== null && forceFallbackPositioningProp !== void 0 ? forceFallbackPositioningProp : ctx === null || ctx === void 0 ? void 0 : ctx.forceFallbackPositioning;
|
|
43
47
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
const isOpen =
|
|
47
|
-
|
|
48
|
-
// Combine internal state sync with consumer's onOpenChange callback.
|
|
49
|
-
// The Popup compound needs setIsOpen to keep aria-expanded on the trigger
|
|
50
|
-
// in sync, and the consumer may also want to respond to open/close events
|
|
51
|
-
// (e.g. for focus management).
|
|
52
|
-
const setIsOpen = ctx === null || ctx === void 0 ? void 0 : ctx.setIsOpen;
|
|
48
|
+
// isOpen: prop wins, otherwise derive from context popupState.
|
|
49
|
+
// 'animating-open' counts as open so <Popover> stays visible during entry animation.
|
|
50
|
+
const isOpen = isOpenProp !== null && isOpenProp !== void 0 ? isOpenProp : ctx !== null ? ctx.popupState === 'open' || ctx.popupState === 'animating-open' : false;
|
|
51
|
+
const setPopupState = ctx === null || ctx === void 0 ? void 0 : ctx.setPopupState;
|
|
53
52
|
const consumerOnOpenChange = ctx === null || ctx === void 0 ? void 0 : ctx.onOpenChange;
|
|
53
|
+
|
|
54
|
+
// ── TPopupState ──
|
|
55
|
+
//
|
|
56
|
+
// popupState is driven by handleOpenChange (below), which fires on every browser
|
|
57
|
+
// toggle event (showPopover / hidePopover / light-dismiss).
|
|
58
|
+
//
|
|
59
|
+
// Exception — initial mount with isOpen=true (prop-controlled usage):
|
|
60
|
+
// handleOpenChange only fires from the Popover's toggle event listener, which
|
|
61
|
+
// is registered in a useEffect. On the first render, useEffect hasn't run yet,
|
|
62
|
+
// so handleOpenChange never fires. We sync popupState here instead.
|
|
63
|
+
//
|
|
64
|
+
// Transitions when animations are active:
|
|
65
|
+
// closed → animating-open → open (enter)
|
|
66
|
+
// open → animating-closed → closed (exit, via onExitFinish)
|
|
67
|
+
//
|
|
68
|
+
// Without animation: closed ↔ open directly.
|
|
69
|
+
const willAnimate = Boolean(animate) && !prefersReducedMotion();
|
|
70
|
+
const hasMountedRef = useRef(false);
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
if (hasMountedRef.current) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
hasMountedRef.current = true;
|
|
76
|
+
if (isOpen) {
|
|
77
|
+
setPopupState === null || setPopupState === void 0 ? void 0 : setPopupState(willAnimate ? 'animating-open' : 'open');
|
|
78
|
+
}
|
|
79
|
+
}, [isOpen, setPopupState, willAnimate]);
|
|
80
|
+
const handleEnterFinish = useCallback(() => {
|
|
81
|
+
setPopupState === null || setPopupState === void 0 ? void 0 : setPopupState('open');
|
|
82
|
+
onEnterFinish === null || onEnterFinish === void 0 ? void 0 : onEnterFinish();
|
|
83
|
+
}, [setPopupState, onEnterFinish]);
|
|
84
|
+
const handleExitFinish = useCallback(() => {
|
|
85
|
+
setPopupState === null || setPopupState === void 0 ? void 0 : setPopupState('closed');
|
|
86
|
+
onExitFinish === null || onExitFinish === void 0 ? void 0 : onExitFinish();
|
|
87
|
+
}, [setPopupState, onExitFinish]);
|
|
88
|
+
|
|
54
89
|
// Note: onOpenChange composition is handled by handleOpenChange below,
|
|
55
90
|
// which also adds nested-popover focus restoration.
|
|
56
91
|
|
|
@@ -82,7 +117,11 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
82
117
|
// See: notes/architecture/focus-restoration.md
|
|
83
118
|
const handleOpenChange = useCallback(args => {
|
|
84
119
|
// Sync compound state
|
|
85
|
-
|
|
120
|
+
if (args.isOpen) {
|
|
121
|
+
setPopupState === null || setPopupState === void 0 ? void 0 : setPopupState(willAnimate ? 'animating-open' : 'open');
|
|
122
|
+
} else {
|
|
123
|
+
setPopupState === null || setPopupState === void 0 ? void 0 : setPopupState(willAnimate ? 'animating-closed' : 'closed');
|
|
124
|
+
}
|
|
86
125
|
consumerOnOpenChange === null || consumerOnOpenChange === void 0 ? void 0 : consumerOnOpenChange(args);
|
|
87
126
|
|
|
88
127
|
// ── Nested popover focus restoration fallback ──
|
|
@@ -112,7 +151,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
112
151
|
});
|
|
113
152
|
}
|
|
114
153
|
}
|
|
115
|
-
}, [
|
|
154
|
+
}, [consumerOnOpenChange, triggerRef, role, setPopupState, willAnimate]);
|
|
116
155
|
|
|
117
156
|
// ── Anchor positioning ──
|
|
118
157
|
// Compose useAnchorPosition with Popover. The hook positions the popover
|
|
@@ -151,6 +190,8 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
151
190
|
labelledBy,
|
|
152
191
|
onClose: onClose !== null && onClose !== void 0 ? onClose : undefined,
|
|
153
192
|
onOpenChange: handleOpenChange,
|
|
193
|
+
onEnterFinish: handleEnterFinish,
|
|
194
|
+
onExitFinish: handleExitFinish,
|
|
154
195
|
testId,
|
|
155
196
|
children,
|
|
156
197
|
// Standalone consumers can pass an explicit `id` - used by their own
|
|
@@ -1,36 +1,20 @@
|
|
|
1
|
-
import { createContext
|
|
2
|
-
const PopupContext = /*#__PURE__*/createContext(null);
|
|
1
|
+
import { createContext } from 'react';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
* Represents the lifecycle state of the popup.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
|
|
11
|
-
export const PopupProvider = PopupContext.Provider;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Returns the nearest `Popup` context value.
|
|
6
|
+
* - `'closed'`: fully closed, not visible, no animation playing.
|
|
7
|
+
* - `'animating-open'`: entry animation is playing (popup is opening but not yet settled).
|
|
8
|
+
* - `'open'`: fully open, browser popover is showing, entry animation complete.
|
|
9
|
+
* - `'animating-closed'`: exit animation is playing (popup is closing but still visible).
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
19
|
-
export function usePopupContext() {
|
|
20
|
-
const ctx = useContext(PopupContext);
|
|
21
|
-
if (ctx === null) {
|
|
22
|
-
throw new Error('@atlaskit/top-layer: Popup compound components must be used within <Popup>.');
|
|
23
|
-
}
|
|
24
|
-
return ctx;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Returns the popup context if available, or `null` when used outside `<Popup>`.
|
|
11
|
+
* Used to derive `aria-expanded` on the trigger:
|
|
12
|
+
* - `aria-expanded` is `true` in `'animating-open'`, `'open'`, and `'animating-closed'`.
|
|
29
13
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
14
|
+
* This avoids screen readers announcing the popup as closed while it is
|
|
15
|
+
* still visually present on screen during animations, and prevents consumers
|
|
16
|
+
* that conditionally show/hide trigger UI based on `aria-expanded` from hiding
|
|
17
|
+
* the trigger (and thus the popup anchor) mid-animation.
|
|
32
18
|
*/
|
|
33
|
-
|
|
34
|
-
export
|
|
35
|
-
return useContext(PopupContext);
|
|
36
|
-
}
|
|
19
|
+
|
|
20
|
+
export const PopupContext = /*#__PURE__*/createContext(null);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PopupContext } from './popup-context';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* __Popup provider__
|
|
5
|
+
*
|
|
6
|
+
* Provides shared popup state (placement, popoverId, triggerRef,
|
|
7
|
+
* isOpen, onClose) to compound child components such as `Popup.Trigger` and
|
|
8
|
+
* `Popup.Content`.
|
|
9
|
+
*/
|
|
10
|
+
export const PopupProvider = PopupContext.Provider;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react';
|
|
2
|
-
import { usePopupContext } from './popup-context';
|
|
2
|
+
import { usePopupContext } from './use-popup-context';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Props passed to the `TriggerFunction` render-prop child.
|
|
@@ -42,7 +42,7 @@ export function PopupTriggerFunction({
|
|
|
42
42
|
triggerRef,
|
|
43
43
|
popoverRef,
|
|
44
44
|
popoverId,
|
|
45
|
-
|
|
45
|
+
popupState,
|
|
46
46
|
ariaHasPopup
|
|
47
47
|
} = usePopupContext();
|
|
48
48
|
const ref = useCallback(node => {
|
|
@@ -58,13 +58,16 @@ export function PopupTriggerFunction({
|
|
|
58
58
|
}
|
|
59
59
|
}, [popoverRef]);
|
|
60
60
|
const ariaAttributes = useMemo(() => ({
|
|
61
|
-
|
|
61
|
+
// aria-expanded stays true throughout entry and exit animations so screen readers
|
|
62
|
+
// don't announce the popup as closed while it's still visible, and consumers
|
|
63
|
+
// relying on aria-expanded to show/hide trigger UI don't lose the anchor mid-animation.
|
|
64
|
+
'aria-expanded': popupState === 'open' || popupState === 'animating-open' || popupState === 'animating-closed',
|
|
62
65
|
'aria-controls': popoverId,
|
|
63
66
|
'aria-haspopup': ariaHasPopup
|
|
64
|
-
}), [
|
|
67
|
+
}), [popupState, popoverId, ariaHasPopup]);
|
|
65
68
|
return children({
|
|
66
69
|
ref,
|
|
67
|
-
isOpen,
|
|
70
|
+
isOpen: popupState === 'open' || popupState === 'animating-open',
|
|
68
71
|
popoverId,
|
|
69
72
|
toggle,
|
|
70
73
|
ariaAttributes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { isValidElement, useCallback } from 'react';
|
|
2
2
|
import { Slot } from '../internal/slot';
|
|
3
|
-
import { usePopupContext } from './popup-context';
|
|
3
|
+
import { usePopupContext } from './use-popup-context';
|
|
4
|
+
|
|
4
5
|
/**
|
|
5
6
|
* Wraps a single trigger element via `cloneElement`. Attaches aria
|
|
6
7
|
* attributes, the toggle handler, and the anchor-name. `aria-haspopup`
|
|
@@ -19,15 +20,16 @@ export function PopupTrigger({
|
|
|
19
20
|
triggerRef,
|
|
20
21
|
popoverRef,
|
|
21
22
|
popoverId,
|
|
22
|
-
|
|
23
|
+
popupState,
|
|
23
24
|
ariaHasPopup
|
|
24
25
|
} = usePopupContext();
|
|
25
26
|
const childOnClick = /*#__PURE__*/isValidElement(children) ? children.props.onClick : undefined;
|
|
26
27
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
isOpenRef
|
|
28
|
+
// Use a ref to track the latest isOpen value so the click handler
|
|
29
|
+
// does not need isOpen in its dependency array (which would recreate
|
|
30
|
+
// the handler on every open/close, breaking cloneElement identity).
|
|
31
|
+
const isOpenRef = React.useRef(popupState === 'open' || popupState === 'animating-open');
|
|
32
|
+
isOpenRef.current = popupState === 'open' || popupState === 'animating-open';
|
|
31
33
|
const handleClick = useCallback(event => {
|
|
32
34
|
const popoverEl = popoverRef.current;
|
|
33
35
|
if (popoverEl) {
|
|
@@ -47,8 +49,12 @@ export function PopupTrigger({
|
|
|
47
49
|
}, [popoverRef, childOnClick]);
|
|
48
50
|
return /*#__PURE__*/React.createElement(Slot, {
|
|
49
51
|
ref: triggerRef,
|
|
50
|
-
onClick: handleClick
|
|
51
|
-
|
|
52
|
+
onClick: handleClick
|
|
53
|
+
// aria-expanded stays true throughout entry and exit animations so screen readers
|
|
54
|
+
// don't announce the popup as closed while it's still visible, and consumers
|
|
55
|
+
// relying on aria-expanded to show/hide trigger UI don't lose the anchor mid-animation.
|
|
56
|
+
,
|
|
57
|
+
"aria-expanded": popupState === 'open' || popupState === 'animating-open' || popupState === 'animating-closed',
|
|
52
58
|
"aria-controls": popoverId,
|
|
53
59
|
"aria-haspopup": ariaHasPopup
|
|
54
60
|
}, children);
|