@atlaskit/top-layer 0.11.0 → 0.12.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 +16 -0
- package/dist/cjs/animations/presets.js +5 -0
- 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 +53 -13
- package/dist/cjs/popup/popup-context.js +17 -0
- package/dist/cjs/popup/popup-trigger-function.js +7 -4
- package/dist/cjs/popup/popup-trigger.js +12 -7
- package/dist/cjs/popup/popup.js +5 -5
- package/dist/es2019/animations/presets.js +5 -0
- 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 +53 -13
- package/dist/es2019/popup/popup-context.js +18 -0
- package/dist/es2019/popup/popup-trigger-function.js +7 -4
- package/dist/es2019/popup/popup-trigger.js +12 -7
- package/dist/es2019/popup/popup.js +3 -3
- package/dist/esm/animations/presets.js +5 -0
- 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 +53 -13
- package/dist/esm/popup/popup-context.js +18 -0
- package/dist/esm/popup/popup-trigger-function.js +7 -4
- package/dist/esm/popup/popup-trigger.js +12 -7
- package/dist/esm/popup/popup.js +5 -5
- package/dist/types/animations/types.d.ts +5 -1
- 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 +24 -4
- package/dist/types/popup/types.d.ts +12 -0
- package/dist/types-ts4.5/animations/types.d.ts +5 -1
- 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 +24 -4
- package/dist/types-ts4.5/popup/types.d.ts +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @atlaskit/top-layer
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`cc76d2e733b71`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/cc76d2e733b71) - -
|
|
8
|
+
The prop `onEnterFinish` has been added to `Popup.Content` and `Popover`, for lifecycle
|
|
9
|
+
coordination after the entry animation completes.
|
|
10
|
+
- The prop `onExitFinish` has been added to `Popup.Content`, for lifecycle coordination after the
|
|
11
|
+
exit animation completes. `Popover` already exposed this prop.
|
|
12
|
+
|
|
13
|
+
- Updates the popup trigger's `aria-expanded` attribute, so it stays `"true"` for the full
|
|
14
|
+
duration of the popup's open and close animations, only going `false` after the exit animation
|
|
15
|
+
completes.
|
|
16
|
+
|
|
17
|
+
- Internal refactor to the popup context to use a string union for state.
|
|
18
|
+
|
|
3
19
|
## 0.11.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -86,6 +86,7 @@ function slideAndFade(options) {
|
|
|
86
86
|
distance: distance
|
|
87
87
|
});
|
|
88
88
|
},
|
|
89
|
+
enterDurationMs: 350,
|
|
89
90
|
exitDurationMs: 175
|
|
90
91
|
};
|
|
91
92
|
}
|
|
@@ -111,6 +112,7 @@ function fade() {
|
|
|
111
112
|
return {
|
|
112
113
|
name: 'fade',
|
|
113
114
|
css: FADE_CSS,
|
|
115
|
+
enterDurationMs: 350,
|
|
114
116
|
exitDurationMs: 175
|
|
115
117
|
};
|
|
116
118
|
}
|
|
@@ -136,6 +138,7 @@ function scaleAndFade() {
|
|
|
136
138
|
return {
|
|
137
139
|
name: 'scale-and-fade',
|
|
138
140
|
css: SCALE_AND_FADE_CSS,
|
|
141
|
+
enterDurationMs: 350,
|
|
139
142
|
exitDurationMs: 175
|
|
140
143
|
};
|
|
141
144
|
}
|
|
@@ -175,6 +178,7 @@ function dialogSlideUpAndFade(options) {
|
|
|
175
178
|
return {
|
|
176
179
|
name: distance === 12 ? 'slide-up-and-fade' : "slide-up-and-fade-".concat(distance),
|
|
177
180
|
css: distance === 12 ? DIALOG_SLIDE_UP_AND_FADE_CSS : DIALOG_SLIDE_UP_AND_FADE_CSS.replace(/12px/g, "".concat(distance, "px")),
|
|
181
|
+
enterDurationMs: 350,
|
|
178
182
|
exitDurationMs: 175
|
|
179
183
|
};
|
|
180
184
|
}
|
|
@@ -203,6 +207,7 @@ function dialogFade() {
|
|
|
203
207
|
return {
|
|
204
208
|
name: 'fade',
|
|
205
209
|
css: DIALOG_FADE_CSS,
|
|
210
|
+
enterDurationMs: 350,
|
|
206
211
|
exitDurationMs: 175
|
|
207
212
|
};
|
|
208
213
|
}
|
|
@@ -51,6 +51,7 @@ function useAnimatedVisibility(_ref) {
|
|
|
51
51
|
var isOpen = _ref.isOpen,
|
|
52
52
|
animate = _ref.animate,
|
|
53
53
|
elementRef = _ref.elementRef,
|
|
54
|
+
onEnterFinish = _ref.onEnterFinish,
|
|
54
55
|
onExitFinish = _ref.onExitFinish;
|
|
55
56
|
// Resolve the animation preset and inject its CSS (idempotent).
|
|
56
57
|
// Returns `null` when `animate` is falsy.
|
|
@@ -87,13 +88,76 @@ function useAnimatedVisibility(_ref) {
|
|
|
87
88
|
setShowChildren(false);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
// Keep
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
// Keep stable refs to the latest callbacks so effects and timeout closures
|
|
92
|
+
// always call the current callback without needing them in dependency arrays.
|
|
93
|
+
var onEnterFinishRef = (0, _react.useRef)(onEnterFinish);
|
|
94
|
+
onEnterFinishRef.current = onEnterFinish;
|
|
94
95
|
var onExitFinishRef = (0, _react.useRef)(onExitFinish);
|
|
95
96
|
onExitFinishRef.current = onExitFinish;
|
|
96
97
|
|
|
98
|
+
// ── Animated entry effect ──
|
|
99
|
+
//
|
|
100
|
+
// Fires `onEnterFinish` after the entry transition completes.
|
|
101
|
+
// When there is no animation, fires it immediately via a separate effect.
|
|
102
|
+
//
|
|
103
|
+
// A fallback timeout (enterDurationMs + 50ms buffer) ensures it always
|
|
104
|
+
// fires even if `transitionend` is never emitted (e.g. zero-duration,
|
|
105
|
+
// browser quirk, or reduced motion).
|
|
106
|
+
(0, _react.useEffect)(function () {
|
|
107
|
+
var _preset$enterDuration;
|
|
108
|
+
if (!isOpen || !showChildren || !willAnimate) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
var element = elementRef.current;
|
|
112
|
+
if (!element) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Both the transitionend listener and the fallback timeout are guarded by
|
|
117
|
+
// a shared `hasFired` flag to prevent double-firing. This handles the race
|
|
118
|
+
// where the 350ms transition finishes after the 400ms fallback fires
|
|
119
|
+
// (or vice versa): whichever fires first sets `hasFired = true`, and the
|
|
120
|
+
// second path becomes a no-op.
|
|
121
|
+
var hasFired = false;
|
|
122
|
+
function handleTransitionEnd() {
|
|
123
|
+
var _onEnterFinishRef$cur;
|
|
124
|
+
if (hasFired) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
hasFired = true;
|
|
128
|
+
clearTimeout(fallbackId);
|
|
129
|
+
unbind();
|
|
130
|
+
(_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 || _onEnterFinishRef$cur.call(onEnterFinishRef);
|
|
131
|
+
}
|
|
132
|
+
var fallbackId = setTimeout(handleTransitionEnd, ((_preset$enterDuration = preset === null || preset === void 0 ? void 0 : preset.enterDurationMs) !== null && _preset$enterDuration !== void 0 ? _preset$enterDuration : 0) + 50);
|
|
133
|
+
var unbind = (0, _bindEventListener.bind)(element, {
|
|
134
|
+
type: 'transitionend',
|
|
135
|
+
listener: handleTransitionEnd,
|
|
136
|
+
options: {
|
|
137
|
+
once: true
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return function () {
|
|
141
|
+
hasFired = true;
|
|
142
|
+
clearTimeout(fallbackId);
|
|
143
|
+
unbind();
|
|
144
|
+
};
|
|
145
|
+
}, [isOpen, showChildren, willAnimate, preset === null || preset === void 0 ? void 0 : preset.enterDurationMs, elementRef]);
|
|
146
|
+
|
|
147
|
+
// ── Non-animated entry callback ──
|
|
148
|
+
//
|
|
149
|
+
// When there is no animation, fire `onEnterFinish` immediately on mount and after `isOpen` goes false->true.
|
|
150
|
+
// The initial value is false to make sure it fires on mount if the current `isOpen` is true.
|
|
151
|
+
var prevIsOpenForEntryRef = (0, _react.useRef)(false);
|
|
152
|
+
(0, _react.useEffect)(function () {
|
|
153
|
+
var prevIsOpen = prevIsOpenForEntryRef.current;
|
|
154
|
+
prevIsOpenForEntryRef.current = isOpen;
|
|
155
|
+
if (!prevIsOpen && isOpen && !willAnimate) {
|
|
156
|
+
var _onEnterFinishRef$cur2;
|
|
157
|
+
(_onEnterFinishRef$cur2 = onEnterFinishRef.current) === null || _onEnterFinishRef$cur2 === void 0 || _onEnterFinishRef$cur2.call(onEnterFinishRef);
|
|
158
|
+
}
|
|
159
|
+
}, [isOpen, willAnimate]);
|
|
160
|
+
|
|
97
161
|
// ── Animated exit effect ──
|
|
98
162
|
//
|
|
99
163
|
// Runs when all three conditions are true:
|
|
@@ -118,20 +182,32 @@ function useAnimatedVisibility(_ref) {
|
|
|
118
182
|
if (!element) {
|
|
119
183
|
return;
|
|
120
184
|
}
|
|
121
|
-
|
|
185
|
+
|
|
186
|
+
// Both the transitionend listener and the fallback timeout are guarded by
|
|
187
|
+
// a shared `hasFired` flag to prevent double-firing. This handles the race
|
|
188
|
+
// where the 350ms transition finishes after the 400ms fallback fires
|
|
189
|
+
// (or vice versa): whichever fires first sets `hasFired = true`, and the
|
|
190
|
+
// second path becomes a no-op.
|
|
191
|
+
var hasFired = false;
|
|
192
|
+
function handleTransitionEnd() {
|
|
122
193
|
var _onExitFinishRef$curr;
|
|
194
|
+
if (hasFired) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
hasFired = true;
|
|
123
198
|
clearTimeout(fallbackId);
|
|
199
|
+
unbind();
|
|
124
200
|
setShowChildren(false);
|
|
125
201
|
(_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 || _onExitFinishRef$curr.call(onExitFinishRef);
|
|
126
202
|
}
|
|
127
203
|
|
|
128
204
|
// Fallback: unmount even if transitionend never fires
|
|
129
|
-
var fallbackId = setTimeout(
|
|
205
|
+
var fallbackId = setTimeout(handleTransitionEnd, ((_preset$exitDurationM = preset === null || preset === void 0 ? void 0 : preset.exitDurationMs) !== null && _preset$exitDurationM !== void 0 ? _preset$exitDurationM : 0) + 50);
|
|
130
206
|
|
|
131
207
|
// Primary: listen for the CSS transition to complete
|
|
132
208
|
var unbind = (0, _bindEventListener.bind)(element, {
|
|
133
209
|
type: 'transitionend',
|
|
134
|
-
listener:
|
|
210
|
+
listener: handleTransitionEnd,
|
|
135
211
|
options: {
|
|
136
212
|
once: true
|
|
137
213
|
}
|
|
@@ -139,6 +215,7 @@ function useAnimatedVisibility(_ref) {
|
|
|
139
215
|
|
|
140
216
|
// Cleanup on re-render or unmount: cancel pending timeout and listener
|
|
141
217
|
return function () {
|
|
218
|
+
hasFired = true;
|
|
142
219
|
clearTimeout(fallbackId);
|
|
143
220
|
unbind();
|
|
144
221
|
};
|
|
@@ -75,6 +75,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
75
75
|
modeProp = _ref$mode === void 0 ? 'auto' : _ref$mode,
|
|
76
76
|
onClose = _ref.onClose,
|
|
77
77
|
onOpenChange = _ref.onOpenChange,
|
|
78
|
+
onEnterFinish = _ref.onEnterFinish,
|
|
78
79
|
onExitFinish = _ref.onExitFinish,
|
|
79
80
|
animate = _ref.animate,
|
|
80
81
|
placement = _ref.placement,
|
|
@@ -98,6 +99,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
98
99
|
isOpen: isOpen,
|
|
99
100
|
animate: animate,
|
|
100
101
|
elementRef: ownRef,
|
|
102
|
+
onEnterFinish: onEnterFinish,
|
|
101
103
|
onExitFinish: onExitFinish
|
|
102
104
|
}),
|
|
103
105
|
showChildren = _useAnimatedVisibilit.showChildren,
|
|
@@ -11,6 +11,7 @@ var _runtime = require("@compiled/react/runtime");
|
|
|
11
11
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
|
12
12
|
var _react = _interopRequireWildcard(require("react"));
|
|
13
13
|
var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs"));
|
|
14
|
+
var _reducedMotion = require("../internal/reduced-motion");
|
|
14
15
|
var _roleTypes = require("../internal/role-types");
|
|
15
16
|
var _useAnchorPosition = require("../internal/use-anchor-position");
|
|
16
17
|
var _useWidthFromAnchor = require("../internal/use-width-from-anchor");
|
|
@@ -25,7 +26,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
25
26
|
* use `Popover` + `useAnchorPosition` directly.
|
|
26
27
|
*/
|
|
27
28
|
var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(function PopupContent(_ref, ref) {
|
|
28
|
-
var _ref2,
|
|
29
|
+
var _ref2, _ctx$popoverRef;
|
|
29
30
|
var children = _ref.children,
|
|
30
31
|
role = _ref.role,
|
|
31
32
|
label = _ref.label,
|
|
@@ -41,7 +42,9 @@ var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(fu
|
|
|
41
42
|
onCloseProp = _ref.onClose,
|
|
42
43
|
testIdProp = _ref.testId,
|
|
43
44
|
forceFallbackPositioningProp = _ref.forceFallbackPositioning,
|
|
44
|
-
idProp = _ref.id
|
|
45
|
+
idProp = _ref.id,
|
|
46
|
+
onEnterFinish = _ref.onEnterFinish,
|
|
47
|
+
onExitFinish = _ref.onExitFinish;
|
|
45
48
|
// When inside <Popup>, context provides defaults. Props always win.
|
|
46
49
|
var ctx = (0, _popupContext.useMaybePopupContext)();
|
|
47
50
|
var triggerRef = triggerRefProp !== null && triggerRefProp !== void 0 ? triggerRefProp : ctx === null || ctx === void 0 ? void 0 : ctx.triggerRef;
|
|
@@ -50,16 +53,47 @@ var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(fu
|
|
|
50
53
|
var testId = testIdProp !== null && testIdProp !== void 0 ? testIdProp : ctx === null || ctx === void 0 ? void 0 : ctx.testId;
|
|
51
54
|
var forceFallbackPositioning = forceFallbackPositioningProp !== null && forceFallbackPositioningProp !== void 0 ? forceFallbackPositioningProp : ctx === null || ctx === void 0 ? void 0 : ctx.forceFallbackPositioning;
|
|
52
55
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
var isOpen =
|
|
56
|
-
|
|
57
|
-
// Combine internal state sync with consumer's onOpenChange callback.
|
|
58
|
-
// The Popup compound needs setIsOpen to keep aria-expanded on the trigger
|
|
59
|
-
// in sync, and the consumer may also want to respond to open/close events
|
|
60
|
-
// (e.g. for focus management).
|
|
61
|
-
var setIsOpen = ctx === null || ctx === void 0 ? void 0 : ctx.setIsOpen;
|
|
56
|
+
// isOpen: prop wins, otherwise derive from context popupState.
|
|
57
|
+
// 'animating-open' counts as open so <Popover> stays visible during entry animation.
|
|
58
|
+
var isOpen = isOpenProp !== null && isOpenProp !== void 0 ? isOpenProp : ctx !== null ? ctx.popupState === 'open' || ctx.popupState === 'animating-open' : false;
|
|
59
|
+
var setPopupState = ctx === null || ctx === void 0 ? void 0 : ctx.setPopupState;
|
|
62
60
|
var consumerOnOpenChange = ctx === null || ctx === void 0 ? void 0 : ctx.onOpenChange;
|
|
61
|
+
|
|
62
|
+
// ── TPopupState ──
|
|
63
|
+
//
|
|
64
|
+
// popupState is driven by handleOpenChange (below), which fires on every browser
|
|
65
|
+
// toggle event (showPopover / hidePopover / light-dismiss).
|
|
66
|
+
//
|
|
67
|
+
// Exception — initial mount with isOpen=true (prop-controlled usage):
|
|
68
|
+
// handleOpenChange only fires from the Popover's toggle event listener, which
|
|
69
|
+
// is registered in a useEffect. On the first render, useEffect hasn't run yet,
|
|
70
|
+
// so handleOpenChange never fires. We sync popupState here instead.
|
|
71
|
+
//
|
|
72
|
+
// Transitions when animations are active:
|
|
73
|
+
// closed → animating-open → open (enter)
|
|
74
|
+
// open → animating-closed → closed (exit, via onExitFinish)
|
|
75
|
+
//
|
|
76
|
+
// Without animation: closed ↔ open directly.
|
|
77
|
+
var willAnimate = Boolean(animate) && !(0, _reducedMotion.prefersReducedMotion)();
|
|
78
|
+
var hasMountedRef = (0, _react.useRef)(false);
|
|
79
|
+
(0, _react.useLayoutEffect)(function () {
|
|
80
|
+
if (hasMountedRef.current) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
hasMountedRef.current = true;
|
|
84
|
+
if (isOpen) {
|
|
85
|
+
setPopupState === null || setPopupState === void 0 || setPopupState(willAnimate ? 'animating-open' : 'open');
|
|
86
|
+
}
|
|
87
|
+
}, [isOpen, setPopupState, willAnimate]);
|
|
88
|
+
var handleEnterFinish = (0, _react.useCallback)(function () {
|
|
89
|
+
setPopupState === null || setPopupState === void 0 || setPopupState('open');
|
|
90
|
+
onEnterFinish === null || onEnterFinish === void 0 || onEnterFinish();
|
|
91
|
+
}, [setPopupState, onEnterFinish]);
|
|
92
|
+
var handleExitFinish = (0, _react.useCallback)(function () {
|
|
93
|
+
setPopupState === null || setPopupState === void 0 || setPopupState('closed');
|
|
94
|
+
onExitFinish === null || onExitFinish === void 0 || onExitFinish();
|
|
95
|
+
}, [setPopupState, onExitFinish]);
|
|
96
|
+
|
|
63
97
|
// Note: onOpenChange composition is handled by handleOpenChange below,
|
|
64
98
|
// which also adds nested-popover focus restoration.
|
|
65
99
|
|
|
@@ -91,7 +125,11 @@ var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(fu
|
|
|
91
125
|
// See: notes/architecture/focus-restoration.md
|
|
92
126
|
var handleOpenChange = (0, _react.useCallback)(function (args) {
|
|
93
127
|
// Sync compound state
|
|
94
|
-
|
|
128
|
+
if (args.isOpen) {
|
|
129
|
+
setPopupState === null || setPopupState === void 0 || setPopupState(willAnimate ? 'animating-open' : 'open');
|
|
130
|
+
} else {
|
|
131
|
+
setPopupState === null || setPopupState === void 0 || setPopupState(willAnimate ? 'animating-closed' : 'closed');
|
|
132
|
+
}
|
|
95
133
|
consumerOnOpenChange === null || consumerOnOpenChange === void 0 || consumerOnOpenChange(args);
|
|
96
134
|
|
|
97
135
|
// ── Nested popover focus restoration fallback ──
|
|
@@ -121,7 +159,7 @@ var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(fu
|
|
|
121
159
|
});
|
|
122
160
|
}
|
|
123
161
|
}
|
|
124
|
-
}, [
|
|
162
|
+
}, [consumerOnOpenChange, triggerRef, role, setPopupState, willAnimate]);
|
|
125
163
|
|
|
126
164
|
// ── Anchor positioning ──
|
|
127
165
|
// Compose useAnchorPosition with Popover. The hook positions the popover
|
|
@@ -160,6 +198,8 @@ var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(fu
|
|
|
160
198
|
labelledBy: labelledBy,
|
|
161
199
|
onClose: onClose !== null && onClose !== void 0 ? onClose : undefined,
|
|
162
200
|
onOpenChange: handleOpenChange,
|
|
201
|
+
onEnterFinish: handleEnterFinish,
|
|
202
|
+
onExitFinish: handleExitFinish,
|
|
163
203
|
testId: testId,
|
|
164
204
|
children: children,
|
|
165
205
|
// Standalone consumers can pass an explicit `id` - used by their own
|
|
@@ -7,6 +7,23 @@ exports.PopupProvider = void 0;
|
|
|
7
7
|
exports.useMaybePopupContext = useMaybePopupContext;
|
|
8
8
|
exports.usePopupContext = usePopupContext;
|
|
9
9
|
var _react = require("react");
|
|
10
|
+
/**
|
|
11
|
+
* Represents the lifecycle state of the popup.
|
|
12
|
+
*
|
|
13
|
+
* - `'closed'`: fully closed, not visible, no animation playing.
|
|
14
|
+
* - `'animating-open'`: entry animation is playing (popup is opening but not yet settled).
|
|
15
|
+
* - `'open'`: fully open, browser popover is showing, entry animation complete.
|
|
16
|
+
* - `'animating-closed'`: exit animation is playing (popup is closing but still visible).
|
|
17
|
+
*
|
|
18
|
+
* Used to derive `aria-expanded` on the trigger:
|
|
19
|
+
* - `aria-expanded` is `true` in `'animating-open'`, `'open'`, and `'animating-closed'`.
|
|
20
|
+
*
|
|
21
|
+
* This avoids screen readers announcing the popup as closed while it is
|
|
22
|
+
* still visually present on screen during animations, and prevents consumers
|
|
23
|
+
* that conditionally show/hide trigger UI based on `aria-expanded` from hiding
|
|
24
|
+
* the trigger (and thus the popup anchor) mid-animation.
|
|
25
|
+
*/
|
|
26
|
+
|
|
10
27
|
var PopupContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
11
28
|
|
|
12
29
|
/**
|
|
@@ -46,7 +46,7 @@ function PopupTriggerFunction(_ref) {
|
|
|
46
46
|
triggerRef = _usePopupContext.triggerRef,
|
|
47
47
|
popoverRef = _usePopupContext.popoverRef,
|
|
48
48
|
popoverId = _usePopupContext.popoverId,
|
|
49
|
-
|
|
49
|
+
popupState = _usePopupContext.popupState,
|
|
50
50
|
ariaHasPopup = _usePopupContext.ariaHasPopup;
|
|
51
51
|
var ref = (0, _react.useCallback)(function (node) {
|
|
52
52
|
triggerRef.current = node;
|
|
@@ -62,14 +62,17 @@ function PopupTriggerFunction(_ref) {
|
|
|
62
62
|
}, [popoverRef]);
|
|
63
63
|
var ariaAttributes = (0, _react.useMemo)(function () {
|
|
64
64
|
return {
|
|
65
|
-
|
|
65
|
+
// aria-expanded stays true throughout entry and exit animations so screen readers
|
|
66
|
+
// don't announce the popup as closed while it's still visible, and consumers
|
|
67
|
+
// relying on aria-expanded to show/hide trigger UI don't lose the anchor mid-animation.
|
|
68
|
+
'aria-expanded': popupState === 'open' || popupState === 'animating-open' || popupState === 'animating-closed',
|
|
66
69
|
'aria-controls': popoverId,
|
|
67
70
|
'aria-haspopup': ariaHasPopup
|
|
68
71
|
};
|
|
69
|
-
}, [
|
|
72
|
+
}, [popupState, popoverId, ariaHasPopup]);
|
|
70
73
|
return children({
|
|
71
74
|
ref: ref,
|
|
72
|
-
isOpen:
|
|
75
|
+
isOpen: popupState === 'open' || popupState === 'animating-open',
|
|
73
76
|
popoverId: popoverId,
|
|
74
77
|
toggle: toggle,
|
|
75
78
|
ariaAttributes: ariaAttributes
|
|
@@ -26,14 +26,15 @@ function PopupTrigger(_ref) {
|
|
|
26
26
|
triggerRef = _usePopupContext.triggerRef,
|
|
27
27
|
popoverRef = _usePopupContext.popoverRef,
|
|
28
28
|
popoverId = _usePopupContext.popoverId,
|
|
29
|
-
|
|
29
|
+
popupState = _usePopupContext.popupState,
|
|
30
30
|
ariaHasPopup = _usePopupContext.ariaHasPopup;
|
|
31
31
|
var childOnClick = /*#__PURE__*/(0, _react.isValidElement)(children) ? children.props.onClick : undefined;
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
isOpenRef
|
|
33
|
+
// Use a ref to track the latest isOpen value so the click handler
|
|
34
|
+
// does not need isOpen in its dependency array (which would recreate
|
|
35
|
+
// the handler on every open/close, breaking cloneElement identity).
|
|
36
|
+
var isOpenRef = _react.default.useRef(popupState === 'open' || popupState === 'animating-open');
|
|
37
|
+
isOpenRef.current = popupState === 'open' || popupState === 'animating-open';
|
|
37
38
|
var handleClick = (0, _react.useCallback)(function (event) {
|
|
38
39
|
var popoverEl = popoverRef.current;
|
|
39
40
|
if (popoverEl) {
|
|
@@ -53,8 +54,12 @@ function PopupTrigger(_ref) {
|
|
|
53
54
|
}, [popoverRef, childOnClick]);
|
|
54
55
|
return /*#__PURE__*/_react.default.createElement(_slot.Slot, {
|
|
55
56
|
ref: triggerRef,
|
|
56
|
-
onClick: handleClick
|
|
57
|
-
|
|
57
|
+
onClick: handleClick
|
|
58
|
+
// aria-expanded stays true throughout entry and exit animations so screen readers
|
|
59
|
+
// don't announce the popup as closed while it's still visible, and consumers
|
|
60
|
+
// relying on aria-expanded to show/hide trigger UI don't lose the anchor mid-animation.
|
|
61
|
+
,
|
|
62
|
+
"aria-expanded": popupState === 'open' || popupState === 'animating-open' || popupState === 'animating-closed',
|
|
58
63
|
"aria-controls": popoverId,
|
|
59
64
|
"aria-haspopup": ariaHasPopup
|
|
60
65
|
}, children);
|
package/dist/cjs/popup/popup.js
CHANGED
|
@@ -44,10 +44,10 @@ function PopupRoot(props) {
|
|
|
44
44
|
var popoverId = "popover-".concat(sanitizedId);
|
|
45
45
|
var triggerRef = (0, _react.useRef)(null);
|
|
46
46
|
var popoverRef = (0, _react.useRef)(null);
|
|
47
|
-
var _useState = (0, _react.useState)(
|
|
47
|
+
var _useState = (0, _react.useState)('closed'),
|
|
48
48
|
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
popupState = _useState2[0],
|
|
50
|
+
setPopupState = _useState2[1];
|
|
51
51
|
// Initial seed before `Popup.Content`'s layout effect resolves the
|
|
52
52
|
// real role on first paint.
|
|
53
53
|
var _useState3 = (0, _react.useState)('dialog'),
|
|
@@ -66,8 +66,8 @@ function PopupRoot(props) {
|
|
|
66
66
|
onClose: onClose ? handleClose : null,
|
|
67
67
|
triggerRef: triggerRef,
|
|
68
68
|
popoverRef: popoverRef,
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
popupState: popupState,
|
|
70
|
+
setPopupState: setPopupState,
|
|
71
71
|
onOpenChange: onOpenChange,
|
|
72
72
|
mode: mode,
|
|
73
73
|
ariaHasPopup: ariaHasPopup,
|
|
@@ -118,6 +118,7 @@ export function slideAndFade(options) {
|
|
|
118
118
|
placement,
|
|
119
119
|
distance
|
|
120
120
|
}),
|
|
121
|
+
enterDurationMs: 350,
|
|
121
122
|
exitDurationMs: 175
|
|
122
123
|
};
|
|
123
124
|
}
|
|
@@ -169,6 +170,7 @@ export function fade() {
|
|
|
169
170
|
return {
|
|
170
171
|
name: 'fade',
|
|
171
172
|
css: FADE_CSS,
|
|
173
|
+
enterDurationMs: 350,
|
|
172
174
|
exitDurationMs: 175
|
|
173
175
|
};
|
|
174
176
|
}
|
|
@@ -224,6 +226,7 @@ export function scaleAndFade() {
|
|
|
224
226
|
return {
|
|
225
227
|
name: 'scale-and-fade',
|
|
226
228
|
css: SCALE_AND_FADE_CSS,
|
|
229
|
+
enterDurationMs: 350,
|
|
227
230
|
exitDurationMs: 175
|
|
228
231
|
};
|
|
229
232
|
}
|
|
@@ -314,6 +317,7 @@ export function dialogSlideUpAndFade(options) {
|
|
|
314
317
|
return {
|
|
315
318
|
name: distance === 12 ? 'slide-up-and-fade' : `slide-up-and-fade-${distance}`,
|
|
316
319
|
css: distance === 12 ? DIALOG_SLIDE_UP_AND_FADE_CSS : DIALOG_SLIDE_UP_AND_FADE_CSS.replace(/12px/g, `${distance}px`),
|
|
320
|
+
enterDurationMs: 350,
|
|
317
321
|
exitDurationMs: 175
|
|
318
322
|
};
|
|
319
323
|
}
|
|
@@ -389,6 +393,7 @@ export function dialogFade() {
|
|
|
389
393
|
return {
|
|
390
394
|
name: 'fade',
|
|
391
395
|
css: DIALOG_FADE_CSS,
|
|
396
|
+
enterDurationMs: 350,
|
|
392
397
|
exitDurationMs: 175
|
|
393
398
|
};
|
|
394
399
|
}
|
|
@@ -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
|
|