@atlaskit/popup 4.25.1 → 5.0.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 +39 -0
- package/__tests__/playwright/top-layer-focus.spec.tsx +274 -0
- package/dist/cjs/compositional/popup-content-top-layer.js +63 -6
- package/dist/cjs/internal/top-layer-bridge.js +14 -9
- package/dist/cjs/popup-top-layer.js +61 -5
- package/dist/es2019/compositional/popup-content-top-layer.js +64 -4
- package/dist/es2019/internal/top-layer-bridge.js +14 -9
- package/dist/es2019/popup-top-layer.js +62 -3
- package/dist/esm/compositional/popup-content-top-layer.js +64 -7
- package/dist/esm/internal/top-layer-bridge.js +14 -9
- package/dist/esm/popup-top-layer.js +62 -6
- package/dist/types/internal/top-layer-bridge.d.ts +14 -0
- package/package.json +22 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @atlaskit/popup
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [`f2dc9097319f0`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/f2dc9097319f0) - ###
|
|
8
|
+
Dropped support for _legacy_ Typescript 4 types. **Typescript 5 is now the new minimum**.
|
|
9
|
+
|
|
10
|
+
Removes the `typesVersions` property and `dist/types-ts4.5` directory from the dist.
|
|
11
|
+
|
|
12
|
+
Types are now exclusively via the `"types": "dist/types/index.d.ts"` property.
|
|
13
|
+
|
|
14
|
+
```diff
|
|
15
|
+
- "typesVersions": {
|
|
16
|
+
- ">=4.5 <4.9": {
|
|
17
|
+
- "*": [
|
|
18
|
+
- "dist/types-ts4.5/*",
|
|
19
|
+
- "dist/types-ts4.5/index.d.ts"
|
|
20
|
+
- ]
|
|
21
|
+
- }
|
|
22
|
+
- },
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- [`2e61d5c836f8f`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2e61d5c836f8f) -
|
|
28
|
+
Internal cleanup of open-layer observer registration in the top-layer Popup adapters.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
|
|
34
|
+
## 4.26.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- [`032135d12186b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/032135d12186b) -
|
|
39
|
+
Top-layer: fix initial focus regressions on the top-layer adapters for `@atlaskit/avatar-group`
|
|
40
|
+
and `@atlaskit/popup`. Behind the `platform-dst-top-layer` feature gate.
|
|
41
|
+
|
|
3
42
|
## 4.25.1
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { expect, test } from '@af/integration-testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Popup: focus contract on the top-layer code path.
|
|
5
|
+
*
|
|
6
|
+
* `Popup` is a generic positioned overlay whose initial-focus behaviour
|
|
7
|
+
* depends on the `role` prop and the `autoFocus` prop. The tests in this
|
|
8
|
+
* spec exercise each branch of `top-layer/useInitialFocus` reachable
|
|
9
|
+
* through the public `Popup` API:
|
|
10
|
+
*
|
|
11
|
+
* - `role="dialog"`: focus moves to the first focusable inside the popup
|
|
12
|
+
* (default case), or to an element carrying the native HTML `autofocus`
|
|
13
|
+
* attribute when present.
|
|
14
|
+
* - no `role`: focus stays on the trigger; no focus movement is performed.
|
|
15
|
+
* - `role="listbox"`: focus prefers `[aria-selected="true"]` over the
|
|
16
|
+
* first option, except when the popup is owned by a combobox.
|
|
17
|
+
* - combobox carve-outs: focus stays on the combobox input when the popup
|
|
18
|
+
* is its owned listbox, whether the combobox is the trigger or rendered
|
|
19
|
+
* outside the popup element (react-select pattern).
|
|
20
|
+
* - `autoFocus={false}`: focus stays on the trigger regardless of role.
|
|
21
|
+
*
|
|
22
|
+
* Escape-to-restore and Tab cycling are also exercised for the default
|
|
23
|
+
* dialog case as representative top-layer focus-trap behaviour.
|
|
24
|
+
*
|
|
25
|
+
* See: `platform/packages/design-system/top-layer/notes/architecture/focus.md`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const featureFlag = 'platform-dst-top-layer';
|
|
29
|
+
|
|
30
|
+
test.describe('Popup: top-layer focus contract', () => {
|
|
31
|
+
test('initial focus: focus moves to first focusable on open', async ({ page }) => {
|
|
32
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
33
|
+
'design-system',
|
|
34
|
+
'popup',
|
|
35
|
+
'testing-initial-focus-matrix',
|
|
36
|
+
{ featureFlag },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await page.getByTestId('dialog-popup-trigger').click();
|
|
40
|
+
await expect(page.getByTestId('dialog-popup-content')).toBeVisible();
|
|
41
|
+
|
|
42
|
+
await expect(page.getByTestId('dialog-popup-first-button')).toBeFocused();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('focus restoration: Escape restores focus to the trigger', async ({ page }) => {
|
|
46
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
47
|
+
'design-system',
|
|
48
|
+
'popup',
|
|
49
|
+
'testing-initial-focus-matrix',
|
|
50
|
+
{ featureFlag },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const trigger = page.getByTestId('dialog-popup-trigger');
|
|
54
|
+
await trigger.click();
|
|
55
|
+
await expect(page.getByTestId('dialog-popup-content')).toBeVisible();
|
|
56
|
+
|
|
57
|
+
await page.keyboard.press('Escape');
|
|
58
|
+
await expect(page.getByTestId('dialog-popup-content')).toBeHidden();
|
|
59
|
+
await expect(trigger).toBeFocused();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('focus movement: Tab moves focus between focusable elements within the popup', async ({
|
|
63
|
+
page,
|
|
64
|
+
}) => {
|
|
65
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
66
|
+
'design-system',
|
|
67
|
+
'popup',
|
|
68
|
+
'testing-initial-focus-matrix',
|
|
69
|
+
{ featureFlag },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await page.getByTestId('dialog-popup-trigger').click();
|
|
73
|
+
await expect(page.getByTestId('dialog-popup-content')).toBeVisible();
|
|
74
|
+
|
|
75
|
+
const first = page.getByTestId('dialog-popup-first-button');
|
|
76
|
+
const second = page.getByTestId('dialog-popup-second-button');
|
|
77
|
+
|
|
78
|
+
await expect(first).toBeFocused();
|
|
79
|
+
await page.keyboard.press('Tab');
|
|
80
|
+
await expect(second).toBeFocused();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// WCAG 2.4.3 Focus Order + WAI-ARIA APG Dialog pattern
|
|
84
|
+
// (https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
|
|
85
|
+
// For `role="dialog"`, when a descendant carries the native HTML
|
|
86
|
+
// `autofocus` attribute, focus must land on that element instead of
|
|
87
|
+
// the first focusable in source order.
|
|
88
|
+
test('initial focus: native [autofocus] element wins for role="dialog"', async ({ page }) => {
|
|
89
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
90
|
+
'design-system',
|
|
91
|
+
'popup',
|
|
92
|
+
'testing-initial-focus-matrix',
|
|
93
|
+
{ featureFlag },
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await page.getByTestId('autofocus-dialog-popup-trigger').click();
|
|
97
|
+
await expect(page.getByTestId('autofocus-dialog-popup-content')).toBeVisible();
|
|
98
|
+
|
|
99
|
+
// The popup's first focusable is a button, but a sibling
|
|
100
|
+
// `<input>` carries `autofocus`, so focus must land on the
|
|
101
|
+
// input.
|
|
102
|
+
await expect(page.getByTestId('autofocus-dialog-popup-input')).toBeFocused();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// WCAG 2.4.3 Focus Order (no opinionated movement when no role is
|
|
106
|
+
// declared). The Popover API does not move focus on open; ARIA roles
|
|
107
|
+
// such as `dialog` / `menu` / `listbox` are what trigger the
|
|
108
|
+
// initial-focus contract. A popover without a role has no APG
|
|
109
|
+
// pattern, so focus must stay on the trigger.
|
|
110
|
+
test('initial focus: no `role` prop keeps focus on the trigger', async ({ page }) => {
|
|
111
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
112
|
+
'design-system',
|
|
113
|
+
'popup',
|
|
114
|
+
'testing-initial-focus-matrix',
|
|
115
|
+
{ featureFlag },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const trigger = page.getByTestId('no-role-popup-trigger');
|
|
119
|
+
// Explicitly focus the trigger before clicking so the assertion
|
|
120
|
+
// reflects the contract under test (focus is preserved across
|
|
121
|
+
// open). `button.click()` does not synchronously focus the
|
|
122
|
+
// element on every browser engine (notably WebKit), so we cannot
|
|
123
|
+
// rely on the click as the focus trigger here.
|
|
124
|
+
await trigger.focus();
|
|
125
|
+
await trigger.click();
|
|
126
|
+
await expect(page.getByTestId('no-role-popup-content')).toBeVisible();
|
|
127
|
+
|
|
128
|
+
// Without a role, the popup is `tabIndex={-1}` and is not a
|
|
129
|
+
// useful focus target, so focus remains on the trigger.
|
|
130
|
+
await expect(trigger).toBeFocused();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// WCAG 2.4.3 Focus Order + WAI-ARIA APG Listbox pattern
|
|
134
|
+
// (https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
|
|
135
|
+
// "If an option is selected, focus is set on the selected option."
|
|
136
|
+
// Focus must land on `[aria-selected="true"]` rather than the first
|
|
137
|
+
// `[role="option"]`.
|
|
138
|
+
test('initial focus: listbox prefers `[aria-selected="true"]` over the first option', async ({
|
|
139
|
+
page,
|
|
140
|
+
}) => {
|
|
141
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
142
|
+
'design-system',
|
|
143
|
+
'popup',
|
|
144
|
+
'testing-initial-focus-matrix',
|
|
145
|
+
{ featureFlag },
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await page.getByTestId('selected-option-listbox-popup-trigger').click();
|
|
149
|
+
await expect(page.getByTestId('selected-option-listbox-popup-content')).toBeVisible();
|
|
150
|
+
|
|
151
|
+
await expect(page.getByTestId('selected-option-listbox-option-2')).toBeFocused();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// WCAG 2.4.3 Focus Order + WAI-ARIA APG Combobox pattern
|
|
155
|
+
// (https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
|
|
156
|
+
// "DOM focus remains on the combobox... the listbox does not
|
|
157
|
+
// receive focus." When the combobox's `aria-controls` references the
|
|
158
|
+
// popup id, focus must remain on the combobox input so the user can
|
|
159
|
+
// keep typing; the active option is surfaced via
|
|
160
|
+
// `aria-activedescendant`.
|
|
161
|
+
test('initial focus: combobox-controlled listbox keeps focus on the combobox input', async ({
|
|
162
|
+
page,
|
|
163
|
+
}) => {
|
|
164
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
165
|
+
'design-system',
|
|
166
|
+
'popup',
|
|
167
|
+
'testing-initial-focus-matrix',
|
|
168
|
+
{ featureFlag },
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const combobox = page.getByTestId('combobox-controls-input');
|
|
172
|
+
await combobox.focus();
|
|
173
|
+
await expect(page.getByTestId('combobox-controls-popup-content')).toBeVisible();
|
|
174
|
+
|
|
175
|
+
await expect(combobox).toBeFocused();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// WCAG 2.4.3 Focus Order. Carve-out scoping: the APG Combobox
|
|
179
|
+
// exception only applies when the focused combobox's `aria-controls`
|
|
180
|
+
// references this popup. An unrelated combobox on the page must not
|
|
181
|
+
// block the standard listbox initial-focus behaviour.
|
|
182
|
+
test('initial focus: an unrelated combobox does not block focus movement into the listbox', async ({
|
|
183
|
+
page,
|
|
184
|
+
}) => {
|
|
185
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
186
|
+
'design-system',
|
|
187
|
+
'popup',
|
|
188
|
+
'testing-initial-focus-matrix',
|
|
189
|
+
{ featureFlag },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Focus an unrelated combobox first so it is the active element
|
|
193
|
+
// at the moment the popup opens. Its `aria-controls` points
|
|
194
|
+
// elsewhere, so the carve-out must not apply.
|
|
195
|
+
await page.getByTestId('unrelated-combobox-input').focus();
|
|
196
|
+
await page.getByTestId('unrelated-combobox-popup-trigger').click();
|
|
197
|
+
await expect(page.getByTestId('unrelated-combobox-popup-content')).toBeVisible();
|
|
198
|
+
|
|
199
|
+
await expect(page.getByTestId('unrelated-combobox-option-1')).toBeFocused();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// WCAG 2.4.3 Focus Order + WAI-ARIA APG Combobox pattern
|
|
203
|
+
// (https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
|
|
204
|
+
// The combobox-carve-out is scoped by the `aria-controls`
|
|
205
|
+
// relationship, not by DOM containment. The `react-select` pattern
|
|
206
|
+
// portals the listbox separately from the textbox, so focus must
|
|
207
|
+
// still stay on the external input.
|
|
208
|
+
test('initial focus: external combobox (react-select pattern) keeps focus on the input', async ({
|
|
209
|
+
page,
|
|
210
|
+
}) => {
|
|
211
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
212
|
+
'design-system',
|
|
213
|
+
'popup',
|
|
214
|
+
'testing-initial-focus-matrix',
|
|
215
|
+
{ featureFlag },
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const combobox = page.getByTestId('external-combobox-input');
|
|
219
|
+
await combobox.focus();
|
|
220
|
+
await expect(page.getByTestId('external-combobox-popup-content')).toBeVisible();
|
|
221
|
+
|
|
222
|
+
await expect(combobox).toBeFocused();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Consumer escape hatch. `autoFocus={false}` disables Popup's
|
|
226
|
+
// initial-focus behaviour entirely. Note: pairing `autoFocus={false}`
|
|
227
|
+
// with `role="dialog"` deviates from the WAI-ARIA APG Dialog
|
|
228
|
+
// pattern, which expects focus to move into the dialog on open.
|
|
229
|
+
// The escape hatch is intended for patterns where the popup is
|
|
230
|
+
// visual-only (e.g. a hover-card / tooltip-like surface) and must
|
|
231
|
+
// not be combined with a focus-receiving role in production code.
|
|
232
|
+
test('initial focus: `autoFocus={false}` keeps focus on the trigger', async ({ page }) => {
|
|
233
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
234
|
+
'design-system',
|
|
235
|
+
'popup',
|
|
236
|
+
'testing-initial-focus-matrix',
|
|
237
|
+
{ featureFlag },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const trigger = page.getByTestId('auto-focus-disabled-popup-trigger');
|
|
241
|
+
// Explicitly focus the trigger before clicking so the assertion
|
|
242
|
+
// reflects the contract under test (focus is preserved across
|
|
243
|
+
// open). `button.click()` does not synchronously focus the
|
|
244
|
+
// element on every browser engine (notably WebKit).
|
|
245
|
+
await trigger.focus();
|
|
246
|
+
await trigger.click();
|
|
247
|
+
await expect(page.getByTestId('auto-focus-disabled-popup-content')).toBeVisible();
|
|
248
|
+
|
|
249
|
+
await expect(trigger).toBeFocused();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// WCAG 2.4.3 Focus Order. `Popup`'s `content` render prop exposes
|
|
253
|
+
// `setInitialFocusRef` so consumers can override the default
|
|
254
|
+
// `getInitialFocusTarget` resolution and pick a specific element to
|
|
255
|
+
// focus on open. The consumer-supplied ref must win over the role-
|
|
256
|
+
// based default (first focusable for `role="dialog"`).
|
|
257
|
+
test('initial focus: consumer-supplied `setInitialFocusRef` overrides the role-based default', async ({
|
|
258
|
+
page,
|
|
259
|
+
}) => {
|
|
260
|
+
await page.visitExample<typeof import('../../examples/97-testing-initial-focus-matrix.tsx')>(
|
|
261
|
+
'design-system',
|
|
262
|
+
'popup',
|
|
263
|
+
'testing-initial-focus-matrix',
|
|
264
|
+
{ featureFlag },
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await page.getByTestId('set-initial-focus-ref-popup-trigger').click();
|
|
268
|
+
await expect(page.getByTestId('set-initial-focus-ref-popup-content')).toBeVisible();
|
|
269
|
+
|
|
270
|
+
// The first focusable in the popup is the first button, but the
|
|
271
|
+
// consumer wired `setInitialFocusRef` to the second button.
|
|
272
|
+
await expect(page.getByTestId('set-initial-focus-ref-popup-second-button')).toBeFocused();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -11,7 +11,6 @@ require("./popup-content-top-layer.compiled.css");
|
|
|
11
11
|
var _react = _interopRequireWildcard(require("react"));
|
|
12
12
|
var React = _react;
|
|
13
13
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
|
14
|
-
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
15
14
|
var _runtime = require("@compiled/react/runtime");
|
|
16
15
|
var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
|
|
17
16
|
var _animations = require("@atlaskit/top-layer/animations");
|
|
@@ -66,11 +65,26 @@ function PopupContentTopLayer(_ref) {
|
|
|
66
65
|
_shouldFlip = _ref.shouldFlip,
|
|
67
66
|
_appearance = _ref.appearance,
|
|
68
67
|
_shouldDisableGpuAcceleration = _ref.shouldDisableGpuAcceleration;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
70
|
+
*
|
|
71
|
+
* The legacy compositional `PopupContent` surfaced
|
|
72
|
+
* `setInitialFocusRef` on the content children render prop with a
|
|
73
|
+
* `Dispatch<SetStateAction<HTMLElement | null>>` signature so it
|
|
74
|
+
* could be passed as a React `ref` callback. We keep the same shape
|
|
75
|
+
* for API compatibility, but store the value in a ref rather than
|
|
76
|
+
* component state. Flipping state on every open caused a needless
|
|
77
|
+
* re-render and, more importantly, raced the initial-focus effect
|
|
78
|
+
* below. The ref is read in the open-side effect to override the
|
|
79
|
+
* role-based default focus chosen by `useInitialFocus` inside the
|
|
80
|
+
* underlying `Popover`.
|
|
81
|
+
*/
|
|
82
|
+
var consumerInitialFocusRef = (0, _react.useRef)(null);
|
|
83
|
+
var setInitialFocusRef = (0, _react.useCallback)(function (value) {
|
|
84
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
85
|
+
}, []);
|
|
72
86
|
|
|
73
|
-
//
|
|
87
|
+
// Placement conversion
|
|
74
88
|
// Legacy `offset` is the popper `[along, away]` tuple; `fromLegacyPlacement`
|
|
75
89
|
// folds it into the new `placement.offset` shape.
|
|
76
90
|
var topLayerPlacement = (0, _react.useMemo)(function () {
|
|
@@ -99,7 +113,7 @@ function PopupContentTopLayer(_ref) {
|
|
|
99
113
|
onClose: onClose,
|
|
100
114
|
setInitialFocusRef: setInitialFocusRef
|
|
101
115
|
};
|
|
102
|
-
}, [isOpen, onClose]);
|
|
116
|
+
}, [isOpen, onClose, setInitialFocusRef]);
|
|
103
117
|
|
|
104
118
|
// ── Role mapping ──
|
|
105
119
|
var roleProps = (0, _topLayerBridge.useRoleProps)({
|
|
@@ -110,6 +124,49 @@ function PopupContentTopLayer(_ref) {
|
|
|
110
124
|
var popoverRef = (0, _react.useRef)(null);
|
|
111
125
|
var anchorRef = (0, _react.useContext)(_triggerRefObjectContext.TriggerRefObjectContext);
|
|
112
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Override the role-based initial focus chosen by the underlying
|
|
129
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
130
|
+
*
|
|
131
|
+
* React flushes child effects before parent effects, so by the time
|
|
132
|
+
* this effect runs the role-based default focus has already moved.
|
|
133
|
+
* We then apply the legacy `Popup` contract:
|
|
134
|
+
*
|
|
135
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
136
|
+
* (escape hatch wins).
|
|
137
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
138
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
139
|
+
* movement, even for roles like `dialog`.
|
|
140
|
+
*
|
|
141
|
+
* When neither applies the role-based default from the underlying
|
|
142
|
+
* `Popover` stays in place.
|
|
143
|
+
*/
|
|
144
|
+
(0, _react.useEffect)(function () {
|
|
145
|
+
if (!isOpen) {
|
|
146
|
+
/**
|
|
147
|
+
* Clear the consumer-supplied focus target on close so a
|
|
148
|
+
* subsequent open does not silently inherit a stale ref from
|
|
149
|
+
* the previous open cycle. The next open will re-collect a
|
|
150
|
+
* target only if the content render prop re-wires
|
|
151
|
+
* `setInitialFocusRef`.
|
|
152
|
+
*/
|
|
153
|
+
consumerInitialFocusRef.current = null;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
var consumerTarget = consumerInitialFocusRef.current;
|
|
157
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
158
|
+
consumerTarget.focus();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!autoFocus) {
|
|
162
|
+
var _anchorRef$current;
|
|
163
|
+
var trigger = (_anchorRef$current = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current) !== null && _anchorRef$current !== void 0 ? _anchorRef$current : null;
|
|
164
|
+
if (trigger && trigger.isConnected) {
|
|
165
|
+
trigger.focus();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}, [isOpen, autoFocus, anchorRef]);
|
|
169
|
+
|
|
113
170
|
// `isOpen` is included so the anchor positioning effect re-runs when
|
|
114
171
|
// the Popover host element is unmounted/remounted across open cycles.
|
|
115
172
|
(0, _useAnchorPosition.useAnchorPosition)({
|
|
@@ -7,6 +7,13 @@ exports.useRoleProps = useRoleProps;
|
|
|
7
7
|
var _react = require("react");
|
|
8
8
|
/**
|
|
9
9
|
* Props shape passed from legacy `role` / `label` / `titleId` into `@atlaskit/top-layer` `Popup.Content`.
|
|
10
|
+
*
|
|
11
|
+
* The `{ role: undefined }` variant covers the legacy contract where a
|
|
12
|
+
* consumer omits `role` to opt out of any popup semantics (and the
|
|
13
|
+
* associated role-based initial-focus movement). Legacy `Popup` left the
|
|
14
|
+
* popup element role-less in that case; we preserve that behaviour here
|
|
15
|
+
* rather than defaulting to `'dialog'`, which would move focus into the
|
|
16
|
+
* popup on open.
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
/**
|
|
@@ -16,6 +23,10 @@ var _react = require("react");
|
|
|
16
23
|
* `Popup.Content` enforces at the type level that dialog/alertdialog/menu
|
|
17
24
|
* roles must have `label` or `labelledBy`. This hook bridges the legacy
|
|
18
25
|
* flat-prop API (`role`, `label`, `titleId`) to that shape.
|
|
26
|
+
*
|
|
27
|
+
* When `role` is omitted entirely, the result carries `role: undefined`
|
|
28
|
+
* (no implicit default). This matches the legacy `Popup` contract and
|
|
29
|
+
* keeps the underlying `Popover` from running role-based initial focus.
|
|
19
30
|
*/
|
|
20
31
|
function useRoleProps(_ref) {
|
|
21
32
|
var role = _ref.role,
|
|
@@ -40,16 +51,10 @@ function useRoleProps(_ref) {
|
|
|
40
51
|
label: label
|
|
41
52
|
};
|
|
42
53
|
}
|
|
43
|
-
//
|
|
44
|
-
if (titleId) {
|
|
45
|
-
return {
|
|
46
|
-
role: 'dialog',
|
|
47
|
-
labelledBy: titleId
|
|
48
|
-
};
|
|
49
|
-
}
|
|
54
|
+
// No role supplied: preserve the legacy "no popup semantics" contract.
|
|
50
55
|
return {
|
|
51
|
-
role:
|
|
52
|
-
label: label
|
|
56
|
+
role: undefined,
|
|
57
|
+
label: label
|
|
53
58
|
};
|
|
54
59
|
}, [role, label, titleId]);
|
|
55
60
|
}
|
|
@@ -12,7 +12,6 @@ var _react = _interopRequireWildcard(require("react"));
|
|
|
12
12
|
var React = _react;
|
|
13
13
|
var _runtime = require("@compiled/react/runtime");
|
|
14
14
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
|
15
|
-
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
16
15
|
var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
|
|
17
16
|
var _animations = require("@atlaskit/top-layer/animations");
|
|
18
17
|
var _getAriaForTrigger = require("@atlaskit/top-layer/get-aria-for-trigger");
|
|
@@ -80,9 +79,66 @@ var PopupTopLayer = exports.PopupTopLayer = /*#__PURE__*/(0, _react.memo)(functi
|
|
|
80
79
|
var triggerRef = (0, _react.useRef)(null);
|
|
81
80
|
var popoverRef = (0, _react.useRef)(null);
|
|
82
81
|
var popupContainerRef = (0, _react.useRef)(null);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
85
|
+
*
|
|
86
|
+
* The legacy `Popup` surfaced `setInitialFocusRef` on the content
|
|
87
|
+
* render prop with a `Dispatch<SetStateAction<HTMLElement | null>>`
|
|
88
|
+
* signature so it could be passed as a React `ref` callback. We
|
|
89
|
+
* keep the same shape for API compatibility, but store the value in
|
|
90
|
+
* a ref rather than component state. Flipping state on every open
|
|
91
|
+
* caused a needless re-render and, more importantly, raced the
|
|
92
|
+
* initial-focus effect below. The ref is read in the open-side
|
|
93
|
+
* effect to override the role-based default focus chosen by
|
|
94
|
+
* `useInitialFocus` inside the underlying `Popover`.
|
|
95
|
+
*/
|
|
96
|
+
var consumerInitialFocusRef = (0, _react.useRef)(null);
|
|
97
|
+
var setInitialFocusRef = (0, _react.useCallback)(function (value) {
|
|
98
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Override the role-based initial focus chosen by the underlying
|
|
103
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
104
|
+
*
|
|
105
|
+
* React flushes child effects before parent effects, so by the time
|
|
106
|
+
* this effect runs the role-based default focus has already moved.
|
|
107
|
+
* We then apply the legacy `Popup` contract:
|
|
108
|
+
*
|
|
109
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
110
|
+
* (escape hatch wins).
|
|
111
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
112
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
113
|
+
* movement, even for roles like `dialog`.
|
|
114
|
+
*
|
|
115
|
+
* When neither applies the role-based default from the underlying
|
|
116
|
+
* `Popover` stays in place.
|
|
117
|
+
*/
|
|
118
|
+
(0, _react.useEffect)(function () {
|
|
119
|
+
if (!isOpen) {
|
|
120
|
+
/**
|
|
121
|
+
* Clear the consumer-supplied focus target on close so a
|
|
122
|
+
* subsequent open does not silently inherit a stale ref from
|
|
123
|
+
* the previous open cycle. The next open will re-collect a
|
|
124
|
+
* target only if the content render prop re-wires
|
|
125
|
+
* `setInitialFocusRef`.
|
|
126
|
+
*/
|
|
127
|
+
consumerInitialFocusRef.current = null;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
var consumerTarget = consumerInitialFocusRef.current;
|
|
131
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
132
|
+
consumerTarget.focus();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!autoFocus) {
|
|
136
|
+
var _trigger = triggerRef.current;
|
|
137
|
+
if (_trigger && _trigger.isConnected) {
|
|
138
|
+
_trigger.focus();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}, [isOpen, autoFocus]);
|
|
86
142
|
var popoverId = (0, _usePopoverId.usePopoverId)();
|
|
87
143
|
|
|
88
144
|
// Placement conversion.
|
|
@@ -135,7 +191,7 @@ var PopupTopLayer = exports.PopupTopLayer = /*#__PURE__*/(0, _react.memo)(functi
|
|
|
135
191
|
onClose: onClose,
|
|
136
192
|
setInitialFocusRef: setInitialFocusRef
|
|
137
193
|
};
|
|
138
|
-
}, [isOpen, onClose]);
|
|
194
|
+
}, [isOpen, onClose, setInitialFocusRef]);
|
|
139
195
|
|
|
140
196
|
// Role mapping.
|
|
141
197
|
// Build the role/label props for Popover.
|
|
@@ -2,7 +2,7 @@
|
|
|
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
8
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
@@ -76,9 +76,26 @@ export function PopupContentTopLayer({
|
|
|
76
76
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
77
77
|
shouldDisableGpuAcceleration: _shouldDisableGpuAcceleration
|
|
78
78
|
}) {
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
81
|
+
*
|
|
82
|
+
* The legacy compositional `PopupContent` surfaced
|
|
83
|
+
* `setInitialFocusRef` on the content children render prop with a
|
|
84
|
+
* `Dispatch<SetStateAction<HTMLElement | null>>` signature so it
|
|
85
|
+
* could be passed as a React `ref` callback. We keep the same shape
|
|
86
|
+
* for API compatibility, but store the value in a ref rather than
|
|
87
|
+
* component state. Flipping state on every open caused a needless
|
|
88
|
+
* re-render and, more importantly, raced the initial-focus effect
|
|
89
|
+
* below. The ref is read in the open-side effect to override the
|
|
90
|
+
* role-based default focus chosen by `useInitialFocus` inside the
|
|
91
|
+
* underlying `Popover`.
|
|
92
|
+
*/
|
|
93
|
+
const consumerInitialFocusRef = useRef(null);
|
|
94
|
+
const setInitialFocusRef = useCallback(value => {
|
|
95
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
96
|
+
}, []);
|
|
80
97
|
|
|
81
|
-
//
|
|
98
|
+
// Placement conversion
|
|
82
99
|
// Legacy `offset` is the popper `[along, away]` tuple; `fromLegacyPlacement`
|
|
83
100
|
// folds it into the new `placement.offset` shape.
|
|
84
101
|
const topLayerPlacement = useMemo(() => fromLegacyPlacement({
|
|
@@ -104,7 +121,7 @@ export function PopupContentTopLayer({
|
|
|
104
121
|
update: noop,
|
|
105
122
|
onClose,
|
|
106
123
|
setInitialFocusRef
|
|
107
|
-
}), [isOpen, onClose]);
|
|
124
|
+
}), [isOpen, onClose, setInitialFocusRef]);
|
|
108
125
|
|
|
109
126
|
// ── Role mapping ──
|
|
110
127
|
const roleProps = useRoleProps({
|
|
@@ -115,6 +132,49 @@ export function PopupContentTopLayer({
|
|
|
115
132
|
const popoverRef = useRef(null);
|
|
116
133
|
const anchorRef = useContext(TriggerRefObjectContext);
|
|
117
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Override the role-based initial focus chosen by the underlying
|
|
137
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
138
|
+
*
|
|
139
|
+
* React flushes child effects before parent effects, so by the time
|
|
140
|
+
* this effect runs the role-based default focus has already moved.
|
|
141
|
+
* We then apply the legacy `Popup` contract:
|
|
142
|
+
*
|
|
143
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
144
|
+
* (escape hatch wins).
|
|
145
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
146
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
147
|
+
* movement, even for roles like `dialog`.
|
|
148
|
+
*
|
|
149
|
+
* When neither applies the role-based default from the underlying
|
|
150
|
+
* `Popover` stays in place.
|
|
151
|
+
*/
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!isOpen) {
|
|
154
|
+
/**
|
|
155
|
+
* Clear the consumer-supplied focus target on close so a
|
|
156
|
+
* subsequent open does not silently inherit a stale ref from
|
|
157
|
+
* the previous open cycle. The next open will re-collect a
|
|
158
|
+
* target only if the content render prop re-wires
|
|
159
|
+
* `setInitialFocusRef`.
|
|
160
|
+
*/
|
|
161
|
+
consumerInitialFocusRef.current = null;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const consumerTarget = consumerInitialFocusRef.current;
|
|
165
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
166
|
+
consumerTarget.focus();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!autoFocus) {
|
|
170
|
+
var _anchorRef$current;
|
|
171
|
+
const trigger = (_anchorRef$current = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current) !== null && _anchorRef$current !== void 0 ? _anchorRef$current : null;
|
|
172
|
+
if (trigger && trigger.isConnected) {
|
|
173
|
+
trigger.focus();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, [isOpen, autoFocus, anchorRef]);
|
|
177
|
+
|
|
118
178
|
// `isOpen` is included so the anchor positioning effect re-runs when
|
|
119
179
|
// the Popover host element is unmounted/remounted across open cycles.
|
|
120
180
|
useAnchorPosition({
|
|
@@ -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,7 +3,7 @@ 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
8
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
9
9
|
import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
|
|
@@ -92,7 +92,66 @@ export const PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer({
|
|
|
92
92
|
const triggerRef = useRef(null);
|
|
93
93
|
const popoverRef = useRef(null);
|
|
94
94
|
const popupContainerRef = useRef(null);
|
|
95
|
-
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
98
|
+
*
|
|
99
|
+
* The legacy `Popup` surfaced `setInitialFocusRef` on the content
|
|
100
|
+
* render prop with a `Dispatch<SetStateAction<HTMLElement | null>>`
|
|
101
|
+
* signature so it could be passed as a React `ref` callback. We
|
|
102
|
+
* keep the same shape for API compatibility, but store the value in
|
|
103
|
+
* a ref rather than component state. Flipping state on every open
|
|
104
|
+
* caused a needless re-render and, more importantly, raced the
|
|
105
|
+
* initial-focus effect below. The ref is read in the open-side
|
|
106
|
+
* effect to override the role-based default focus chosen by
|
|
107
|
+
* `useInitialFocus` inside the underlying `Popover`.
|
|
108
|
+
*/
|
|
109
|
+
const consumerInitialFocusRef = useRef(null);
|
|
110
|
+
const setInitialFocusRef = useCallback(value => {
|
|
111
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Override the role-based initial focus chosen by the underlying
|
|
116
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
117
|
+
*
|
|
118
|
+
* React flushes child effects before parent effects, so by the time
|
|
119
|
+
* this effect runs the role-based default focus has already moved.
|
|
120
|
+
* We then apply the legacy `Popup` contract:
|
|
121
|
+
*
|
|
122
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
123
|
+
* (escape hatch wins).
|
|
124
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
125
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
126
|
+
* movement, even for roles like `dialog`.
|
|
127
|
+
*
|
|
128
|
+
* When neither applies the role-based default from the underlying
|
|
129
|
+
* `Popover` stays in place.
|
|
130
|
+
*/
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!isOpen) {
|
|
133
|
+
/**
|
|
134
|
+
* Clear the consumer-supplied focus target on close so a
|
|
135
|
+
* subsequent open does not silently inherit a stale ref from
|
|
136
|
+
* the previous open cycle. The next open will re-collect a
|
|
137
|
+
* target only if the content render prop re-wires
|
|
138
|
+
* `setInitialFocusRef`.
|
|
139
|
+
*/
|
|
140
|
+
consumerInitialFocusRef.current = null;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const consumerTarget = consumerInitialFocusRef.current;
|
|
144
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
145
|
+
consumerTarget.focus();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!autoFocus) {
|
|
149
|
+
const trigger = triggerRef.current;
|
|
150
|
+
if (trigger && trigger.isConnected) {
|
|
151
|
+
trigger.focus();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}, [isOpen, autoFocus]);
|
|
96
155
|
const popoverId = usePopoverId();
|
|
97
156
|
|
|
98
157
|
// Placement conversion.
|
|
@@ -142,7 +201,7 @@ export const PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer({
|
|
|
142
201
|
update: noop,
|
|
143
202
|
onClose,
|
|
144
203
|
setInitialFocusRef
|
|
145
|
-
}), [isOpen, onClose]);
|
|
204
|
+
}), [isOpen, onClose, setInitialFocusRef]);
|
|
146
205
|
|
|
147
206
|
// Role mapping.
|
|
148
207
|
// Build the role/label props for Popover.
|
|
@@ -1,9 +1,8 @@
|
|
|
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';
|
|
9
8
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
@@ -57,11 +56,26 @@ export function PopupContentTopLayer(_ref) {
|
|
|
57
56
|
_shouldFlip = _ref.shouldFlip,
|
|
58
57
|
_appearance = _ref.appearance,
|
|
59
58
|
_shouldDisableGpuAcceleration = _ref.shouldDisableGpuAcceleration;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
61
|
+
*
|
|
62
|
+
* The legacy compositional `PopupContent` surfaced
|
|
63
|
+
* `setInitialFocusRef` on the content children render prop with a
|
|
64
|
+
* `Dispatch<SetStateAction<HTMLElement | null>>` signature so it
|
|
65
|
+
* could be passed as a React `ref` callback. We keep the same shape
|
|
66
|
+
* for API compatibility, but store the value in a ref rather than
|
|
67
|
+
* component state. Flipping state on every open caused a needless
|
|
68
|
+
* re-render and, more importantly, raced the initial-focus effect
|
|
69
|
+
* below. The ref is read in the open-side effect to override the
|
|
70
|
+
* role-based default focus chosen by `useInitialFocus` inside the
|
|
71
|
+
* underlying `Popover`.
|
|
72
|
+
*/
|
|
73
|
+
var consumerInitialFocusRef = useRef(null);
|
|
74
|
+
var setInitialFocusRef = useCallback(function (value) {
|
|
75
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
76
|
+
}, []);
|
|
63
77
|
|
|
64
|
-
//
|
|
78
|
+
// Placement conversion
|
|
65
79
|
// Legacy `offset` is the popper `[along, away]` tuple; `fromLegacyPlacement`
|
|
66
80
|
// folds it into the new `placement.offset` shape.
|
|
67
81
|
var topLayerPlacement = useMemo(function () {
|
|
@@ -90,7 +104,7 @@ export function PopupContentTopLayer(_ref) {
|
|
|
90
104
|
onClose: onClose,
|
|
91
105
|
setInitialFocusRef: setInitialFocusRef
|
|
92
106
|
};
|
|
93
|
-
}, [isOpen, onClose]);
|
|
107
|
+
}, [isOpen, onClose, setInitialFocusRef]);
|
|
94
108
|
|
|
95
109
|
// ── Role mapping ──
|
|
96
110
|
var roleProps = useRoleProps({
|
|
@@ -101,6 +115,49 @@ export function PopupContentTopLayer(_ref) {
|
|
|
101
115
|
var popoverRef = useRef(null);
|
|
102
116
|
var anchorRef = useContext(TriggerRefObjectContext);
|
|
103
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Override the role-based initial focus chosen by the underlying
|
|
120
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
121
|
+
*
|
|
122
|
+
* React flushes child effects before parent effects, so by the time
|
|
123
|
+
* this effect runs the role-based default focus has already moved.
|
|
124
|
+
* We then apply the legacy `Popup` contract:
|
|
125
|
+
*
|
|
126
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
127
|
+
* (escape hatch wins).
|
|
128
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
129
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
130
|
+
* movement, even for roles like `dialog`.
|
|
131
|
+
*
|
|
132
|
+
* When neither applies the role-based default from the underlying
|
|
133
|
+
* `Popover` stays in place.
|
|
134
|
+
*/
|
|
135
|
+
useEffect(function () {
|
|
136
|
+
if (!isOpen) {
|
|
137
|
+
/**
|
|
138
|
+
* Clear the consumer-supplied focus target on close so a
|
|
139
|
+
* subsequent open does not silently inherit a stale ref from
|
|
140
|
+
* the previous open cycle. The next open will re-collect a
|
|
141
|
+
* target only if the content render prop re-wires
|
|
142
|
+
* `setInitialFocusRef`.
|
|
143
|
+
*/
|
|
144
|
+
consumerInitialFocusRef.current = null;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
var consumerTarget = consumerInitialFocusRef.current;
|
|
148
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
149
|
+
consumerTarget.focus();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!autoFocus) {
|
|
153
|
+
var _anchorRef$current;
|
|
154
|
+
var trigger = (_anchorRef$current = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current) !== null && _anchorRef$current !== void 0 ? _anchorRef$current : null;
|
|
155
|
+
if (trigger && trigger.isConnected) {
|
|
156
|
+
trigger.focus();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}, [isOpen, autoFocus, anchorRef]);
|
|
160
|
+
|
|
104
161
|
// `isOpen` is included so the anchor positioning effect re-runs when
|
|
105
162
|
// the Popover host element is unmounted/remounted across open cycles.
|
|
106
163
|
useAnchorPosition({
|
|
@@ -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,10 +1,9 @@
|
|
|
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';
|
|
9
8
|
import { popupMotion } from '@atlaskit/top-layer/animations';
|
|
10
9
|
import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
|
|
@@ -71,9 +70,66 @@ export var PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer(_ref) {
|
|
|
71
70
|
var triggerRef = useRef(null);
|
|
72
71
|
var popoverRef = useRef(null);
|
|
73
72
|
var popupContainerRef = useRef(null);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Escape hatch for consumer-chosen initial focus target.
|
|
76
|
+
*
|
|
77
|
+
* The legacy `Popup` surfaced `setInitialFocusRef` on the content
|
|
78
|
+
* render prop with a `Dispatch<SetStateAction<HTMLElement | null>>`
|
|
79
|
+
* signature so it could be passed as a React `ref` callback. We
|
|
80
|
+
* keep the same shape for API compatibility, but store the value in
|
|
81
|
+
* a ref rather than component state. Flipping state on every open
|
|
82
|
+
* caused a needless re-render and, more importantly, raced the
|
|
83
|
+
* initial-focus effect below. The ref is read in the open-side
|
|
84
|
+
* effect to override the role-based default focus chosen by
|
|
85
|
+
* `useInitialFocus` inside the underlying `Popover`.
|
|
86
|
+
*/
|
|
87
|
+
var consumerInitialFocusRef = useRef(null);
|
|
88
|
+
var setInitialFocusRef = useCallback(function (value) {
|
|
89
|
+
consumerInitialFocusRef.current = typeof value === 'function' ? value(consumerInitialFocusRef.current) : value;
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Override the role-based initial focus chosen by the underlying
|
|
94
|
+
* `Popover` (its internal `useInitialFocus`).
|
|
95
|
+
*
|
|
96
|
+
* React flushes child effects before parent effects, so by the time
|
|
97
|
+
* this effect runs the role-based default focus has already moved.
|
|
98
|
+
* We then apply the legacy `Popup` contract:
|
|
99
|
+
*
|
|
100
|
+
* 1. If the consumer wired `setInitialFocusRef`, focus that element
|
|
101
|
+
* (escape hatch wins).
|
|
102
|
+
* 2. Otherwise, if `autoFocus={false}`, return focus to the trigger.
|
|
103
|
+
* The `autoFocus` prop on `Popup` opts out of any initial-focus
|
|
104
|
+
* movement, even for roles like `dialog`.
|
|
105
|
+
*
|
|
106
|
+
* When neither applies the role-based default from the underlying
|
|
107
|
+
* `Popover` stays in place.
|
|
108
|
+
*/
|
|
109
|
+
useEffect(function () {
|
|
110
|
+
if (!isOpen) {
|
|
111
|
+
/**
|
|
112
|
+
* Clear the consumer-supplied focus target on close so a
|
|
113
|
+
* subsequent open does not silently inherit a stale ref from
|
|
114
|
+
* the previous open cycle. The next open will re-collect a
|
|
115
|
+
* target only if the content render prop re-wires
|
|
116
|
+
* `setInitialFocusRef`.
|
|
117
|
+
*/
|
|
118
|
+
consumerInitialFocusRef.current = null;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
var consumerTarget = consumerInitialFocusRef.current;
|
|
122
|
+
if (consumerTarget && consumerTarget.isConnected) {
|
|
123
|
+
consumerTarget.focus();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!autoFocus) {
|
|
127
|
+
var _trigger = triggerRef.current;
|
|
128
|
+
if (_trigger && _trigger.isConnected) {
|
|
129
|
+
_trigger.focus();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, [isOpen, autoFocus]);
|
|
77
133
|
var popoverId = usePopoverId();
|
|
78
134
|
|
|
79
135
|
// Placement conversion.
|
|
@@ -126,7 +182,7 @@ export var PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer(_ref) {
|
|
|
126
182
|
onClose: onClose,
|
|
127
183
|
setInitialFocusRef: setInitialFocusRef
|
|
128
184
|
};
|
|
129
|
-
}, [isOpen, onClose]);
|
|
185
|
+
}, [isOpen, onClose, setInitialFocusRef]);
|
|
130
186
|
|
|
131
187
|
// Role mapping.
|
|
132
188
|
// 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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/popup",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "A popup displays brief content in an overlay.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -12,14 +12,6 @@
|
|
|
12
12
|
"module": "dist/esm/index.js",
|
|
13
13
|
"module:es2019": "dist/es2019/index.js",
|
|
14
14
|
"types": "dist/types/index.d.ts",
|
|
15
|
-
"typesVersions": {
|
|
16
|
-
">=4.5 <4.9": {
|
|
17
|
-
"*": [
|
|
18
|
-
"dist/types-ts4.5/*",
|
|
19
|
-
"dist/types-ts4.5/index.d.ts"
|
|
20
|
-
]
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
15
|
"sideEffects": [
|
|
24
16
|
"**/*.compiled.css"
|
|
25
17
|
],
|
|
@@ -39,16 +31,16 @@
|
|
|
39
31
|
}
|
|
40
32
|
},
|
|
41
33
|
"dependencies": {
|
|
42
|
-
"@atlaskit/css": "^0.
|
|
43
|
-
"@atlaskit/ds-lib": "^
|
|
44
|
-
"@atlaskit/layering": "^
|
|
45
|
-
"@atlaskit/motion": "^
|
|
46
|
-
"@atlaskit/platform-feature-flags": "^
|
|
47
|
-
"@atlaskit/popper": "^
|
|
48
|
-
"@atlaskit/portal": "^
|
|
49
|
-
"@atlaskit/primitives": "^
|
|
50
|
-
"@atlaskit/tokens": "^
|
|
51
|
-
"@atlaskit/top-layer": "^0.
|
|
34
|
+
"@atlaskit/css": "^1.0.0",
|
|
35
|
+
"@atlaskit/ds-lib": "^8.0.0",
|
|
36
|
+
"@atlaskit/layering": "^4.0.0",
|
|
37
|
+
"@atlaskit/motion": "^7.0.0",
|
|
38
|
+
"@atlaskit/platform-feature-flags": "^2.0.0",
|
|
39
|
+
"@atlaskit/popper": "^8.0.0",
|
|
40
|
+
"@atlaskit/portal": "^6.0.0",
|
|
41
|
+
"@atlaskit/primitives": "^20.0.0",
|
|
42
|
+
"@atlaskit/tokens": "^14.0.0",
|
|
43
|
+
"@atlaskit/top-layer": "^1.0.0",
|
|
52
44
|
"@babel/runtime": "^7.0.0",
|
|
53
45
|
"@compiled/react": "^0.20.0",
|
|
54
46
|
"bind-event-listener": "^3.0.0",
|
|
@@ -64,17 +56,17 @@
|
|
|
64
56
|
"@af/accessibility-testing": "workspace:^",
|
|
65
57
|
"@af/integration-testing": "workspace:^",
|
|
66
58
|
"@af/visual-regression": "workspace:^",
|
|
67
|
-
"@atlaskit/button": "^
|
|
68
|
-
"@atlaskit/code": "^
|
|
69
|
-
"@atlaskit/docs": "^
|
|
70
|
-
"@atlaskit/form": "^
|
|
71
|
-
"@atlaskit/heading": "^
|
|
72
|
-
"@atlaskit/icon": "^
|
|
73
|
-
"@atlaskit/link": "^
|
|
74
|
-
"@atlaskit/modal-dialog": "^
|
|
75
|
-
"@atlaskit/section-message": "^
|
|
76
|
-
"@atlaskit/textfield": "^
|
|
77
|
-
"@atlaskit/toggle": "^
|
|
59
|
+
"@atlaskit/button": "^24.0.0",
|
|
60
|
+
"@atlaskit/code": "^18.0.0",
|
|
61
|
+
"@atlaskit/docs": "^12.0.0",
|
|
62
|
+
"@atlaskit/form": "^16.0.0",
|
|
63
|
+
"@atlaskit/heading": "^6.0.0",
|
|
64
|
+
"@atlaskit/icon": "^36.0.0",
|
|
65
|
+
"@atlaskit/link": "^4.0.0",
|
|
66
|
+
"@atlaskit/modal-dialog": "^16.0.0",
|
|
67
|
+
"@atlaskit/section-message": "^9.0.0",
|
|
68
|
+
"@atlaskit/textfield": "^9.0.0",
|
|
69
|
+
"@atlaskit/toggle": "^17.0.0",
|
|
78
70
|
"@atlassian/feature-flags-test-utils": "^1.1.0",
|
|
79
71
|
"@atlassian/react-compiler-gating": "workspace:^",
|
|
80
72
|
"@atlassian/ssr-tests": "workspace:^",
|