@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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* restrict which focusable elements are included.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { getDocument } from '@atlaskit/browser-apis';
|
|
13
14
|
const selectors = (() => {
|
|
14
15
|
// Common exclusion filter applied to every selector.
|
|
15
16
|
const not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
|
|
@@ -17,9 +18,54 @@ const selectors = (() => {
|
|
|
17
18
|
const focusable = [`a[href]${not}`, `area[href]${not}`, `input${notDisabled}:not([type="hidden"])`, `select${notDisabled}`, `textarea${notDisabled}`, `button${notDisabled}`, `iframe${not}`, `audio[controls]${not}`, `video[controls]${not}`, `[contenteditable]:not([contenteditable="false"])${not}`, `[tabindex]${notDisabled}${not}`];
|
|
18
19
|
return {
|
|
19
20
|
focusable: focusable.join(','),
|
|
20
|
-
|
|
21
|
+
// Build the `:focus` selector
|
|
22
|
+
focused: focusable.map(selector => `${selector}:focus`).join(',')
|
|
21
23
|
};
|
|
22
24
|
})();
|
|
25
|
+
const topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when `element` and `container` resolve to the same nearest
|
|
29
|
+
* top-layer host (or both resolve to `null`, meaning the document layer).
|
|
30
|
+
*/
|
|
31
|
+
function isInSameLayer({
|
|
32
|
+
element,
|
|
33
|
+
container
|
|
34
|
+
}) {
|
|
35
|
+
const elementLayer = element.closest(topLayerSelector);
|
|
36
|
+
const containerLayer = container.closest(topLayerSelector);
|
|
37
|
+
// Both elements are in the "document" layer
|
|
38
|
+
if (!elementLayer && !containerLayer) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return elementLayer === containerLayer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
46
|
+
* descendant of `container` (a `[popover]`, `<dialog>`, `[role="dialog"]` or
|
|
47
|
+
* `[role="alertdialog"]` that is itself a descendant of `container`).
|
|
48
|
+
*/
|
|
49
|
+
export function isNestedLayerFocused({
|
|
50
|
+
container
|
|
51
|
+
}) {
|
|
52
|
+
var _getDocument;
|
|
53
|
+
const focused = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
|
|
54
|
+
if (!(focused instanceof HTMLElement)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Cannot be in a nested layer if it is not in the container
|
|
59
|
+
if (!container.contains(focused)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If not in the same layer, must be in a nested layer
|
|
64
|
+
return !isInSameLayer({
|
|
65
|
+
element: focused,
|
|
66
|
+
container
|
|
67
|
+
});
|
|
68
|
+
}
|
|
23
69
|
|
|
24
70
|
/**
|
|
25
71
|
* An optional callback to restrict which focusable elements are included.
|
|
@@ -29,22 +75,25 @@ const selectors = (() => {
|
|
|
29
75
|
/**
|
|
30
76
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
31
77
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* is
|
|
36
|
-
* assumption changes, this would need to be scoped to the current layer.
|
|
78
|
+
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
79
|
+
* `<dialog>` descendant of the container) are excluded - those elements
|
|
80
|
+
* are owned by the inner layer's focus management. The container itself
|
|
81
|
+
* is allowed to be a popover/dialog; only nested ones are filtered.
|
|
37
82
|
*/
|
|
38
83
|
function getFocusables({
|
|
39
84
|
container,
|
|
40
85
|
filter
|
|
41
86
|
}) {
|
|
42
|
-
return Array.from(container.querySelectorAll(selectors.focusable)).filter(element => element instanceof HTMLElement).filter(element =>
|
|
87
|
+
return Array.from(container.querySelectorAll(selectors.focusable)).filter(element => element instanceof HTMLElement).filter(element => isInSameLayer({
|
|
88
|
+
element,
|
|
89
|
+
container
|
|
90
|
+
})).filter(element => !filter || filter(element, container));
|
|
43
91
|
}
|
|
44
92
|
|
|
45
93
|
/**
|
|
46
94
|
* Returns the first focusable element within the container.
|
|
47
95
|
*/
|
|
96
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
48
97
|
export function getFirstFocusable({
|
|
49
98
|
container,
|
|
50
99
|
filter
|
|
@@ -59,6 +108,7 @@ export function getFirstFocusable({
|
|
|
59
108
|
/**
|
|
60
109
|
* Returns the last focusable element within the container.
|
|
61
110
|
*/
|
|
111
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
62
112
|
export function getLastFocusable({
|
|
63
113
|
container,
|
|
64
114
|
filter
|
|
@@ -77,6 +127,7 @@ export function getLastFocusable({
|
|
|
77
127
|
* Returns `null` if there is no focused element within the container or if
|
|
78
128
|
* the focused element is not in the list of focusable elements.
|
|
79
129
|
*/
|
|
130
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
80
131
|
export function getNextFocusable({
|
|
81
132
|
container,
|
|
82
133
|
direction,
|
package/dist/es2019/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// This file exists for module resolution purposes.
|
|
2
2
|
// Not exporting anything from the root entry point.
|
|
3
|
-
// Please use individual entry points for minimum bundle size:
|
|
4
3
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// Use individual entry points for minimum bundle size - the full list of
|
|
5
|
+
// available subpaths is the `"exports"` field in this package's
|
|
6
|
+
// `package.json`. The README documents the most common ones.
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
const elementCaches = new WeakMap();
|
|
2
|
+
function canCacheResolvedLength(value) {
|
|
3
|
+
// Custom-property values can be retargeted by inline overrides on the
|
|
4
|
+
// consumer's wrapper, so we cannot prove a cached pixel value is still valid.
|
|
5
|
+
return !value.includes('var(');
|
|
6
|
+
}
|
|
7
|
+
function getCachedResolvedLength({
|
|
8
|
+
element,
|
|
9
|
+
value
|
|
10
|
+
}) {
|
|
11
|
+
var _cacheForElement$get;
|
|
12
|
+
if (!canCacheResolvedLength(value)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const cacheForElement = elementCaches.get(element);
|
|
16
|
+
return (_cacheForElement$get = cacheForElement === null || cacheForElement === void 0 ? void 0 : cacheForElement.get(value)) !== null && _cacheForElement$get !== void 0 ? _cacheForElement$get : null;
|
|
17
|
+
}
|
|
18
|
+
function cacheResolvedLength({
|
|
19
|
+
element,
|
|
20
|
+
value,
|
|
21
|
+
result
|
|
22
|
+
}) {
|
|
23
|
+
var _elementCaches$get;
|
|
24
|
+
if (!canCacheResolvedLength(value)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const cacheForElement = (_elementCaches$get = elementCaches.get(element)) !== null && _elementCaches$get !== void 0 ? _elementCaches$get : new Map();
|
|
28
|
+
cacheForElement.set(value, result);
|
|
29
|
+
elementCaches.set(element, cacheForElement);
|
|
30
|
+
}
|
|
31
|
+
|
|
1
32
|
/**
|
|
2
33
|
* Resolves a CSS length value to a pixel number using a hidden DOM probe.
|
|
3
34
|
*
|
|
@@ -8,13 +39,11 @@
|
|
|
8
39
|
* the browser do the math.
|
|
9
40
|
*
|
|
10
41
|
* The probe is appended to `container` so it inherits the same containing
|
|
11
|
-
* block, font size, and custom-property scope as the popover.
|
|
12
|
-
* because it accepts any CSS length, including negative `calc()` results
|
|
13
|
-
* (negative widths are clamped to 0 — see note below).
|
|
42
|
+
* block, font size, and custom-property scope as the popover.
|
|
14
43
|
*
|
|
15
|
-
* Cost: one synchronous reflow
|
|
16
|
-
*
|
|
17
|
-
*
|
|
44
|
+
* Cost: one synchronous reflow on first cacheable resolution per
|
|
45
|
+
* (container, value) pair, then a `WeakMap` lookup. Resolutions involving
|
|
46
|
+
* `var(` are intentionally not cached.
|
|
18
47
|
*/
|
|
19
48
|
export function resolveCssLengthToPixels({
|
|
20
49
|
value,
|
|
@@ -24,12 +53,26 @@ export function resolveCssLengthToPixels({
|
|
|
24
53
|
return value;
|
|
25
54
|
}
|
|
26
55
|
|
|
56
|
+
// Fast path: unitless zero is universally zero pixels - accept `'0'` and
|
|
57
|
+
// `'-0'` without a probe. (Consumers occasionally pass `'0'` instead of
|
|
58
|
+
// the number `0`.)
|
|
59
|
+
if (value === '0' || value === '-0') {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
27
63
|
// Fast path: bare `${n}px` strings come from the consumer normalisation
|
|
28
64
|
// in `getPlacement`. Avoid a reflow when we can.
|
|
29
65
|
const pxMatch = value.match(/^(-?\d+(?:\.\d+)?)px$/);
|
|
30
66
|
if (pxMatch) {
|
|
31
67
|
return Number(pxMatch[1]);
|
|
32
68
|
}
|
|
69
|
+
const cached = getCachedResolvedLength({
|
|
70
|
+
element: container,
|
|
71
|
+
value
|
|
72
|
+
});
|
|
73
|
+
if (cached !== null) {
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
33
76
|
const probe = container.ownerDocument.createElement('div');
|
|
34
77
|
probe.style.position = 'absolute';
|
|
35
78
|
probe.style.visibility = 'hidden';
|
|
@@ -41,8 +84,18 @@ export function resolveCssLengthToPixels({
|
|
|
41
84
|
// preserved. `getComputedStyle` returns the resolved pixel value for
|
|
42
85
|
// margin-* properties even when the source was a token / calc / var.
|
|
43
86
|
probe.style.marginLeft = value;
|
|
87
|
+
// Hide the probe from assistive tech and event targeting; defensive
|
|
88
|
+
// against `getElementsByTagName('div')` walks in consumer code.
|
|
89
|
+
probe.setAttribute('aria-hidden', 'true');
|
|
90
|
+
probe.setAttribute('inert', '');
|
|
44
91
|
container.appendChild(probe);
|
|
45
92
|
const resolved = parseFloat(getComputedStyle(probe).marginLeft);
|
|
46
93
|
container.removeChild(probe);
|
|
47
|
-
|
|
94
|
+
const result = Number.isFinite(resolved) ? resolved : 0;
|
|
95
|
+
cacheResolvedLength({
|
|
96
|
+
element: container,
|
|
97
|
+
value,
|
|
98
|
+
result
|
|
99
|
+
});
|
|
100
|
+
return result;
|
|
48
101
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Valid values for the `aria-haspopup` attribute.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Derived from the HTML spec - maps popover roles to what the trigger
|
|
5
|
+
* announces. `undefined` means the attribute is omitted entirely (used for
|
|
6
|
+
* non-popup roles like `tooltip`/`status`/`alert`/`note`/`log` where
|
|
7
|
+
* `aria-haspopup` would be misleading).
|
|
8
|
+
*
|
|
9
|
+
* `true` is intentionally NOT in the union - the runtime only ever produces
|
|
10
|
+
* the explicit string forms, so widening the type would invite consumers to
|
|
11
|
+
* pass `true` and get an `aria-haspopup="true"` serialisation that the
|
|
12
|
+
* runtime no longer emits.
|
|
4
13
|
*/
|
|
5
14
|
|
|
6
15
|
/**
|
|
@@ -22,21 +31,27 @@
|
|
|
22
31
|
/**
|
|
23
32
|
* ARIA role props where a role is always required.
|
|
24
33
|
*
|
|
25
|
-
* Used by `TPopupContentProps
|
|
34
|
+
* Used by `TPopupContentProps`. Popup content must always declare
|
|
26
35
|
* its role so assistive technology can identify the layer.
|
|
27
36
|
*/
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
39
|
* ARIA role props where a role is optional.
|
|
31
40
|
*
|
|
32
|
-
* Used by `TPopoverProps
|
|
41
|
+
* Used by `TPopoverProps`. The low-level primitive allows omitting
|
|
33
42
|
* the role for cases where it is set externally or not needed.
|
|
34
43
|
*/
|
|
35
44
|
|
|
36
45
|
/**
|
|
37
46
|
* Maps a popover content role to the correct `aria-haspopup` value
|
|
38
47
|
* for its trigger element.
|
|
48
|
+
*
|
|
49
|
+
* Returns `undefined` for roles that are not popups in the ARIA sense
|
|
50
|
+
* (`tooltip`, `note`, `status`, `alert`, `log`). Triggers for those roles
|
|
51
|
+
* should not announce a popup at all - emitting `aria-haspopup="dialog"`
|
|
52
|
+
* on a tooltip trigger would mislead assistive technology.
|
|
39
53
|
*/
|
|
54
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
40
55
|
export function roleToAriaHasPopup({
|
|
41
56
|
role
|
|
42
57
|
}) {
|
|
@@ -52,10 +67,26 @@ export function roleToAriaHasPopup({
|
|
|
52
67
|
if (role === 'grid') {
|
|
53
68
|
return 'grid';
|
|
54
69
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
70
|
+
if (role === 'dialog' || role === 'alertdialog') {
|
|
71
|
+
return 'dialog';
|
|
72
|
+
}
|
|
73
|
+
// Non-popup roles (tooltip, note, status, alert, log) and the
|
|
74
|
+
// no-role case: omit `aria-haspopup` entirely. React drops the
|
|
75
|
+
// attribute when the value is `undefined`.
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const rolesThatMoveFocus = new Set(['dialog', 'alertdialog', 'menu', 'listbox', 'tree', 'grid']);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns `true` when the given popover role moves focus into the popover
|
|
82
|
+
* on open, and `false` otherwise (including when `role` is `undefined`).
|
|
83
|
+
*/
|
|
84
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
85
|
+
export function shouldFocusIntoPopover({
|
|
86
|
+
role
|
|
87
|
+
}) {
|
|
88
|
+
if (!role) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return rolesThatMoveFocus.has(role);
|
|
61
92
|
}
|
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sets inline styles on an element and returns a cleanup function
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Uses `style.setProperty()` / `style.removeProperty()` so that
|
|
6
|
-
* hyphenated CSS property names (e.g. `position-area`, `anchor-name`)
|
|
7
|
-
* work correctly. These newer properties do not have camelCase
|
|
8
|
-
* mappings in `CSSStyleDeclaration` yet.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* const cleanup = setStyle({ el: popover, styles: [
|
|
13
|
-
* { property: 'position-anchor', value: '--my-anchor' },
|
|
14
|
-
* { property: 'position-area', value: 'block-end' },
|
|
15
|
-
* { property: 'margin', value: '0' },
|
|
16
|
-
* ] });
|
|
17
|
-
*
|
|
18
|
-
* // Later: removes all three properties
|
|
19
|
-
* cleanup();
|
|
20
|
-
* ```
|
|
2
|
+
* Sets inline styles on an element and returns a cleanup function that
|
|
3
|
+
* restores the prior inline values (so we do not stomp consumer styles).
|
|
21
4
|
*/
|
|
22
5
|
export function setStyle({
|
|
23
6
|
el,
|
|
24
7
|
styles
|
|
25
8
|
}) {
|
|
9
|
+
// Snapshot the prior inline value (NOT the computed style - we only want
|
|
10
|
+
// to restore values that the consumer/our previous run inlined). An empty
|
|
11
|
+
// string means "no inline value", in which case cleanup uses removeProperty.
|
|
12
|
+
const priorValues = styles.map(({
|
|
13
|
+
property
|
|
14
|
+
}) => ({
|
|
15
|
+
property,
|
|
16
|
+
value: el.style.getPropertyValue(property)
|
|
17
|
+
}));
|
|
26
18
|
styles.forEach(({
|
|
27
19
|
property,
|
|
28
20
|
value
|
|
@@ -30,10 +22,15 @@ export function setStyle({
|
|
|
30
22
|
el.style.setProperty(property, value);
|
|
31
23
|
});
|
|
32
24
|
return function cleanup() {
|
|
33
|
-
|
|
34
|
-
property
|
|
25
|
+
priorValues.forEach(({
|
|
26
|
+
property,
|
|
27
|
+
value
|
|
35
28
|
}) => {
|
|
36
|
-
|
|
29
|
+
if (value === '') {
|
|
30
|
+
el.style.removeProperty(property);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
el.style.setProperty(property, value);
|
|
37
34
|
});
|
|
38
35
|
};
|
|
39
36
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* popover with `opacity: 0` until the first measurement completes, so it
|
|
13
13
|
* is never painted at the wrong location.
|
|
14
14
|
*/
|
|
15
|
-
import { useId, useLayoutEffect,
|
|
15
|
+
import { useId, useLayoutEffect, useMemo } from 'react';
|
|
16
16
|
import { bind } from 'bind-event-listener';
|
|
17
17
|
import rafSchedule from 'raf-schd';
|
|
18
18
|
import once from '@atlaskit/ds-lib/once';
|
|
@@ -21,7 +21,20 @@ import { combine } from './combine';
|
|
|
21
21
|
import { resolveCssLengthToPixels } from './resolve-css-length-to-pixels';
|
|
22
22
|
import { getPlacement } from './resolve-placement';
|
|
23
23
|
import { setStyle } from './set-style';
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
// `getPlacement` is intentionally NOT re-exported here - import it directly
|
|
26
|
+
// from `./resolve-placement` so search-and-jump lands at the source of truth.
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Module-scope no-op `ResizeObserver` used in non-DOM jest environments where
|
|
30
|
+
* `ResizeObserver` is `undefined`. Hoisted out of the effect to avoid
|
|
31
|
+
* allocating a new class declaration on every run.
|
|
32
|
+
*/
|
|
33
|
+
const NoopResizeObserver = class {
|
|
34
|
+
observe() {}
|
|
35
|
+
unobserve() {}
|
|
36
|
+
disconnect() {}
|
|
37
|
+
};
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
40
|
* Resolves placement options to CSS `position-area` syntax.
|
|
@@ -86,7 +99,7 @@ export function placementToPositionArea({
|
|
|
86
99
|
* top-right), flipping only the primary axis (`flip-inline`) moves the popup
|
|
87
100
|
* to the left but keeps the same cross-axis span direction (e.g. upward),
|
|
88
101
|
* which is still off-screen. The combined keyword `flip-inline flip-block`
|
|
89
|
-
* mirrors the popup diagonally
|
|
102
|
+
* mirrors the popup diagonally. It flips BOTH the position-area AND both
|
|
90
103
|
* inline and block margins simultaneously, placing the popup at the
|
|
91
104
|
* diagonally opposite corner where there is always space.
|
|
92
105
|
*
|
|
@@ -94,6 +107,7 @@ export function placementToPositionArea({
|
|
|
94
107
|
* `position-try-fallbacks` do NOT update margins, so only the `flip-*`
|
|
95
108
|
* tactic keywords correctly flip the gap margin along with the position.
|
|
96
109
|
*/
|
|
110
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
97
111
|
export function placementToTryFallbacks({
|
|
98
112
|
placement
|
|
99
113
|
}) {
|
|
@@ -106,19 +120,36 @@ export function placementToTryFallbacks({
|
|
|
106
120
|
});
|
|
107
121
|
const crossAxis = axis === 'block' ? 'inline' : 'block';
|
|
108
122
|
const flippedEdge = edge === 'start' ? 'end' : 'start';
|
|
109
|
-
if (align !== 'center') {
|
|
110
|
-
const sameEdge = `${axis}-${edge}`;
|
|
111
|
-
const oppositeEdge = `${axis}-${flippedEdge}`;
|
|
112
|
-
const flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
|
|
113
|
-
const flipCrossKeyword = axis === 'block' ? 'flip-inline' : 'flip-block';
|
|
114
|
-
// Try same-edge shifts first; two flip entries cover single-edge then corner overflow.
|
|
115
|
-
// Legacy Popper fallbacks for e.g. bottom-start: [bottom, bottom-end, top-start, top, top-end, auto].
|
|
116
|
-
return [`${sameEdge} span-${crossAxis}-${align === 'start' ? 'end' : 'start'}`, sameEdge, `${sameEdge} span-${crossAxis}-${align === 'start' ? 'start' : 'end'}`, flipKeyword, `${flipKeyword} ${flipCrossKeyword}`, `${oppositeEdge} span-${crossAxis}-${align === 'start' ? 'end' : 'start'}`, oppositeEdge, `${oppositeEdge} span-${crossAxis}-${align === 'start' ? 'start' : 'end'}`].join(', ');
|
|
117
|
-
}
|
|
118
123
|
const sameEdge = `${axis}-${edge}`;
|
|
119
124
|
const oppositeEdge = `${axis}-${flippedEdge}`;
|
|
120
125
|
const flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
|
|
121
|
-
|
|
126
|
+
const flipCrossKeyword = axis === 'block' ? 'flip-inline' : 'flip-block';
|
|
127
|
+
|
|
128
|
+
// Single algorithm: produce the shift list against any base alignment.
|
|
129
|
+
// For `align: 'start'` the "near" shift expands toward `end`; for
|
|
130
|
+
// `align: 'end'` it expands toward `start`; for `align: 'center'`
|
|
131
|
+
// either order is symmetric (we use end-then-start to match the
|
|
132
|
+
// historical fallback list).
|
|
133
|
+
function shifts(baseAlign, edgeKey) {
|
|
134
|
+
if (baseAlign === 'center') {
|
|
135
|
+
return [`${edgeKey} span-${crossAxis}-end`, `${edgeKey} span-${crossAxis}-start`];
|
|
136
|
+
}
|
|
137
|
+
const near = baseAlign === 'start' ? 'end' : 'start';
|
|
138
|
+
const far = baseAlign === 'start' ? 'start' : 'end';
|
|
139
|
+
return [`${edgeKey} span-${crossAxis}-${near}`, edgeKey, `${edgeKey} span-${crossAxis}-${far}`];
|
|
140
|
+
}
|
|
141
|
+
if (align !== 'center') {
|
|
142
|
+
// Aligned (start/end): try same-edge shifts, then single-axis flip,
|
|
143
|
+
// then diagonal flip for corner overflow, then opposite-edge shifts.
|
|
144
|
+
// Legacy Popper fallback ordering for e.g. bottom-start:
|
|
145
|
+
// [bottom, bottom-end, top-start, top, top-end, auto].
|
|
146
|
+
return [...shifts(align, sameEdge), flipKeyword, `${flipKeyword} ${flipCrossKeyword}`, ...shifts(align, oppositeEdge)].join(', ');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Centered: same-edge shifts, single-axis flip, opposite-edge shifts.
|
|
150
|
+
// Diagonal flip is unnecessary because centered placements expand
|
|
151
|
+
// equally in both cross-axis directions.
|
|
152
|
+
return [...shifts('center', sameEdge), flipKeyword, ...shifts('center', oppositeEdge)].join(', ');
|
|
122
153
|
}
|
|
123
154
|
|
|
124
155
|
/**
|
|
@@ -136,7 +167,7 @@ function isPopoverOpen(popover) {
|
|
|
136
167
|
|
|
137
168
|
/**
|
|
138
169
|
* Detects whether the browser supports CSS Anchor Positioning.
|
|
139
|
-
* Uses `once()` for lazy evaluation
|
|
170
|
+
* Uses `once()` for lazy evaluation: safe for SSR and avoids hydration mismatches.
|
|
140
171
|
*/
|
|
141
172
|
const supportsAnchorPositioning = once(() => {
|
|
142
173
|
if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
|
|
@@ -222,34 +253,49 @@ function crossAxisShiftMargin({
|
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
/**
|
|
225
|
-
* Returns a referentially-stable, fully-resolved `TPlacement
|
|
226
|
-
*
|
|
227
|
-
* does not
|
|
256
|
+
* Returns a referentially-stable, fully-resolved `TPlacement`. `useMemo` is
|
|
257
|
+
* keyed on the primitive fields of the input so a fresh inline `placement`
|
|
258
|
+
* object on every render does not produce a fresh resolved object. Both
|
|
259
|
+
* `getPlacement` and the equality check are skipped when the inputs are
|
|
260
|
+
* unchanged.
|
|
228
261
|
*/
|
|
229
262
|
function useStablePlacement(placement) {
|
|
263
|
+
// Resolve to the fully-defaulted primitives BEFORE the memo so the deps
|
|
264
|
+
// array sees the same key for `{}`, `undefined` fields, and explicit
|
|
265
|
+
// defaults. Otherwise `axis: undefined` and `axis: 'block'` would be
|
|
266
|
+
// treated as different inputs and re-run the downstream effect.
|
|
230
267
|
const resolved = getPlacement({
|
|
231
268
|
placement
|
|
232
269
|
});
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
270
|
+
const axis = resolved.axis;
|
|
271
|
+
const edge = resolved.edge;
|
|
272
|
+
const align = resolved.align;
|
|
273
|
+
const gap = resolved.offset.gap;
|
|
274
|
+
const shiftValue = resolved.offset.crossAxisShift.value;
|
|
275
|
+
const shiftDirection = resolved.offset.crossAxisShift.direction;
|
|
276
|
+
// Rebuild the placement object inside the memo from the resolved primitive
|
|
277
|
+
// deps so the factory does not close over the (potentially fresh) outer
|
|
278
|
+
// `resolved` reference, and `react-hooks/exhaustive-deps` can verify the
|
|
279
|
+
// deps array.
|
|
280
|
+
return useMemo(() => ({
|
|
281
|
+
axis,
|
|
282
|
+
edge,
|
|
283
|
+
align,
|
|
284
|
+
offset: {
|
|
285
|
+
gap,
|
|
286
|
+
crossAxisShift: {
|
|
287
|
+
value: shiftValue,
|
|
288
|
+
direction: shiftDirection
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}), [axis, edge, align, gap, shiftValue, shiftDirection]);
|
|
246
292
|
}
|
|
247
293
|
|
|
248
294
|
/**
|
|
249
295
|
* Hook that positions an element relative to an anchor element using
|
|
250
296
|
* CSS Anchor Positioning (with a JS fallback for older browsers).
|
|
251
297
|
*
|
|
252
|
-
* This hook is the positioning primitive
|
|
298
|
+
* This hook is the positioning primitive. It has no knowledge of popovers,
|
|
253
299
|
* visibility, or animation. Compose it with `Popover` for anchor-positioned
|
|
254
300
|
* top-layer content.
|
|
255
301
|
*
|
|
@@ -265,6 +311,7 @@ function isResolvedPlacementEqual(a, b) {
|
|
|
265
311
|
* comparison is used internally so re-runs are skipped when the resolved
|
|
266
312
|
* placement shape is unchanged.
|
|
267
313
|
*/
|
|
314
|
+
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
268
315
|
export function useAnchorPosition({
|
|
269
316
|
anchorRef,
|
|
270
317
|
popoverRef,
|
|
@@ -292,6 +339,9 @@ export function useAnchorPosition({
|
|
|
292
339
|
placement: stablePlacement,
|
|
293
340
|
offset: gapCssValue
|
|
294
341
|
});
|
|
342
|
+
// Hoist crossAxisShift to a single computation; reused below to
|
|
343
|
+
// populate the active `--ds-cross-axis-shift-margin-*` custom
|
|
344
|
+
// property for the named arrow @position-try rules.
|
|
295
345
|
const crossAxisShift = crossAxisShiftMargin({
|
|
296
346
|
placement: stablePlacement,
|
|
297
347
|
crossAxisShiftCssValue,
|
|
@@ -351,11 +401,8 @@ export function useAnchorPosition({
|
|
|
351
401
|
property: '--ds-cross-axis-shift-margin-block-end',
|
|
352
402
|
value: '0px'
|
|
353
403
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
crossAxisShiftCssValue,
|
|
357
|
-
direction: stablePlacement.offset.crossAxisShift.direction
|
|
358
|
-
});
|
|
404
|
+
// `crossAxisShift` IS the active margin - reuse instead of recomputing.
|
|
405
|
+
const crossAxisShiftActive = crossAxisShift;
|
|
359
406
|
// Map margin property to its corresponding custom property name.
|
|
360
407
|
const customPropertyByMarginProperty = {
|
|
361
408
|
'margin-inline-start': '--ds-cross-axis-shift-margin-start',
|
|
@@ -380,7 +427,7 @@ export function useAnchorPosition({
|
|
|
380
427
|
* - Sometimes the `useAnchorPosition` hook and `Popover`
|
|
381
428
|
* are not in the same component.
|
|
382
429
|
* - There can be times when, even with reference counting, we
|
|
383
|
-
* could remove the anchor-name while it
|
|
430
|
+
* could remove the anchor-name while it is still being used
|
|
384
431
|
* by something else - especially with async react updates
|
|
385
432
|
* which can cause tearing
|
|
386
433
|
*
|
|
@@ -411,7 +458,6 @@ export function useAnchorPosition({
|
|
|
411
458
|
}]
|
|
412
459
|
});
|
|
413
460
|
function update() {
|
|
414
|
-
var _popover$parentElemen;
|
|
415
461
|
if (!trigger || !popover) {
|
|
416
462
|
return;
|
|
417
463
|
}
|
|
@@ -422,19 +468,19 @@ export function useAnchorPosition({
|
|
|
422
468
|
};
|
|
423
469
|
|
|
424
470
|
// Resolve consumer-supplied CSS length strings (tokens, calc, var,
|
|
425
|
-
// px, rem, etc) to pixels. The probe is mounted
|
|
426
|
-
// so
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
471
|
+
// px, rem, etc) to pixels. The probe is mounted INSIDE the popover
|
|
472
|
+
// itself so the resolved length matches the scope a token would
|
|
473
|
+
// resolve against from inside the popover content. (The popover
|
|
474
|
+
// lives in the top layer, not the trigger's DOM tree, so probing
|
|
475
|
+
// at `popover.parentElement` would resolve in a different scope
|
|
476
|
+
// than the consumer's token authoring expects.)
|
|
431
477
|
const gapPx = resolveCssLengthToPixels({
|
|
432
478
|
value: gapCssValue,
|
|
433
|
-
container:
|
|
479
|
+
container: popover
|
|
434
480
|
});
|
|
435
481
|
const crossAxisShiftPx = resolveCssLengthToPixels({
|
|
436
482
|
value: crossAxisShiftCssValue,
|
|
437
|
-
container:
|
|
483
|
+
container: popover
|
|
438
484
|
});
|
|
439
485
|
const {
|
|
440
486
|
top,
|
|
@@ -468,15 +514,12 @@ export function useAnchorPosition({
|
|
|
468
514
|
// scroll/resize is handled by the window listeners below.
|
|
469
515
|
// `ResizeObserver` is missing in some non-DOM jest environments
|
|
470
516
|
// (e.g. post-office's `node` environment). Fall back to a no-op
|
|
471
|
-
// observer there
|
|
517
|
+
// observer there. The scroll/resize listeners below still keep
|
|
472
518
|
// the popover positioned in the rare case the consumer also
|
|
473
519
|
// polyfilled `showPopover` but not `ResizeObserver`. Real
|
|
474
|
-
// browsers always have it.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
unobserve() {}
|
|
478
|
-
disconnect() {}
|
|
479
|
-
};
|
|
520
|
+
// browsers always have it. The `NoopResizeObserver` class is
|
|
521
|
+
// hoisted to module scope (see top of file) so we do not allocate
|
|
522
|
+
// a new constructor on every effect run.
|
|
480
523
|
const ResizeObserverImpl = typeof ResizeObserver !== 'undefined' ? ResizeObserver : NoopResizeObserver;
|
|
481
524
|
const resizeObserver = new ResizeObserverImpl(() => {
|
|
482
525
|
if (popover.offsetWidth > 0 && popover.offsetHeight > 0) {
|
|
@@ -499,7 +542,7 @@ export function useAnchorPosition({
|
|
|
499
542
|
// the UA-default position, then re-observe for a fresh
|
|
500
543
|
// measurement. We use `opacity: 0` rather than
|
|
501
544
|
// `visibility: hidden` because Firefox skips visibility-hidden
|
|
502
|
-
// elements during `<dialog>` initial-focus traversal
|
|
545
|
+
// elements during `<dialog>` initial-focus traversal. See
|
|
503
546
|
// `form-in-popup.spec.tsx` on `desktop-firefox`.
|
|
504
547
|
bind(popover, {
|
|
505
548
|
type: 'toggle',
|