@atlaskit/top-layer 0.12.2 → 0.13.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/entry-points/focus.js +6 -4
  3. package/dist/cjs/entry-points/use-anchor-position-at-point.js +12 -0
  4. package/dist/cjs/focus/get-first-focusable.js +19 -0
  5. package/dist/cjs/focus/get-focusables.js +30 -0
  6. package/dist/cjs/focus/get-last-focusable.js +19 -0
  7. package/dist/cjs/focus/get-next-focusable.js +43 -0
  8. package/dist/cjs/focus/is-in-same-layer.js +23 -0
  9. package/dist/cjs/focus/is-nested-layer-focused.js +32 -0
  10. package/dist/cjs/focus/selectors.js +19 -0
  11. package/dist/cjs/focus/types.js +1 -0
  12. package/dist/cjs/internal/role-to-aria-has-popup.js +37 -0
  13. package/dist/cjs/internal/role-types.js +1 -97
  14. package/dist/cjs/internal/should-focus-into-popover.js +19 -0
  15. package/dist/cjs/internal/use-anchor-position-at-point.js +127 -0
  16. package/dist/cjs/internal/use-anchor-position.js +7 -3
  17. package/dist/cjs/internal/use-focus-wrap.js +8 -5
  18. package/dist/cjs/internal/use-initial-focus.js +4 -4
  19. package/dist/cjs/placement-map/index.js +2 -2
  20. package/dist/cjs/placement-map/legacy-placements.js +2 -101
  21. package/dist/cjs/placement-map/placement-mapping.js +103 -0
  22. package/dist/cjs/popup/popup-content.js +4 -3
  23. package/dist/cjs/use-arrow-navigation/use-arrow-navigation.js +7 -5
  24. package/dist/es2019/entry-points/focus.js +3 -1
  25. package/dist/es2019/entry-points/use-anchor-position-at-point.js +1 -0
  26. package/dist/es2019/focus/get-first-focusable.js +14 -0
  27. package/dist/es2019/focus/get-focusables.js +19 -0
  28. package/dist/es2019/focus/get-last-focusable.js +14 -0
  29. package/dist/es2019/focus/get-next-focusable.js +38 -0
  30. package/dist/es2019/focus/is-in-same-layer.js +18 -0
  31. package/dist/es2019/focus/is-nested-layer-focused.js +28 -0
  32. package/dist/es2019/focus/selectors.js +11 -0
  33. package/dist/es2019/focus/types.js +0 -0
  34. package/dist/es2019/internal/role-to-aria-has-popup.js +32 -0
  35. package/dist/es2019/internal/role-types.js +0 -92
  36. package/dist/es2019/internal/should-focus-into-popover.js +14 -0
  37. package/dist/es2019/internal/use-anchor-position-at-point.js +122 -0
  38. package/dist/es2019/internal/use-anchor-position.js +6 -3
  39. package/dist/es2019/internal/use-focus-wrap.js +4 -1
  40. package/dist/es2019/internal/use-initial-focus.js +1 -1
  41. package/dist/es2019/placement-map/index.js +1 -1
  42. package/dist/es2019/placement-map/legacy-placements.js +1 -100
  43. package/dist/es2019/placement-map/placement-mapping.js +97 -0
  44. package/dist/es2019/popup/popup-content.js +2 -1
  45. package/dist/es2019/use-arrow-navigation/use-arrow-navigation.js +3 -1
  46. package/dist/esm/entry-points/focus.js +3 -1
  47. package/dist/esm/entry-points/use-anchor-position-at-point.js +1 -0
  48. package/dist/esm/focus/get-first-focusable.js +13 -0
  49. package/dist/esm/focus/get-focusables.js +24 -0
  50. package/dist/esm/focus/get-last-focusable.js +13 -0
  51. package/dist/esm/focus/get-next-focusable.js +37 -0
  52. package/dist/esm/focus/is-in-same-layer.js +17 -0
  53. package/dist/esm/focus/is-nested-layer-focused.js +27 -0
  54. package/dist/esm/focus/selectors.js +13 -0
  55. package/dist/esm/focus/types.js +0 -0
  56. package/dist/esm/internal/role-to-aria-has-popup.js +31 -0
  57. package/dist/esm/internal/role-types.js +0 -90
  58. package/dist/esm/internal/should-focus-into-popover.js +13 -0
  59. package/dist/esm/internal/use-anchor-position-at-point.js +122 -0
  60. package/dist/esm/internal/use-anchor-position.js +7 -3
  61. package/dist/esm/internal/use-focus-wrap.js +4 -1
  62. package/dist/esm/internal/use-initial-focus.js +1 -1
  63. package/dist/esm/placement-map/index.js +1 -1
  64. package/dist/esm/placement-map/legacy-placements.js +1 -100
  65. package/dist/esm/placement-map/placement-mapping.js +97 -0
  66. package/dist/esm/popup/popup-content.js +2 -1
  67. package/dist/esm/use-arrow-navigation/use-arrow-navigation.js +3 -1
  68. package/dist/types/entry-points/focus.d.ts +4 -2
  69. package/dist/types/entry-points/use-anchor-position-at-point.d.ts +1 -0
  70. package/dist/types/focus/get-first-focusable.d.ts +8 -0
  71. package/dist/types/focus/get-focusables.d.ts +13 -0
  72. package/dist/types/focus/get-last-focusable.d.ts +8 -0
  73. package/dist/types/focus/get-next-focusable.d.ts +13 -0
  74. package/dist/types/focus/is-in-same-layer.d.ts +8 -0
  75. package/dist/types/focus/is-nested-layer-focused.d.ts +8 -0
  76. package/dist/types/focus/selectors.d.ts +4 -0
  77. package/dist/types/focus/types.d.ts +5 -0
  78. package/dist/types/internal/role-to-aria-has-popup.d.ts +13 -0
  79. package/dist/types/internal/role-types.d.ts +0 -19
  80. package/dist/types/internal/should-focus-into-popover.d.ts +8 -0
  81. package/dist/types/internal/use-anchor-position-at-point.d.ts +62 -0
  82. package/dist/types/internal/use-anchor-position.d.ts +6 -1
  83. package/dist/types/placement-map/index.d.ts +1 -1
  84. package/dist/types/placement-map/legacy-placements.d.ts +0 -12
  85. package/dist/types/placement-map/placement-mapping.d.ts +13 -0
  86. package/dist/types/use-arrow-navigation/use-arrow-navigation.d.ts +1 -1
  87. package/dist/types-ts4.5/entry-points/focus.d.ts +4 -2
  88. package/dist/types-ts4.5/entry-points/use-anchor-position-at-point.d.ts +1 -0
  89. package/dist/types-ts4.5/focus/get-first-focusable.d.ts +8 -0
  90. package/dist/types-ts4.5/focus/get-focusables.d.ts +13 -0
  91. package/dist/types-ts4.5/focus/get-last-focusable.d.ts +8 -0
  92. package/dist/types-ts4.5/focus/get-next-focusable.d.ts +13 -0
  93. package/dist/types-ts4.5/focus/is-in-same-layer.d.ts +8 -0
  94. package/dist/types-ts4.5/focus/is-nested-layer-focused.d.ts +8 -0
  95. package/dist/types-ts4.5/focus/selectors.d.ts +4 -0
  96. package/dist/types-ts4.5/focus/types.d.ts +5 -0
  97. package/dist/types-ts4.5/internal/role-to-aria-has-popup.d.ts +13 -0
  98. package/dist/types-ts4.5/internal/role-types.d.ts +0 -19
  99. package/dist/types-ts4.5/internal/should-focus-into-popover.d.ts +8 -0
  100. package/dist/types-ts4.5/internal/use-anchor-position-at-point.d.ts +62 -0
  101. package/dist/types-ts4.5/internal/use-anchor-position.d.ts +6 -1
  102. package/dist/types-ts4.5/placement-map/index.d.ts +1 -1
  103. package/dist/types-ts4.5/placement-map/legacy-placements.d.ts +0 -12
  104. package/dist/types-ts4.5/placement-map/placement-mapping.d.ts +13 -0
  105. package/dist/types-ts4.5/use-arrow-navigation/use-arrow-navigation.d.ts +1 -1
  106. package/package.json +1 -1
  107. package/use-anchor-position-at-point/package.json +17 -0
  108. package/dist/cjs/focus/focus.js +0 -169
  109. package/dist/cjs/focus/index.js +0 -24
  110. package/dist/es2019/focus/focus.js +0 -158
  111. package/dist/es2019/focus/index.js +0 -1
  112. package/dist/esm/focus/focus.js +0 -160
  113. package/dist/esm/focus/index.js +0 -1
  114. package/dist/types/focus/focus.d.ts +0 -50
  115. package/dist/types/focus/index.d.ts +0 -2
  116. package/dist/types-ts4.5/focus/focus.d.ts +0 -50
  117. package/dist/types-ts4.5/focus/index.d.ts +0 -2
@@ -0,0 +1,122 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+ import { useAnchorPosition } from './use-anchor-position';
3
+
4
+ /**
5
+ * A point in viewport coordinates (CSS pixels) at which the synthetic
6
+ * anchor should be placed.
7
+ *
8
+ * Axis-named (`x`/`y`) rather than edge-named (`left`/`top`) so the
9
+ * shape is unambiguous in RTL writing modes — there is no ambiguity
10
+ * about whether a coordinate is logical (`inline-start`/`block-start`)
11
+ * or physical (`left`/`top`). Viewport coordinates are always
12
+ * physical, so we name them with the physical axis labels.
13
+ */
14
+
15
+ /**
16
+ * Styles the synthetic anchor as an invisible, zero-size point.
17
+ * Dynamic `inset-*` overrides are written by the positioning effect.
18
+ *
19
+ * Uses `Object.assign` rather than Compiled CSS (element is outside the React tree)
20
+ * or the internal `setStyle` helper (we own this element entirely — it is created
21
+ * and removed by this hook — so the snapshot/restore semantics `setStyle`
22
+ * provides for caller-owned elements would be dead work here).
23
+ */
24
+ function applyStaticAnchorStyles(el) {
25
+ Object.assign(el.style, {
26
+ position: 'fixed',
27
+ // `top`/`left` rather than `inset-block-start`/`inset-inline-start`
28
+ // for consistency with the dynamic positioning effect below, which
29
+ // uses physical properties (see comment there for the rationale).
30
+ top: '0',
31
+ left: '0',
32
+ // Zero size — placement decides which side the popover sits on.
33
+ width: '0',
34
+ height: '0',
35
+ // Prevent the overlay from intercepting pointer events.
36
+ pointerEvents: 'none'
37
+ });
38
+ }
39
+
40
+ // `useAnchorPosition` requires a real DOM element. When the caller only
41
+ // has a viewport coordinate (e.g. cursor at hover time), this hook
42
+ // manages a hidden `<div>` at that point and delegates the rest.
43
+ //
44
+ // The `<div>` is appended directly to `document.body` to escape any
45
+ // transformed ancestor, which would otherwise break `position: fixed`
46
+ // viewport-relative resolution.
47
+
48
+ /**
49
+ * Positions a popover at a consumer-supplied viewport coordinate using a
50
+ * hidden synthetic anchor.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * function PointPositionedPopover({ popoverRef, placement, mousePos }) {
55
+ * useAnchorPositionAtPoint({
56
+ * popoverRef,
57
+ * placement,
58
+ * isEnabled: Boolean(mousePos),
59
+ * getPoint: () => {
60
+ * if (!mousePos) return null;
61
+ * return { x: mousePos.clientX, y: mousePos.clientY };
62
+ * },
63
+ * });
64
+ * return <Popover ref={popoverRef}>...</Popover>;
65
+ * }
66
+ * ```
67
+ */
68
+ export function useAnchorPositionAtPoint({
69
+ popoverRef,
70
+ placement,
71
+ getPoint,
72
+ isEnabled = true
73
+ }) {
74
+ const syntheticAnchorRef = useRef(null);
75
+
76
+ // `getPoint` is kept in a ref so the layout effect can call the
77
+ // latest version without listing it as a dep — that would re-fire
78
+ // on every render and re-latch the point.
79
+ const getPointRef = useRef(getPoint);
80
+ getPointRef.current = getPoint;
81
+
82
+ // Lazy element creation: the `<div>` is only created when both
83
+ // `isEnabled` is true AND `getPoint()` returns a non-null point.
84
+ // `useLayoutEffect` ensures the element exists before the inner
85
+ // `useAnchorPosition` (also a layout effect) reads the ref.
86
+ useLayoutEffect(() => {
87
+ if (!isEnabled) {
88
+ return;
89
+ }
90
+ const point = getPointRef.current();
91
+ if (!point) {
92
+ return;
93
+ }
94
+ const anchor = document.createElement('div');
95
+ anchor.setAttribute('aria-hidden', 'true');
96
+ applyStaticAnchorStyles(anchor);
97
+ // `point.x`/`y` are physical viewport coordinates (from
98
+ // `MouseEvent.clientX/Y`), so map directly onto physical
99
+ // `left`/`top`. This is RTL-safe: `position: fixed` + physical
100
+ // insets is direction-independent. Using logical
101
+ // `inset-inline-start` would need `viewportWidth - point.x` in
102
+ // RTL to land at the same physical pixel.
103
+ anchor.style.top = `${point.y}px`;
104
+ anchor.style.left = `${point.x}px`;
105
+ document.body.appendChild(anchor);
106
+ syntheticAnchorRef.current = anchor;
107
+ return function cleanup() {
108
+ anchor.remove();
109
+ syntheticAnchorRef.current = null;
110
+ };
111
+ }, [isEnabled]);
112
+
113
+ // Mirror `isEnabled` so both hooks stay in sync — when disabled,
114
+ // neither this hook nor the inner one touches the popover, leaving
115
+ // any other positioning the consumer has set up free to take effect.
116
+ useAnchorPosition({
117
+ anchorRef: syntheticAnchorRef,
118
+ popoverRef,
119
+ placement,
120
+ isEnabled
121
+ });
122
+ }
@@ -197,18 +197,21 @@ function useStablePlacement(placement) {
197
197
  * comparison is used internally so re-runs are skipped when the resolved
198
198
  * placement shape is unchanged.
199
199
  */
200
- // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
201
200
  export function useAnchorPosition({
202
201
  anchorRef,
203
202
  popoverRef,
204
203
  placement = {},
205
- forceFallbackPositioning = false
204
+ forceFallbackPositioning = false,
205
+ isEnabled = true
206
206
  }) {
207
207
  const id = useId();
208
208
  // Stabilize `placement` object so the same object literal
209
209
  // value returns an object with the same reference
210
210
  const stablePlacement = useStablePlacement(placement);
211
211
  useLayoutEffect(() => {
212
+ if (!isEnabled) {
213
+ return;
214
+ }
212
215
  const trigger = anchorRef.current;
213
216
  const popover = popoverRef.current;
214
217
  if (!trigger || !popover) {
@@ -456,5 +459,5 @@ export function useAnchorPosition({
456
459
  popover.style.removeProperty('opacity');
457
460
  });
458
461
  return undoPositioning;
459
- }, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, id]);
462
+ }, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id]);
460
463
  }
@@ -1,6 +1,9 @@
1
1
  import { useEffect } from 'react';
2
2
  import { bind } from 'bind-event-listener';
3
- import { getFirstFocusable, getLastFocusable, getNextFocusable, isNestedLayerFocused } from '../focus/focus';
3
+ import { getFirstFocusable } from '../focus/get-first-focusable';
4
+ import { getLastFocusable } from '../focus/get-last-focusable';
5
+ import { getNextFocusable } from '../focus/get-next-focusable';
6
+ import { isNestedLayerFocused } from '../focus/is-nested-layer-focused';
4
7
 
5
8
  /**
6
9
  * Roles that require focus wrapping per WAI-ARIA APG.
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef } from 'react';
2
- import { getFirstFocusable } from '../focus/focus';
2
+ import { getFirstFocusable } from '../focus/get-first-focusable';
3
3
 
4
4
  /**
5
5
  * Determines the correct element to receive initial focus within a popover,
@@ -1,4 +1,4 @@
1
- import { placementMapping } from './legacy-placements';
1
+ import { placementMapping } from './placement-mapping';
2
2
 
3
3
  /**
4
4
  * Maps legacy Popper.js placement values to the new object-based
@@ -5,103 +5,4 @@
5
5
  * to enumerate every placement (e.g. for a settings dropdown). New code
6
6
  * should use the object-based `TPlacementOptions` directly.
7
7
  */
8
- // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
9
- export const LEGACY_PLACEMENTS = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-center', 'top-end', 'bottom', 'bottom-start', 'bottom-center', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'];
10
- /**
11
- * The full mapping record. Internal - use `fromLegacyPlacement` from the
12
- * public entry-point so the offset-tuple translation is applied consistently.
13
- *
14
- * Note: `auto`, `auto-start`, `auto-end` map to `block-end` placements. This
15
- * is a reduction in expressiveness vs Popper.js (which picked the best
16
- * placement at runtime). CSS `position-try-fallbacks` flips at the edge but
17
- * the initial placement is fixed. Migration consumers who relied on Popper's
18
- * dynamic auto-flip should pick an explicit placement instead.
19
- */
20
- // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
21
- export const placementMapping = {
22
- top: {
23
- axis: 'block',
24
- edge: 'start',
25
- align: 'center'
26
- },
27
- 'top-start': {
28
- axis: 'block',
29
- edge: 'start',
30
- align: 'start'
31
- },
32
- 'top-center': {
33
- axis: 'block',
34
- edge: 'start',
35
- align: 'center'
36
- },
37
- 'top-end': {
38
- axis: 'block',
39
- edge: 'start',
40
- align: 'end'
41
- },
42
- bottom: {
43
- axis: 'block',
44
- edge: 'end',
45
- align: 'center'
46
- },
47
- 'bottom-start': {
48
- axis: 'block',
49
- edge: 'end',
50
- align: 'start'
51
- },
52
- 'bottom-center': {
53
- axis: 'block',
54
- edge: 'end',
55
- align: 'center'
56
- },
57
- 'bottom-end': {
58
- axis: 'block',
59
- edge: 'end',
60
- align: 'end'
61
- },
62
- right: {
63
- axis: 'inline',
64
- edge: 'end',
65
- align: 'center'
66
- },
67
- 'right-start': {
68
- axis: 'inline',
69
- edge: 'end',
70
- align: 'start'
71
- },
72
- 'right-end': {
73
- axis: 'inline',
74
- edge: 'end',
75
- align: 'end'
76
- },
77
- left: {
78
- axis: 'inline',
79
- edge: 'start',
80
- align: 'center'
81
- },
82
- 'left-start': {
83
- axis: 'inline',
84
- edge: 'start',
85
- align: 'start'
86
- },
87
- 'left-end': {
88
- axis: 'inline',
89
- edge: 'start',
90
- align: 'end'
91
- },
92
- auto: {
93
- axis: 'block',
94
- edge: 'end',
95
- align: 'center'
96
- },
97
- 'auto-start': {
98
- axis: 'block',
99
- edge: 'end',
100
- align: 'start'
101
- },
102
- 'auto-end': {
103
- axis: 'block',
104
- edge: 'end',
105
- align: 'end'
106
- }
107
- };
8
+ export const LEGACY_PLACEMENTS = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-center', 'top-end', 'bottom', 'bottom-start', 'bottom-center', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'];
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The full mapping record. Internal - use `fromLegacyPlacement` from the
3
+ * public entry-point so the offset-tuple translation is applied consistently.
4
+ *
5
+ * Note: `auto`, `auto-start`, `auto-end` map to `block-end` placements. This
6
+ * is a reduction in expressiveness vs Popper.js (which picked the best
7
+ * placement at runtime). CSS `position-try-fallbacks` flips at the edge but
8
+ * the initial placement is fixed. Migration consumers who relied on Popper's
9
+ * dynamic auto-flip should pick an explicit placement instead.
10
+ */
11
+ export const placementMapping = {
12
+ top: {
13
+ axis: 'block',
14
+ edge: 'start',
15
+ align: 'center'
16
+ },
17
+ 'top-start': {
18
+ axis: 'block',
19
+ edge: 'start',
20
+ align: 'start'
21
+ },
22
+ 'top-center': {
23
+ axis: 'block',
24
+ edge: 'start',
25
+ align: 'center'
26
+ },
27
+ 'top-end': {
28
+ axis: 'block',
29
+ edge: 'start',
30
+ align: 'end'
31
+ },
32
+ bottom: {
33
+ axis: 'block',
34
+ edge: 'end',
35
+ align: 'center'
36
+ },
37
+ 'bottom-start': {
38
+ axis: 'block',
39
+ edge: 'end',
40
+ align: 'start'
41
+ },
42
+ 'bottom-center': {
43
+ axis: 'block',
44
+ edge: 'end',
45
+ align: 'center'
46
+ },
47
+ 'bottom-end': {
48
+ axis: 'block',
49
+ edge: 'end',
50
+ align: 'end'
51
+ },
52
+ right: {
53
+ axis: 'inline',
54
+ edge: 'end',
55
+ align: 'center'
56
+ },
57
+ 'right-start': {
58
+ axis: 'inline',
59
+ edge: 'end',
60
+ align: 'start'
61
+ },
62
+ 'right-end': {
63
+ axis: 'inline',
64
+ edge: 'end',
65
+ align: 'end'
66
+ },
67
+ left: {
68
+ axis: 'inline',
69
+ edge: 'start',
70
+ align: 'center'
71
+ },
72
+ 'left-start': {
73
+ axis: 'inline',
74
+ edge: 'start',
75
+ align: 'start'
76
+ },
77
+ 'left-end': {
78
+ axis: 'inline',
79
+ edge: 'start',
80
+ align: 'end'
81
+ },
82
+ auto: {
83
+ axis: 'block',
84
+ edge: 'end',
85
+ align: 'center'
86
+ },
87
+ 'auto-start': {
88
+ axis: 'block',
89
+ edge: 'end',
90
+ align: 'start'
91
+ },
92
+ 'auto-end': {
93
+ axis: 'block',
94
+ edge: 'end',
95
+ align: 'end'
96
+ }
97
+ };
@@ -4,7 +4,8 @@ import { ax, ix } from "@compiled/react/runtime";
4
4
  import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
5
5
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
6
  import { prefersReducedMotion } from '../internal/reduced-motion';
7
- import { roleToAriaHasPopup, shouldFocusIntoPopover } from '../internal/role-types';
7
+ import { roleToAriaHasPopup } from '../internal/role-to-aria-has-popup';
8
+ import { shouldFocusIntoPopover } from '../internal/should-focus-into-popover';
8
9
  import { useAnchorPosition } from '../internal/use-anchor-position';
9
10
  import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
10
11
  import { Popover } from '../popover/popover';
@@ -2,7 +2,9 @@ import { useCallback, useEffect } from 'react';
2
2
  import { bind } from 'bind-event-listener';
3
3
  import { getDocument } from '@atlaskit/browser-apis';
4
4
  import { KEY_DOWN, KEY_END, KEY_HOME, KEY_TAB, KEY_UP } from '@atlaskit/ds-lib/keycodes';
5
- import { getFirstFocusable, getLastFocusable, getNextFocusable } from '../focus';
5
+ import { getFirstFocusable } from '../focus/get-first-focusable';
6
+ import { getLastFocusable } from '../focus/get-last-focusable';
7
+ import { getNextFocusable } from '../focus/get-next-focusable';
6
8
  const menuScope = '[role="menu"]';
7
9
 
8
10
  /**
@@ -1 +1,3 @@
1
- export { getFirstFocusable, getLastFocusable, getNextFocusable } from '../focus';
1
+ export { getFirstFocusable } from '../focus/get-first-focusable';
2
+ export { getLastFocusable } from '../focus/get-last-focusable';
3
+ export { getNextFocusable } from '../focus/get-next-focusable';
@@ -0,0 +1 @@
1
+ export { useAnchorPositionAtPoint } from '../internal/use-anchor-position-at-point';
@@ -0,0 +1,13 @@
1
+ import { getFocusables } from './get-focusables';
2
+ /**
3
+ * Returns the first focusable element within the container.
4
+ */
5
+ export function getFirstFocusable(_ref) {
6
+ var container = _ref.container,
7
+ filter = _ref.filter;
8
+ var items = getFocusables({
9
+ container: container,
10
+ filter: filter
11
+ });
12
+ return items.length > 0 ? items[0] : null;
13
+ }
@@ -0,0 +1,24 @@
1
+ import { isInSameLayer } from './is-in-same-layer';
2
+ import { selectors } from './selectors';
3
+ /**
4
+ * Returns all focusable HTMLElements within the container, optionally filtered.
5
+ *
6
+ * Focusables that belong to a nested top-layer scope (a `[popover]` or
7
+ * `<dialog>` descendant of the container) are excluded - those elements
8
+ * are owned by the inner layer's focus management. The container itself
9
+ * is allowed to be a popover/dialog; only nested ones are filtered.
10
+ */
11
+ export function getFocusables(_ref) {
12
+ var container = _ref.container,
13
+ 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) {
22
+ return !filter || filter(element, container);
23
+ });
24
+ }
@@ -0,0 +1,13 @@
1
+ import { getFocusables } from './get-focusables';
2
+ /**
3
+ * Returns the last focusable element within the container.
4
+ */
5
+ export function getLastFocusable(_ref) {
6
+ var container = _ref.container,
7
+ filter = _ref.filter;
8
+ var items = getFocusables({
9
+ container: container,
10
+ filter: filter
11
+ });
12
+ return items.length > 0 ? items[items.length - 1] : null;
13
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Returns the next focusable element in the given direction relative to the
3
+ * currently focused element within the container. Wraps around at both ends.
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.
7
+ */
8
+
9
+ import { getFocusables } from './get-focusables';
10
+ import { selectors } from './selectors';
11
+ export function getNextFocusable(_ref) {
12
+ var _all$nextIndex;
13
+ var container = _ref.container,
14
+ direction = _ref.direction,
15
+ filter = _ref.filter;
16
+ var all = getFocusables({
17
+ container: container,
18
+ filter: filter
19
+ });
20
+ var current = container.querySelector(selectors.focused);
21
+ if (!(current instanceof HTMLElement)) {
22
+ return null;
23
+ }
24
+ var index = all.indexOf(current);
25
+ if (index === -1) {
26
+ return null;
27
+ }
28
+ var nextIndex = function () {
29
+ if (direction === 'forwards') {
30
+ return (index + 1) % all.length;
31
+ }
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;
37
+ }
@@ -0,0 +1,17 @@
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
+ var elementLayer = element.closest(topLayerSelector);
11
+ var containerLayer = container.closest(topLayerSelector);
12
+ // Both elements are in the "document" layer
13
+ if (!elementLayer && !containerLayer) {
14
+ return true;
15
+ }
16
+ return elementLayer === containerLayer;
17
+ }
@@ -0,0 +1,27 @@
1
+ import { getDocument } from '@atlaskit/browser-apis';
2
+ import { isInSameLayer } from './is-in-same-layer';
3
+
4
+ /**
5
+ * 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`).
8
+ */
9
+ export function isNestedLayerFocused(_ref) {
10
+ var _getDocument;
11
+ var container = _ref.container;
12
+ var focused = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
13
+ if (!(focused instanceof HTMLElement)) {
14
+ return false;
15
+ }
16
+
17
+ // Cannot be in a nested layer if it is not in the container
18
+ if (!container.contains(focused)) {
19
+ return false;
20
+ }
21
+
22
+ // If not in the same layer, must be in a nested layer
23
+ return !isInSameLayer({
24
+ element: focused,
25
+ container: container
26
+ });
27
+ }
@@ -0,0 +1,13 @@
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).concat(not)];
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
+ }();
File without changes
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Maps a popover content role to the correct `aria-haspopup` value
3
+ * for its trigger element.
4
+ *
5
+ * Returns `undefined` for roles that are not popups in the ARIA sense
6
+ * (`tooltip`, `note`, `status`, `alert`, `log`). Triggers for those roles
7
+ * should not announce a popup at all - emitting `aria-haspopup="dialog"`
8
+ * on a tooltip trigger would mislead assistive technology.
9
+ */
10
+ export function roleToAriaHasPopup(_ref) {
11
+ var role = _ref.role;
12
+ if (role === 'menu') {
13
+ return 'menu';
14
+ }
15
+ if (role === 'listbox') {
16
+ return 'listbox';
17
+ }
18
+ if (role === 'tree') {
19
+ return 'tree';
20
+ }
21
+ if (role === 'grid') {
22
+ return 'grid';
23
+ }
24
+ if (role === 'dialog' || role === 'alertdialog') {
25
+ return 'dialog';
26
+ }
27
+ // Non-popup roles (tooltip, note, status, alert, log) and the
28
+ // no-role case: omit `aria-haspopup` entirely. React drops the
29
+ // attribute when the value is `undefined`.
30
+ return undefined;
31
+ }