@atlaskit/popup 4.25.0 → 4.26.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 +14 -0
- package/__tests__/playwright/top-layer-focus.spec.tsx +274 -0
- package/compositional/popup/package.json +1 -8
- package/compositional/popup-content/package.json +1 -8
- package/compositional/popup-trigger/package.json +1 -8
- package/dist/cjs/compositional/popup-content-top-layer.js +91 -6
- package/dist/cjs/internal/top-layer-bridge.js +14 -9
- package/dist/cjs/popup-top-layer.js +89 -5
- package/dist/es2019/compositional/popup-content-top-layer.js +92 -4
- package/dist/es2019/internal/top-layer-bridge.js +14 -9
- package/dist/es2019/popup-top-layer.js +90 -3
- package/dist/esm/compositional/popup-content-top-layer.js +92 -7
- package/dist/esm/internal/top-layer-bridge.js +14 -9
- package/dist/esm/popup-top-layer.js +90 -6
- package/dist/types/internal/top-layer-bridge.d.ts +14 -0
- package/experimental/package.json +1 -8
- package/package.json +2 -2
- package/popup/package.json +1 -8
- package/types/package.json +1 -8
- package/dist/types-ts4.5/compositional/id-context.d.ts +0 -2
- package/dist/types-ts4.5/compositional/is-inside-popup-context.d.ts +0 -11
- package/dist/types-ts4.5/compositional/is-open-context.d.ts +0 -11
- package/dist/types-ts4.5/compositional/popup-content-top-layer.d.ts +0 -20
- package/dist/types-ts4.5/compositional/popup-content.d.ts +0 -35
- package/dist/types-ts4.5/compositional/popup-trigger.d.ts +0 -13
- package/dist/types-ts4.5/compositional/popup.d.ts +0 -27
- package/dist/types-ts4.5/compositional/role-context.d.ts +0 -2
- package/dist/types-ts4.5/compositional/set-trigger-ref-context.d.ts +0 -2
- package/dist/types-ts4.5/compositional/trigger-ref-context.d.ts +0 -2
- package/dist/types-ts4.5/compositional/trigger-ref-object-context.d.ts +0 -10
- package/dist/types-ts4.5/compositional/use-ensure-is-inside-popup.d.ts +0 -1
- package/dist/types-ts4.5/entry-points/compositional-popup-content.d.ts +0 -2
- package/dist/types-ts4.5/entry-points/compositional-popup-trigger.d.ts +0 -2
- package/dist/types-ts4.5/entry-points/compositional-popup.d.ts +0 -2
- package/dist/types-ts4.5/entry-points/experimental/compositional.d.ts +0 -6
- package/dist/types-ts4.5/entry-points/popup.d.ts +0 -1
- package/dist/types-ts4.5/entry-points/types.d.ts +0 -1
- package/dist/types-ts4.5/index.d.ts +0 -4
- package/dist/types-ts4.5/internal/top-layer-bridge.d.ts +0 -29
- package/dist/types-ts4.5/popper-wrapper.d.ts +0 -3
- package/dist/types-ts4.5/popup-top-layer.d.ts +0 -16
- package/dist/types-ts4.5/popup.d.ts +0 -7
- package/dist/types-ts4.5/reposition-on-update.d.ts +0 -3
- package/dist/types-ts4.5/types.d.ts +0 -357
- package/dist/types-ts4.5/use-appearance.d.ts +0 -15
- package/dist/types-ts4.5/use-close-manager.d.ts +0 -2
- package/dist/types-ts4.5/use-focus-manager.d.ts +0 -2
- package/dist/types-ts4.5/use-get-memoized-merged-trigger-ref-new.d.ts +0 -9
- package/dist/types-ts4.5/use-get-memoized-merged-trigger-ref.d.ts +0 -3
- package/dist/types-ts4.5/utils/is-element-interactive.d.ts +0 -1
- package/dist/types-ts4.5/utils/use-animation-frame.d.ts +0 -5
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import _extends from "@babel/runtime/helpers/extends";
|
|
3
3
|
import "./popup-content-top-layer.compiled.css";
|
|
4
4
|
import * as React from 'react';
|
|
5
|
-
import { useCallback, useContext, useMemo, useRef
|
|
5
|
+
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
6
6
|
import { ax, ix } from '@compiled/react/runtime';
|
|
7
7
|
import noop from '@atlaskit/ds-lib/noop';
|
|
8
|
+
import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
|
|
8
9
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
9
10
|
import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
|
|
10
11
|
import { createPopoverCloseEvent, Popover } from '@atlaskit/top-layer/popover';
|
|
@@ -76,9 +77,26 @@ export function PopupContentTopLayer({
|
|
|
76
77
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
77
78
|
shouldDisableGpuAcceleration: _shouldDisableGpuAcceleration
|
|
78
79
|
}) {
|
|
79
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
82
|
+
*
|
|
83
|
+
* The legacy compositional `PopupContent` surfaced
|
|
84
|
+
* `setInitialFocusRef` on the content children render prop with a
|
|
85
|
+
* `Dispatch<SetStateAction<HTMLElement | null>>` signature so it
|
|
86
|
+
* could be passed as a React `ref` callback. We keep the same shape
|
|
87
|
+
* for API compatibility, but store the value in a ref rather than
|
|
88
|
+
* component state. Flipping state on every open caused a needless
|
|
89
|
+
* re-render and, more importantly, raced the initial-focus effect
|
|
90
|
+
* below. The ref is read in the open-side effect to override the
|
|
91
|
+
* role-based default focus chosen by `useInitialFocus` inside the
|
|
92
|
+
* underlying `Popover`.
|
|
93
|
+
*/
|
|
94
|
+
const consumerInitialFocusRef = useRef(null);
|
|
95
|
+
const setInitialFocusRef = useCallback(value => {
|
|
96
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
97
|
+
}, []);
|
|
80
98
|
|
|
81
|
-
//
|
|
99
|
+
// Placement conversion
|
|
82
100
|
// Legacy `offset` is the popper `[along, away]` tuple; `fromLegacyPlacement`
|
|
83
101
|
// folds it into the new `placement.offset` shape.
|
|
84
102
|
const topLayerPlacement = useMemo(() => fromLegacyPlacement({
|
|
@@ -104,7 +122,7 @@ export function PopupContentTopLayer({
|
|
|
104
122
|
update: noop,
|
|
105
123
|
onClose,
|
|
106
124
|
setInitialFocusRef
|
|
107
|
-
}), [isOpen, onClose]);
|
|
125
|
+
}), [isOpen, onClose, setInitialFocusRef]);
|
|
108
126
|
|
|
109
127
|
// ── Role mapping ──
|
|
110
128
|
const roleProps = useRoleProps({
|
|
@@ -115,6 +133,49 @@ export function PopupContentTopLayer({
|
|
|
115
133
|
const popoverRef = useRef(null);
|
|
116
134
|
const anchorRef = useContext(TriggerRefObjectContext);
|
|
117
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Override the role-based initial focus chosen by the underlying
|
|
138
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
139
|
+
*
|
|
140
|
+
* React flushes child effects before parent effects, so by the time
|
|
141
|
+
* this effect runs the role-based default focus has already moved.
|
|
142
|
+
* We then apply the legacy `Popup` contract:
|
|
143
|
+
*
|
|
144
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
145
|
+
* (escape hatch wins).
|
|
146
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
147
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
148
|
+
* movement, even for roles like `dialog`.
|
|
149
|
+
*
|
|
150
|
+
* When neither applies the role-based default from the underlying
|
|
151
|
+
* `Popover` stays in place.
|
|
152
|
+
*/
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!isOpen) {
|
|
155
|
+
/**
|
|
156
|
+
* Clear the consumer-supplied focus target on close so a
|
|
157
|
+
* subsequent open does not silently inherit a stale ref from
|
|
158
|
+
* the previous open cycle. The next open will re-collect a
|
|
159
|
+
* target only if the content render prop re-wires
|
|
160
|
+
* `setInitialFocusRef`.
|
|
161
|
+
*/
|
|
162
|
+
consumerInitialFocusRef.current = null;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const consumerTarget = consumerInitialFocusRef.current;
|
|
166
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
167
|
+
consumerTarget.focus();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!autoFocus) {
|
|
171
|
+
var _anchorRef$current;
|
|
172
|
+
const trigger = (_anchorRef$current = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current) !== null && _anchorRef$current !== void 0 ? _anchorRef$current : null;
|
|
173
|
+
if (trigger && trigger.isConnected) {
|
|
174
|
+
trigger.focus();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}, [isOpen, autoFocus, anchorRef]);
|
|
178
|
+
|
|
118
179
|
// `isOpen` is included so the anchor positioning effect re-runs when
|
|
119
180
|
// the Popover host element is unmounted/remounted across open cycles.
|
|
120
181
|
useAnchorPosition({
|
|
@@ -130,6 +191,33 @@ export function PopupContentTopLayer({
|
|
|
130
191
|
isOpen
|
|
131
192
|
});
|
|
132
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Register with the open-layer observer as a `popup` while open.
|
|
196
|
+
*
|
|
197
|
+
* The legacy compositional `PopupContent` registered every open
|
|
198
|
+
* popup with the observer regardless of role, and product surfaces
|
|
199
|
+
* such as the side-nav panel splitter rely on this signal to detect
|
|
200
|
+
* open popups via `getCount({ type: 'popup' }) > 0`. The underlying
|
|
201
|
+
* top-layer `Popover` only self-registers when its role is one of
|
|
202
|
+
* `menu | listbox | dialog | alertdialog | tree | grid`, which would
|
|
203
|
+
* miss role-less popups and silently regress the splitter.
|
|
204
|
+
*
|
|
205
|
+
* Registering here unconditionally preserves the legacy contract.
|
|
206
|
+
* When the role does opt the `Popover` into observer registration
|
|
207
|
+
* the count is incremented twice, which is safe because every known
|
|
208
|
+
* consumer treats the count as a boolean (`> 0` / `=== 0`).
|
|
209
|
+
*/
|
|
210
|
+
const handleObserverClose = useCallback(() => {
|
|
211
|
+
onClose === null || onClose === void 0 ? void 0 : onClose(createPopoverCloseEvent({
|
|
212
|
+
reason: 'programmatic'
|
|
213
|
+
}));
|
|
214
|
+
}, [onClose]);
|
|
215
|
+
useNotifyOpenLayerObserver({
|
|
216
|
+
type: 'popup',
|
|
217
|
+
isOpen,
|
|
218
|
+
onClose: handleObserverClose
|
|
219
|
+
});
|
|
220
|
+
|
|
133
221
|
// Narrow to ForwardRefExoticComponent so JSX accepts the ref prop.
|
|
134
222
|
// All popupComponent implementations use forwardRef per the PopupComponentProps contract.
|
|
135
223
|
const Container = PopupContainer;
|
|
@@ -2,6 +2,13 @@ import { useMemo } from 'react';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Props shape passed from legacy `role` / `label` / `titleId` into `@atlaskit/top-layer` `Popup.Content`.
|
|
5
|
+
*
|
|
6
|
+
* The `{ role: undefined }` variant covers the legacy contract where a
|
|
7
|
+
* consumer omits `role` to opt out of any popup semantics (and the
|
|
8
|
+
* associated role-based initial-focus movement). Legacy `Popup` left the
|
|
9
|
+
* popup element role-less in that case; we preserve that behaviour here
|
|
10
|
+
* rather than defaulting to `'dialog'`, which would move focus into the
|
|
11
|
+
* popup on open.
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
14
|
/**
|
|
@@ -11,6 +18,10 @@ import { useMemo } from 'react';
|
|
|
11
18
|
* `Popup.Content` enforces at the type level that dialog/alertdialog/menu
|
|
12
19
|
* roles must have `label` or `labelledBy`. This hook bridges the legacy
|
|
13
20
|
* flat-prop API (`role`, `label`, `titleId`) to that shape.
|
|
21
|
+
*
|
|
22
|
+
* When `role` is omitted entirely, the result carries `role: undefined`
|
|
23
|
+
* (no implicit default). This matches the legacy `Popup` contract and
|
|
24
|
+
* keeps the underlying `Popover` from running role-based initial focus.
|
|
14
25
|
*/
|
|
15
26
|
export function useRoleProps({
|
|
16
27
|
role,
|
|
@@ -36,16 +47,10 @@ export function useRoleProps({
|
|
|
36
47
|
label
|
|
37
48
|
};
|
|
38
49
|
}
|
|
39
|
-
//
|
|
40
|
-
if (titleId) {
|
|
41
|
-
return {
|
|
42
|
-
role: 'dialog',
|
|
43
|
-
labelledBy: titleId
|
|
44
|
-
};
|
|
45
|
-
}
|
|
50
|
+
// No role supplied: preserve the legacy "no popup semantics" contract.
|
|
46
51
|
return {
|
|
47
|
-
role:
|
|
48
|
-
label
|
|
52
|
+
role: undefined,
|
|
53
|
+
label
|
|
49
54
|
};
|
|
50
55
|
}, [role, label, titleId]);
|
|
51
56
|
}
|
|
@@ -3,8 +3,9 @@ import _extends from "@babel/runtime/helpers/extends";
|
|
|
3
3
|
import "./popup-top-layer.compiled.css";
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import { ax, ix } from "@compiled/react/runtime";
|
|
6
|
-
import { Fragment, memo, useCallback, useMemo, useRef
|
|
6
|
+
import { Fragment, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
7
7
|
import noop from '@atlaskit/ds-lib/noop';
|
|
8
|
+
import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
|
|
8
9
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
9
10
|
import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
|
|
10
11
|
import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
|
|
@@ -92,7 +93,66 @@ export const PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer({
|
|
|
92
93
|
const triggerRef = useRef(null);
|
|
93
94
|
const popoverRef = useRef(null);
|
|
94
95
|
const popupContainerRef = useRef(null);
|
|
95
|
-
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
99
|
+
*
|
|
100
|
+
* The legacy `Popup` surfaced `setInitialFocusRef` on the content
|
|
101
|
+
* render prop with a `Dispatch<SetStateAction<HTMLElement | null>>`
|
|
102
|
+
* signature so it could be passed as a React `ref` callback. We
|
|
103
|
+
* keep the same shape for API compatibility, but store the value in
|
|
104
|
+
* a ref rather than component state. Flipping state on every open
|
|
105
|
+
* caused a needless re-render and, more importantly, raced the
|
|
106
|
+
* initial-focus effect below. The ref is read in the open-side
|
|
107
|
+
* effect to override the role-based default focus chosen by
|
|
108
|
+
* `useInitialFocus` inside the underlying `Popover`.
|
|
109
|
+
*/
|
|
110
|
+
const consumerInitialFocusRef = useRef(null);
|
|
111
|
+
const setInitialFocusRef = useCallback(value => {
|
|
112
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Override the role-based initial focus chosen by the underlying
|
|
117
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
118
|
+
*
|
|
119
|
+
* React flushes child effects before parent effects, so by the time
|
|
120
|
+
* this effect runs the role-based default focus has already moved.
|
|
121
|
+
* We then apply the legacy `Popup` contract:
|
|
122
|
+
*
|
|
123
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
124
|
+
* (escape hatch wins).
|
|
125
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
126
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
127
|
+
* movement, even for roles like `dialog`.
|
|
128
|
+
*
|
|
129
|
+
* When neither applies the role-based default from the underlying
|
|
130
|
+
* `Popover` stays in place.
|
|
131
|
+
*/
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!isOpen) {
|
|
134
|
+
/**
|
|
135
|
+
* Clear the consumer-supplied focus target on close so a
|
|
136
|
+
* subsequent open does not silently inherit a stale ref from
|
|
137
|
+
* the previous open cycle. The next open will re-collect a
|
|
138
|
+
* target only if the content render prop re-wires
|
|
139
|
+
* `setInitialFocusRef`.
|
|
140
|
+
*/
|
|
141
|
+
consumerInitialFocusRef.current = null;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const consumerTarget = consumerInitialFocusRef.current;
|
|
145
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
146
|
+
consumerTarget.focus();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!autoFocus) {
|
|
150
|
+
const trigger = triggerRef.current;
|
|
151
|
+
if (trigger && trigger.isConnected) {
|
|
152
|
+
trigger.focus();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}, [isOpen, autoFocus]);
|
|
96
156
|
const popoverId = usePopoverId();
|
|
97
157
|
|
|
98
158
|
// Placement conversion.
|
|
@@ -115,6 +175,33 @@ export const PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer({
|
|
|
115
175
|
isOpen
|
|
116
176
|
});
|
|
117
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Register with the open-layer observer as a `popup` while open.
|
|
180
|
+
*
|
|
181
|
+
* The legacy `Popup` registered every open popup with the observer
|
|
182
|
+
* regardless of role, and product surfaces such as the side-nav
|
|
183
|
+
* panel splitter rely on this signal to detect open popups via
|
|
184
|
+
* `getCount({ type: 'popup' }) > 0`. The underlying top-layer
|
|
185
|
+
* `Popover` only self-registers when its role is one of
|
|
186
|
+
* `menu | listbox | dialog | alertdialog | tree | grid`, which would
|
|
187
|
+
* miss role-less popups and silently regress the splitter.
|
|
188
|
+
*
|
|
189
|
+
* Registering here unconditionally preserves the legacy contract.
|
|
190
|
+
* When the role does opt the `Popover` into observer registration
|
|
191
|
+
* the count is incremented twice, which is safe because every known
|
|
192
|
+
* consumer treats the count as a boolean (`> 0` / `=== 0`).
|
|
193
|
+
*/
|
|
194
|
+
const handleObserverClose = useCallback(() => {
|
|
195
|
+
onClose === null || onClose === void 0 ? void 0 : onClose(createPopoverCloseEvent({
|
|
196
|
+
reason: 'programmatic'
|
|
197
|
+
}));
|
|
198
|
+
}, [onClose]);
|
|
199
|
+
useNotifyOpenLayerObserver({
|
|
200
|
+
type: 'popup',
|
|
201
|
+
isOpen,
|
|
202
|
+
onClose: handleObserverClose
|
|
203
|
+
});
|
|
204
|
+
|
|
118
205
|
// onClose bridge.
|
|
119
206
|
// Translates top-layer's { reason: TPopoverCloseReason } into legacy
|
|
120
207
|
// onClose(event, currentLevel?) by synthesizing DOM events.
|
|
@@ -142,7 +229,7 @@ export const PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer({
|
|
|
142
229
|
update: noop,
|
|
143
230
|
onClose,
|
|
144
231
|
setInitialFocusRef
|
|
145
|
-
}), [isOpen, onClose]);
|
|
232
|
+
}), [isOpen, onClose, setInitialFocusRef]);
|
|
146
233
|
|
|
147
234
|
// Role mapping.
|
|
148
235
|
// Build the role/label props for Popover.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* popup-content-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
|
|
2
2
|
import _extends from "@babel/runtime/helpers/extends";
|
|
3
|
-
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
4
3
|
import "./popup-content-top-layer.compiled.css";
|
|
5
4
|
import * as React from 'react';
|
|
6
|
-
import { useCallback, useContext, useMemo, useRef
|
|
5
|
+
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
7
6
|
import { ax, ix } from '@compiled/react/runtime';
|
|
8
7
|
import noop from '@atlaskit/ds-lib/noop';
|
|
8
|
+
import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
|
|
9
9
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
10
10
|
import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
|
|
11
11
|
import { createPopoverCloseEvent, Popover } from '@atlaskit/top-layer/popover';
|
|
@@ -57,11 +57,26 @@ export function PopupContentTopLayer(_ref) {
|
|
|
57
57
|
_shouldFlip = _ref.shouldFlip,
|
|
58
58
|
_appearance = _ref.appearance,
|
|
59
59
|
_shouldDisableGpuAcceleration = _ref.shouldDisableGpuAcceleration;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
62
|
+
*
|
|
63
|
+
* The legacy compositional `PopupContent` surfaced
|
|
64
|
+
* `setInitialFocusRef` on the content children render prop with a
|
|
65
|
+
* `Dispatch<SetStateAction<HTMLElement | null>>` signature so it
|
|
66
|
+
* could be passed as a React `ref` callback. We keep the same shape
|
|
67
|
+
* for API compatibility, but store the value in a ref rather than
|
|
68
|
+
* component state. Flipping state on every open caused a needless
|
|
69
|
+
* re-render and, more importantly, raced the initial-focus effect
|
|
70
|
+
* below. The ref is read in the open-side effect to override the
|
|
71
|
+
* role-based default focus chosen by `useInitialFocus` inside the
|
|
72
|
+
* underlying `Popover`.
|
|
73
|
+
*/
|
|
74
|
+
var consumerInitialFocusRef = useRef(null);
|
|
75
|
+
var setInitialFocusRef = useCallback(function (value) {
|
|
76
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
77
|
+
}, []);
|
|
63
78
|
|
|
64
|
-
//
|
|
79
|
+
// Placement conversion
|
|
65
80
|
// Legacy `offset` is the popper `[along, away]` tuple; `fromLegacyPlacement`
|
|
66
81
|
// folds it into the new `placement.offset` shape.
|
|
67
82
|
var topLayerPlacement = useMemo(function () {
|
|
@@ -90,7 +105,7 @@ export function PopupContentTopLayer(_ref) {
|
|
|
90
105
|
onClose: onClose,
|
|
91
106
|
setInitialFocusRef: setInitialFocusRef
|
|
92
107
|
};
|
|
93
|
-
}, [isOpen, onClose]);
|
|
108
|
+
}, [isOpen, onClose, setInitialFocusRef]);
|
|
94
109
|
|
|
95
110
|
// ── Role mapping ──
|
|
96
111
|
var roleProps = useRoleProps({
|
|
@@ -101,6 +116,49 @@ export function PopupContentTopLayer(_ref) {
|
|
|
101
116
|
var popoverRef = useRef(null);
|
|
102
117
|
var anchorRef = useContext(TriggerRefObjectContext);
|
|
103
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Override the role-based initial focus chosen by the underlying
|
|
121
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
122
|
+
*
|
|
123
|
+
* React flushes child effects before parent effects, so by the time
|
|
124
|
+
* this effect runs the role-based default focus has already moved.
|
|
125
|
+
* We then apply the legacy `Popup` contract:
|
|
126
|
+
*
|
|
127
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
128
|
+
* (escape hatch wins).
|
|
129
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
130
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
131
|
+
* movement, even for roles like `dialog`.
|
|
132
|
+
*
|
|
133
|
+
* When neither applies the role-based default from the underlying
|
|
134
|
+
* `Popover` stays in place.
|
|
135
|
+
*/
|
|
136
|
+
useEffect(function () {
|
|
137
|
+
if (!isOpen) {
|
|
138
|
+
/**
|
|
139
|
+
* Clear the consumer-supplied focus target on close so a
|
|
140
|
+
* subsequent open does not silently inherit a stale ref from
|
|
141
|
+
* the previous open cycle. The next open will re-collect a
|
|
142
|
+
* target only if the content render prop re-wires
|
|
143
|
+
* `setInitialFocusRef`.
|
|
144
|
+
*/
|
|
145
|
+
consumerInitialFocusRef.current = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
var consumerTarget = consumerInitialFocusRef.current;
|
|
149
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
150
|
+
consumerTarget.focus();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!autoFocus) {
|
|
154
|
+
var _anchorRef$current;
|
|
155
|
+
var trigger = (_anchorRef$current = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current) !== null && _anchorRef$current !== void 0 ? _anchorRef$current : null;
|
|
156
|
+
if (trigger && trigger.isConnected) {
|
|
157
|
+
trigger.focus();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}, [isOpen, autoFocus, anchorRef]);
|
|
161
|
+
|
|
104
162
|
// `isOpen` is included so the anchor positioning effect re-runs when
|
|
105
163
|
// the Popover host element is unmounted/remounted across open cycles.
|
|
106
164
|
useAnchorPosition({
|
|
@@ -116,6 +174,33 @@ export function PopupContentTopLayer(_ref) {
|
|
|
116
174
|
isOpen: isOpen
|
|
117
175
|
});
|
|
118
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Register with the open-layer observer as a `popup` while open.
|
|
179
|
+
*
|
|
180
|
+
* The legacy compositional `PopupContent` registered every open
|
|
181
|
+
* popup with the observer regardless of role, and product surfaces
|
|
182
|
+
* such as the side-nav panel splitter rely on this signal to detect
|
|
183
|
+
* open popups via `getCount({ type: 'popup' }) > 0`. The underlying
|
|
184
|
+
* top-layer `Popover` only self-registers when its role is one of
|
|
185
|
+
* `menu | listbox | dialog | alertdialog | tree | grid`, which would
|
|
186
|
+
* miss role-less popups and silently regress the splitter.
|
|
187
|
+
*
|
|
188
|
+
* Registering here unconditionally preserves the legacy contract.
|
|
189
|
+
* When the role does opt the `Popover` into observer registration
|
|
190
|
+
* the count is incremented twice, which is safe because every known
|
|
191
|
+
* consumer treats the count as a boolean (`> 0` / `=== 0`).
|
|
192
|
+
*/
|
|
193
|
+
var handleObserverClose = useCallback(function () {
|
|
194
|
+
onClose === null || onClose === void 0 || onClose(createPopoverCloseEvent({
|
|
195
|
+
reason: 'programmatic'
|
|
196
|
+
}));
|
|
197
|
+
}, [onClose]);
|
|
198
|
+
useNotifyOpenLayerObserver({
|
|
199
|
+
type: 'popup',
|
|
200
|
+
isOpen: isOpen,
|
|
201
|
+
onClose: handleObserverClose
|
|
202
|
+
});
|
|
203
|
+
|
|
119
204
|
// Narrow to ForwardRefExoticComponent so JSX accepts the ref prop.
|
|
120
205
|
// All popupComponent implementations use forwardRef per the PopupComponentProps contract.
|
|
121
206
|
var Container = PopupContainer;
|
|
@@ -2,6 +2,13 @@ import { useMemo } from 'react';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Props shape passed from legacy `role` / `label` / `titleId` into `@atlaskit/top-layer` `Popup.Content`.
|
|
5
|
+
*
|
|
6
|
+
* The `{ role: undefined }` variant covers the legacy contract where a
|
|
7
|
+
* consumer omits `role` to opt out of any popup semantics (and the
|
|
8
|
+
* associated role-based initial-focus movement). Legacy `Popup` left the
|
|
9
|
+
* popup element role-less in that case; we preserve that behaviour here
|
|
10
|
+
* rather than defaulting to `'dialog'`, which would move focus into the
|
|
11
|
+
* popup on open.
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
14
|
/**
|
|
@@ -11,6 +18,10 @@ import { useMemo } from 'react';
|
|
|
11
18
|
* `Popup.Content` enforces at the type level that dialog/alertdialog/menu
|
|
12
19
|
* roles must have `label` or `labelledBy`. This hook bridges the legacy
|
|
13
20
|
* flat-prop API (`role`, `label`, `titleId`) to that shape.
|
|
21
|
+
*
|
|
22
|
+
* When `role` is omitted entirely, the result carries `role: undefined`
|
|
23
|
+
* (no implicit default). This matches the legacy `Popup` contract and
|
|
24
|
+
* keeps the underlying `Popover` from running role-based initial focus.
|
|
14
25
|
*/
|
|
15
26
|
export function useRoleProps(_ref) {
|
|
16
27
|
var role = _ref.role,
|
|
@@ -35,16 +46,10 @@ export function useRoleProps(_ref) {
|
|
|
35
46
|
label: label
|
|
36
47
|
};
|
|
37
48
|
}
|
|
38
|
-
//
|
|
39
|
-
if (titleId) {
|
|
40
|
-
return {
|
|
41
|
-
role: 'dialog',
|
|
42
|
-
labelledBy: titleId
|
|
43
|
-
};
|
|
44
|
-
}
|
|
49
|
+
// No role supplied: preserve the legacy "no popup semantics" contract.
|
|
45
50
|
return {
|
|
46
|
-
role:
|
|
47
|
-
label: label
|
|
51
|
+
role: undefined,
|
|
52
|
+
label: label
|
|
48
53
|
};
|
|
49
54
|
}, [role, label, titleId]);
|
|
50
55
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* popup-top-layer.tsx generated by @compiled/babel-plugin v0.39.1 */
|
|
2
2
|
import _extends from "@babel/runtime/helpers/extends";
|
|
3
|
-
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
4
3
|
import "./popup-top-layer.compiled.css";
|
|
5
4
|
import * as React from 'react';
|
|
6
5
|
import { ax, ix } from "@compiled/react/runtime";
|
|
7
|
-
import { Fragment, memo, useCallback, useMemo, useRef
|
|
6
|
+
import { Fragment, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
8
7
|
import noop from '@atlaskit/ds-lib/noop';
|
|
8
|
+
import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
|
|
9
9
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
10
10
|
import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
|
|
11
11
|
import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
|
|
@@ -71,9 +71,66 @@ export var PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer(_ref) {
|
|
|
71
71
|
var triggerRef = useRef(null);
|
|
72
72
|
var popoverRef = useRef(null);
|
|
73
73
|
var popupContainerRef = useRef(null);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
77
|
+
*
|
|
78
|
+
* The legacy `Popup` surfaced `setInitialFocusRef` on the content
|
|
79
|
+
* render prop with a `Dispatch<SetStateAction<HTMLElement | null>>`
|
|
80
|
+
* signature so it could be passed as a React `ref` callback. We
|
|
81
|
+
* keep the same shape for API compatibility, but store the value in
|
|
82
|
+
* a ref rather than component state. Flipping state on every open
|
|
83
|
+
* caused a needless re-render and, more importantly, raced the
|
|
84
|
+
* initial-focus effect below. The ref is read in the open-side
|
|
85
|
+
* effect to override the role-based default focus chosen by
|
|
86
|
+
* `useInitialFocus` inside the underlying `Popover`.
|
|
87
|
+
*/
|
|
88
|
+
var consumerInitialFocusRef = useRef(null);
|
|
89
|
+
var setInitialFocusRef = useCallback(function (value) {
|
|
90
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Override the role-based initial focus chosen by the underlying
|
|
95
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
96
|
+
*
|
|
97
|
+
* React flushes child effects before parent effects, so by the time
|
|
98
|
+
* this effect runs the role-based default focus has already moved.
|
|
99
|
+
* We then apply the legacy `Popup` contract:
|
|
100
|
+
*
|
|
101
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
102
|
+
* (escape hatch wins).
|
|
103
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
104
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
105
|
+
* movement, even for roles like `dialog`.
|
|
106
|
+
*
|
|
107
|
+
* When neither applies the role-based default from the underlying
|
|
108
|
+
* `Popover` stays in place.
|
|
109
|
+
*/
|
|
110
|
+
useEffect(function () {
|
|
111
|
+
if (!isOpen) {
|
|
112
|
+
/**
|
|
113
|
+
* Clear the consumer-supplied focus target on close so a
|
|
114
|
+
* subsequent open does not silently inherit a stale ref from
|
|
115
|
+
* the previous open cycle. The next open will re-collect a
|
|
116
|
+
* target only if the content render prop re-wires
|
|
117
|
+
* `setInitialFocusRef`.
|
|
118
|
+
*/
|
|
119
|
+
consumerInitialFocusRef.current = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
var consumerTarget = consumerInitialFocusRef.current;
|
|
123
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
124
|
+
consumerTarget.focus();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!autoFocus) {
|
|
128
|
+
var _trigger = triggerRef.current;
|
|
129
|
+
if (_trigger && _trigger.isConnected) {
|
|
130
|
+
_trigger.focus();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, [isOpen, autoFocus]);
|
|
77
134
|
var popoverId = usePopoverId();
|
|
78
135
|
|
|
79
136
|
// Placement conversion.
|
|
@@ -98,6 +155,33 @@ export var PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer(_ref) {
|
|
|
98
155
|
isOpen: isOpen
|
|
99
156
|
});
|
|
100
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Register with the open-layer observer as a `popup` while open.
|
|
160
|
+
*
|
|
161
|
+
* The legacy `Popup` registered every open popup with the observer
|
|
162
|
+
* regardless of role, and product surfaces such as the side-nav
|
|
163
|
+
* panel splitter rely on this signal to detect open popups via
|
|
164
|
+
* `getCount({ type: 'popup' }) > 0`. The underlying top-layer
|
|
165
|
+
* `Popover` only self-registers when its role is one of
|
|
166
|
+
* `menu | listbox | dialog | alertdialog | tree | grid`, which would
|
|
167
|
+
* miss role-less popups and silently regress the splitter.
|
|
168
|
+
*
|
|
169
|
+
* Registering here unconditionally preserves the legacy contract.
|
|
170
|
+
* When the role does opt the `Popover` into observer registration
|
|
171
|
+
* the count is incremented twice, which is safe because every known
|
|
172
|
+
* consumer treats the count as a boolean (`> 0` / `=== 0`).
|
|
173
|
+
*/
|
|
174
|
+
var handleObserverClose = useCallback(function () {
|
|
175
|
+
onClose === null || onClose === void 0 || onClose(createPopoverCloseEvent({
|
|
176
|
+
reason: 'programmatic'
|
|
177
|
+
}));
|
|
178
|
+
}, [onClose]);
|
|
179
|
+
useNotifyOpenLayerObserver({
|
|
180
|
+
type: 'popup',
|
|
181
|
+
isOpen: isOpen,
|
|
182
|
+
onClose: handleObserverClose
|
|
183
|
+
});
|
|
184
|
+
|
|
101
185
|
// onClose bridge.
|
|
102
186
|
// Translates top-layer's { reason: TPopoverCloseReason } into legacy
|
|
103
187
|
// onClose(event, currentLevel?) by synthesizing DOM events.
|
|
@@ -126,7 +210,7 @@ export var PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer(_ref) {
|
|
|
126
210
|
onClose: onClose,
|
|
127
211
|
setInitialFocusRef: setInitialFocusRef
|
|
128
212
|
};
|
|
129
|
-
}, [isOpen, onClose]);
|
|
213
|
+
}, [isOpen, onClose, setInitialFocusRef]);
|
|
130
214
|
|
|
131
215
|
// Role mapping.
|
|
132
216
|
// Build the role/label props for Popover.
|
|
@@ -2,6 +2,13 @@ type TRestrictedDialogRole = 'dialog' | 'alertdialog' | 'menu';
|
|
|
2
2
|
type TWcagSupplementalRole = 'tooltip' | 'listbox' | 'tree' | 'grid' | 'note' | 'status' | 'alert' | 'log';
|
|
3
3
|
/**
|
|
4
4
|
* Props shape passed from legacy `role` / `label` / `titleId` into `@atlaskit/top-layer` `Popup.Content`.
|
|
5
|
+
*
|
|
6
|
+
* The `{ role: undefined }` variant covers the legacy contract where a
|
|
7
|
+
* consumer omits `role` to opt out of any popup semantics (and the
|
|
8
|
+
* associated role-based initial-focus movement). Legacy `Popup` left the
|
|
9
|
+
* popup element role-less in that case; we preserve that behaviour here
|
|
10
|
+
* rather than defaulting to `'dialog'`, which would move focus into the
|
|
11
|
+
* popup on open.
|
|
5
12
|
*/
|
|
6
13
|
export type TUseRolePropsResult = {
|
|
7
14
|
role: TRestrictedDialogRole;
|
|
@@ -12,6 +19,9 @@ export type TUseRolePropsResult = {
|
|
|
12
19
|
} | {
|
|
13
20
|
role: TWcagSupplementalRole;
|
|
14
21
|
label: string | undefined;
|
|
22
|
+
} | {
|
|
23
|
+
role: undefined;
|
|
24
|
+
label: string | undefined;
|
|
15
25
|
};
|
|
16
26
|
/**
|
|
17
27
|
* Maps legacy popup role/label/titleId props to the discriminated union
|
|
@@ -20,6 +30,10 @@ export type TUseRolePropsResult = {
|
|
|
20
30
|
* `Popup.Content` enforces at the type level that dialog/alertdialog/menu
|
|
21
31
|
* roles must have `label` or `labelledBy`. This hook bridges the legacy
|
|
22
32
|
* flat-prop API (`role`, `label`, `titleId`) to that shape.
|
|
33
|
+
*
|
|
34
|
+
* When `role` is omitted entirely, the result carries `role: undefined`
|
|
35
|
+
* (no implicit default). This matches the legacy `Popup` contract and
|
|
36
|
+
* keeps the underlying `Popover` from running role-based initial focus.
|
|
23
37
|
*/
|
|
24
38
|
export declare function useRoleProps({ role, label, titleId, }: {
|
|
25
39
|
role: string | undefined;
|
|
@@ -6,12 +6,5 @@
|
|
|
6
6
|
"sideEffects": [
|
|
7
7
|
"**/*.compiled.css"
|
|
8
8
|
],
|
|
9
|
-
"types": "../dist/types/entry-points/experimental/compositional.d.ts"
|
|
10
|
-
"typesVersions": {
|
|
11
|
-
">=4.5 <5.9": {
|
|
12
|
-
"*": [
|
|
13
|
-
"../dist/types-ts4.5/entry-points/experimental/compositional.d.ts"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
}
|
|
9
|
+
"types": "../dist/types/entry-points/experimental/compositional.d.ts"
|
|
17
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/popup",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.26.0",
|
|
4
4
|
"description": "A popup displays brief content in an overlay.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@atlaskit/portal": "^5.5.0",
|
|
49
49
|
"@atlaskit/primitives": "^19.0.0",
|
|
50
50
|
"@atlaskit/tokens": "^13.4.0",
|
|
51
|
-
"@atlaskit/top-layer": "^0.
|
|
51
|
+
"@atlaskit/top-layer": "^0.18.0",
|
|
52
52
|
"@babel/runtime": "^7.0.0",
|
|
53
53
|
"@compiled/react": "^0.20.0",
|
|
54
54
|
"bind-event-listener": "^3.0.0",
|