@primer/behaviors 0.0.0-202342222036 → 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,7 +20,7 @@ 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
  }
@@ -40,7 +40,7 @@ function getPositionedParent(element) {
40
40
  function isOnTopLayer(element) {
41
41
  var _a;
42
42
  if (element.tagName === 'DIALOG') {
43
- return true;
43
+ return element.matches(':modal');
44
44
  }
45
45
  try {
46
46
  if (element.matches(':popover-open') && /native code/.test((_a = document.body.showPopover) === null || _a === void 0 ? void 0 : _a.toString())) {
@@ -48,14 +48,13 @@ function isOnTopLayer(element) {
48
48
  }
49
49
  }
50
50
  catch (_b) {
51
- return false;
52
51
  }
53
- return false;
52
+ return element.matches(':fullscreen');
54
53
  }
55
54
  function getClippingRect(element) {
56
55
  let parentNode = element;
57
56
  while (parentNode !== null) {
58
- if (parentNode === document.body) {
57
+ if (!(parentNode instanceof Element)) {
59
58
  break;
60
59
  }
61
60
  const parentNodeStyle = getComputedStyle(parentNode);
@@ -71,13 +70,13 @@ function getClippingRect(element) {
71
70
  elemStyle.borderTopWidth,
72
71
  elemStyle.borderLeftWidth,
73
72
  elemStyle.borderRightWidth,
74
- elemStyle.borderBottomWidth
73
+ elemStyle.borderBottomWidth,
75
74
  ].map(v => parseInt(v, 10) || 0);
76
75
  return {
77
76
  top: elemRect.top + borderTop,
78
77
  left: elemRect.left + borderLeft,
79
78
  width: elemRect.width - borderRight - borderLeft,
80
- 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),
81
80
  };
82
81
  }
83
82
  const positionDefaults = {
@@ -85,7 +84,7 @@ const positionDefaults = {
85
84
  align: 'start',
86
85
  anchorOffset: 4,
87
86
  alignmentOffset: 4,
88
- allowOutOfBounds: false
87
+ allowOutOfBounds: false,
89
88
  };
90
89
  function getDefaultSettings(settings = {}) {
91
90
  var _a, _b, _c, _d, _e;
@@ -96,7 +95,7 @@ function getDefaultSettings(settings = {}) {
96
95
  align,
97
96
  anchorOffset: (_c = settings.anchorOffset) !== null && _c !== void 0 ? _c : (side === 'inside-center' ? 0 : positionDefaults.anchorOffset),
98
97
  alignmentOffset: (_d = settings.alignmentOffset) !== null && _d !== void 0 ? _d : (align !== 'center' && side.startsWith('inside') ? positionDefaults.alignmentOffset : 0),
99
- allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds
98
+ allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds,
100
99
  };
101
100
  }
102
101
  function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingRect, anchorRect, { side, align, allowOutOfBounds, anchorOffset, alignmentOffset }) {
@@ -104,7 +103,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
104
103
  top: viewportRect.top - relativePosition.top,
105
104
  left: viewportRect.left - relativePosition.left,
106
105
  width: viewportRect.width,
107
- height: viewportRect.height
106
+ height: viewportRect.height,
108
107
  };
109
108
  let pos = calculatePosition(floatingRect, anchorRect, side, align, anchorOffset, alignmentOffset);
110
109
  let anchorSide = side;
@@ -151,7 +150,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
151
150
  }
152
151
  if (alternateOrder && positionAttempt < alternateOrder.length) {
153
152
  if (pos.top + floatingRect.height > viewportRect.height + relativeViewportRect.top) {
154
- pos.top = viewportRect.height + relativeViewportRect.top - floatingRect.height;
153
+ pos.top = Math.max(viewportRect.height + relativeViewportRect.top - floatingRect.height, 0);
155
154
  }
156
155
  }
157
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' | 'Backspace';
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,
@@ -16,7 +17,7 @@ export declare enum FocusKeys {
16
17
  WASD = 96,
17
18
  All = 511
18
19
  }
19
- export interface FocusZoneSettings {
20
+ export type FocusZoneSettings = IterateFocusableElements & {
20
21
  focusOutBehavior?: 'stop' | 'wrap';
21
22
  getNextFocusable?: (direction: Direction, from: Element | undefined, event: KeyboardEvent) => HTMLElement | undefined;
22
23
  focusableElementFilter?: (element: HTMLElement) => boolean;
@@ -26,7 +27,7 @@ export interface FocusZoneSettings {
26
27
  onActiveDescendantChanged?: (newActiveDescendant: HTMLElement | undefined, previousActiveDescendant: HTMLElement | undefined, directlyActivated: boolean) => void;
27
28
  focusInStrategy?: 'first' | 'closest' | 'previous' | ((previousFocusedElement: Element) => HTMLElement | undefined);
28
29
  preventScroll?: boolean;
29
- }
30
+ };
30
31
  export declare const isActiveDescendantAttribute = "data-is-active-descendant";
31
32
  export declare const activeDescendantActivatedDirectly = "activated-directly";
32
33
  export declare const activeDescendantActivatedIndirectly = "activated-indirectly";
@@ -22,7 +22,7 @@ var FocusKeys;
22
22
  FocusKeys[FocusKeys["HJKL"] = 12] = "HJKL";
23
23
  FocusKeys[FocusKeys["WASD"] = 96] = "WASD";
24
24
  FocusKeys[FocusKeys["All"] = 511] = "All";
25
- })(FocusKeys = exports.FocusKeys || (exports.FocusKeys = {}));
25
+ })(FocusKeys || (exports.FocusKeys = FocusKeys = {}));
26
26
  const KEY_TO_BIT = {
27
27
  ArrowLeft: FocusKeys.ArrowHorizontal,
28
28
  ArrowDown: FocusKeys.ArrowVertical,
@@ -41,7 +41,7 @@ const KEY_TO_BIT = {
41
41
  End: FocusKeys.HomeAndEnd,
42
42
  PageUp: FocusKeys.PageUpDown,
43
43
  PageDown: FocusKeys.PageUpDown,
44
- Backspace: FocusKeys.Backspace
44
+ Backspace: FocusKeys.Backspace,
45
45
  };
46
46
  const KEY_TO_DIRECTION = {
47
47
  ArrowLeft: 'previous',
@@ -61,7 +61,7 @@ const KEY_TO_DIRECTION = {
61
61
  End: 'end',
62
62
  PageUp: 'start',
63
63
  PageDown: 'end',
64
- Backspace: 'previous'
64
+ Backspace: 'previous',
65
65
  };
66
66
  function getDirection(keyboardEvent) {
67
67
  const direction = KEY_TO_DIRECTION[keyboardEvent.key];
@@ -244,28 +244,33 @@ function focusZone(container, settings) {
244
244
  }
245
245
  }
246
246
  }
247
- beginFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(container));
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));
248
253
  const initialElement = typeof focusInStrategy === 'function' ? focusInStrategy(document.body) : getFirstFocusableElement();
249
254
  updateFocusedElement(initialElement);
250
255
  const observer = new MutationObserver(mutations => {
251
256
  for (const mutation of mutations) {
252
257
  for (const removedNode of mutation.removedNodes) {
253
258
  if (removedNode instanceof HTMLElement) {
254
- endFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(removedNode));
259
+ endFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(removedNode, iterateFocusableElementsOptions));
255
260
  }
256
261
  }
257
262
  }
258
263
  for (const mutation of mutations) {
259
264
  for (const addedNode of mutation.addedNodes) {
260
265
  if (addedNode instanceof HTMLElement) {
261
- beginFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(addedNode));
266
+ beginFocusManagement(...(0, iterate_focusable_elements_js_1.iterateFocusableElements)(addedNode, iterateFocusableElementsOptions));
262
267
  }
263
268
  }
264
269
  }
265
270
  });
266
271
  observer.observe(container, {
267
272
  subtree: true,
268
- childList: true
273
+ childList: true,
269
274
  });
270
275
  const controller = new AbortController();
271
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,7 +17,7 @@ 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
  }
@@ -36,7 +36,7 @@ function getPositionedParent(element) {
36
36
  function isOnTopLayer(element) {
37
37
  var _a;
38
38
  if (element.tagName === 'DIALOG') {
39
- return true;
39
+ return element.matches(':modal');
40
40
  }
41
41
  try {
42
42
  if (element.matches(':popover-open') && /native code/.test((_a = document.body.showPopover) === null || _a === void 0 ? void 0 : _a.toString())) {
@@ -44,14 +44,13 @@ function isOnTopLayer(element) {
44
44
  }
45
45
  }
46
46
  catch (_b) {
47
- return false;
48
47
  }
49
- return false;
48
+ return element.matches(':fullscreen');
50
49
  }
51
50
  function getClippingRect(element) {
52
51
  let parentNode = element;
53
52
  while (parentNode !== null) {
54
- if (parentNode === document.body) {
53
+ if (!(parentNode instanceof Element)) {
55
54
  break;
56
55
  }
57
56
  const parentNodeStyle = getComputedStyle(parentNode);
@@ -67,13 +66,13 @@ function getClippingRect(element) {
67
66
  elemStyle.borderTopWidth,
68
67
  elemStyle.borderLeftWidth,
69
68
  elemStyle.borderRightWidth,
70
- elemStyle.borderBottomWidth
69
+ elemStyle.borderBottomWidth,
71
70
  ].map(v => parseInt(v, 10) || 0);
72
71
  return {
73
72
  top: elemRect.top + borderTop,
74
73
  left: elemRect.left + borderLeft,
75
74
  width: elemRect.width - borderRight - borderLeft,
76
- 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),
77
76
  };
78
77
  }
79
78
  const positionDefaults = {
@@ -81,7 +80,7 @@ const positionDefaults = {
81
80
  align: 'start',
82
81
  anchorOffset: 4,
83
82
  alignmentOffset: 4,
84
- allowOutOfBounds: false
83
+ allowOutOfBounds: false,
85
84
  };
86
85
  function getDefaultSettings(settings = {}) {
87
86
  var _a, _b, _c, _d, _e;
@@ -92,7 +91,7 @@ function getDefaultSettings(settings = {}) {
92
91
  align,
93
92
  anchorOffset: (_c = settings.anchorOffset) !== null && _c !== void 0 ? _c : (side === 'inside-center' ? 0 : positionDefaults.anchorOffset),
94
93
  alignmentOffset: (_d = settings.alignmentOffset) !== null && _d !== void 0 ? _d : (align !== 'center' && side.startsWith('inside') ? positionDefaults.alignmentOffset : 0),
95
- allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds
94
+ allowOutOfBounds: (_e = settings.allowOutOfBounds) !== null && _e !== void 0 ? _e : positionDefaults.allowOutOfBounds,
96
95
  };
97
96
  }
98
97
  function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingRect, anchorRect, { side, align, allowOutOfBounds, anchorOffset, alignmentOffset }) {
@@ -100,7 +99,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
100
99
  top: viewportRect.top - relativePosition.top,
101
100
  left: viewportRect.left - relativePosition.left,
102
101
  width: viewportRect.width,
103
- height: viewportRect.height
102
+ height: viewportRect.height,
104
103
  };
105
104
  let pos = calculatePosition(floatingRect, anchorRect, side, align, anchorOffset, alignmentOffset);
106
105
  let anchorSide = side;
@@ -147,7 +146,7 @@ function pureCalculateAnchoredPosition(viewportRect, relativePosition, floatingR
147
146
  }
148
147
  if (alternateOrder && positionAttempt < alternateOrder.length) {
149
148
  if (pos.top + floatingRect.height > viewportRect.height + relativeViewportRect.top) {
150
- pos.top = viewportRect.height + relativeViewportRect.top - floatingRect.height;
149
+ pos.top = Math.max(viewportRect.height + relativeViewportRect.top - floatingRect.height, 0);
151
150
  }
152
151
  }
153
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' | 'Backspace';
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,
@@ -16,7 +17,7 @@ export declare enum FocusKeys {
16
17
  WASD = 96,
17
18
  All = 511
18
19
  }
19
- export interface FocusZoneSettings {
20
+ export type FocusZoneSettings = IterateFocusableElements & {
20
21
  focusOutBehavior?: 'stop' | 'wrap';
21
22
  getNextFocusable?: (direction: Direction, from: Element | undefined, event: KeyboardEvent) => HTMLElement | undefined;
22
23
  focusableElementFilter?: (element: HTMLElement) => boolean;
@@ -26,7 +27,7 @@ export interface FocusZoneSettings {
26
27
  onActiveDescendantChanged?: (newActiveDescendant: HTMLElement | undefined, previousActiveDescendant: HTMLElement | undefined, directlyActivated: boolean) => void;
27
28
  focusInStrategy?: 'first' | 'closest' | 'previous' | ((previousFocusedElement: Element) => HTMLElement | undefined);
28
29
  preventScroll?: boolean;
29
- }
30
+ };
30
31
  export declare const isActiveDescendantAttribute = "data-is-active-descendant";
31
32
  export declare const activeDescendantActivatedDirectly = "activated-directly";
32
33
  export declare const activeDescendantActivatedIndirectly = "activated-indirectly";
@@ -38,7 +38,7 @@ const KEY_TO_BIT = {
38
38
  End: FocusKeys.HomeAndEnd,
39
39
  PageUp: FocusKeys.PageUpDown,
40
40
  PageDown: FocusKeys.PageUpDown,
41
- Backspace: FocusKeys.Backspace
41
+ Backspace: FocusKeys.Backspace,
42
42
  };
43
43
  const KEY_TO_DIRECTION = {
44
44
  ArrowLeft: 'previous',
@@ -58,7 +58,7 @@ const KEY_TO_DIRECTION = {
58
58
  End: 'end',
59
59
  PageUp: 'start',
60
60
  PageDown: 'end',
61
- Backspace: 'previous'
61
+ Backspace: 'previous',
62
62
  };
63
63
  function getDirection(keyboardEvent) {
64
64
  const direction = KEY_TO_DIRECTION[keyboardEvent.key];
@@ -241,28 +241,33 @@ export function focusZone(container, settings) {
241
241
  }
242
242
  }
243
243
  }
244
- beginFocusManagement(...iterateFocusableElements(container));
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));
245
250
  const initialElement = typeof focusInStrategy === 'function' ? focusInStrategy(document.body) : getFirstFocusableElement();
246
251
  updateFocusedElement(initialElement);
247
252
  const observer = new MutationObserver(mutations => {
248
253
  for (const mutation of mutations) {
249
254
  for (const removedNode of mutation.removedNodes) {
250
255
  if (removedNode instanceof HTMLElement) {
251
- endFocusManagement(...iterateFocusableElements(removedNode));
256
+ endFocusManagement(...iterateFocusableElements(removedNode, iterateFocusableElementsOptions));
252
257
  }
253
258
  }
254
259
  }
255
260
  for (const mutation of mutations) {
256
261
  for (const addedNode of mutation.addedNodes) {
257
262
  if (addedNode instanceof HTMLElement) {
258
- beginFocusManagement(...iterateFocusableElements(addedNode));
263
+ beginFocusManagement(...iterateFocusableElements(addedNode, iterateFocusableElementsOptions));
259
264
  }
260
265
  }
261
266
  }
262
267
  });
263
268
  observer.observe(container, {
264
269
  subtree: true,
265
- childList: true
270
+ childList: true,
266
271
  });
267
272
  const controller = new AbortController();
268
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-202342222036",
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.15.16",
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
  }