@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 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
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getClosestLayer = getClosestLayer;
7
+ function getClosestLayer(_ref) {
8
+ var element = _ref.element;
9
+ return element.closest('[popover], dialog');
10
+ }
@@ -4,10 +4,35 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.getFocusables = getFocusables;
7
- var _isInSameLayer = require("./is-in-same-layer");
8
- var _selectors = require("./selectors");
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
- return Array.from(container.querySelectorAll(_selectors.selectors.focusable)).filter(function (element) {
21
- return element instanceof HTMLElement;
22
- }).filter(function (element) {
23
- return (0, _isInSameLayer.isInSameLayer)({
24
- element: element,
25
- container: container
26
- });
27
- }).filter(function (element) {
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
- * Returns `null` if there is no focused element within the container or if
14
- * the focused element is not in the list of focusable elements.
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 _all$nextIndex;
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 current = container.querySelector(_selectors.selectors.focused);
27
- if (!(current instanceof HTMLElement)) {
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 index = all.indexOf(current);
31
- if (index === -1) {
36
+ var current = container.ownerDocument.activeElement;
37
+ if (!(current instanceof HTMLElement) || !container.contains(current)) {
32
38
  return null;
33
39
  }
34
- var nextIndex = function () {
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
- return (index + 1) % all.length;
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
- var proposed = index - 1;
39
- // if going into negative numbers, loop back to the last item
40
- return proposed < 0 ? all.length - 1 : proposed;
41
- }();
42
- return (_all$nextIndex = all[nextIndex]) !== null && _all$nextIndex !== void 0 ? _all$nextIndex : null;
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 _browserApis = require("@atlaskit/browser-apis");
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]`, `<dialog>`, `[role="dialog"]` or
12
- * `[role="alertdialog"]` that is itself a descendant of `container`).
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 = (_getDocument = (0, _browserApis.getDocument)()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
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 !(0, _isInSameLayer.isInSameLayer)({
29
- element: focused,
30
- container: container
26
+ return (0, _getClosestLayer.getClosestLayer)({
27
+ element: focused
28
+ }) !== (0, _getClosestLayer.getClosestLayer)({
29
+ element: container
31
30
  });
32
31
  }
@@ -0,0 +1,5 @@
1
+ export function getClosestLayer({
2
+ element
3
+ }) {
4
+ return element.closest('[popover], dialog');
5
+ }
@@ -1,7 +1,28 @@
1
- import { isInSameLayer } from './is-in-same-layer';
2
- import { selectors } from './selectors';
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
- return Array.from(container.querySelectorAll(selectors.focusable)).filter(element => element instanceof HTMLElement).filter(element => isInSameLayer({
16
- element,
17
- container
18
- })).filter(element => !filter || filter(element, container));
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
- * Returns `null` if there is no focused element within the container or if
6
- * the focused element is not in the list of focusable elements.
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 _all$nextIndex;
19
+ var _all3;
17
20
  const all = getFocusables({
18
21
  container,
19
22
  filter
20
23
  });
21
- const current = container.querySelector(selectors.focused);
22
- if (!(current instanceof HTMLElement)) {
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 index = all.indexOf(current);
26
- if (index === -1) {
31
+ const current = container.ownerDocument.activeElement;
32
+ if (!(current instanceof HTMLElement) || !container.contains(current)) {
27
33
  return null;
28
34
  }
29
- const nextIndex = (() => {
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
- return (index + 1) % all.length;
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
- const proposed = index - 1;
34
- // if going into negative numbers, loop back to the last item
35
- return proposed < 0 ? all.length - 1 : proposed;
36
- })();
37
- return (_all$nextIndex = all[nextIndex]) !== null && _all$nextIndex !== void 0 ? _all$nextIndex : null;
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 { getDocument } from '@atlaskit/browser-apis';
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]`, `<dialog>`, `[role="dialog"]` or
7
- * `[role="alertdialog"]` that is itself a descendant of `container`).
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
- var _getDocument;
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 !isInSameLayer({
25
- element: focused,
26
- container
22
+ return getClosestLayer({
23
+ element: focused
24
+ }) !== getClosestLayer({
25
+ element: container
27
26
  });
28
27
  }
@@ -0,0 +1,4 @@
1
+ export function getClosestLayer(_ref) {
2
+ var element = _ref.element;
3
+ return element.closest('[popover], dialog');
4
+ }
@@ -1,7 +1,32 @@
1
- import { isInSameLayer } from './is-in-same-layer';
2
- import { selectors } from './selectors';
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
- return Array.from(container.querySelectorAll(selectors.focusable)).filter(function (element) {
15
- return element instanceof HTMLElement;
16
- }).filter(function (element) {
17
- return isInSameLayer({
18
- element: element,
19
- container: container
20
- });
21
- }).filter(function (element) {
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
- * Returns `null` if there is no focused element within the container or if
6
- * the focused element is not in the list of focusable elements.
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 _all$nextIndex;
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 current = container.querySelector(selectors.focused);
21
- if (!(current instanceof HTMLElement)) {
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 index = all.indexOf(current);
25
- if (index === -1) {
30
+ var current = container.ownerDocument.activeElement;
31
+ if (!(current instanceof HTMLElement) || !container.contains(current)) {
26
32
  return null;
27
33
  }
28
- var nextIndex = function () {
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
- return (index + 1) % all.length;
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
- var proposed = index - 1;
33
- // if going into negative numbers, loop back to the last item
34
- return proposed < 0 ? all.length - 1 : proposed;
35
- }();
36
- return (_all$nextIndex = all[nextIndex]) !== null && _all$nextIndex !== void 0 ? _all$nextIndex : null;
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 { getDocument } from '@atlaskit/browser-apis';
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]`, `<dialog>`, `[role="dialog"]` or
7
- * `[role="alertdialog"]` that is itself a descendant of `container`).
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 = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
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 !isInSameLayer({
24
- element: focused,
25
- container: container
21
+ return getClosestLayer({
22
+ element: focused
23
+ }) !== getClosestLayer({
24
+ element: container
26
25
  });
27
26
  }
@@ -0,0 +1,3 @@
1
+ export declare function getClosestLayer({ element }: {
2
+ element: HTMLElement;
3
+ }): Element | null;
@@ -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
- * Returns `null` if there is no focused element within the container or if
6
- * the focused element is not in the list of focusable elements.
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]`, `<dialog>`, `[role="dialog"]` or
4
- * `[role="alertdialog"]` that is itself a descendant of `container`).
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.2",
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.0.0",
39
+ "@atlaskit/css": "^1.1.0",
40
40
  "@atlaskit/ds-lib": "^8.0.0",
41
- "@atlaskit/layering": "^5.1.0",
41
+ "@atlaskit/layering": "^5.2.0",
42
42
  "@atlaskit/react-compiler-gating": "^0.2.0",
43
- "@atlaskit/tokens": "^16.8.0",
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.2.0",
57
- "@atlaskit/heading": "^7.1.0",
58
- "@atlaskit/lozenge": "^15.3.0",
59
- "@atlaskit/primitives": "^22.4.0",
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.1.0",
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;
@@ -1,4 +0,0 @@
1
- export declare const selectors: {
2
- focusable: string;
3
- focused: string;
4
- };