@primer/behaviors 0.0.0-2023818155639 → 0.0.0-20240222105352

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.
@@ -1,5 +1,5 @@
1
- export declare type AnchorAlignment = 'start' | 'center' | 'end';
2
- export declare type AnchorSide = 'inside-top' | 'inside-bottom' | 'inside-left' | 'inside-right' | 'inside-center' | 'outside-top' | 'outside-bottom' | 'outside-left' | 'outside-right';
1
+ export type AnchorAlignment = 'start' | 'center' | 'end';
2
+ export type AnchorSide = 'inside-top' | 'inside-bottom' | 'inside-left' | 'inside-right' | 'inside-center' | 'outside-top' | 'outside-bottom' | 'outside-left' | 'outside-right';
3
3
  export interface PositionSettings {
4
4
  side: AnchorSide;
5
5
  align: AnchorAlignment;
@@ -5,12 +5,12 @@ const alternateOrders = {
5
5
  'outside-top': ['outside-bottom', 'outside-right', 'outside-left', 'outside-bottom'],
6
6
  'outside-bottom': ['outside-top', 'outside-right', 'outside-left', 'outside-bottom'],
7
7
  'outside-left': ['outside-right', 'outside-bottom', 'outside-top', 'outside-bottom'],
8
- 'outside-right': ['outside-left', 'outside-bottom', 'outside-top', 'outside-bottom']
8
+ 'outside-right': ['outside-left', 'outside-bottom', 'outside-top', 'outside-bottom'],
9
9
  };
10
10
  const alternateAlignments = {
11
11
  start: ['end', 'center'],
12
12
  end: ['start', 'center'],
13
- center: ['end', 'start']
13
+ center: ['end', 'start'],
14
14
  };
15
15
  function getAnchoredPosition(floatingElement, anchorElement, settings = {}) {
16
16
  const parentElement = getPositionedParent(floatingElement);
@@ -20,12 +20,14 @@ function getAnchoredPosition(floatingElement, anchorElement, settings = {}) {
20
20
  const [borderTop, borderLeft] = [parentElementStyle.borderTopWidth, parentElementStyle.borderLeftWidth].map(v => parseInt(v, 10) || 0);
21
21
  const relativeRect = {
22
22
  top: parentElementRect.top + borderTop,
23
- left: parentElementRect.left + borderLeft
23
+ left: parentElementRect.left + borderLeft,
24
24
  };
25
25
  return pureCalculateAnchoredPosition(clippingRect, relativeRect, floatingElement.getBoundingClientRect(), anchorElement instanceof Element ? anchorElement.getBoundingClientRect() : anchorElement, getDefaultSettings(settings));
26
26
  }
27
27
  exports.getAnchoredPosition = getAnchoredPosition;
28
28
  function getPositionedParent(element) {
29
+ if (isOnTopLayer(element))
30
+ return document.body;
29
31
  let parentNode = element.parentNode;
30
32
  while (parentNode !== null) {
31
33
  if (parentNode instanceof HTMLElement && getComputedStyle(parentNode).position !== 'static') {
@@ -35,10 +37,24 @@ function getPositionedParent(element) {
35
37
  }
36
38
  return document.body;
37
39
  }
40
+ function isOnTopLayer(element) {
41
+ var _a;
42
+ if (element.tagName === 'DIALOG') {
43
+ return element.matches(':modal');
44
+ }
45
+ try {
46
+ if (element.matches(':popover-open') && /native code/.test((_a = document.body.showPopover) === null || _a === void 0 ? void 0 : _a.toString())) {
47
+ return true;
48
+ }
49
+ }
50
+ catch (_b) {
51
+ }
52
+ return element.matches(':fullscreen');
53
+ }
38
54
  function getClippingRect(element) {
39
55
  let parentNode = element;
40
56
  while (parentNode !== null) {
41
- if (parentNode === document.body) {
57
+ if (!(parentNode instanceof Element)) {
42
58
  break;
43
59
  }
44
60
  const parentNodeStyle = getComputedStyle(parentNode);
@@ -54,13 +70,13 @@ function getClippingRect(element) {
54
70
  elemStyle.borderTopWidth,
55
71
  elemStyle.borderLeftWidth,
56
72
  elemStyle.borderRightWidth,
57
- elemStyle.borderBottomWidth
73
+ elemStyle.borderBottomWidth,
58
74
  ].map(v => parseInt(v, 10) || 0);
59
75
  return {
60
76
  top: elemRect.top + borderTop,
61
77
  left: elemRect.left + borderLeft,
62
78
  width: elemRect.width - borderRight - borderLeft,
63
- height: Math.max(elemRect.height - borderTop - borderBottom, clippingNode === document.body ? window.innerHeight : -Infinity)
79
+ height: Math.max(elemRect.height - borderTop - borderBottom, clippingNode === document.body ? window.innerHeight : -Infinity),
64
80
  };
65
81
  }
66
82
  const positionDefaults = {
@@ -68,7 +84,7 @@ const positionDefaults = {
68
84
  align: 'start',
69
85
  anchorOffset: 4,
70
86
  alignmentOffset: 4,
71
- allowOutOfBounds: false
87
+ allowOutOfBounds: false,
72
88
  };
73
89
  function getDefaultSettings(settings = {}) {
74
90
  var _a, _b, _c, _d, _e;
@@ -79,7 +95,7 @@ function getDefaultSettings(settings = {}) {
79
95
  align,
80
96
  anchorOffset: (_c = settings.anchorOffset) !== null && _c !== void 0 ? _c : (side === 'inside-center' ? 0 : positionDefaults.anchorOffset),
81
97
  alignmentOffset: (_d = settings.alignmentOffset) !== null && _d !== void 0 ? _d : (align !== 'center' && side.startsWith('inside') ? positionDefaults.alignmentOffset : 0),
82
- allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds
98
+ allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds,
83
99
  };
84
100
  }
85
101
  function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingRect, anchorRect, { side, align, allowOutOfBounds, anchorOffset, alignmentOffset }) {
@@ -87,7 +103,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
87
103
  top: viewportRect.top - relativePosition.top,
88
104
  left: viewportRect.left - relativePosition.left,
89
105
  width: viewportRect.width,
90
- height: viewportRect.height
106
+ height: viewportRect.height,
91
107
  };
92
108
  let pos = calculatePosition(floatingRect, anchorRect, side, align, anchorOffset, alignmentOffset);
93
109
  let anchorSide = side;
@@ -134,7 +150,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
134
150
  }
135
151
  if (alternateOrder && positionAttempt < alternateOrder.length) {
136
152
  if (pos.top + floatingRect.height > viewportRect.height + relativeViewportRect.top) {
137
- pos.top = viewportRect.height + relativeViewportRect.top - floatingRect.height;
153
+ pos.top = Math.max(viewportRect.height + relativeViewportRect.top - floatingRect.height, 0);
138
154
  }
139
155
  }
140
156
  }
@@ -1,4 +1,4 @@
1
- declare type Dimensions = {
1
+ type Dimensions = {
2
2
  top: number;
3
3
  left: number;
4
4
  bottom: number;
@@ -6,7 +6,7 @@ declare type Dimensions = {
6
6
  height?: number;
7
7
  width?: number;
8
8
  };
9
- declare type Offset = {
9
+ type Offset = {
10
10
  top: number;
11
11
  left: number;
12
12
  };
@@ -5,7 +5,7 @@ function offset(element) {
5
5
  const rect = element.getBoundingClientRect();
6
6
  return {
7
7
  top: rect.top + window.pageYOffset,
8
- left: rect.left + window.pageXOffset
8
+ left: rect.left + window.pageXOffset,
9
9
  };
10
10
  }
11
11
  exports.offset = offset;
@@ -62,11 +62,11 @@ function overflowOffset(element, targetContainer) {
62
62
  const scroll = container === document.documentElement && document.defaultView
63
63
  ? {
64
64
  top: document.defaultView.pageYOffset,
65
- left: document.defaultView.pageXOffset
65
+ left: document.defaultView.pageXOffset,
66
66
  }
67
67
  : {
68
68
  top: container.scrollTop,
69
- left: container.scrollLeft
69
+ left: container.scrollLeft,
70
70
  };
71
71
  const top = elementOffset.top - scroll.top;
72
72
  const left = elementOffset.left - scroll.left;
@@ -94,7 +94,7 @@ function focusTrap(container, initialFocus, abortSignal) {
94
94
  container,
95
95
  controller: wrappingController,
96
96
  initialFocus,
97
- originalSignal: signal
97
+ originalSignal: signal,
98
98
  };
99
99
  const suspendedTrapIndex = suspendedTrapStack.findIndex(t => t.container === container);
100
100
  if (suspendedTrapIndex >= 0) {
@@ -1,5 +1,6 @@
1
- export declare type Direction = 'previous' | 'next' | 'start' | 'end';
2
- export declare type FocusMovementKeys = 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'ArrowRight' | 'h' | 'j' | 'k' | 'l' | 'a' | 's' | 'w' | 'd' | 'Tab' | 'Home' | 'End' | 'PageUp' | 'PageDown';
1
+ import { IterateFocusableElements } from './utils/iterate-focusable-elements.js';
2
+ export type Direction = 'previous' | 'next' | 'start' | 'end';
3
+ export type FocusMovementKeys = 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'ArrowRight' | 'h' | 'j' | 'k' | 'l' | 'a' | 's' | 'w' | 'd' | 'Tab' | 'Home' | 'End' | 'PageUp' | 'PageDown' | 'Backspace';
3
4
  export declare enum FocusKeys {
4
5
  ArrowHorizontal = 1,
5
6
  ArrowVertical = 2,
@@ -10,12 +11,13 @@ export declare enum FocusKeys {
10
11
  WS = 32,
11
12
  AD = 64,
12
13
  Tab = 128,
14
+ Backspace = 512,
13
15
  ArrowAll = 3,
14
16
  HJKL = 12,
15
17
  WASD = 96,
16
18
  All = 511
17
19
  }
18
- export interface FocusZoneSettings {
20
+ export type FocusZoneSettings = IterateFocusableElements & {
19
21
  focusOutBehavior?: 'stop' | 'wrap';
20
22
  getNextFocusable?: (direction: Direction, from: Element | undefined, event: KeyboardEvent) => HTMLElement | undefined;
21
23
  focusableElementFilter?: (element: HTMLElement) => boolean;
@@ -25,7 +27,7 @@ export interface FocusZoneSettings {
25
27
  onActiveDescendantChanged?: (newActiveDescendant: HTMLElement | undefined, previousActiveDescendant: HTMLElement | undefined, directlyActivated: boolean) => void;
26
28
  focusInStrategy?: 'first' | 'closest' | 'previous' | ((previousFocusedElement: Element) => HTMLElement | undefined);
27
29
  preventScroll?: boolean;
28
- }
30
+ };
29
31
  export declare const isActiveDescendantAttribute = "data-is-active-descendant";
30
32
  export declare const activeDescendantActivatedDirectly = "activated-directly";
31
33
  export declare const activeDescendantActivatedIndirectly = "activated-indirectly";
@@ -17,11 +17,12 @@ var FocusKeys;
17
17
  FocusKeys[FocusKeys["WS"] = 32] = "WS";
18
18
  FocusKeys[FocusKeys["AD"] = 64] = "AD";
19
19
  FocusKeys[FocusKeys["Tab"] = 128] = "Tab";
20
+ FocusKeys[FocusKeys["Backspace"] = 512] = "Backspace";
20
21
  FocusKeys[FocusKeys["ArrowAll"] = 3] = "ArrowAll";
21
22
  FocusKeys[FocusKeys["HJKL"] = 12] = "HJKL";
22
23
  FocusKeys[FocusKeys["WASD"] = 96] = "WASD";
23
24
  FocusKeys[FocusKeys["All"] = 511] = "All";
24
- })(FocusKeys = exports.FocusKeys || (exports.FocusKeys = {}));
25
+ })(FocusKeys || (exports.FocusKeys = FocusKeys = {}));
25
26
  const KEY_TO_BIT = {
26
27
  ArrowLeft: FocusKeys.ArrowHorizontal,
27
28
  ArrowDown: FocusKeys.ArrowVertical,
@@ -39,7 +40,8 @@ const KEY_TO_BIT = {
39
40
  Home: FocusKeys.HomeAndEnd,
40
41
  End: FocusKeys.HomeAndEnd,
41
42
  PageUp: FocusKeys.PageUpDown,
42
- PageDown: FocusKeys.PageUpDown
43
+ PageDown: FocusKeys.PageUpDown,
44
+ Backspace: FocusKeys.Backspace,
43
45
  };
44
46
  const KEY_TO_DIRECTION = {
45
47
  ArrowLeft: 'previous',
@@ -58,7 +60,8 @@ const KEY_TO_DIRECTION = {
58
60
  Home: 'start',
59
61
  End: 'end',
60
62
  PageUp: 'start',
61
- PageDown: 'end'
63
+ PageDown: 'end',
64
+ Backspace: 'previous',
62
65
  };
63
66
  function getDirection(keyboardEvent) {
64
67
  const direction = KEY_TO_DIRECTION[keyboardEvent.key];
@@ -187,8 +190,7 @@ function focusZone(container, settings) {
187
190
  if (filteredElements.length === 0) {
188
191
  return;
189
192
  }
190
- const insertIndex = focusableElements.findIndex(e => (e.compareDocumentPosition(filteredElements[0]) & Node.DOCUMENT_POSITION_PRECEDING) > 0);
191
- focusableElements.splice(insertIndex === -1 ? focusableElements.length : insertIndex, 0, ...filteredElements);
193
+ focusableElements.splice(findInsertionIndex(filteredElements), 0, ...filteredElements);
192
194
  for (const element of filteredElements) {
193
195
  if (!savedTabIndex.has(element)) {
194
196
  savedTabIndex.set(element, element.getAttribute('tabindex'));
@@ -199,6 +201,27 @@ function focusZone(container, settings) {
199
201
  updateFocusedElement(getFirstFocusableElement());
200
202
  }
201
203
  }
204
+ function findInsertionIndex(elementsToInsert) {
205
+ const firstElementToInsert = elementsToInsert[0];
206
+ if (focusableElements.length === 0)
207
+ return 0;
208
+ let iMin = 0;
209
+ let iMax = focusableElements.length - 1;
210
+ while (iMin <= iMax) {
211
+ const i = Math.floor((iMin + iMax) / 2);
212
+ const element = focusableElements[i];
213
+ if (followsInDocument(firstElementToInsert, element)) {
214
+ iMax = i - 1;
215
+ }
216
+ else {
217
+ iMin = i + 1;
218
+ }
219
+ }
220
+ return iMin;
221
+ }
222
+ function followsInDocument(first, second) {
223
+ return (second.compareDocumentPosition(first) & Node.DOCUMENT_POSITION_PRECEDING) > 0;
224
+ }
202
225
  function endFocusManagement(...elements) {
203
226
  for (const element of elements) {
204
227
  const focusableElementIndex = focusableElements.indexOf(element);
@@ -221,27 +244,33 @@ function focusZone(container, settings) {
221
244
  }
222
245
  }
223
246
  }
224
- beginFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(container));
225
- updateFocusedElement(getFirstFocusableElement());
247
+ const iterateFocusableElementsOptions = {
248
+ reverse: settings === null || settings === void 0 ? void 0 : settings.reverse,
249
+ strict: settings === null || settings === void 0 ? void 0 : settings.strict,
250
+ onlyTabbable: settings === null || settings === void 0 ? void 0 : settings.onlyTabbable,
251
+ };
252
+ beginFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(container, iterateFocusableElementsOptions));
253
+ const initialElement = typeof focusInStrategy === 'function' ? focusInStrategy(document.body) : getFirstFocusableElement();
254
+ updateFocusedElement(initialElement);
226
255
  const observer = new MutationObserver(mutations => {
227
256
  for (const mutation of mutations) {
228
257
  for (const removedNode of mutation.removedNodes) {
229
258
  if (removedNode instanceof HTMLElement) {
230
- endFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(removedNode));
259
+ endFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(removedNode, iterateFocusableElementsOptions));
231
260
  }
232
261
  }
233
262
  }
234
263
  for (const mutation of mutations) {
235
264
  for (const addedNode of mutation.addedNodes) {
236
265
  if (addedNode instanceof HTMLElement) {
237
- beginFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(addedNode));
266
+ beginFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(addedNode, iterateFocusableElementsOptions));
238
267
  }
239
268
  }
240
269
  }
241
270
  });
242
271
  observer.observe(container, {
243
272
  subtree: true,
244
- childList: true
273
+ childList: true,
245
274
  });
246
275
  const controller = new AbortController();
247
276
  const signal = (_e = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _e !== void 0 ? _e : controller.signal;
package/dist/cjs/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -8,8 +8,8 @@ try {
8
8
  signal: {
9
9
  get() {
10
10
  signalSupported = true;
11
- }
12
- }
11
+ },
12
+ },
13
13
  });
14
14
  window.addEventListener('test', noop, options);
15
15
  window.removeEventListener('test', noop, options);
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -6,7 +6,7 @@ function* iterateFocusableElements(container, options = {}) {
6
6
  const strict = (_a = options.strict) !== null && _a !== void 0 ? _a : false;
7
7
  const acceptFn = ((_b = options.onlyTabbable) !== null && _b !== void 0 ? _b : false) ? isTabbable : isFocusable;
8
8
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
9
- acceptNode: node => node instanceof HTMLElement && acceptFn(node, strict) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
9
+ acceptNode: node => node instanceof HTMLElement && acceptFn(node, strict) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
10
10
  });
11
11
  let nextNode = null;
12
12
  if (!options.reverse && acceptFn(container, strict)) {
@@ -46,16 +46,21 @@ function isFocusable(elem, strict = false) {
46
46
  return false;
47
47
  }
48
48
  if (strict) {
49
+ const style = getComputedStyle(elem);
49
50
  const sizeInert = elem.offsetWidth === 0 || elem.offsetHeight === 0;
50
- const visibilityInert = ['hidden', 'collapse'].includes(getComputedStyle(elem).visibility);
51
+ const visibilityInert = ['hidden', 'collapse'].includes(style.visibility);
52
+ const displayInert = style.display === 'none' || !elem.offsetParent;
51
53
  const clientRectsInert = elem.getClientRects().length === 0;
52
- if (sizeInert || visibilityInert || clientRectsInert) {
54
+ if (sizeInert || visibilityInert || clientRectsInert || displayInert) {
53
55
  return false;
54
56
  }
55
57
  }
56
58
  if (elem.getAttribute('tabindex') != null) {
57
59
  return true;
58
60
  }
61
+ if (elem.getAttribute('contenteditable') === 'true' || elem.getAttribute('contenteditable') === 'plaintext-only') {
62
+ return true;
63
+ }
59
64
  if (elem instanceof HTMLAnchorElement && elem.getAttribute('href') == null) {
60
65
  return false;
61
66
  }
@@ -1,5 +1,5 @@
1
- export declare type AnchorAlignment = 'start' | 'center' | 'end';
2
- export declare type AnchorSide = 'inside-top' | 'inside-bottom' | 'inside-left' | 'inside-right' | 'inside-center' | 'outside-top' | 'outside-bottom' | 'outside-left' | 'outside-right';
1
+ export type AnchorAlignment = 'start' | 'center' | 'end';
2
+ export type AnchorSide = 'inside-top' | 'inside-bottom' | 'inside-left' | 'inside-right' | 'inside-center' | 'outside-top' | 'outside-bottom' | 'outside-left' | 'outside-right';
3
3
  export interface PositionSettings {
4
4
  side: AnchorSide;
5
5
  align: AnchorAlignment;
@@ -2,12 +2,12 @@ const alternateOrders = {
2
2
  'outside-top': ['outside-bottom', 'outside-right', 'outside-left', 'outside-bottom'],
3
3
  'outside-bottom': ['outside-top', 'outside-right', 'outside-left', 'outside-bottom'],
4
4
  'outside-left': ['outside-right', 'outside-bottom', 'outside-top', 'outside-bottom'],
5
- 'outside-right': ['outside-left', 'outside-bottom', 'outside-top', 'outside-bottom']
5
+ 'outside-right': ['outside-left', 'outside-bottom', 'outside-top', 'outside-bottom'],
6
6
  };
7
7
  const alternateAlignments = {
8
8
  start: ['end', 'center'],
9
9
  end: ['start', 'center'],
10
- center: ['end', 'start']
10
+ center: ['end', 'start'],
11
11
  };
12
12
  export function getAnchoredPosition(floatingElement, anchorElement, settings = {}) {
13
13
  const parentElement = getPositionedParent(floatingElement);
@@ -17,11 +17,13 @@ export function getAnchoredPosition(floatingElement, anchorElement, settings = {
17
17
  const [borderTop, borderLeft] = [parentElementStyle.borderTopWidth, parentElementStyle.borderLeftWidth].map(v => parseInt(v, 10) || 0);
18
18
  const relativeRect = {
19
19
  top: parentElementRect.top + borderTop,
20
- left: parentElementRect.left + borderLeft
20
+ left: parentElementRect.left + borderLeft,
21
21
  };
22
22
  return pureCalculateAnchoredPosition(clippingRect, relativeRect, floatingElement.getBoundingClientRect(), anchorElement instanceof Element ? anchorElement.getBoundingClientRect() : anchorElement, getDefaultSettings(settings));
23
23
  }
24
24
  function getPositionedParent(element) {
25
+ if (isOnTopLayer(element))
26
+ return document.body;
25
27
  let parentNode = element.parentNode;
26
28
  while (parentNode !== null) {
27
29
  if (parentNode instanceof HTMLElement && getComputedStyle(parentNode).position !== 'static') {
@@ -31,10 +33,24 @@ function getPositionedParent(element) {
31
33
  }
32
34
  return document.body;
33
35
  }
36
+ function isOnTopLayer(element) {
37
+ var _a;
38
+ if (element.tagName === 'DIALOG') {
39
+ return element.matches(':modal');
40
+ }
41
+ try {
42
+ if (element.matches(':popover-open') && /native code/.test((_a = document.body.showPopover) === null || _a === void 0 ? void 0 : _a.toString())) {
43
+ return true;
44
+ }
45
+ }
46
+ catch (_b) {
47
+ }
48
+ return element.matches(':fullscreen');
49
+ }
34
50
  function getClippingRect(element) {
35
51
  let parentNode = element;
36
52
  while (parentNode !== null) {
37
- if (parentNode === document.body) {
53
+ if (!(parentNode instanceof Element)) {
38
54
  break;
39
55
  }
40
56
  const parentNodeStyle = getComputedStyle(parentNode);
@@ -50,13 +66,13 @@ function getClippingRect(element) {
50
66
  elemStyle.borderTopWidth,
51
67
  elemStyle.borderLeftWidth,
52
68
  elemStyle.borderRightWidth,
53
- elemStyle.borderBottomWidth
69
+ elemStyle.borderBottomWidth,
54
70
  ].map(v => parseInt(v, 10) || 0);
55
71
  return {
56
72
  top: elemRect.top + borderTop,
57
73
  left: elemRect.left + borderLeft,
58
74
  width: elemRect.width - borderRight - borderLeft,
59
- height: Math.max(elemRect.height - borderTop - borderBottom, clippingNode === document.body ? window.innerHeight : -Infinity)
75
+ height: Math.max(elemRect.height - borderTop - borderBottom, clippingNode === document.body ? window.innerHeight : -Infinity),
60
76
  };
61
77
  }
62
78
  const positionDefaults = {
@@ -64,7 +80,7 @@ const positionDefaults = {
64
80
  align: 'start',
65
81
  anchorOffset: 4,
66
82
  alignmentOffset: 4,
67
- allowOutOfBounds: false
83
+ allowOutOfBounds: false,
68
84
  };
69
85
  function getDefaultSettings(settings = {}) {
70
86
  var _a, _b, _c, _d, _e;
@@ -75,7 +91,7 @@ function getDefaultSettings(settings = {}) {
75
91
  align,
76
92
  anchorOffset: (_c = settings.anchorOffset) !== null && _c !== void 0 ? _c : (side === 'inside-center' ? 0 : positionDefaults.anchorOffset),
77
93
  alignmentOffset: (_d = settings.alignmentOffset) !== null && _d !== void 0 ? _d : (align !== 'center' && side.startsWith('inside') ? positionDefaults.alignmentOffset : 0),
78
- allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds
94
+ allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds,
79
95
  };
80
96
  }
81
97
  function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingRect, anchorRect, { side, align, allowOutOfBounds, anchorOffset, alignmentOffset }) {
@@ -83,7 +99,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
83
99
  top: viewportRect.top - relativePosition.top,
84
100
  left: viewportRect.left - relativePosition.left,
85
101
  width: viewportRect.width,
86
- height: viewportRect.height
102
+ height: viewportRect.height,
87
103
  };
88
104
  let pos = calculatePosition(floatingRect, anchorRect, side, align, anchorOffset, alignmentOffset);
89
105
  let anchorSide = side;
@@ -130,7 +146,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
130
146
  }
131
147
  if (alternateOrder && positionAttempt < alternateOrder.length) {
132
148
  if (pos.top + floatingRect.height > viewportRect.height + relativeViewportRect.top) {
133
- pos.top = viewportRect.height + relativeViewportRect.top - floatingRect.height;
149
+ pos.top = Math.max(viewportRect.height + relativeViewportRect.top - floatingRect.height, 0);
134
150
  }
135
151
  }
136
152
  }
@@ -1,4 +1,4 @@
1
- declare type Dimensions = {
1
+ type Dimensions = {
2
2
  top: number;
3
3
  left: number;
4
4
  bottom: number;
@@ -6,7 +6,7 @@ declare type Dimensions = {
6
6
  height?: number;
7
7
  width?: number;
8
8
  };
9
- declare type Offset = {
9
+ type Offset = {
10
10
  top: number;
11
11
  left: number;
12
12
  };
@@ -2,7 +2,7 @@ export function offset(element) {
2
2
  const rect = element.getBoundingClientRect();
3
3
  return {
4
4
  top: rect.top + window.pageYOffset,
5
- left: rect.left + window.pageXOffset
5
+ left: rect.left + window.pageXOffset,
6
6
  };
7
7
  }
8
8
  export function overflowParent(targetElement) {
@@ -57,11 +57,11 @@ export function overflowOffset(element, targetContainer) {
57
57
  const scroll = container === document.documentElement && document.defaultView
58
58
  ? {
59
59
  top: document.defaultView.pageYOffset,
60
- left: document.defaultView.pageXOffset
60
+ left: document.defaultView.pageXOffset,
61
61
  }
62
62
  : {
63
63
  top: container.scrollTop,
64
- left: container.scrollLeft
64
+ left: container.scrollLeft,
65
65
  };
66
66
  const top = elementOffset.top - scroll.top;
67
67
  const left = elementOffset.left - scroll.left;
@@ -91,7 +91,7 @@ export function focusTrap(container, initialFocus, abortSignal) {
91
91
  container,
92
92
  controller: wrappingController,
93
93
  initialFocus,
94
- originalSignal: signal
94
+ originalSignal: signal,
95
95
  };
96
96
  const suspendedTrapIndex = suspendedTrapStack.findIndex(t => t.container === container);
97
97
  if (suspendedTrapIndex >= 0) {
@@ -1,5 +1,6 @@
1
- export declare type Direction = 'previous' | 'next' | 'start' | 'end';
2
- export declare type FocusMovementKeys = 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'ArrowRight' | 'h' | 'j' | 'k' | 'l' | 'a' | 's' | 'w' | 'd' | 'Tab' | 'Home' | 'End' | 'PageUp' | 'PageDown';
1
+ import { IterateFocusableElements } from './utils/iterate-focusable-elements.js';
2
+ export type Direction = 'previous' | 'next' | 'start' | 'end';
3
+ export type FocusMovementKeys = 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'ArrowRight' | 'h' | 'j' | 'k' | 'l' | 'a' | 's' | 'w' | 'd' | 'Tab' | 'Home' | 'End' | 'PageUp' | 'PageDown' | 'Backspace';
3
4
  export declare enum FocusKeys {
4
5
  ArrowHorizontal = 1,
5
6
  ArrowVertical = 2,
@@ -10,12 +11,13 @@ export declare enum FocusKeys {
10
11
  WS = 32,
11
12
  AD = 64,
12
13
  Tab = 128,
14
+ Backspace = 512,
13
15
  ArrowAll = 3,
14
16
  HJKL = 12,
15
17
  WASD = 96,
16
18
  All = 511
17
19
  }
18
- export interface FocusZoneSettings {
20
+ export type FocusZoneSettings = IterateFocusableElements & {
19
21
  focusOutBehavior?: 'stop' | 'wrap';
20
22
  getNextFocusable?: (direction: Direction, from: Element | undefined, event: KeyboardEvent) => HTMLElement | undefined;
21
23
  focusableElementFilter?: (element: HTMLElement) => boolean;
@@ -25,7 +27,7 @@ export interface FocusZoneSettings {
25
27
  onActiveDescendantChanged?: (newActiveDescendant: HTMLElement | undefined, previousActiveDescendant: HTMLElement | undefined, directlyActivated: boolean) => void;
26
28
  focusInStrategy?: 'first' | 'closest' | 'previous' | ((previousFocusedElement: Element) => HTMLElement | undefined);
27
29
  preventScroll?: boolean;
28
- }
30
+ };
29
31
  export declare const isActiveDescendantAttribute = "data-is-active-descendant";
30
32
  export declare const activeDescendantActivatedDirectly = "activated-directly";
31
33
  export declare const activeDescendantActivatedIndirectly = "activated-indirectly";
@@ -14,6 +14,7 @@ export var FocusKeys;
14
14
  FocusKeys[FocusKeys["WS"] = 32] = "WS";
15
15
  FocusKeys[FocusKeys["AD"] = 64] = "AD";
16
16
  FocusKeys[FocusKeys["Tab"] = 128] = "Tab";
17
+ FocusKeys[FocusKeys["Backspace"] = 512] = "Backspace";
17
18
  FocusKeys[FocusKeys["ArrowAll"] = 3] = "ArrowAll";
18
19
  FocusKeys[FocusKeys["HJKL"] = 12] = "HJKL";
19
20
  FocusKeys[FocusKeys["WASD"] = 96] = "WASD";
@@ -36,7 +37,8 @@ const KEY_TO_BIT = {
36
37
  Home: FocusKeys.HomeAndEnd,
37
38
  End: FocusKeys.HomeAndEnd,
38
39
  PageUp: FocusKeys.PageUpDown,
39
- PageDown: FocusKeys.PageUpDown
40
+ PageDown: FocusKeys.PageUpDown,
41
+ Backspace: FocusKeys.Backspace,
40
42
  };
41
43
  const KEY_TO_DIRECTION = {
42
44
  ArrowLeft: 'previous',
@@ -55,7 +57,8 @@ const KEY_TO_DIRECTION = {
55
57
  Home: 'start',
56
58
  End: 'end',
57
59
  PageUp: 'start',
58
- PageDown: 'end'
60
+ PageDown: 'end',
61
+ Backspace: 'previous',
59
62
  };
60
63
  function getDirection(keyboardEvent) {
61
64
  const direction = KEY_TO_DIRECTION[keyboardEvent.key];
@@ -184,8 +187,7 @@ export function focusZone(container, settings) {
184
187
  if (filteredElements.length === 0) {
185
188
  return;
186
189
  }
187
- const insertIndex = focusableElements.findIndex(e => (e.compareDocumentPosition(filteredElements[0]) & Node.DOCUMENT_POSITION_PRECEDING) > 0);
188
- focusableElements.splice(insertIndex === -1 ? focusableElements.length : insertIndex, 0, ...filteredElements);
190
+ focusableElements.splice(findInsertionIndex(filteredElements), 0, ...filteredElements);
189
191
  for (const element of filteredElements) {
190
192
  if (!savedTabIndex.has(element)) {
191
193
  savedTabIndex.set(element, element.getAttribute('tabindex'));
@@ -196,6 +198,27 @@ export function focusZone(container, settings) {
196
198
  updateFocusedElement(getFirstFocusableElement());
197
199
  }
198
200
  }
201
+ function findInsertionIndex(elementsToInsert) {
202
+ const firstElementToInsert = elementsToInsert[0];
203
+ if (focusableElements.length === 0)
204
+ return 0;
205
+ let iMin = 0;
206
+ let iMax = focusableElements.length - 1;
207
+ while (iMin <= iMax) {
208
+ const i = Math.floor((iMin + iMax) / 2);
209
+ const element = focusableElements[i];
210
+ if (followsInDocument(firstElementToInsert, element)) {
211
+ iMax = i - 1;
212
+ }
213
+ else {
214
+ iMin = i + 1;
215
+ }
216
+ }
217
+ return iMin;
218
+ }
219
+ function followsInDocument(first, second) {
220
+ return (second.compareDocumentPosition(first) & Node.DOCUMENT_POSITION_PRECEDING) > 0;
221
+ }
199
222
  function endFocusManagement(...elements) {
200
223
  for (const element of elements) {
201
224
  const focusableElementIndex = focusableElements.indexOf(element);
@@ -218,27 +241,33 @@ export function focusZone(container, settings) {
218
241
  }
219
242
  }
220
243
  }
221
- beginFocusManagement(...iterateFocusableElements(container));
222
- updateFocusedElement(getFirstFocusableElement());
244
+ const iterateFocusableElementsOptions = {
245
+ reverse: settings === null || settings === void 0 ? void 0 : settings.reverse,
246
+ strict: settings === null || settings === void 0 ? void 0 : settings.strict,
247
+ onlyTabbable: settings === null || settings === void 0 ? void 0 : settings.onlyTabbable,
248
+ };
249
+ beginFocusManagement(...iterateFocusableElements(container, iterateFocusableElementsOptions));
250
+ const initialElement = typeof focusInStrategy === 'function' ? focusInStrategy(document.body) : getFirstFocusableElement();
251
+ updateFocusedElement(initialElement);
223
252
  const observer = new MutationObserver(mutations => {
224
253
  for (const mutation of mutations) {
225
254
  for (const removedNode of mutation.removedNodes) {
226
255
  if (removedNode instanceof HTMLElement) {
227
- endFocusManagement(...iterateFocusableElements(removedNode));
256
+ endFocusManagement(...iterateFocusableElements(removedNode, iterateFocusableElementsOptions));
228
257
  }
229
258
  }
230
259
  }
231
260
  for (const mutation of mutations) {
232
261
  for (const addedNode of mutation.addedNodes) {
233
262
  if (addedNode instanceof HTMLElement) {
234
- beginFocusManagement(...iterateFocusableElements(addedNode));
263
+ beginFocusManagement(...iterateFocusableElements(addedNode, iterateFocusableElementsOptions));
235
264
  }
236
265
  }
237
266
  }
238
267
  });
239
268
  observer.observe(container, {
240
269
  subtree: true,
241
- childList: true
270
+ childList: true,
242
271
  });
243
272
  const controller = new AbortController();
244
273
  const signal = (_e = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _e !== void 0 ? _e : controller.signal;
@@ -5,8 +5,8 @@ try {
5
5
  signal: {
6
6
  get() {
7
7
  signalSupported = true;
8
- }
9
- }
8
+ },
9
+ },
10
10
  });
11
11
  window.addEventListener('test', noop, options);
12
12
  window.removeEventListener('test', noop, options);
@@ -3,7 +3,7 @@ export function* iterateFocusableElements(container, options = {}) {
3
3
  const strict = (_a = options.strict) !== null && _a !== void 0 ? _a : false;
4
4
  const acceptFn = ((_b = options.onlyTabbable) !== null && _b !== void 0 ? _b : false) ? isTabbable : isFocusable;
5
5
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
6
- acceptNode: node => node instanceof HTMLElement && acceptFn(node, strict) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
6
+ acceptNode: node => node instanceof HTMLElement && acceptFn(node, strict) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
7
7
  });
8
8
  let nextNode = null;
9
9
  if (!options.reverse && acceptFn(container, strict)) {
@@ -41,16 +41,21 @@ export function isFocusable(elem, strict = false) {
41
41
  return false;
42
42
  }
43
43
  if (strict) {
44
+ const style = getComputedStyle(elem);
44
45
  const sizeInert = elem.offsetWidth === 0 || elem.offsetHeight === 0;
45
- const visibilityInert = ['hidden', 'collapse'].includes(getComputedStyle(elem).visibility);
46
+ const visibilityInert = ['hidden', 'collapse'].includes(style.visibility);
47
+ const displayInert = style.display === 'none' || !elem.offsetParent;
46
48
  const clientRectsInert = elem.getClientRects().length === 0;
47
- if (sizeInert || visibilityInert || clientRectsInert) {
49
+ if (sizeInert || visibilityInert || clientRectsInert || displayInert) {
48
50
  return false;
49
51
  }
50
52
  }
51
53
  if (elem.getAttribute('tabindex') != null) {
52
54
  return true;
53
55
  }
56
+ if (elem.getAttribute('contenteditable') === 'true' || elem.getAttribute('contenteditable') === 'plaintext-only') {
57
+ return true;
58
+ }
54
59
  if (elem instanceof HTMLAnchorElement && elem.getAttribute('href') == null) {
55
60
  return false;
56
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/behaviors",
3
- "version": "0.0.0-2023818155639",
3
+ "version": "0.0.0-20240222105352",
4
4
  "description": "Shared behaviors for JavaScript components",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -30,17 +30,17 @@
30
30
  "dist/cjs/focus-trap.js"
31
31
  ],
32
32
  "scripts": {
33
- "lint": "eslint src/",
34
- "test": "npm run jest && npm run lint",
35
- "test:watch": "jest --watch",
36
- "jest": "jest",
37
33
  "clean": "rm -rf dist",
38
- "prebuild": "npm run clean",
39
34
  "build": "npm run build:esm && npm run build:cjs",
40
35
  "build:esm": "tsc",
41
36
  "build:cjs": "tsc --module commonjs --outDir dist/cjs",
37
+ "lint": "eslint src/",
38
+ "test": "jest",
39
+ "test:watch": "jest --watch",
40
+ "prebuild": "npm run clean",
41
+ "release": "npm run build && changeset publish",
42
42
  "size-limit": "npm run build && size-limit",
43
- "release": "npm run build && changeset publish"
43
+ "type-check": "tsc --noEmit"
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
@@ -66,23 +66,26 @@
66
66
  }
67
67
  ],
68
68
  "devDependencies": {
69
- "@changesets/changelog-github": "^0.4.2",
69
+ "@changesets/changelog-github": "^0.5.0",
70
70
  "@changesets/cli": "^2.18.1",
71
- "@github/prettier-config": "0.0.4",
72
- "@size-limit/preset-small-lib": "^7.0.3",
73
- "@testing-library/react": "^12.1.2",
74
- "@testing-library/user-event": "^13.5.0",
75
- "@types/jest": "^27.0.3",
76
- "@types/react": "^17.0.37",
77
- "esbuild": "^0.14.1",
71
+ "@github/prettier-config": "^0.0.6",
72
+ "@size-limit/preset-small-lib": "^8.2.4",
73
+ "@testing-library/react": "^14.0.0",
74
+ "@testing-library/user-event": "^14.5.1",
75
+ "@types/jest": "^29.5.11",
76
+ "@types/node": "^20.10.5",
77
+ "@types/react": "^18.2.23",
78
+ "esbuild": "^0.20.0",
78
79
  "esbuild-jest": "^0.5.0",
79
- "eslint": "^8.3.0",
80
- "eslint-plugin-github": "^4.3.5",
81
- "jest": "^27.4.3",
82
- "prettier": "^2.5.0",
83
- "react": "^17.0.2",
84
- "react-dom": "^17.0.2",
85
- "size-limit": "^7.0.3",
86
- "typescript": "^4.5.2"
80
+ "eslint": "^8.50.0",
81
+ "eslint-plugin-github": "^4.10.0",
82
+ "eslint-plugin-prettier": "^5.0.0",
83
+ "jest": "^29.7.0",
84
+ "jest-environment-jsdom": "^29.7.0",
85
+ "prettier": "^3.0.3",
86
+ "react": "^18.2.0",
87
+ "react-dom": "^18.2.0",
88
+ "size-limit": "^8.2.4",
89
+ "typescript": "^5.2.2"
87
90
  }
88
91
  }