power-focusable 4.2.1 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,7 @@ interface PowerFocusableOptions {
39
39
  composed?: boolean; // default: false
40
40
  filter?: (element: Element) => boolean;
41
41
  include?: (element: Element) => boolean;
42
+ skipNegativeTabIndexCheck?: boolean; // default: false
42
43
  skipVisibilityCheck?: boolean; // default: false
43
44
  wrap?: boolean; // default: false
44
45
  }
@@ -72,9 +73,15 @@ The function should return `true` to include the element, or `false` to ignore i
72
73
 
73
74
  Used by `getFocusables`, `getNextFocusable`, `getPreviousFocusable`, and `hasFocusable`.
74
75
 
76
+ ### `skipNegativeTabIndexCheck`
77
+
78
+ If `true`, skips the negative `tabindex` check when determining focusability.
79
+
80
+ Used by `getFocusables`, `getNextFocusable`, `getPreviousFocusable`, `hasFocusable`, and `isFocusable`.
81
+
75
82
  ### `skipVisibilityCheck`
76
83
 
77
- If `true`, skips `checkVisibility()` when determining focusability. Useful for initializing.
84
+ If `true`, skips `checkVisibility()` when determining focusability.
78
85
 
79
86
  Used by `getFocusables`, `getNextFocusable`, `getPreviousFocusable`, `hasFocusable`, and `isFocusable`.
80
87
 
@@ -114,7 +121,7 @@ getFocusables(container, { composed: true });
114
121
  getFocusables(container, { filter: (element) => !element.matches('[data-skip-focus]') });
115
122
 
116
123
  // Uses custom include function
117
- getFocusables(container, { filter: (element) => element.matches('[data-roving-tabindex]') });
124
+ getFocusables(container, { filter: (element) => element.matches('[data-force-focusable]') });
118
125
  ```
119
126
 
120
127
  ### `getNextFocusable`
@@ -137,7 +144,7 @@ getNextFocusable(container, { composed: true });
137
144
  getNextFocusable(container, { filter: (element) => !element.matches('[data-skip-focus]') });
138
145
 
139
146
  // Uses custom include function
140
- getNextFocusable(container, { filter: (element) => element.matches('[data-roving-tabindex]') });
147
+ getNextFocusable(container, { filter: (element) => element.matches('[data-force-focusable]') });
141
148
 
142
149
  // Wraps around to the first element when reaching the end
143
150
  getNextFocusable(container, { wrap: true });
@@ -163,7 +170,7 @@ getPreviousFocusable(container, { composed: true });
163
170
  getPreviousFocusable(container, { filter: (element) => !element.matches('[data-skip-focus]') });
164
171
 
165
172
  // Uses custom include function
166
- getPreviousFocusable(container, { filter: (element) => element.matches('[data-roving-tabindex]') });
173
+ getPreviousFocusable(container, { filter: (element) => element.matches('[data-force-focusable]') });
167
174
 
168
175
  // Wraps around to the last element when reaching the end
169
176
  getPreviousFocusable(container, { wrap: true });
@@ -187,7 +194,7 @@ hasFocusable(container, { composed: true });
187
194
  hasFocusable(container, { filter: (element) => !element.matches('[data-skip-focus]') });
188
195
 
189
196
  // Uses custom include function
190
- hasFocusable(container, { filter: (element) => element.matches('[data-roving-tabindex]') });
197
+ hasFocusable(container, { filter: (element) => element.matches('[data-force-focusable]') });
191
198
  ```
192
199
 
193
200
  ### `inertOutside`
package/dist/index.cjs CHANGED
@@ -48,6 +48,7 @@ function getFocusables(container = document.body, options = {}) {
48
48
  composed = false,
49
49
  filter,
50
50
  include,
51
+ skipNegativeTabIndexCheck = false,
51
52
  skipVisibilityCheck = false
52
53
  } = options;
53
54
  if (typeof composed !== "boolean") {
@@ -66,6 +67,10 @@ function getFocusables(container = document.body, options = {}) {
66
67
  );
67
68
  include = void 0;
68
69
  }
70
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
71
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
72
+ skipNegativeTabIndexCheck = false;
73
+ }
69
74
  if (typeof skipVisibilityCheck !== "boolean") {
70
75
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
71
76
  skipVisibilityCheck = false;
@@ -74,7 +79,10 @@ function getFocusables(container = document.body, options = {}) {
74
79
  if (composed || include) {
75
80
  let traverse2 = function(node) {
76
81
  if (node instanceof Element) {
77
- if (isFocusable(node, { skipVisibilityCheck }) || include?.(node)) {
82
+ if (isFocusable(node, {
83
+ skipNegativeTabIndexCheck,
84
+ skipVisibilityCheck
85
+ }) || include?.(node)) {
78
86
  elements[elements.length] = node;
79
87
  }
80
88
  }
@@ -95,7 +103,10 @@ function getFocusables(container = document.body, options = {}) {
95
103
  if (!(candidate instanceof Element)) {
96
104
  continue;
97
105
  }
98
- if (isFocusable(candidate, { skipVisibilityCheck })) {
106
+ if (isFocusable(candidate, {
107
+ skipNegativeTabIndexCheck,
108
+ skipVisibilityCheck
109
+ })) {
99
110
  elements[elements.length] = candidate;
100
111
  }
101
112
  }
@@ -146,7 +157,11 @@ function isFocusable(element, options = {}) {
146
157
  console.warn("Invalid element");
147
158
  return false;
148
159
  }
149
- let { skipVisibilityCheck = false } = options;
160
+ let { skipNegativeTabIndexCheck = false, skipVisibilityCheck = false } = options;
161
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
162
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
163
+ skipNegativeTabIndexCheck = false;
164
+ }
150
165
  if (typeof skipVisibilityCheck !== "boolean") {
151
166
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
152
167
  skipVisibilityCheck = false;
@@ -154,10 +169,12 @@ function isFocusable(element, options = {}) {
154
169
  if (element.hasAttribute("hidden") || isInert(element)) {
155
170
  return false;
156
171
  }
157
- if (getTabIndex(element) < 0) {
172
+ if (!skipNegativeTabIndexCheck && getTabIndex(element) < 0) {
158
173
  return false;
159
174
  }
160
- if (!element.matches(FOCUSABLE_SELECTOR)) {
175
+ if (!element.matches(
176
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
177
+ )) {
161
178
  return false;
162
179
  }
163
180
  if (isDisabledDeep(element)) {
@@ -182,6 +199,7 @@ function getRelativeFocusable(container, offset, options) {
182
199
  composed = false,
183
200
  filter,
184
201
  include,
202
+ skipNegativeTabIndexCheck = false,
185
203
  skipVisibilityCheck = false,
186
204
  wrap = false
187
205
  } = options;
@@ -215,6 +233,10 @@ function getRelativeFocusable(container, offset, options) {
215
233
  );
216
234
  include = void 0;
217
235
  }
236
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
237
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
238
+ skipNegativeTabIndexCheck = false;
239
+ }
218
240
  if (typeof skipVisibilityCheck !== "boolean") {
219
241
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
220
242
  skipVisibilityCheck = false;
@@ -223,12 +245,14 @@ function getRelativeFocusable(container, offset, options) {
223
245
  console.warn("Invalid wrap option. Fallback: false.");
224
246
  wrap = false;
225
247
  }
226
- const focusables = getFocusables(container, {
227
- composed,
228
- filter,
229
- include,
230
- skipVisibilityCheck
231
- });
248
+ const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
249
+ if (filter !== void 0) {
250
+ Object.assign(settings, { filter });
251
+ }
252
+ if (include !== void 0) {
253
+ Object.assign(settings, { include });
254
+ }
255
+ const focusables = getFocusables(container, settings);
232
256
  const { length } = focusables;
233
257
  if (!length) {
234
258
  return null;
@@ -459,7 +483,7 @@ function isUngroupedRadio(element) {
459
483
  * High-precision focus management utility with full composed tree support.
460
484
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
461
485
  *
462
- * @version 4.2.1
486
+ * @version 4.3.1
463
487
  * @author Yusuke Kamiyamane
464
488
  * @license MIT
465
489
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@
3
3
  * High-precision focus management utility with full composed tree support.
4
4
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
5
5
  *
6
- * @version 4.2.1
6
+ * @version 4.3.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -12,8 +12,9 @@
12
12
  interface PowerFocusableOptions {
13
13
  readonly anchor?: Element | null;
14
14
  readonly composed?: boolean;
15
- readonly filter?: ((element: Element) => boolean) | undefined;
16
- readonly include?: ((element: Element) => boolean) | undefined;
15
+ readonly filter?: (element: Element) => boolean;
16
+ readonly include?: (element: Element) => boolean;
17
+ readonly skipNegativeTabIndexCheck?: boolean;
17
18
  readonly skipVisibilityCheck?: boolean;
18
19
  readonly wrap?: boolean;
19
20
  }
@@ -24,6 +25,7 @@ declare function getPreviousFocusable(container?: Element, options?: PowerFocusa
24
25
  declare function hasFocusable(container?: Element, options?: Omit<PowerFocusableOptions, 'anchor' | 'wrap'>): boolean;
25
26
  declare function inertOutside(element: Element): () => void;
26
27
  declare function isFocusable(element: Element, options?: {
28
+ skipNegativeTabIndexCheck?: boolean;
27
29
  skipVisibilityCheck?: boolean;
28
30
  }): boolean;
29
31
 
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * High-precision focus management utility with full composed tree support.
4
4
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
5
5
  *
6
- * @version 4.2.1
6
+ * @version 4.3.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -12,8 +12,9 @@
12
12
  interface PowerFocusableOptions {
13
13
  readonly anchor?: Element | null;
14
14
  readonly composed?: boolean;
15
- readonly filter?: ((element: Element) => boolean) | undefined;
16
- readonly include?: ((element: Element) => boolean) | undefined;
15
+ readonly filter?: (element: Element) => boolean;
16
+ readonly include?: (element: Element) => boolean;
17
+ readonly skipNegativeTabIndexCheck?: boolean;
17
18
  readonly skipVisibilityCheck?: boolean;
18
19
  readonly wrap?: boolean;
19
20
  }
@@ -24,6 +25,7 @@ declare function getPreviousFocusable(container?: Element, options?: PowerFocusa
24
25
  declare function hasFocusable(container?: Element, options?: Omit<PowerFocusableOptions, 'anchor' | 'wrap'>): boolean;
25
26
  declare function inertOutside(element: Element): () => void;
26
27
  declare function isFocusable(element: Element, options?: {
28
+ skipNegativeTabIndexCheck?: boolean;
27
29
  skipVisibilityCheck?: boolean;
28
30
  }): boolean;
29
31
 
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ function getFocusables(container = document.body, options = {}) {
46
46
  composed = false,
47
47
  filter,
48
48
  include,
49
+ skipNegativeTabIndexCheck = false,
49
50
  skipVisibilityCheck = false
50
51
  } = options;
51
52
  if (typeof composed !== "boolean") {
@@ -64,6 +65,10 @@ function getFocusables(container = document.body, options = {}) {
64
65
  );
65
66
  include = void 0;
66
67
  }
68
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
69
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
70
+ skipNegativeTabIndexCheck = false;
71
+ }
67
72
  if (typeof skipVisibilityCheck !== "boolean") {
68
73
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
69
74
  skipVisibilityCheck = false;
@@ -72,7 +77,10 @@ function getFocusables(container = document.body, options = {}) {
72
77
  if (composed || include) {
73
78
  let traverse2 = function(node) {
74
79
  if (node instanceof Element) {
75
- if (isFocusable(node, { skipVisibilityCheck }) || include?.(node)) {
80
+ if (isFocusable(node, {
81
+ skipNegativeTabIndexCheck,
82
+ skipVisibilityCheck
83
+ }) || include?.(node)) {
76
84
  elements[elements.length] = node;
77
85
  }
78
86
  }
@@ -93,7 +101,10 @@ function getFocusables(container = document.body, options = {}) {
93
101
  if (!(candidate instanceof Element)) {
94
102
  continue;
95
103
  }
96
- if (isFocusable(candidate, { skipVisibilityCheck })) {
104
+ if (isFocusable(candidate, {
105
+ skipNegativeTabIndexCheck,
106
+ skipVisibilityCheck
107
+ })) {
97
108
  elements[elements.length] = candidate;
98
109
  }
99
110
  }
@@ -144,7 +155,11 @@ function isFocusable(element, options = {}) {
144
155
  console.warn("Invalid element");
145
156
  return false;
146
157
  }
147
- let { skipVisibilityCheck = false } = options;
158
+ let { skipNegativeTabIndexCheck = false, skipVisibilityCheck = false } = options;
159
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
160
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
161
+ skipNegativeTabIndexCheck = false;
162
+ }
148
163
  if (typeof skipVisibilityCheck !== "boolean") {
149
164
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
150
165
  skipVisibilityCheck = false;
@@ -152,10 +167,12 @@ function isFocusable(element, options = {}) {
152
167
  if (element.hasAttribute("hidden") || isInert(element)) {
153
168
  return false;
154
169
  }
155
- if (getTabIndex(element) < 0) {
170
+ if (!skipNegativeTabIndexCheck && getTabIndex(element) < 0) {
156
171
  return false;
157
172
  }
158
- if (!element.matches(FOCUSABLE_SELECTOR)) {
173
+ if (!element.matches(
174
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
175
+ )) {
159
176
  return false;
160
177
  }
161
178
  if (isDisabledDeep(element)) {
@@ -180,6 +197,7 @@ function getRelativeFocusable(container, offset, options) {
180
197
  composed = false,
181
198
  filter,
182
199
  include,
200
+ skipNegativeTabIndexCheck = false,
183
201
  skipVisibilityCheck = false,
184
202
  wrap = false
185
203
  } = options;
@@ -213,6 +231,10 @@ function getRelativeFocusable(container, offset, options) {
213
231
  );
214
232
  include = void 0;
215
233
  }
234
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
235
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
236
+ skipNegativeTabIndexCheck = false;
237
+ }
216
238
  if (typeof skipVisibilityCheck !== "boolean") {
217
239
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
218
240
  skipVisibilityCheck = false;
@@ -221,12 +243,14 @@ function getRelativeFocusable(container, offset, options) {
221
243
  console.warn("Invalid wrap option. Fallback: false.");
222
244
  wrap = false;
223
245
  }
224
- const focusables = getFocusables(container, {
225
- composed,
226
- filter,
227
- include,
228
- skipVisibilityCheck
229
- });
246
+ const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
247
+ if (filter !== void 0) {
248
+ Object.assign(settings, { filter });
249
+ }
250
+ if (include !== void 0) {
251
+ Object.assign(settings, { include });
252
+ }
253
+ const focusables = getFocusables(container, settings);
230
254
  const { length } = focusables;
231
255
  if (!length) {
232
256
  return null;
@@ -457,7 +481,7 @@ function isUngroupedRadio(element) {
457
481
  * High-precision focus management utility with full composed tree support.
458
482
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
459
483
  *
460
- * @version 4.2.1
484
+ * @version 4.3.1
461
485
  * @author Yusuke Kamiyamane
462
486
  * @license MIT
463
487
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "4.2.1",
3
+ "version": "4.3.1",
4
4
  "description": "High-precision focus management utility with full composed tree support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",