@atlaskit/top-layer 4.4.2 → 4.4.3
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 +7 -0
- package/dist/cjs/focus/get-closest-layer.js +10 -0
- package/dist/cjs/focus/get-focusables.js +45 -10
- package/dist/cjs/focus/get-next-focusable.js +40 -15
- package/dist/cjs/focus/is-nested-layer-focused.js +8 -9
- package/dist/es2019/focus/get-closest-layer.js +5 -0
- package/dist/es2019/focus/get-focusables.js +43 -6
- package/dist/es2019/focus/get-next-focusable.js +38 -15
- package/dist/es2019/focus/is-nested-layer-focused.js +8 -9
- package/dist/esm/focus/get-closest-layer.js +4 -0
- package/dist/esm/focus/get-focusables.js +45 -10
- package/dist/esm/focus/get-next-focusable.js +40 -15
- package/dist/esm/focus/is-nested-layer-focused.js +8 -9
- package/dist/types/focus/get-closest-layer.d.ts +3 -0
- package/dist/types/focus/get-focusables.d.ts +2 -0
- package/dist/types/focus/get-next-focusable.d.ts +6 -2
- package/dist/types/focus/is-nested-layer-focused.d.ts +2 -2
- package/package.json +12 -9
- package/dist/cjs/focus/is-in-same-layer.js +0 -17
- package/dist/cjs/focus/selectors.js +0 -19
- package/dist/es2019/focus/is-in-same-layer.js +0 -12
- package/dist/es2019/focus/selectors.js +0 -11
- package/dist/esm/focus/is-in-same-layer.js +0 -11
- package/dist/esm/focus/selectors.js +0 -13
- package/dist/types/focus/is-in-same-layer.d.ts +0 -8
- package/dist/types/focus/selectors.d.ts +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @atlaskit/top-layer
|
|
2
2
|
|
|
3
|
+
## 4.4.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`25d19b9cc0200`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/25d19b9cc0200) -
|
|
8
|
+
Handle non-tabbable focus origins and skip hidden, disabled, or inert elements when wrapping focus
|
|
9
|
+
|
|
3
10
|
## 4.4.2
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
@@ -4,10 +4,35 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.getFocusables = getFocusables;
|
|
7
|
-
var
|
|
8
|
-
var
|
|
7
|
+
var _getClosestLayer = require("./get-closest-layer");
|
|
8
|
+
var focusableExclusions = ['[tabindex="-1"]', ':disabled', '[aria-disabled="true"]', '[aria-hidden="true"]'];
|
|
9
|
+
var exclusionSelector = focusableExclusions.map(function (selector) {
|
|
10
|
+
return ":not(".concat(selector, ")");
|
|
11
|
+
}).join('');
|
|
12
|
+
var focusableSelector = ['a[href]', 'area[href]', 'input:not([type="hidden"])', 'select', 'textarea', 'button', 'iframe', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]'].map(function (selector) {
|
|
13
|
+
return "".concat(selector).concat(exclusionSelector);
|
|
14
|
+
}).join(',');
|
|
15
|
+
function isVisible(element) {
|
|
16
|
+
// Visibility filtering must not break focus navigation when the API is unsupported.
|
|
17
|
+
if (typeof element.checkVisibility !== 'function') {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
// Exclude elements hidden by display, content-visibility or visibility because
|
|
22
|
+
// they cannot receive focus. Offscreen, occluded and opacity-zero elements can
|
|
23
|
+
// receive focus, so they intentionally remain included.
|
|
24
|
+
return element.checkVisibility({
|
|
25
|
+
checkVisibilityCSS: true
|
|
26
|
+
});
|
|
27
|
+
} catch (_unused) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
/**
|
|
10
33
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
34
|
+
* Elements with `tabindex="-1"` are excluded because they can be focused
|
|
35
|
+
* programmatically but are not sequential focus navigation destinations.
|
|
11
36
|
*
|
|
12
37
|
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
13
38
|
* `<dialog>` descendant of the container) are excluded - those elements
|
|
@@ -17,14 +42,24 @@ var _selectors = require("./selectors");
|
|
|
17
42
|
function getFocusables(_ref) {
|
|
18
43
|
var container = _ref.container,
|
|
19
44
|
filter = _ref.filter;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
45
|
+
var containerLayer = (0, _getClosestLayer.getClosestLayer)({
|
|
46
|
+
element: container
|
|
47
|
+
});
|
|
48
|
+
return Array.from(container.querySelectorAll(focusableSelector)).filter(function (element) {
|
|
49
|
+
if (!(element instanceof HTMLElement)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if ((0, _getClosestLayer.getClosestLayer)({
|
|
53
|
+
element: element
|
|
54
|
+
}) !== containerLayer) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (element.closest('[inert]')) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!isVisible(element)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
28
63
|
return !filter || filter(element, container);
|
|
29
64
|
});
|
|
30
65
|
}
|
|
@@ -5,17 +5,20 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.getNextFocusable = getNextFocusable;
|
|
7
7
|
var _getFocusables = require("./get-focusables");
|
|
8
|
-
var _selectors = require("./selectors");
|
|
9
8
|
/**
|
|
10
9
|
* Returns the next focusable element in the given direction relative to the
|
|
11
10
|
* currently focused element within the container. Wraps around at both ends.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The currently focused element does not need to be tabbable. This prevents
|
|
13
|
+
* navigation from getting stuck on an element that was programmatically or
|
|
14
|
+
* pointer focused with `tabindex="-1"`. Negative-tab-index elements remain
|
|
15
|
+
* excluded as destinations.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` if there is no focused element within the container.
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
function getNextFocusable(_ref) {
|
|
18
|
-
var
|
|
21
|
+
var _all3;
|
|
19
22
|
var container = _ref.container,
|
|
20
23
|
direction = _ref.direction,
|
|
21
24
|
filter = _ref.filter;
|
|
@@ -23,21 +26,43 @@ function getNextFocusable(_ref) {
|
|
|
23
26
|
container: container,
|
|
24
27
|
filter: filter
|
|
25
28
|
});
|
|
26
|
-
var
|
|
27
|
-
|
|
29
|
+
var first = all[0];
|
|
30
|
+
var last = all[all.length - 1];
|
|
31
|
+
|
|
32
|
+
// Checking the elements directly narrows types from `HTMLElement | undefined`.
|
|
33
|
+
if (!first || !last) {
|
|
28
34
|
return null;
|
|
29
35
|
}
|
|
30
|
-
var
|
|
31
|
-
if (
|
|
36
|
+
var current = container.ownerDocument.activeElement;
|
|
37
|
+
if (!(current instanceof HTMLElement) || !container.contains(current)) {
|
|
32
38
|
return null;
|
|
33
39
|
}
|
|
34
|
-
var
|
|
40
|
+
var index = all.indexOf(current);
|
|
41
|
+
|
|
42
|
+
// The focused element can be absent from `all` when it is non-tabbable or
|
|
43
|
+
// excluded by the filter. Find the first focusable element that follows it in
|
|
44
|
+
// the DOM, which identifies the nearest focusable destination in either direction.
|
|
45
|
+
if (index === -1) {
|
|
46
|
+
var _all;
|
|
47
|
+
var followingIndex = all.findIndex(function (element) {
|
|
48
|
+
return Boolean(current.compareDocumentPosition(element) & current.DOCUMENT_POSITION_FOLLOWING);
|
|
49
|
+
});
|
|
35
50
|
if (direction === 'forwards') {
|
|
36
|
-
|
|
51
|
+
var _all$followingIndex;
|
|
52
|
+
// If nothing follows the current element, then wrap to the start.
|
|
53
|
+
return (_all$followingIndex = all[followingIndex]) !== null && _all$followingIndex !== void 0 ? _all$followingIndex : first;
|
|
37
54
|
}
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
|
|
56
|
+
// The closest preceding element is immediately before the first following one.
|
|
57
|
+
// A missing adjacent entry means the backward sequence wraps to the end.
|
|
58
|
+
return (_all = all[followingIndex - 1]) !== null && _all !== void 0 ? _all : last;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The current element is tabbable, so use its adjacent list entry and wrap
|
|
62
|
+
// when it is already at the boundary.
|
|
63
|
+
if (direction === 'forwards') {
|
|
64
|
+
var _all2;
|
|
65
|
+
return (_all2 = all[index + 1]) !== null && _all2 !== void 0 ? _all2 : first;
|
|
66
|
+
}
|
|
67
|
+
return (_all3 = all[index - 1]) !== null && _all3 !== void 0 ? _all3 : last;
|
|
43
68
|
}
|
|
@@ -4,17 +4,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.isNestedLayerFocused = isNestedLayerFocused;
|
|
7
|
-
var
|
|
8
|
-
var _isInSameLayer = require("./is-in-same-layer");
|
|
7
|
+
var _getClosestLayer = require("./get-closest-layer");
|
|
9
8
|
/**
|
|
10
9
|
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
11
|
-
* descendant of `container` (a `[popover]
|
|
12
|
-
*
|
|
10
|
+
* descendant of `container` (a `[popover]` or `<dialog>` that is itself a
|
|
11
|
+
* descendant of `container`).
|
|
13
12
|
*/
|
|
14
13
|
function isNestedLayerFocused(_ref) {
|
|
15
|
-
var _getDocument;
|
|
16
14
|
var container = _ref.container;
|
|
17
|
-
var focused =
|
|
15
|
+
var focused = container.ownerDocument.activeElement;
|
|
18
16
|
if (!(focused instanceof HTMLElement)) {
|
|
19
17
|
return false;
|
|
20
18
|
}
|
|
@@ -25,8 +23,9 @@ function isNestedLayerFocused(_ref) {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
// If not in the same layer, must be in a nested layer
|
|
28
|
-
return
|
|
29
|
-
element: focused
|
|
30
|
-
|
|
26
|
+
return (0, _getClosestLayer.getClosestLayer)({
|
|
27
|
+
element: focused
|
|
28
|
+
}) !== (0, _getClosestLayer.getClosestLayer)({
|
|
29
|
+
element: container
|
|
31
30
|
});
|
|
32
31
|
}
|
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { getClosestLayer } from './get-closest-layer';
|
|
2
|
+
const focusableExclusions = ['[tabindex="-1"]', ':disabled', '[aria-disabled="true"]', '[aria-hidden="true"]'];
|
|
3
|
+
const exclusionSelector = focusableExclusions.map(selector => `:not(${selector})`).join('');
|
|
4
|
+
const focusableSelector = ['a[href]', 'area[href]', 'input:not([type="hidden"])', 'select', 'textarea', 'button', 'iframe', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]'].map(selector => `${selector}${exclusionSelector}`).join(',');
|
|
5
|
+
function isVisible(element) {
|
|
6
|
+
// Visibility filtering must not break focus navigation when the API is unsupported.
|
|
7
|
+
if (typeof element.checkVisibility !== 'function') {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
// Exclude elements hidden by display, content-visibility or visibility because
|
|
12
|
+
// they cannot receive focus. Offscreen, occluded and opacity-zero elements can
|
|
13
|
+
// receive focus, so they intentionally remain included.
|
|
14
|
+
return element.checkVisibility({
|
|
15
|
+
checkVisibilityCSS: true
|
|
16
|
+
});
|
|
17
|
+
} catch {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
/**
|
|
4
23
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
24
|
+
* Elements with `tabindex="-1"` are excluded because they can be focused
|
|
25
|
+
* programmatically but are not sequential focus navigation destinations.
|
|
5
26
|
*
|
|
6
27
|
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
7
28
|
* `<dialog>` descendant of the container) are excluded - those elements
|
|
@@ -12,8 +33,24 @@ export function getFocusables({
|
|
|
12
33
|
container,
|
|
13
34
|
filter
|
|
14
35
|
}) {
|
|
15
|
-
|
|
16
|
-
element
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
const containerLayer = getClosestLayer({
|
|
37
|
+
element: container
|
|
38
|
+
});
|
|
39
|
+
return Array.from(container.querySelectorAll(focusableSelector)).filter(element => {
|
|
40
|
+
if (!(element instanceof HTMLElement)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (getClosestLayer({
|
|
44
|
+
element
|
|
45
|
+
}) !== containerLayer) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (element.closest('[inert]')) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!isVisible(element)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return !filter || filter(element, container);
|
|
55
|
+
});
|
|
19
56
|
}
|
|
@@ -2,37 +2,60 @@
|
|
|
2
2
|
* Returns the next focusable element in the given direction relative to the
|
|
3
3
|
* currently focused element within the container. Wraps around at both ends.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The currently focused element does not need to be tabbable. This prevents
|
|
6
|
+
* navigation from getting stuck on an element that was programmatically or
|
|
7
|
+
* pointer focused with `tabindex="-1"`. Negative-tab-index elements remain
|
|
8
|
+
* excluded as destinations.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` if there is no focused element within the container.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { getFocusables } from './get-focusables';
|
|
10
|
-
import { selectors } from './selectors';
|
|
11
14
|
export function getNextFocusable({
|
|
12
15
|
container,
|
|
13
16
|
direction,
|
|
14
17
|
filter
|
|
15
18
|
}) {
|
|
16
|
-
var
|
|
19
|
+
var _all3;
|
|
17
20
|
const all = getFocusables({
|
|
18
21
|
container,
|
|
19
22
|
filter
|
|
20
23
|
});
|
|
21
|
-
const
|
|
22
|
-
|
|
24
|
+
const first = all[0];
|
|
25
|
+
const last = all[all.length - 1];
|
|
26
|
+
|
|
27
|
+
// Checking the elements directly narrows types from `HTMLElement | undefined`.
|
|
28
|
+
if (!first || !last) {
|
|
23
29
|
return null;
|
|
24
30
|
}
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
31
|
+
const current = container.ownerDocument.activeElement;
|
|
32
|
+
if (!(current instanceof HTMLElement) || !container.contains(current)) {
|
|
27
33
|
return null;
|
|
28
34
|
}
|
|
29
|
-
const
|
|
35
|
+
const index = all.indexOf(current);
|
|
36
|
+
|
|
37
|
+
// The focused element can be absent from `all` when it is non-tabbable or
|
|
38
|
+
// excluded by the filter. Find the first focusable element that follows it in
|
|
39
|
+
// the DOM, which identifies the nearest focusable destination in either direction.
|
|
40
|
+
if (index === -1) {
|
|
41
|
+
var _all;
|
|
42
|
+
const followingIndex = all.findIndex(element => Boolean(current.compareDocumentPosition(element) & current.DOCUMENT_POSITION_FOLLOWING));
|
|
30
43
|
if (direction === 'forwards') {
|
|
31
|
-
|
|
44
|
+
var _all$followingIndex;
|
|
45
|
+
// If nothing follows the current element, then wrap to the start.
|
|
46
|
+
return (_all$followingIndex = all[followingIndex]) !== null && _all$followingIndex !== void 0 ? _all$followingIndex : first;
|
|
32
47
|
}
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
|
|
49
|
+
// The closest preceding element is immediately before the first following one.
|
|
50
|
+
// A missing adjacent entry means the backward sequence wraps to the end.
|
|
51
|
+
return (_all = all[followingIndex - 1]) !== null && _all !== void 0 ? _all : last;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The current element is tabbable, so use its adjacent list entry and wrap
|
|
55
|
+
// when it is already at the boundary.
|
|
56
|
+
if (direction === 'forwards') {
|
|
57
|
+
var _all2;
|
|
58
|
+
return (_all2 = all[index + 1]) !== null && _all2 !== void 0 ? _all2 : first;
|
|
59
|
+
}
|
|
60
|
+
return (_all3 = all[index - 1]) !== null && _all3 !== void 0 ? _all3 : last;
|
|
38
61
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isInSameLayer } from './is-in-same-layer';
|
|
1
|
+
import { getClosestLayer } from './get-closest-layer';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
6
|
-
* descendant of `container` (a `[popover]
|
|
7
|
-
*
|
|
5
|
+
* descendant of `container` (a `[popover]` or `<dialog>` that is itself a
|
|
6
|
+
* descendant of `container`).
|
|
8
7
|
*/
|
|
9
8
|
export function isNestedLayerFocused({
|
|
10
9
|
container
|
|
11
10
|
}) {
|
|
12
|
-
|
|
13
|
-
const focused = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
|
|
11
|
+
const focused = container.ownerDocument.activeElement;
|
|
14
12
|
if (!(focused instanceof HTMLElement)) {
|
|
15
13
|
return false;
|
|
16
14
|
}
|
|
@@ -21,8 +19,9 @@ export function isNestedLayerFocused({
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
// If not in the same layer, must be in a nested layer
|
|
24
|
-
return
|
|
25
|
-
element: focused
|
|
26
|
-
|
|
22
|
+
return getClosestLayer({
|
|
23
|
+
element: focused
|
|
24
|
+
}) !== getClosestLayer({
|
|
25
|
+
element: container
|
|
27
26
|
});
|
|
28
27
|
}
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { getClosestLayer } from './get-closest-layer';
|
|
2
|
+
var focusableExclusions = ['[tabindex="-1"]', ':disabled', '[aria-disabled="true"]', '[aria-hidden="true"]'];
|
|
3
|
+
var exclusionSelector = focusableExclusions.map(function (selector) {
|
|
4
|
+
return ":not(".concat(selector, ")");
|
|
5
|
+
}).join('');
|
|
6
|
+
var focusableSelector = ['a[href]', 'area[href]', 'input:not([type="hidden"])', 'select', 'textarea', 'button', 'iframe', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]'].map(function (selector) {
|
|
7
|
+
return "".concat(selector).concat(exclusionSelector);
|
|
8
|
+
}).join(',');
|
|
9
|
+
function isVisible(element) {
|
|
10
|
+
// Visibility filtering must not break focus navigation when the API is unsupported.
|
|
11
|
+
if (typeof element.checkVisibility !== 'function') {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
// Exclude elements hidden by display, content-visibility or visibility because
|
|
16
|
+
// they cannot receive focus. Offscreen, occluded and opacity-zero elements can
|
|
17
|
+
// receive focus, so they intentionally remain included.
|
|
18
|
+
return element.checkVisibility({
|
|
19
|
+
checkVisibilityCSS: true
|
|
20
|
+
});
|
|
21
|
+
} catch (_unused) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
3
26
|
/**
|
|
4
27
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
28
|
+
* Elements with `tabindex="-1"` are excluded because they can be focused
|
|
29
|
+
* programmatically but are not sequential focus navigation destinations.
|
|
5
30
|
*
|
|
6
31
|
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
7
32
|
* `<dialog>` descendant of the container) are excluded - those elements
|
|
@@ -11,14 +36,24 @@ import { selectors } from './selectors';
|
|
|
11
36
|
export function getFocusables(_ref) {
|
|
12
37
|
var container = _ref.container,
|
|
13
38
|
filter = _ref.filter;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
39
|
+
var containerLayer = getClosestLayer({
|
|
40
|
+
element: container
|
|
41
|
+
});
|
|
42
|
+
return Array.from(container.querySelectorAll(focusableSelector)).filter(function (element) {
|
|
43
|
+
if (!(element instanceof HTMLElement)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (getClosestLayer({
|
|
47
|
+
element: element
|
|
48
|
+
}) !== containerLayer) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (element.closest('[inert]')) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (!isVisible(element)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
22
57
|
return !filter || filter(element, container);
|
|
23
58
|
});
|
|
24
59
|
}
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
* Returns the next focusable element in the given direction relative to the
|
|
3
3
|
* currently focused element within the container. Wraps around at both ends.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The currently focused element does not need to be tabbable. This prevents
|
|
6
|
+
* navigation from getting stuck on an element that was programmatically or
|
|
7
|
+
* pointer focused with `tabindex="-1"`. Negative-tab-index elements remain
|
|
8
|
+
* excluded as destinations.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` if there is no focused element within the container.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { getFocusables } from './get-focusables';
|
|
10
|
-
import { selectors } from './selectors';
|
|
11
14
|
export function getNextFocusable(_ref) {
|
|
12
|
-
var
|
|
15
|
+
var _all3;
|
|
13
16
|
var container = _ref.container,
|
|
14
17
|
direction = _ref.direction,
|
|
15
18
|
filter = _ref.filter;
|
|
@@ -17,21 +20,43 @@ export function getNextFocusable(_ref) {
|
|
|
17
20
|
container: container,
|
|
18
21
|
filter: filter
|
|
19
22
|
});
|
|
20
|
-
var
|
|
21
|
-
|
|
23
|
+
var first = all[0];
|
|
24
|
+
var last = all[all.length - 1];
|
|
25
|
+
|
|
26
|
+
// Checking the elements directly narrows types from `HTMLElement | undefined`.
|
|
27
|
+
if (!first || !last) {
|
|
22
28
|
return null;
|
|
23
29
|
}
|
|
24
|
-
var
|
|
25
|
-
if (
|
|
30
|
+
var current = container.ownerDocument.activeElement;
|
|
31
|
+
if (!(current instanceof HTMLElement) || !container.contains(current)) {
|
|
26
32
|
return null;
|
|
27
33
|
}
|
|
28
|
-
var
|
|
34
|
+
var index = all.indexOf(current);
|
|
35
|
+
|
|
36
|
+
// The focused element can be absent from `all` when it is non-tabbable or
|
|
37
|
+
// excluded by the filter. Find the first focusable element that follows it in
|
|
38
|
+
// the DOM, which identifies the nearest focusable destination in either direction.
|
|
39
|
+
if (index === -1) {
|
|
40
|
+
var _all;
|
|
41
|
+
var followingIndex = all.findIndex(function (element) {
|
|
42
|
+
return Boolean(current.compareDocumentPosition(element) & current.DOCUMENT_POSITION_FOLLOWING);
|
|
43
|
+
});
|
|
29
44
|
if (direction === 'forwards') {
|
|
30
|
-
|
|
45
|
+
var _all$followingIndex;
|
|
46
|
+
// If nothing follows the current element, then wrap to the start.
|
|
47
|
+
return (_all$followingIndex = all[followingIndex]) !== null && _all$followingIndex !== void 0 ? _all$followingIndex : first;
|
|
31
48
|
}
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
|
|
50
|
+
// The closest preceding element is immediately before the first following one.
|
|
51
|
+
// A missing adjacent entry means the backward sequence wraps to the end.
|
|
52
|
+
return (_all = all[followingIndex - 1]) !== null && _all !== void 0 ? _all : last;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The current element is tabbable, so use its adjacent list entry and wrap
|
|
56
|
+
// when it is already at the boundary.
|
|
57
|
+
if (direction === 'forwards') {
|
|
58
|
+
var _all2;
|
|
59
|
+
return (_all2 = all[index + 1]) !== null && _all2 !== void 0 ? _all2 : first;
|
|
60
|
+
}
|
|
61
|
+
return (_all3 = all[index - 1]) !== null && _all3 !== void 0 ? _all3 : last;
|
|
37
62
|
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isInSameLayer } from './is-in-same-layer';
|
|
1
|
+
import { getClosestLayer } from './get-closest-layer';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
6
|
-
* descendant of `container` (a `[popover]
|
|
7
|
-
*
|
|
5
|
+
* descendant of `container` (a `[popover]` or `<dialog>` that is itself a
|
|
6
|
+
* descendant of `container`).
|
|
8
7
|
*/
|
|
9
8
|
export function isNestedLayerFocused(_ref) {
|
|
10
|
-
var _getDocument;
|
|
11
9
|
var container = _ref.container;
|
|
12
|
-
var focused =
|
|
10
|
+
var focused = container.ownerDocument.activeElement;
|
|
13
11
|
if (!(focused instanceof HTMLElement)) {
|
|
14
12
|
return false;
|
|
15
13
|
}
|
|
@@ -20,8 +18,9 @@ export function isNestedLayerFocused(_ref) {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
// If not in the same layer, must be in a nested layer
|
|
23
|
-
return
|
|
24
|
-
element: focused
|
|
25
|
-
|
|
21
|
+
return getClosestLayer({
|
|
22
|
+
element: focused
|
|
23
|
+
}) !== getClosestLayer({
|
|
24
|
+
element: container
|
|
26
25
|
});
|
|
27
26
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TFocusableFilter } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
4
|
+
* Elements with `tabindex="-1"` are excluded because they can be focused
|
|
5
|
+
* programmatically but are not sequential focus navigation destinations.
|
|
4
6
|
*
|
|
5
7
|
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
6
8
|
* `<dialog>` descendant of the container) are excluded - those elements
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* Returns the next focusable element in the given direction relative to the
|
|
3
3
|
* currently focused element within the container. Wraps around at both ends.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The currently focused element does not need to be tabbable. This prevents
|
|
6
|
+
* navigation from getting stuck on an element that was programmatically or
|
|
7
|
+
* pointer focused with `tabindex="-1"`. Negative-tab-index elements remain
|
|
8
|
+
* excluded as destinations.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` if there is no focused element within the container.
|
|
7
11
|
*/
|
|
8
12
|
import type { TFocusableFilter } from './types';
|
|
9
13
|
export declare function getNextFocusable({ container, direction, filter, }: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
3
|
-
* descendant of `container` (a `[popover]
|
|
4
|
-
*
|
|
3
|
+
* descendant of `container` (a `[popover]` or `<dialog>` that is itself a
|
|
4
|
+
* descendant of `container`).
|
|
5
5
|
*/
|
|
6
6
|
export declare function isNestedLayerFocused({ container }: {
|
|
7
7
|
container: HTMLElement;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/top-layer",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.3",
|
|
4
4
|
"description": "Low-level layering primitives based on popover and dialog",
|
|
5
5
|
"homepage": "https://atlassian.design/components/top-layer",
|
|
6
6
|
"repository": "https://bitbucket.org/atlassian/atlassian-frontend-mirror",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@atlaskit/browser-apis": "^1.2.0",
|
|
39
|
-
"@atlaskit/css": "^1.
|
|
39
|
+
"@atlaskit/css": "^1.1.0",
|
|
40
40
|
"@atlaskit/ds-lib": "^8.0.0",
|
|
41
|
-
"@atlaskit/layering": "^5.
|
|
41
|
+
"@atlaskit/layering": "^5.2.0",
|
|
42
42
|
"@atlaskit/react-compiler-gating": "^0.2.0",
|
|
43
|
-
"@atlaskit/tokens": "^16.
|
|
43
|
+
"@atlaskit/tokens": "^16.11.0",
|
|
44
44
|
"@babel/runtime": "^7.0.0",
|
|
45
45
|
"@compiled/react": "^1.0.2",
|
|
46
46
|
"bind-event-listener": "^3.0.0",
|
|
@@ -53,12 +53,15 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@af/integration-testing": "workspace:^",
|
|
55
55
|
"@af/visual-regression": "workspace:^",
|
|
56
|
-
"@atlaskit/button": "^25.
|
|
57
|
-
"@atlaskit/
|
|
58
|
-
"@atlaskit/
|
|
59
|
-
"@atlaskit/
|
|
56
|
+
"@atlaskit/button": "^25.3.0",
|
|
57
|
+
"@atlaskit/checkbox": "^19.2.0",
|
|
58
|
+
"@atlaskit/heading": "^7.2.0",
|
|
59
|
+
"@atlaskit/lozenge": "^15.4.0",
|
|
60
|
+
"@atlaskit/primitives": "^22.5.0",
|
|
61
|
+
"@atlaskit/select": "^22.11.0",
|
|
60
62
|
"@atlaskit/ssr": "workspace:^",
|
|
61
|
-
"@atlaskit/textfield": "^10.
|
|
63
|
+
"@atlaskit/textfield": "^10.2.0",
|
|
64
|
+
"@atlaskit/toggle": "^17.3.0",
|
|
62
65
|
"@atlassian/ssr-tests": "workspace:^",
|
|
63
66
|
"@atlassian/testing-library": "^0.11.0",
|
|
64
67
|
"@testing-library/jest-dom": "^6.4.5",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.isInSameLayer = isInSameLayer;
|
|
7
|
-
var topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Returns true when `element` and `container` resolve to the same nearest
|
|
11
|
-
* top-layer host (or both resolve to `null`, meaning the document layer).
|
|
12
|
-
*/
|
|
13
|
-
function isInSameLayer(_ref) {
|
|
14
|
-
var element = _ref.element,
|
|
15
|
-
container = _ref.container;
|
|
16
|
-
return element.closest(topLayerSelector) === container.closest(topLayerSelector);
|
|
17
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.selectors = void 0;
|
|
7
|
-
var selectors = exports.selectors = function () {
|
|
8
|
-
// Common exclusion filter applied to every selector.
|
|
9
|
-
var not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
|
|
10
|
-
var notDisabled = ":not([disabled])".concat(not);
|
|
11
|
-
var focusable = ["a[href]".concat(not), "area[href]".concat(not), "input".concat(notDisabled, ":not([type=\"hidden\"])"), "select".concat(notDisabled), "textarea".concat(notDisabled), "button".concat(notDisabled), "iframe".concat(not), "audio[controls]".concat(not), "video[controls]".concat(not), "[contenteditable]:not([contenteditable=\"false\"])".concat(not), "[tabindex]".concat(notDisabled)];
|
|
12
|
-
return {
|
|
13
|
-
focusable: focusable.join(','),
|
|
14
|
-
// Build the `:focus` selector
|
|
15
|
-
focused: focusable.map(function (selector) {
|
|
16
|
-
return "".concat(selector, ":focus");
|
|
17
|
-
}).join(',')
|
|
18
|
-
};
|
|
19
|
-
}();
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Returns true when `element` and `container` resolve to the same nearest
|
|
5
|
-
* top-layer host (or both resolve to `null`, meaning the document layer).
|
|
6
|
-
*/
|
|
7
|
-
export function isInSameLayer({
|
|
8
|
-
element,
|
|
9
|
-
container
|
|
10
|
-
}) {
|
|
11
|
-
return element.closest(topLayerSelector) === container.closest(topLayerSelector);
|
|
12
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const selectors = (() => {
|
|
2
|
-
// Common exclusion filter applied to every selector.
|
|
3
|
-
const not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
|
|
4
|
-
const notDisabled = `:not([disabled])${not}`;
|
|
5
|
-
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}`];
|
|
6
|
-
return {
|
|
7
|
-
focusable: focusable.join(','),
|
|
8
|
-
// Build the `:focus` selector
|
|
9
|
-
focused: focusable.map(selector => `${selector}:focus`).join(',')
|
|
10
|
-
};
|
|
11
|
-
})();
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
var topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Returns true when `element` and `container` resolve to the same nearest
|
|
5
|
-
* top-layer host (or both resolve to `null`, meaning the document layer).
|
|
6
|
-
*/
|
|
7
|
-
export function isInSameLayer(_ref) {
|
|
8
|
-
var element = _ref.element,
|
|
9
|
-
container = _ref.container;
|
|
10
|
-
return element.closest(topLayerSelector) === container.closest(topLayerSelector);
|
|
11
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export var selectors = function () {
|
|
2
|
-
// Common exclusion filter applied to every selector.
|
|
3
|
-
var not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
|
|
4
|
-
var notDisabled = ":not([disabled])".concat(not);
|
|
5
|
-
var focusable = ["a[href]".concat(not), "area[href]".concat(not), "input".concat(notDisabled, ":not([type=\"hidden\"])"), "select".concat(notDisabled), "textarea".concat(notDisabled), "button".concat(notDisabled), "iframe".concat(not), "audio[controls]".concat(not), "video[controls]".concat(not), "[contenteditable]:not([contenteditable=\"false\"])".concat(not), "[tabindex]".concat(notDisabled)];
|
|
6
|
-
return {
|
|
7
|
-
focusable: focusable.join(','),
|
|
8
|
-
// Build the `:focus` selector
|
|
9
|
-
focused: focusable.map(function (selector) {
|
|
10
|
-
return "".concat(selector, ":focus");
|
|
11
|
-
}).join(',')
|
|
12
|
-
};
|
|
13
|
-
}();
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Returns true when `element` and `container` resolve to the same nearest
|
|
3
|
-
* top-layer host (or both resolve to `null`, meaning the document layer).
|
|
4
|
-
*/
|
|
5
|
-
export declare function isInSameLayer({ element, container, }: {
|
|
6
|
-
element: HTMLElement;
|
|
7
|
-
container: HTMLElement;
|
|
8
|
-
}): boolean;
|