@atlaskit/top-layer 0.8.0 → 0.10.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 +41 -0
- package/README.md +5 -1
- package/dialog/create-close-event/package.json +17 -0
- package/dist/cjs/animations/presets.js +11 -4
- package/dist/cjs/arrow/index.js +131 -0
- package/dist/cjs/arrow/types.js +5 -0
- package/dist/cjs/dialog/dialog-content.js +23 -6
- package/dist/cjs/entry-points/dialog/create-close-event.js +12 -0
- package/dist/cjs/entry-points/placement-map.js +6 -5
- package/dist/cjs/entry-points/popover/create-close-event.js +12 -0
- package/dist/cjs/focus/focus.js +73 -19
- package/dist/cjs/index.js +3 -4
- package/dist/cjs/internal/resolve-css-length-to-pixels.js +61 -10
- package/dist/cjs/internal/role-types.js +40 -9
- package/dist/cjs/internal/set-style.js +23 -25
- package/dist/cjs/internal/use-anchor-position.js +109 -68
- package/dist/cjs/internal/use-focus-wrap.js +13 -2
- package/dist/cjs/internal/use-initial-focus.js +31 -10
- package/dist/cjs/placement-map/index.js +9 -110
- package/dist/cjs/placement-map/legacy-placements.js +113 -0
- package/dist/cjs/popover/popover.js +44 -11
- package/dist/cjs/popup/popup-content.js +19 -13
- package/dist/cjs/popup/popup-context.js +2 -0
- package/dist/cjs/popup/popup-surface.js +6 -0
- package/dist/cjs/popup/popup-trigger-function.js +1 -1
- package/dist/cjs/popup/popup-trigger.js +17 -37
- package/dist/cjs/popup/popup.js +21 -10
- package/dist/cjs/use-arrow-navigation/use-arrow-navigation.js +27 -10
- package/dist/es2019/animations/presets.js +10 -3
- package/dist/es2019/arrow/index.js +269 -0
- package/dist/es2019/arrow/types.js +1 -0
- package/dist/es2019/dialog/dialog-content.js +23 -6
- package/dist/es2019/entry-points/create-close-event.js +7 -0
- package/dist/es2019/entry-points/dialog/create-close-event.js +1 -0
- package/dist/es2019/entry-points/placement-map.js +2 -1
- package/dist/es2019/entry-points/popover/create-close-event.js +1 -0
- package/dist/es2019/focus/focus.js +58 -7
- package/dist/es2019/index.js +3 -4
- package/dist/es2019/internal/resolve-css-length-to-pixels.js +60 -7
- package/dist/es2019/internal/role-types.js +40 -9
- package/dist/es2019/internal/set-style.js +19 -22
- package/dist/es2019/internal/use-anchor-position.js +97 -54
- package/dist/es2019/internal/use-focus-wrap.js +14 -3
- package/dist/es2019/internal/use-initial-focus.js +32 -11
- package/dist/es2019/placement-map/index.js +8 -108
- package/dist/es2019/placement-map/legacy-placements.js +107 -0
- package/dist/es2019/popover/popover.js +44 -10
- package/dist/es2019/popup/popup-content.js +20 -14
- package/dist/es2019/popup/popup-context.js +2 -0
- package/dist/es2019/popup/popup-surface.js +6 -0
- package/dist/es2019/popup/popup-trigger-function.js +1 -1
- package/dist/es2019/popup/popup-trigger.js +17 -37
- package/dist/es2019/popup/popup.js +20 -9
- package/dist/es2019/use-arrow-navigation/use-arrow-navigation.js +27 -10
- package/dist/esm/animations/presets.js +10 -3
- package/dist/esm/arrow/index.js +124 -0
- package/dist/esm/arrow/types.js +1 -0
- package/dist/esm/dialog/dialog-content.js +23 -6
- package/dist/esm/entry-points/create-close-event.js +7 -0
- package/dist/esm/entry-points/dialog/create-close-event.js +1 -0
- package/dist/esm/entry-points/placement-map.js +2 -1
- package/dist/esm/entry-points/popover/create-close-event.js +1 -0
- package/dist/esm/focus/focus.js +72 -19
- package/dist/esm/index.js +3 -4
- package/dist/esm/internal/resolve-css-length-to-pixels.js +61 -10
- package/dist/esm/internal/role-types.js +39 -9
- package/dist/esm/internal/set-style.js +23 -25
- package/dist/esm/internal/use-anchor-position.js +110 -64
- package/dist/esm/internal/use-focus-wrap.js +14 -3
- package/dist/esm/internal/use-initial-focus.js +32 -11
- package/dist/esm/placement-map/index.js +8 -108
- package/dist/esm/placement-map/legacy-placements.js +107 -0
- package/dist/esm/popover/popover.js +44 -11
- package/dist/esm/popup/popup-content.js +20 -14
- package/dist/esm/popup/popup-context.js +2 -0
- package/dist/esm/popup/popup-surface.js +6 -0
- package/dist/esm/popup/popup-trigger-function.js +1 -1
- package/dist/esm/popup/popup-trigger.js +17 -37
- package/dist/esm/popup/popup.js +21 -10
- package/dist/esm/use-arrow-navigation/use-arrow-navigation.js +27 -10
- package/dist/types/arrow/index.d.ts +24 -0
- package/dist/types/arrow/types.d.ts +29 -0
- package/dist/types/dialog/dialog-content.d.ts +5 -1
- package/dist/types/dialog/types.d.ts +50 -24
- package/dist/types/entry-points/create-close-event.d.ts +7 -0
- package/dist/types/entry-points/dialog/create-close-event.d.ts +2 -0
- package/dist/types/entry-points/placement-map.d.ts +2 -1
- package/dist/types/entry-points/popover/create-close-event.d.ts +2 -0
- package/dist/types/focus/focus.d.ts +8 -0
- package/dist/types/internal/resolve-css-length-to-pixels.d.ts +4 -6
- package/dist/types/internal/role-types.d.ts +25 -4
- package/dist/types/internal/set-style.d.ts +2 -19
- package/dist/types/internal/use-anchor-position.d.ts +2 -3
- package/dist/types/internal/use-initial-focus.d.ts +5 -4
- package/dist/types/placement-map/index.d.ts +8 -21
- package/dist/types/placement-map/legacy-placements.d.ts +21 -0
- package/dist/types/popover/popover.d.ts +1 -1
- package/dist/types/popover/types.d.ts +20 -19
- package/dist/types/popup/popup-surface.d.ts +6 -0
- package/dist/types/popup/popup-trigger-function.d.ts +10 -6
- package/dist/types/popup/popup-trigger.d.ts +8 -17
- package/dist/types/popup/popup.d.ts +2 -2
- package/dist/types/popup/types.d.ts +68 -22
- package/dist/types/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
- package/dist/types-ts4.5/arrow/index.d.ts +24 -0
- package/dist/types-ts4.5/arrow/types.d.ts +29 -0
- package/dist/types-ts4.5/dialog/dialog-content.d.ts +5 -1
- package/dist/types-ts4.5/dialog/types.d.ts +50 -24
- package/dist/types-ts4.5/entry-points/create-close-event.d.ts +7 -0
- package/dist/types-ts4.5/entry-points/dialog/create-close-event.d.ts +2 -0
- package/dist/types-ts4.5/entry-points/placement-map.d.ts +2 -1
- package/dist/types-ts4.5/entry-points/popover/create-close-event.d.ts +2 -0
- package/dist/types-ts4.5/focus/focus.d.ts +8 -0
- package/dist/types-ts4.5/internal/resolve-css-length-to-pixels.d.ts +4 -6
- package/dist/types-ts4.5/internal/role-types.d.ts +25 -4
- package/dist/types-ts4.5/internal/set-style.d.ts +2 -19
- package/dist/types-ts4.5/internal/use-anchor-position.d.ts +2 -3
- package/dist/types-ts4.5/internal/use-initial-focus.d.ts +5 -4
- package/dist/types-ts4.5/placement-map/index.d.ts +8 -21
- package/dist/types-ts4.5/placement-map/legacy-placements.d.ts +39 -0
- package/dist/types-ts4.5/popover/popover.d.ts +1 -1
- package/dist/types-ts4.5/popover/types.d.ts +20 -19
- package/dist/types-ts4.5/popup/popup-surface.d.ts +6 -0
- package/dist/types-ts4.5/popup/popup-trigger-function.d.ts +10 -6
- package/dist/types-ts4.5/popup/popup-trigger.d.ts +8 -17
- package/dist/types-ts4.5/popup/popup.d.ts +2 -2
- package/dist/types-ts4.5/popup/types.d.ts +68 -22
- package/dist/types-ts4.5/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
- package/package.json +3 -1
- package/popover/create-close-event/package.json +17 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { bind } from 'bind-event-listener';
|
|
3
|
-
import { getFirstFocusable, getLastFocusable, getNextFocusable } from '../focus/focus';
|
|
3
|
+
import { getFirstFocusable, getLastFocusable, getNextFocusable, isNestedLayerFocused } from '../focus/focus';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Roles that require focus wrapping per WAI-ARIA APG.
|
|
@@ -14,7 +14,7 @@ import { getFirstFocusable, getLastFocusable, getNextFocusable } from '../focus/
|
|
|
14
14
|
* not Tab. Tab/Shift+Tab should move focus OUT of a menu entirely.
|
|
15
15
|
*
|
|
16
16
|
* Menu keyboard behavior (arrow keys, Home/End, type-ahead, Enter/Space)
|
|
17
|
-
* is context-dependent
|
|
17
|
+
* is context-dependent. It varies by orientation, nesting depth, item
|
|
18
18
|
* type, and control pattern (menu button vs menubar). This makes it
|
|
19
19
|
* impossible to implement generically here. The consumer component
|
|
20
20
|
* (e.g. `dropdown-menu`) owns all menu keyboard navigation.
|
|
@@ -55,6 +55,17 @@ export function useFocusWrap({
|
|
|
55
55
|
if (event.key !== 'Tab') {
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
// If focus is already in a nested layer, then
|
|
60
|
+
// we continue to let that nested layer own focus
|
|
61
|
+
if (isNestedLayerFocused({
|
|
62
|
+
container: element
|
|
63
|
+
})) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Prevent the browser giving focus to where it wants to
|
|
68
|
+
// (we will be handling it, including with wrapping)
|
|
58
69
|
event.preventDefault();
|
|
59
70
|
const direction = event.shiftKey ? 'backwards' : 'forwards';
|
|
60
71
|
|
|
@@ -70,7 +81,7 @@ export function useFocusWrap({
|
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
// Nothing is currently focused inside the container (or the
|
|
73
|
-
// focused element
|
|
84
|
+
// focused element is not in the focusable list). Fall back to
|
|
74
85
|
// the first or last focusable element depending on direction.
|
|
75
86
|
const fallback = direction === 'forwards' ? getFirstFocusable({
|
|
76
87
|
container: element
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { getFirstFocusable } from '../focus/focus';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -19,9 +19,9 @@ import { getFirstFocusable } from '../focus/focus';
|
|
|
19
19
|
* React 19+ reflects the JSX prop as the HTML attribute automatically.
|
|
20
20
|
*/
|
|
21
21
|
function getInitialFocusTarget({
|
|
22
|
-
container
|
|
22
|
+
container,
|
|
23
|
+
role
|
|
23
24
|
}) {
|
|
24
|
-
const role = container.getAttribute('role');
|
|
25
25
|
if (role === 'dialog' || role === 'alertdialog') {
|
|
26
26
|
// Prefer element with the native HTML autofocus attribute.
|
|
27
27
|
const autofocusEl = container.querySelector('[autofocus]');
|
|
@@ -62,14 +62,15 @@ function getInitialFocusTarget({
|
|
|
62
62
|
/**
|
|
63
63
|
* Moves focus into a popover element when it opens, based on its ARIA role.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* Follows the WAI-ARIA APG initial-focus patterns for each role:
|
|
66
66
|
* - `dialog` / `alertdialog` → focus first focusable (or `[autofocus]`)
|
|
67
67
|
* - `menu` → focus first menu item
|
|
68
68
|
* - `listbox` → focus selected or first option
|
|
69
|
-
* - `tooltip` / no role → no focus movement
|
|
69
|
+
* - `tooltip` / no role → no focus movement (focus must remain on trigger;
|
|
70
|
+
* moving focus into the tooltip would dismiss it)
|
|
70
71
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* Role is taken from the typed `role` prop - this is the consumer's contract
|
|
73
|
+
* and is not subject to drift from a directly-mutated DOM attribute.
|
|
73
74
|
*
|
|
74
75
|
* Always call this hook unconditionally. Focus is only moved when `isOpen`
|
|
75
76
|
* transitions to `true` and the role requires it.
|
|
@@ -79,6 +80,12 @@ export function useInitialFocus({
|
|
|
79
80
|
isOpen,
|
|
80
81
|
role
|
|
81
82
|
}) {
|
|
83
|
+
// Track the most recently scheduled RAF so a rapid `isOpen → false → true`
|
|
84
|
+
// transition can cancel the previous frame before its closure runs against
|
|
85
|
+
// a stale popover element. Without this, the RAF queued during the first
|
|
86
|
+
// open could fire after the second open had begun and focus the wrong
|
|
87
|
+
// instance.
|
|
88
|
+
const pendingRafRef = useRef(null);
|
|
82
89
|
useEffect(() => {
|
|
83
90
|
if (!isOpen) {
|
|
84
91
|
return;
|
|
@@ -88,6 +95,11 @@ export function useInitialFocus({
|
|
|
88
95
|
return;
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
// Cancel any RAF queued by the previous open cycle.
|
|
99
|
+
if (pendingRafRef.current !== null) {
|
|
100
|
+
cancelAnimationFrame(pendingRafRef.current);
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
// Use requestAnimationFrame to ensure the popover is fully rendered
|
|
92
104
|
// and visible in the top layer before attempting to move focus.
|
|
93
105
|
// showPopover() is called in useLayoutEffect (which runs before this
|
|
@@ -95,15 +107,24 @@ export function useInitialFocus({
|
|
|
95
107
|
// element is guaranteed to be in the top layer.
|
|
96
108
|
const rafId = requestAnimationFrame(() => {
|
|
97
109
|
var _getInitialFocusTarge;
|
|
110
|
+
pendingRafRef.current = null;
|
|
111
|
+
// Verify the element we captured is still the live popover. In
|
|
112
|
+
// rapid-toggle scenarios the ref may already point to a different
|
|
113
|
+
// instance - skip rather than focus the wrong popover.
|
|
114
|
+
if (elementRef.current !== el) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
98
117
|
(_getInitialFocusTarge = getInitialFocusTarget({
|
|
99
|
-
container: el
|
|
118
|
+
container: el,
|
|
119
|
+
role
|
|
100
120
|
})) === null || _getInitialFocusTarge === void 0 ? void 0 : _getInitialFocusTarge.focus();
|
|
101
121
|
});
|
|
122
|
+
pendingRafRef.current = rafId;
|
|
102
123
|
return () => {
|
|
103
124
|
cancelAnimationFrame(rafId);
|
|
125
|
+
if (pendingRafRef.current === rafId) {
|
|
126
|
+
pendingRafRef.current = null;
|
|
127
|
+
}
|
|
104
128
|
};
|
|
105
|
-
// We intentionally depend on `role` even though `getInitialFocusTarget`
|
|
106
|
-
// reads from the DOM attribute. This ensures the effect re-runs if
|
|
107
|
-
// the role prop changes while the popover is open.
|
|
108
129
|
}, [elementRef, isOpen, role]);
|
|
109
130
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Legacy Popper.js placement values (used by `@atlaskit/popper`, `@atlaskit/popup`,
|
|
3
|
-
* `@atlaskit/tooltip`, etc.).
|
|
4
|
-
*/
|
|
1
|
+
import { placementMapping } from './legacy-placements';
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Maps legacy Popper.js placement values to the new object-based
|
|
@@ -13,9 +10,11 @@
|
|
|
13
10
|
* | ------------------- | --------------------------------------------------- |
|
|
14
11
|
* | `top` | `{ axis: 'block', edge: 'start', align: 'center' }` |
|
|
15
12
|
* | `top-start` | `{ axis: 'block', edge: 'start', align: 'start' }` |
|
|
13
|
+
* | `top-center` | `{ axis: 'block', edge: 'start', align: 'center' }` |
|
|
16
14
|
* | `top-end` | `{ axis: 'block', edge: 'start', align: 'end' }` |
|
|
17
15
|
* | `bottom` | `{ axis: 'block', edge: 'end', align: 'center' }` |
|
|
18
16
|
* | `bottom-start` | `{ axis: 'block', edge: 'end', align: 'start' }` |
|
|
17
|
+
* | `bottom-center` | `{ axis: 'block', edge: 'end', align: 'center' }` |
|
|
19
18
|
* | `bottom-end` | `{ axis: 'block', edge: 'end', align: 'end' }` |
|
|
20
19
|
* | `right` | `{ axis: 'inline', edge: 'end', align: 'center' }` |
|
|
21
20
|
* | `right-start` | `{ axis: 'inline', edge: 'end', align: 'start' }` |
|
|
@@ -27,7 +26,10 @@
|
|
|
27
26
|
* | `auto-start` | `{ axis: 'block', edge: 'end', align: 'start' }` |
|
|
28
27
|
* | `auto-end` | `{ axis: 'block', edge: 'end', align: 'end' }` |
|
|
29
28
|
*
|
|
30
|
-
*
|
|
29
|
+
* `auto*` is mapped to `block-end` (a reduction in expressiveness vs Popper's
|
|
30
|
+
* runtime auto-flip). CSS `position-try-fallbacks` provides flipping at the
|
|
31
|
+
* edge - but the initial placement is fixed. If migrating away from Popper's
|
|
32
|
+
* dynamic auto, prefer an explicit placement.
|
|
31
33
|
*
|
|
32
34
|
* @example
|
|
33
35
|
* ```ts
|
|
@@ -39,103 +41,6 @@
|
|
|
39
41
|
* // newPlacement === { axis: 'block', edge: 'end', align: 'start' }
|
|
40
42
|
* ```
|
|
41
43
|
*/
|
|
42
|
-
const placementMapping = {
|
|
43
|
-
top: {
|
|
44
|
-
axis: 'block',
|
|
45
|
-
edge: 'start',
|
|
46
|
-
align: 'center'
|
|
47
|
-
},
|
|
48
|
-
'top-start': {
|
|
49
|
-
axis: 'block',
|
|
50
|
-
edge: 'start',
|
|
51
|
-
align: 'start'
|
|
52
|
-
},
|
|
53
|
-
'top-center': {
|
|
54
|
-
axis: 'block',
|
|
55
|
-
edge: 'start',
|
|
56
|
-
align: 'center'
|
|
57
|
-
},
|
|
58
|
-
'top-end': {
|
|
59
|
-
axis: 'block',
|
|
60
|
-
edge: 'start',
|
|
61
|
-
align: 'end'
|
|
62
|
-
},
|
|
63
|
-
bottom: {
|
|
64
|
-
axis: 'block',
|
|
65
|
-
edge: 'end',
|
|
66
|
-
align: 'center'
|
|
67
|
-
},
|
|
68
|
-
'bottom-start': {
|
|
69
|
-
axis: 'block',
|
|
70
|
-
edge: 'end',
|
|
71
|
-
align: 'start'
|
|
72
|
-
},
|
|
73
|
-
'bottom-center': {
|
|
74
|
-
axis: 'block',
|
|
75
|
-
edge: 'end',
|
|
76
|
-
align: 'center'
|
|
77
|
-
},
|
|
78
|
-
'bottom-end': {
|
|
79
|
-
axis: 'block',
|
|
80
|
-
edge: 'end',
|
|
81
|
-
align: 'end'
|
|
82
|
-
},
|
|
83
|
-
right: {
|
|
84
|
-
axis: 'inline',
|
|
85
|
-
edge: 'end',
|
|
86
|
-
align: 'center'
|
|
87
|
-
},
|
|
88
|
-
'right-start': {
|
|
89
|
-
axis: 'inline',
|
|
90
|
-
edge: 'end',
|
|
91
|
-
align: 'start'
|
|
92
|
-
},
|
|
93
|
-
'right-end': {
|
|
94
|
-
axis: 'inline',
|
|
95
|
-
edge: 'end',
|
|
96
|
-
align: 'end'
|
|
97
|
-
},
|
|
98
|
-
left: {
|
|
99
|
-
axis: 'inline',
|
|
100
|
-
edge: 'start',
|
|
101
|
-
align: 'center'
|
|
102
|
-
},
|
|
103
|
-
'left-start': {
|
|
104
|
-
axis: 'inline',
|
|
105
|
-
edge: 'start',
|
|
106
|
-
align: 'start'
|
|
107
|
-
},
|
|
108
|
-
'left-end': {
|
|
109
|
-
axis: 'inline',
|
|
110
|
-
edge: 'start',
|
|
111
|
-
align: 'end'
|
|
112
|
-
},
|
|
113
|
-
auto: {
|
|
114
|
-
axis: 'block',
|
|
115
|
-
edge: 'end',
|
|
116
|
-
align: 'center'
|
|
117
|
-
},
|
|
118
|
-
'auto-start': {
|
|
119
|
-
axis: 'block',
|
|
120
|
-
edge: 'end',
|
|
121
|
-
align: 'start'
|
|
122
|
-
},
|
|
123
|
-
'auto-end': {
|
|
124
|
-
axis: 'block',
|
|
125
|
-
edge: 'end',
|
|
126
|
-
align: 'end'
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Convert a legacy Popper.js placement to the new `position-area`-based Placement.
|
|
132
|
-
* Optional `offset` is the legacy Popper `[along, away]` tuple (also known as
|
|
133
|
-
* `[skidding, distance]`); negative `along` becomes `direction: 'backwards'`.
|
|
134
|
-
*
|
|
135
|
-
* The tuple keeps the historical popper names so call sites that already pass
|
|
136
|
-
* `[along, away]` migrate without thinking; the new internal field names
|
|
137
|
-
* (`gap` / `crossAxisShift`) are produced inside this adapter.
|
|
138
|
-
*/
|
|
139
44
|
export function fromLegacyPlacement({
|
|
140
45
|
legacy,
|
|
141
46
|
offset
|
|
@@ -156,9 +61,4 @@ export function fromLegacyPlacement({
|
|
|
156
61
|
}
|
|
157
62
|
}
|
|
158
63
|
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* The full mapping record, exposed for consumers that need to iterate or introspect.
|
|
163
|
-
*/
|
|
164
|
-
export { placementMapping };
|
|
64
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy Popper.js placement values (used by `@atlaskit/popper`,
|
|
3
|
+
* `@atlaskit/popup`, `@atlaskit/tooltip`, etc.). Re-exported from
|
|
4
|
+
* `@atlaskit/top-layer/placement-map` for migration consumers that need
|
|
5
|
+
* to enumerate every placement (e.g. for a settings dropdown). New code
|
|
6
|
+
* should use the object-based `TPlacementOptions` directly.
|
|
7
|
+
*/
|
|
8
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
9
|
+
export const LEGACY_PLACEMENTS = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-center', 'top-end', 'bottom', 'bottom-start', 'bottom-center', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'];
|
|
10
|
+
/**
|
|
11
|
+
* The full mapping record. Internal - use `fromLegacyPlacement` from the
|
|
12
|
+
* public entry-point so the offset-tuple translation is applied consistently.
|
|
13
|
+
*
|
|
14
|
+
* Note: `auto`, `auto-start`, `auto-end` map to `block-end` placements. This
|
|
15
|
+
* is a reduction in expressiveness vs Popper.js (which picked the best
|
|
16
|
+
* placement at runtime). CSS `position-try-fallbacks` flips at the edge but
|
|
17
|
+
* the initial placement is fixed. Migration consumers who relied on Popper's
|
|
18
|
+
* dynamic auto-flip should pick an explicit placement instead.
|
|
19
|
+
*/
|
|
20
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
21
|
+
export const placementMapping = {
|
|
22
|
+
top: {
|
|
23
|
+
axis: 'block',
|
|
24
|
+
edge: 'start',
|
|
25
|
+
align: 'center'
|
|
26
|
+
},
|
|
27
|
+
'top-start': {
|
|
28
|
+
axis: 'block',
|
|
29
|
+
edge: 'start',
|
|
30
|
+
align: 'start'
|
|
31
|
+
},
|
|
32
|
+
'top-center': {
|
|
33
|
+
axis: 'block',
|
|
34
|
+
edge: 'start',
|
|
35
|
+
align: 'center'
|
|
36
|
+
},
|
|
37
|
+
'top-end': {
|
|
38
|
+
axis: 'block',
|
|
39
|
+
edge: 'start',
|
|
40
|
+
align: 'end'
|
|
41
|
+
},
|
|
42
|
+
bottom: {
|
|
43
|
+
axis: 'block',
|
|
44
|
+
edge: 'end',
|
|
45
|
+
align: 'center'
|
|
46
|
+
},
|
|
47
|
+
'bottom-start': {
|
|
48
|
+
axis: 'block',
|
|
49
|
+
edge: 'end',
|
|
50
|
+
align: 'start'
|
|
51
|
+
},
|
|
52
|
+
'bottom-center': {
|
|
53
|
+
axis: 'block',
|
|
54
|
+
edge: 'end',
|
|
55
|
+
align: 'center'
|
|
56
|
+
},
|
|
57
|
+
'bottom-end': {
|
|
58
|
+
axis: 'block',
|
|
59
|
+
edge: 'end',
|
|
60
|
+
align: 'end'
|
|
61
|
+
},
|
|
62
|
+
right: {
|
|
63
|
+
axis: 'inline',
|
|
64
|
+
edge: 'end',
|
|
65
|
+
align: 'center'
|
|
66
|
+
},
|
|
67
|
+
'right-start': {
|
|
68
|
+
axis: 'inline',
|
|
69
|
+
edge: 'end',
|
|
70
|
+
align: 'start'
|
|
71
|
+
},
|
|
72
|
+
'right-end': {
|
|
73
|
+
axis: 'inline',
|
|
74
|
+
edge: 'end',
|
|
75
|
+
align: 'end'
|
|
76
|
+
},
|
|
77
|
+
left: {
|
|
78
|
+
axis: 'inline',
|
|
79
|
+
edge: 'start',
|
|
80
|
+
align: 'center'
|
|
81
|
+
},
|
|
82
|
+
'left-start': {
|
|
83
|
+
axis: 'inline',
|
|
84
|
+
edge: 'start',
|
|
85
|
+
align: 'start'
|
|
86
|
+
},
|
|
87
|
+
'left-end': {
|
|
88
|
+
axis: 'inline',
|
|
89
|
+
edge: 'start',
|
|
90
|
+
align: 'end'
|
|
91
|
+
},
|
|
92
|
+
auto: {
|
|
93
|
+
axis: 'block',
|
|
94
|
+
edge: 'end',
|
|
95
|
+
align: 'center'
|
|
96
|
+
},
|
|
97
|
+
'auto-start': {
|
|
98
|
+
axis: 'block',
|
|
99
|
+
edge: 'end',
|
|
100
|
+
align: 'start'
|
|
101
|
+
},
|
|
102
|
+
'auto-end': {
|
|
103
|
+
axis: 'block',
|
|
104
|
+
edge: 'end',
|
|
105
|
+
align: 'end'
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -5,8 +5,8 @@ import { ax, ix } from "@compiled/react/runtime";
|
|
|
5
5
|
import React, { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef } from 'react';
|
|
6
6
|
import { bind } from 'bind-event-listener';
|
|
7
7
|
import mergeRefs from '@atlaskit/ds-lib/merge-refs';
|
|
8
|
-
import noop from '@atlaskit/ds-lib/noop';
|
|
9
8
|
import once from '@atlaskit/ds-lib/once';
|
|
9
|
+
import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
|
|
10
10
|
import { useAnimatedVisibility } from '../internal/use-animated-visibility';
|
|
11
11
|
import { useFocusWrap } from '../internal/use-focus-wrap';
|
|
12
12
|
import { useInitialFocus } from '../internal/use-initial-focus';
|
|
@@ -29,6 +29,18 @@ const supportsPopoverHint = once(() => {
|
|
|
29
29
|
const styles = {
|
|
30
30
|
root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28"
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* ARIA roles on the Popover primitive that register with the open layer observer
|
|
34
|
+
* as LayerType 'popup'. These are interactive overlays that should be tracked
|
|
35
|
+
* and dismissed when closeLayers() is called (e.g. when the side nav resizes).
|
|
36
|
+
*
|
|
37
|
+
* Note on open layer observer types not included here:
|
|
38
|
+
* - 'modal' only ever comes from the Dialog primitive, not Popover.
|
|
39
|
+
* - Other layer types (e.g. 'tooltip') do not yet exist in LayerType because
|
|
40
|
+
* there is no current use case for distinguishing them in the observer.
|
|
41
|
+
*/
|
|
42
|
+
const POPUP_ROLES = new Set(['menu', 'listbox', 'dialog', 'alertdialog', 'tree', 'grid']);
|
|
43
|
+
|
|
32
44
|
/**
|
|
33
45
|
* Unopinionated top-layer primitive.
|
|
34
46
|
*
|
|
@@ -43,13 +55,13 @@ const styles = {
|
|
|
43
55
|
*
|
|
44
56
|
* For `mode="auto"`, the browser can dismiss the popover via light dismiss
|
|
45
57
|
* (Escape, click outside). When that happens, `onClose` is called and the
|
|
46
|
-
* consumer must set `isOpen` to `false`. The DOM owns the dismiss
|
|
58
|
+
* consumer must set `isOpen` to `false`. The DOM owns the dismiss; React
|
|
47
59
|
* must follow.
|
|
48
60
|
*/
|
|
49
61
|
export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
50
62
|
children,
|
|
51
63
|
mode: modeProp = 'auto',
|
|
52
|
-
onClose
|
|
64
|
+
onClose,
|
|
53
65
|
onOpenChange,
|
|
54
66
|
onExitFinish,
|
|
55
67
|
animate,
|
|
@@ -66,7 +78,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
66
78
|
const autoId = useId();
|
|
67
79
|
const ownRef = useRef(null);
|
|
68
80
|
const combinedRef = mergeRefs([ownRef, ref].filter(Boolean));
|
|
69
|
-
// Regex is intentional
|
|
81
|
+
// Regex is intentional: `replaceAll` is not available in our supported
|
|
70
82
|
// browser matrix. In React 18, `useId()` returns IDs containing colons
|
|
71
83
|
// (e.g. `:r1:`) which are invalid in CSS selectors and `popover` target
|
|
72
84
|
// attributes. React 19.2.0 replaced colons with underscores, making this
|
|
@@ -85,7 +97,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
85
97
|
// ── Focus management ──
|
|
86
98
|
// Initial focus: on open, moves focus into the popover (role-dependent).
|
|
87
99
|
// Focus wrap: Tab/Shift+Tab cycle within the popover for dialog roles.
|
|
88
|
-
// Focus restoration: handled natively by the Popover API
|
|
100
|
+
// Focus restoration: handled natively by the Popover API. The browser
|
|
89
101
|
// tracks `previouslyFocusedElement` on show and restores it on hide.
|
|
90
102
|
// No custom restoration hook is needed. See: notes/architecture/focus-restoration.md
|
|
91
103
|
useFocusWrap({
|
|
@@ -98,6 +110,24 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
98
110
|
role
|
|
99
111
|
});
|
|
100
112
|
|
|
113
|
+
// ── Open layer observer registration ──
|
|
114
|
+
// Notifies the open layer observer so app-coordination features
|
|
115
|
+
// (closeLayers(), open-count subscriptions) work with top-layer popovers.
|
|
116
|
+
//
|
|
117
|
+
// Only set type: 'popup' for roles that are interactive overlay popups.
|
|
118
|
+
// Passive/informational roles (tooltip, note, status, alert, log) and
|
|
119
|
+
// no-role cases do not map to LayerType 'popup', so type is left undefined.
|
|
120
|
+
const handleObserverClose = useCallback(() => {
|
|
121
|
+
onClose === null || onClose === void 0 ? void 0 : onClose({
|
|
122
|
+
reason: 'programmatic'
|
|
123
|
+
});
|
|
124
|
+
}, [onClose]);
|
|
125
|
+
useNotifyOpenLayerObserver({
|
|
126
|
+
type: role && POPUP_ROLES.has(role) ? 'popup' : undefined,
|
|
127
|
+
isOpen,
|
|
128
|
+
onClose: handleObserverClose
|
|
129
|
+
});
|
|
130
|
+
|
|
101
131
|
// Hint mode fallback: "hint" downgrades to "auto" if unsupported.
|
|
102
132
|
//
|
|
103
133
|
// Decision (2026-03-17 audit): We keep "auto" as the fallback (not "manual")
|
|
@@ -110,8 +140,8 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
110
140
|
const mode = modeProp === 'hint' && !supportsPopoverHint() ? 'auto' : modeProp;
|
|
111
141
|
|
|
112
142
|
// Distinguishes programmatic hidePopover() from browser-initiated dismiss.
|
|
113
|
-
// When we call hidePopover() ourselves the beforetoggle event fires synchronously
|
|
114
|
-
// this ref prevents the toggle handler from calling onClose redundantly.
|
|
143
|
+
// When we call hidePopover() ourselves the beforetoggle event fires synchronously,
|
|
144
|
+
// so this ref prevents the toggle handler from calling onClose redundantly.
|
|
115
145
|
const programmaticCloseRef = useRef(false);
|
|
116
146
|
|
|
117
147
|
// Escape key tracking: set in a capture-phase keydown listener (fires before
|
|
@@ -144,7 +174,11 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
144
174
|
// Browser dismiss (Escape or click-outside). Notify consumer.
|
|
145
175
|
const reason = closeReasonRef.current;
|
|
146
176
|
closeReasonRef.current = 'light-dismiss'; // reset for next dismiss
|
|
147
|
-
onClose
|
|
177
|
+
// `onClose` is required for `auto`/`hint` and forbidden for
|
|
178
|
+
// `manual`, but `TPopoverForwardedProps` is a flat type used
|
|
179
|
+
// internally by `PopupContent` - the optional check guards
|
|
180
|
+
// the manual-mode forwarded path where the field is undefined.
|
|
181
|
+
onClose === null || onClose === void 0 ? void 0 : onClose({
|
|
148
182
|
reason
|
|
149
183
|
});
|
|
150
184
|
}
|
|
@@ -179,7 +213,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
179
213
|
|
|
180
214
|
// Placement-dependent animation CSS vars (e.g. slide direction).
|
|
181
215
|
// Separate from show/hide so that preset reference changes
|
|
182
|
-
// (e.g. inline `animate={slideAndFade()}`)
|
|
216
|
+
// (e.g. inline `animate={slideAndFade()}`) do not re-trigger visibility.
|
|
183
217
|
useLayoutEffect(() => {
|
|
184
218
|
const el = ownRef.current;
|
|
185
219
|
if (!el || !(preset !== null && preset !== void 0 && preset.getProperties) || !placement) {
|
|
@@ -198,7 +232,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
|
|
|
198
232
|
// in the target state (e.g. browser dismissed via light-dismiss before our
|
|
199
233
|
// effect ran).
|
|
200
234
|
// Defensive try/catch: these APIs throw InvalidStateError if the element
|
|
201
|
-
// is disconnected or missing the popover attribute. While we
|
|
235
|
+
// is disconnected or missing the popover attribute. While we do not expect
|
|
202
236
|
// that to happen in normal usage, it can occur in edge cases (e.g. React
|
|
203
237
|
// concurrent mode, StrictMode double-effects, or unmount races).
|
|
204
238
|
useLayoutEffect(() => {
|
|
@@ -3,7 +3,7 @@ import _extends from "@babel/runtime/helpers/extends";
|
|
|
3
3
|
import { ax, ix } from "@compiled/react/runtime";
|
|
4
4
|
import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
|
|
5
5
|
import mergeRefs from '@atlaskit/ds-lib/merge-refs';
|
|
6
|
-
import { roleToAriaHasPopup } from '../internal/role-types';
|
|
6
|
+
import { roleToAriaHasPopup, shouldFocusIntoPopover } from '../internal/role-types';
|
|
7
7
|
import { useAnchorPosition } from '../internal/use-anchor-position';
|
|
8
8
|
import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
|
|
9
9
|
import { Popover } from '../popover/popover';
|
|
@@ -29,7 +29,8 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
29
29
|
placement: placementProp,
|
|
30
30
|
onClose: onCloseProp,
|
|
31
31
|
testId: testIdProp,
|
|
32
|
-
forceFallbackPositioning: forceFallbackPositioningProp
|
|
32
|
+
forceFallbackPositioning: forceFallbackPositioningProp,
|
|
33
|
+
id: idProp
|
|
33
34
|
}, ref) {
|
|
34
35
|
var _ref, _ref2, _ctx$popoverRef;
|
|
35
36
|
// When inside <Popup>, context provides defaults. Props always win.
|
|
@@ -74,7 +75,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
74
75
|
// does not restore focus when they close.
|
|
75
76
|
//
|
|
76
77
|
// To handle this, we listen for the close transition via `onOpenChange` and
|
|
77
|
-
// restore focus to the trigger if the browser
|
|
78
|
+
// restore focus to the trigger if the browser did not. This runs after the
|
|
78
79
|
// browser's native restoration (which happens synchronously during hidePopover),
|
|
79
80
|
// so it only takes effect when the browser skipped restoration (nested case).
|
|
80
81
|
//
|
|
@@ -91,7 +92,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
91
92
|
// focus when they close.
|
|
92
93
|
//
|
|
93
94
|
// We detect this case by checking if focus is still inside the
|
|
94
|
-
// closing popover element. If it is, the browser
|
|
95
|
+
// closing popover element. If it is, the browser did not restore,
|
|
95
96
|
// and we need to move focus to the trigger ourselves.
|
|
96
97
|
//
|
|
97
98
|
// This does NOT fire for:
|
|
@@ -100,13 +101,15 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
100
101
|
if (!args.isOpen) {
|
|
101
102
|
const trigger = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current;
|
|
102
103
|
const focusStillInPopover = trigger && args.element.contains(document.activeElement);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
// Single source of truth for "this role moves focus on open
|
|
105
|
+
// and therefore needs to be restored on close" - see
|
|
106
|
+
// `shouldFocusIntoPopover` in `internal/role-types.tsx`.
|
|
107
|
+
if (focusStillInPopover && shouldFocusIntoPopover({
|
|
108
|
+
role
|
|
109
|
+
})) {
|
|
110
|
+
trigger.focus({
|
|
111
|
+
preventScroll: true
|
|
112
|
+
});
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
}, [setIsOpen, consumerOnOpenChange, triggerRef, role]);
|
|
@@ -136,8 +139,8 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
136
139
|
const combinedRef = mergeRefs([popoverRef, ref]);
|
|
137
140
|
|
|
138
141
|
// TPopupContentProps already validates the discriminated union (role + label,
|
|
139
|
-
// mode + onClose) at its API boundary. Popover accepts
|
|
140
|
-
// internally so individual prop forwarding
|
|
142
|
+
// mode + onClose) at its API boundary. Popover accepts TPopoverForwardedProps
|
|
143
|
+
// internally so individual prop forwarding does not need to re-prove the union.
|
|
141
144
|
const popoverProps = {
|
|
142
145
|
isOpen,
|
|
143
146
|
animate,
|
|
@@ -150,7 +153,10 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
150
153
|
onOpenChange: handleOpenChange,
|
|
151
154
|
testId,
|
|
152
155
|
children,
|
|
153
|
-
|
|
156
|
+
// Standalone consumers can pass an explicit `id` - used by their own
|
|
157
|
+
// trigger to wire `aria-controls`. Inside the `<Popup>` compound, the
|
|
158
|
+
// id flows from context (`ctx.popoverId`).
|
|
159
|
+
id: idProp !== null && idProp !== void 0 ? idProp : ctx === null || ctx === void 0 ? void 0 : ctx.popoverId,
|
|
154
160
|
xcss
|
|
155
161
|
};
|
|
156
162
|
return /*#__PURE__*/React.createElement(Popover, _extends({
|
|
@@ -15,6 +15,7 @@ export const PopupProvider = PopupContext.Provider;
|
|
|
15
15
|
*
|
|
16
16
|
* Throws if called outside of a `<Popup>` compound component.
|
|
17
17
|
*/
|
|
18
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
18
19
|
export function usePopupContext() {
|
|
19
20
|
const ctx = useContext(PopupContext);
|
|
20
21
|
if (ctx === null) {
|
|
@@ -29,6 +30,7 @@ export function usePopupContext() {
|
|
|
29
30
|
* Used by `PopupContent` to support standalone usage (e.g. tooltip)
|
|
30
31
|
* where values are passed as props instead of coming from a compound component.
|
|
31
32
|
*/
|
|
33
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
32
34
|
export function useMaybePopupContext() {
|
|
33
35
|
return useContext(PopupContext);
|
|
34
36
|
}
|
|
@@ -14,6 +14,12 @@ const styles = {
|
|
|
14
14
|
* should use `Popup.Content` without wrapping in `PopupSurface`.
|
|
15
15
|
*
|
|
16
16
|
* Exposed as `Popup.Surface` on the compound component.
|
|
17
|
+
*
|
|
18
|
+
* **Presentational primitive - `children`-only by design.** This component
|
|
19
|
+
* intentionally exposes no `style`, `className`, `xcss`, `id`, or `ref`
|
|
20
|
+
* props. If you need any of those, render your own wrapper element inside
|
|
21
|
+
* `Popup.Content` instead - `PopupSurface` is the no-config "give me the
|
|
22
|
+
* default ADS overlay look" surface, nothing more.
|
|
17
23
|
*/
|
|
18
24
|
export function PopupSurface({
|
|
19
25
|
children
|
|
@@ -8,7 +8,7 @@ import { usePopupContext } from './popup-context';
|
|
|
8
8
|
/**
|
|
9
9
|
* Render-prop alternative to `Popup.Trigger`.
|
|
10
10
|
*
|
|
11
|
-
* Use when you need full control over the trigger element
|
|
11
|
+
* Use when you need full control over the trigger element. For example,
|
|
12
12
|
* to compose your own trigger with custom event handlers, wrap a third-party
|
|
13
13
|
* component, or build a trigger from multiple elements.
|
|
14
14
|
*
|