@y14e/portal 1.2.31 → 1.3.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.
package/README.md CHANGED
@@ -13,11 +13,11 @@ npm i @y14e/portal
13
13
  import { createPortal } from '@y14e/portal';
14
14
 
15
15
  // CDNs
16
- import { createPortal } from 'https://esm.sh/@y14e/portal@1.2.31';
16
+ import { createPortal } from 'https://esm.sh/@y14e/portal@1.3.0';
17
17
  // or
18
- import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal@1.2.31/+esm';
18
+ import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal@1.3.0/+esm';
19
19
  // or
20
- import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.2.31';
20
+ import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.3.0';
21
21
  ```
22
22
 
23
23
  ## 📦 APIs
@@ -27,11 +27,20 @@ import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.2.31';
27
27
  Creates a portal and preserves keyboard focus order between the original DOM and the portal.
28
28
 
29
29
  ```ts
30
- const cleanup = createPortal(host, container);
30
+ const cleanup = createPortal(host, container, options);
31
31
  // => () => void
32
32
  //
33
33
  // host: Element
34
34
  // container (optional): Element (default: <body>)
35
+ // options (optional): PortalOptions
36
+ ```
37
+
38
+ ## 🪄 Options
39
+
40
+ ```ts
41
+ interface PortalOptions {
42
+ noInlineStyle: boolean; // default: false
43
+ }
35
44
  ```
36
45
 
37
46
  ## Demo
@@ -2,8 +2,8 @@
2
2
 
3
3
  // node_modules/@y14e/attributes-utils/dist/index.js
4
4
  var snapshots = /* @__PURE__ */ new WeakMap();
5
- function restoreAttributes(elements) {
6
- for (const element of elements) {
5
+ function restoreAttributes(element_or_elements) {
6
+ for (const element of Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]) {
7
7
  const snapshot = snapshots.get(element);
8
8
  if (!snapshot) {
9
9
  continue;
@@ -14,8 +14,9 @@ function restoreAttributes(elements) {
14
14
  snapshots.delete(element);
15
15
  }
16
16
  }
17
- function saveAttributes(elements, attributes) {
18
- elements.forEach((element) => {
17
+ function saveAttributes(element_or_elements, attribute_or_attributes) {
18
+ const attributes = Array.isArray(attribute_or_attributes) ? attribute_or_attributes : [attribute_or_attributes];
19
+ (Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]).forEach((element) => {
19
20
  let snapshot = snapshots.get(element);
20
21
  if (!snapshot) {
21
22
  snapshot = /* @__PURE__ */ new Map();
@@ -29,6 +30,10 @@ function saveAttributes(elements, attributes) {
29
30
 
30
31
  // node_modules/power-focusable/dist/index.js
31
32
  var FOCUSABLE_SELECTOR = `:is(a[href], area[href], button, embed, iframe, input:not([type="hidden" i]), object, select, details > summary:first-of-type, textarea, [contenteditable]:not([contenteditable="false" i]), [controls], [tabindex]):not(:disabled, [hidden], [inert], [tabindex="-1"])`;
33
+ var FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX = FOCUSABLE_SELECTOR.replace(
34
+ /(,\s*)?\[tabindex="-1"\]/g,
35
+ ""
36
+ );
32
37
  function getFocusables(container = document.body, options = {}) {
33
38
  if (!(container instanceof Element)) {
34
39
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -65,36 +70,43 @@ function getFocusables(container = document.body, options = {}) {
65
70
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
66
71
  skipVisibilityCheck = false;
67
72
  }
68
- const elements = [];
73
+ const candidates = [];
69
74
  if (composed || include) {
70
75
  let traverse2 = function(node) {
71
- if (!(node instanceof Element)) {
72
- return;
73
- }
74
- if (isFocusable(node, { skipNegativeTabIndexCheck, skipVisibilityCheck }) || include?.(node)) {
75
- elements[elements.length] = node;
76
+ if (isFocusable(node, {
77
+ skipNegativeTabIndexCheck,
78
+ skipVisibilityCheck
79
+ }) || include?.(node)) {
80
+ candidates[candidates.length] = node;
76
81
  }
77
- const children = getComposedChildren(node);
78
- for (let i = 0, l = children.length; i < l; i++) {
79
- const child = children[i];
82
+ const children2 = composed ? getComposedChildren(node) : getChildren(node);
83
+ for (let i = 0, l = children2.length; i < l; i++) {
84
+ const child = children2[i];
80
85
  child && traverse2(child);
81
86
  }
82
87
  };
83
- traverse2(container);
88
+ const children = composed ? getComposedChildren(container) : getChildren(container);
89
+ for (let i = 0, l = children.length; i < l; i++) {
90
+ const child = children[i];
91
+ child && traverse2(child);
92
+ }
84
93
  } else {
85
- const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
86
- for (let i = 0, l = candidates.length; i < l; i++) {
87
- const candidate = candidates[i];
88
- if (candidate && isFocusable(candidate, {
94
+ const matches = container.querySelectorAll(
95
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
96
+ );
97
+ for (let i = 0, l = matches.length; i < l; i++) {
98
+ const matched = matches[i];
99
+ if (matched && isFocusable(matched, {
89
100
  skipNegativeTabIndexCheck,
90
101
  skipVisibilityCheck
91
102
  })) {
92
- elements[elements.length] = candidate;
103
+ candidates[candidates.length] = matched;
93
104
  }
94
105
  }
95
106
  }
96
- const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
97
- return filter ? unfiltered.filter(filter) : unfiltered;
107
+ return normalizeRadioGroup(
108
+ sortByTabIndex(filter ? candidates.filter(filter) : candidates)
109
+ );
98
110
  }
99
111
  function getNextFocusable(container = document.body, options = {}) {
100
112
  return getRelativeFocusable(container, 1, options);
@@ -123,7 +135,7 @@ function isFocusable(element, options = {}) {
123
135
  return false;
124
136
  }
125
137
  if (!element.matches(
126
- skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
138
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
127
139
  )) {
128
140
  return false;
129
141
  }
@@ -195,23 +207,23 @@ function getRelativeFocusable(container, offset, options) {
195
207
  console.warn("Invalid wrap option. Fallback: false.");
196
208
  wrap = false;
197
209
  }
198
- const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
199
- if (filter !== void 0) {
200
- Object.assign(settings, { filter });
201
- }
202
- if (include !== void 0) {
203
- Object.assign(settings, { include });
204
- }
210
+ const settings = {
211
+ composed,
212
+ filter,
213
+ include,
214
+ skipNegativeTabIndexCheck,
215
+ skipVisibilityCheck
216
+ };
205
217
  const focusables = getFocusables(container, settings);
206
218
  const { length } = focusables;
207
219
  if (!length) {
208
220
  return null;
209
221
  }
210
- const currentIndex = focusables.indexOf(anchor);
211
- if (currentIndex === -1) {
222
+ const anchorIndex = focusables.indexOf(anchor);
223
+ if (anchorIndex === -1) {
212
224
  return null;
213
225
  }
214
- const offsetIndex = currentIndex + offset;
226
+ const offsetIndex = anchorIndex + offset;
215
227
  if ((offsetIndex < 0 || offsetIndex >= length) && !wrap) {
216
228
  return null;
217
229
  }
@@ -259,22 +271,42 @@ function normalizeRadioGroup(elements) {
259
271
  if (!map) {
260
272
  map = /* @__PURE__ */ new Map();
261
273
  }
262
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
263
- const group = map.get(key) ?? map.set(key, []).get(key);
264
- if (group) {
265
- group[group.length] = element;
274
+ const root = element.getRootNode();
275
+ let value = map.get(root);
276
+ if (!value) {
277
+ value = /* @__PURE__ */ new Map();
278
+ map.set(root, value);
279
+ }
280
+ let v = value.get(element.form);
281
+ if (!v) {
282
+ v = /* @__PURE__ */ new Map();
283
+ value.set(element.form, v);
284
+ }
285
+ let vv = v.get(element.name);
286
+ if (!vv) {
287
+ vv = [];
288
+ v.set(element.name, vv);
266
289
  }
290
+ vv[vv.length] = element;
267
291
  }
268
292
  if (!map) {
269
293
  return elements;
270
294
  }
271
295
  const placeholder = /* @__PURE__ */ new Set();
272
- for (const group of map.values()) {
273
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
296
+ for (const value of map.values()) {
297
+ for (const v of value.values()) {
298
+ for (const vv of v.values()) {
299
+ const radio = vv.find((element) => element.checked) ?? vv[0];
300
+ radio && placeholder.add(radio);
301
+ }
302
+ }
274
303
  }
275
- return elements.filter(
276
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
277
- );
304
+ return elements.filter((element) => {
305
+ if (!(element instanceof HTMLInputElement)) {
306
+ return true;
307
+ }
308
+ return !isUngroupedRadio(element) || placeholder.has(element);
309
+ });
278
310
  }
279
311
  function sortByTabIndex(elements) {
280
312
  const ordered = [];
@@ -357,12 +389,12 @@ function isInert(element) {
357
389
  return "inert" in element && !!element.inert;
358
390
  }
359
391
  function isUngroupedRadio(element) {
360
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
392
+ return element.type === "radio" && !!element.name;
361
393
  }
362
394
 
363
395
  // src/index.ts
364
396
  var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
365
- function createPortal(host, container = document.body) {
397
+ function createPortal(host, container = document.body, options = {}) {
366
398
  if (!(host instanceof Element)) {
367
399
  console.warn("Invalid host element");
368
400
  return () => {
@@ -378,24 +410,31 @@ function createPortal(host, container = document.body) {
378
410
  return () => {
379
411
  };
380
412
  }
381
- const portal = new Portal(host, container);
413
+ const portal = new Portal(host, container, options);
382
414
  return () => portal.destroy();
383
415
  }
384
416
  var Portal = class {
385
417
  #host;
386
418
  #container;
419
+ #settings;
387
420
  #entranceSentinel;
388
421
  #exitSentinel;
389
422
  #focusables = /* @__PURE__ */ new Set();
390
423
  #controller = null;
391
424
  #isDestroyed = false;
392
- constructor(host, container) {
425
+ constructor(host, container, options = {}) {
393
426
  this.#host = host;
394
427
  if (!(container instanceof Element)) {
395
428
  console.warn("Invalid container element. Fallback: <body> element.");
396
429
  container = document.body;
397
430
  }
398
431
  this.#container = container;
432
+ let { noInlineStyle = false } = options;
433
+ if (typeof noInlineStyle !== "boolean") {
434
+ console.warn("Invalid noInlineStyle option. Fallback: false.");
435
+ noInlineStyle = false;
436
+ }
437
+ this.#settings = { noInlineStyle };
399
438
  this.#entranceSentinel = this.#createSentinel();
400
439
  this.#exitSentinel = this.#createSentinel();
401
440
  this.#initialize();
@@ -483,14 +522,14 @@ var Portal = class {
483
522
  ]);
484
523
  for (const focusable of this.#focusables) {
485
524
  if (!current.has(focusable)) {
486
- focusable.isConnected && restoreAttributes([focusable]);
525
+ restoreAttributes(focusable);
487
526
  this.#focusables.delete(focusable);
488
527
  }
489
528
  }
490
529
  for (const focusable of current) {
491
530
  if (!this.#focusables.has(focusable)) {
492
531
  this.#focusables.add(focusable);
493
- saveAttributes([focusable], ["tabindex"]);
532
+ saveAttributes(focusable, "tabindex");
494
533
  focusable.setAttribute("tabindex", "-1");
495
534
  }
496
535
  }
@@ -500,7 +539,9 @@ var Portal = class {
500
539
  sentinel.setAttribute("aria-hidden", "true");
501
540
  sentinel.setAttribute("data-portal-sentinel", "");
502
541
  sentinel.setAttribute("tabindex", "0");
503
- sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
542
+ if (!this.#settings.noInlineStyle) {
543
+ sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
544
+ }
504
545
  return sentinel;
505
546
  }
506
547
  #focusSentinel(isPrevious) {
@@ -536,7 +577,7 @@ function containsComposed2(container, element) {
536
577
  * Lightweight DOM portal (teleport) utility with fully focus management.
537
578
  * Designed for accessible dialogs, menus, overlays, popovers.
538
579
  *
539
- * @version 1.2.31
580
+ * @version 1.3.0
540
581
  * @author Yusuke Kamiyamane
541
582
  * @license MIT
542
583
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -548,7 +589,7 @@ function containsComposed2(container, element) {
548
589
  (**
549
590
  * Attributes Utils
550
591
  *
551
- * @version 1.1.3
592
+ * @version 1.1.4
552
593
  * @author Yusuke Kamiyamane
553
594
  * @license MIT
554
595
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -561,7 +602,7 @@ power-focusable/dist/index.js:
561
602
  * High-precision focus management utility with full composed tree support.
562
603
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
563
604
  *
564
- * @version 4.3.8
605
+ * @version 4.3.21
565
606
  * @author Yusuke Kamiyamane
566
607
  * @license MIT
567
608
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1,7 +1,7 @@
1
1
  // node_modules/@y14e/attributes-utils/dist/index.js
2
2
  var snapshots = /* @__PURE__ */ new WeakMap();
3
- function restoreAttributes(elements) {
4
- for (const element of elements) {
3
+ function restoreAttributes(element_or_elements) {
4
+ for (const element of Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]) {
5
5
  const snapshot = snapshots.get(element);
6
6
  if (!snapshot) {
7
7
  continue;
@@ -12,8 +12,9 @@ function restoreAttributes(elements) {
12
12
  snapshots.delete(element);
13
13
  }
14
14
  }
15
- function saveAttributes(elements, attributes) {
16
- elements.forEach((element) => {
15
+ function saveAttributes(element_or_elements, attribute_or_attributes) {
16
+ const attributes = Array.isArray(attribute_or_attributes) ? attribute_or_attributes : [attribute_or_attributes];
17
+ (Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]).forEach((element) => {
17
18
  let snapshot = snapshots.get(element);
18
19
  if (!snapshot) {
19
20
  snapshot = /* @__PURE__ */ new Map();
@@ -27,6 +28,10 @@ function saveAttributes(elements, attributes) {
27
28
 
28
29
  // node_modules/power-focusable/dist/index.js
29
30
  var FOCUSABLE_SELECTOR = `:is(a[href], area[href], button, embed, iframe, input:not([type="hidden" i]), object, select, details > summary:first-of-type, textarea, [contenteditable]:not([contenteditable="false" i]), [controls], [tabindex]):not(:disabled, [hidden], [inert], [tabindex="-1"])`;
31
+ var FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX = FOCUSABLE_SELECTOR.replace(
32
+ /(,\s*)?\[tabindex="-1"\]/g,
33
+ ""
34
+ );
30
35
  function getFocusables(container = document.body, options = {}) {
31
36
  if (!(container instanceof Element)) {
32
37
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -63,36 +68,43 @@ function getFocusables(container = document.body, options = {}) {
63
68
  console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
64
69
  skipVisibilityCheck = false;
65
70
  }
66
- const elements = [];
71
+ const candidates = [];
67
72
  if (composed || include) {
68
73
  let traverse2 = function(node) {
69
- if (!(node instanceof Element)) {
70
- return;
71
- }
72
- if (isFocusable(node, { skipNegativeTabIndexCheck, skipVisibilityCheck }) || include?.(node)) {
73
- elements[elements.length] = node;
74
+ if (isFocusable(node, {
75
+ skipNegativeTabIndexCheck,
76
+ skipVisibilityCheck
77
+ }) || include?.(node)) {
78
+ candidates[candidates.length] = node;
74
79
  }
75
- const children = getComposedChildren(node);
76
- for (let i = 0, l = children.length; i < l; i++) {
77
- const child = children[i];
80
+ const children2 = composed ? getComposedChildren(node) : getChildren(node);
81
+ for (let i = 0, l = children2.length; i < l; i++) {
82
+ const child = children2[i];
78
83
  child && traverse2(child);
79
84
  }
80
85
  };
81
- traverse2(container);
86
+ const children = composed ? getComposedChildren(container) : getChildren(container);
87
+ for (let i = 0, l = children.length; i < l; i++) {
88
+ const child = children[i];
89
+ child && traverse2(child);
90
+ }
82
91
  } else {
83
- const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
84
- for (let i = 0, l = candidates.length; i < l; i++) {
85
- const candidate = candidates[i];
86
- if (candidate && isFocusable(candidate, {
92
+ const matches = container.querySelectorAll(
93
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
94
+ );
95
+ for (let i = 0, l = matches.length; i < l; i++) {
96
+ const matched = matches[i];
97
+ if (matched && isFocusable(matched, {
87
98
  skipNegativeTabIndexCheck,
88
99
  skipVisibilityCheck
89
100
  })) {
90
- elements[elements.length] = candidate;
101
+ candidates[candidates.length] = matched;
91
102
  }
92
103
  }
93
104
  }
94
- const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
95
- return filter ? unfiltered.filter(filter) : unfiltered;
105
+ return normalizeRadioGroup(
106
+ sortByTabIndex(filter ? candidates.filter(filter) : candidates)
107
+ );
96
108
  }
97
109
  function getNextFocusable(container = document.body, options = {}) {
98
110
  return getRelativeFocusable(container, 1, options);
@@ -121,7 +133,7 @@ function isFocusable(element, options = {}) {
121
133
  return false;
122
134
  }
123
135
  if (!element.matches(
124
- skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
136
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
125
137
  )) {
126
138
  return false;
127
139
  }
@@ -193,23 +205,23 @@ function getRelativeFocusable(container, offset, options) {
193
205
  console.warn("Invalid wrap option. Fallback: false.");
194
206
  wrap = false;
195
207
  }
196
- const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
197
- if (filter !== void 0) {
198
- Object.assign(settings, { filter });
199
- }
200
- if (include !== void 0) {
201
- Object.assign(settings, { include });
202
- }
208
+ const settings = {
209
+ composed,
210
+ filter,
211
+ include,
212
+ skipNegativeTabIndexCheck,
213
+ skipVisibilityCheck
214
+ };
203
215
  const focusables = getFocusables(container, settings);
204
216
  const { length } = focusables;
205
217
  if (!length) {
206
218
  return null;
207
219
  }
208
- const currentIndex = focusables.indexOf(anchor);
209
- if (currentIndex === -1) {
220
+ const anchorIndex = focusables.indexOf(anchor);
221
+ if (anchorIndex === -1) {
210
222
  return null;
211
223
  }
212
- const offsetIndex = currentIndex + offset;
224
+ const offsetIndex = anchorIndex + offset;
213
225
  if ((offsetIndex < 0 || offsetIndex >= length) && !wrap) {
214
226
  return null;
215
227
  }
@@ -257,22 +269,42 @@ function normalizeRadioGroup(elements) {
257
269
  if (!map) {
258
270
  map = /* @__PURE__ */ new Map();
259
271
  }
260
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
261
- const group = map.get(key) ?? map.set(key, []).get(key);
262
- if (group) {
263
- group[group.length] = element;
272
+ const root = element.getRootNode();
273
+ let value = map.get(root);
274
+ if (!value) {
275
+ value = /* @__PURE__ */ new Map();
276
+ map.set(root, value);
277
+ }
278
+ let v = value.get(element.form);
279
+ if (!v) {
280
+ v = /* @__PURE__ */ new Map();
281
+ value.set(element.form, v);
282
+ }
283
+ let vv = v.get(element.name);
284
+ if (!vv) {
285
+ vv = [];
286
+ v.set(element.name, vv);
264
287
  }
288
+ vv[vv.length] = element;
265
289
  }
266
290
  if (!map) {
267
291
  return elements;
268
292
  }
269
293
  const placeholder = /* @__PURE__ */ new Set();
270
- for (const group of map.values()) {
271
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
294
+ for (const value of map.values()) {
295
+ for (const v of value.values()) {
296
+ for (const vv of v.values()) {
297
+ const radio = vv.find((element) => element.checked) ?? vv[0];
298
+ radio && placeholder.add(radio);
299
+ }
300
+ }
272
301
  }
273
- return elements.filter(
274
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
275
- );
302
+ return elements.filter((element) => {
303
+ if (!(element instanceof HTMLInputElement)) {
304
+ return true;
305
+ }
306
+ return !isUngroupedRadio(element) || placeholder.has(element);
307
+ });
276
308
  }
277
309
  function sortByTabIndex(elements) {
278
310
  const ordered = [];
@@ -355,12 +387,12 @@ function isInert(element) {
355
387
  return "inert" in element && !!element.inert;
356
388
  }
357
389
  function isUngroupedRadio(element) {
358
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
390
+ return element.type === "radio" && !!element.name;
359
391
  }
360
392
 
361
393
  // src/index.ts
362
394
  var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
363
- function createPortal(host, container = document.body) {
395
+ function createPortal(host, container = document.body, options = {}) {
364
396
  if (!(host instanceof Element)) {
365
397
  console.warn("Invalid host element");
366
398
  return () => {
@@ -376,24 +408,31 @@ function createPortal(host, container = document.body) {
376
408
  return () => {
377
409
  };
378
410
  }
379
- const portal = new Portal(host, container);
411
+ const portal = new Portal(host, container, options);
380
412
  return () => portal.destroy();
381
413
  }
382
414
  var Portal = class {
383
415
  #host;
384
416
  #container;
417
+ #settings;
385
418
  #entranceSentinel;
386
419
  #exitSentinel;
387
420
  #focusables = /* @__PURE__ */ new Set();
388
421
  #controller = null;
389
422
  #isDestroyed = false;
390
- constructor(host, container) {
423
+ constructor(host, container, options = {}) {
391
424
  this.#host = host;
392
425
  if (!(container instanceof Element)) {
393
426
  console.warn("Invalid container element. Fallback: <body> element.");
394
427
  container = document.body;
395
428
  }
396
429
  this.#container = container;
430
+ let { noInlineStyle = false } = options;
431
+ if (typeof noInlineStyle !== "boolean") {
432
+ console.warn("Invalid noInlineStyle option. Fallback: false.");
433
+ noInlineStyle = false;
434
+ }
435
+ this.#settings = { noInlineStyle };
397
436
  this.#entranceSentinel = this.#createSentinel();
398
437
  this.#exitSentinel = this.#createSentinel();
399
438
  this.#initialize();
@@ -481,14 +520,14 @@ var Portal = class {
481
520
  ]);
482
521
  for (const focusable of this.#focusables) {
483
522
  if (!current.has(focusable)) {
484
- focusable.isConnected && restoreAttributes([focusable]);
523
+ restoreAttributes(focusable);
485
524
  this.#focusables.delete(focusable);
486
525
  }
487
526
  }
488
527
  for (const focusable of current) {
489
528
  if (!this.#focusables.has(focusable)) {
490
529
  this.#focusables.add(focusable);
491
- saveAttributes([focusable], ["tabindex"]);
530
+ saveAttributes(focusable, "tabindex");
492
531
  focusable.setAttribute("tabindex", "-1");
493
532
  }
494
533
  }
@@ -498,7 +537,9 @@ var Portal = class {
498
537
  sentinel.setAttribute("aria-hidden", "true");
499
538
  sentinel.setAttribute("data-portal-sentinel", "");
500
539
  sentinel.setAttribute("tabindex", "0");
501
- sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
540
+ if (!this.#settings.noInlineStyle) {
541
+ sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
542
+ }
502
543
  return sentinel;
503
544
  }
504
545
  #focusSentinel(isPrevious) {
@@ -534,7 +575,7 @@ function containsComposed2(container, element) {
534
575
  * Lightweight DOM portal (teleport) utility with fully focus management.
535
576
  * Designed for accessible dialogs, menus, overlays, popovers.
536
577
  *
537
- * @version 1.2.31
578
+ * @version 1.3.0
538
579
  * @author Yusuke Kamiyamane
539
580
  * @license MIT
540
581
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -546,7 +587,7 @@ function containsComposed2(container, element) {
546
587
  (**
547
588
  * Attributes Utils
548
589
  *
549
- * @version 1.1.3
590
+ * @version 1.1.4
550
591
  * @author Yusuke Kamiyamane
551
592
  * @license MIT
552
593
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -559,7 +600,7 @@ power-focusable/dist/index.js:
559
600
  * High-precision focus management utility with full composed tree support.
560
601
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
561
602
  *
562
- * @version 4.3.8
603
+ * @version 4.3.21
563
604
  * @author Yusuke Kamiyamane
564
605
  * @license MIT
565
606
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.cjs CHANGED
@@ -5,7 +5,7 @@ var powerFocusable = require('power-focusable');
5
5
 
6
6
  // src/index.ts
7
7
  var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
8
- function createPortal(host, container = document.body) {
8
+ function createPortal(host, container = document.body, options = {}) {
9
9
  if (!(host instanceof Element)) {
10
10
  console.warn("Invalid host element");
11
11
  return () => {
@@ -21,24 +21,31 @@ function createPortal(host, container = document.body) {
21
21
  return () => {
22
22
  };
23
23
  }
24
- const portal = new Portal(host, container);
24
+ const portal = new Portal(host, container, options);
25
25
  return () => portal.destroy();
26
26
  }
27
27
  var Portal = class {
28
28
  #host;
29
29
  #container;
30
+ #settings;
30
31
  #entranceSentinel;
31
32
  #exitSentinel;
32
33
  #focusables = /* @__PURE__ */ new Set();
33
34
  #controller = null;
34
35
  #isDestroyed = false;
35
- constructor(host, container) {
36
+ constructor(host, container, options = {}) {
36
37
  this.#host = host;
37
38
  if (!(container instanceof Element)) {
38
39
  console.warn("Invalid container element. Fallback: <body> element.");
39
40
  container = document.body;
40
41
  }
41
42
  this.#container = container;
43
+ let { noInlineStyle = false } = options;
44
+ if (typeof noInlineStyle !== "boolean") {
45
+ console.warn("Invalid noInlineStyle option. Fallback: false.");
46
+ noInlineStyle = false;
47
+ }
48
+ this.#settings = { noInlineStyle };
42
49
  this.#entranceSentinel = this.#createSentinel();
43
50
  this.#exitSentinel = this.#createSentinel();
44
51
  this.#initialize();
@@ -126,14 +133,14 @@ var Portal = class {
126
133
  ]);
127
134
  for (const focusable of this.#focusables) {
128
135
  if (!current.has(focusable)) {
129
- focusable.isConnected && attributesUtils.restoreAttributes([focusable]);
136
+ attributesUtils.restoreAttributes(focusable);
130
137
  this.#focusables.delete(focusable);
131
138
  }
132
139
  }
133
140
  for (const focusable of current) {
134
141
  if (!this.#focusables.has(focusable)) {
135
142
  this.#focusables.add(focusable);
136
- attributesUtils.saveAttributes([focusable], ["tabindex"]);
143
+ attributesUtils.saveAttributes(focusable, "tabindex");
137
144
  focusable.setAttribute("tabindex", "-1");
138
145
  }
139
146
  }
@@ -143,7 +150,9 @@ var Portal = class {
143
150
  sentinel.setAttribute("aria-hidden", "true");
144
151
  sentinel.setAttribute("data-portal-sentinel", "");
145
152
  sentinel.setAttribute("tabindex", "0");
146
- sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
153
+ if (!this.#settings.noInlineStyle) {
154
+ sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
155
+ }
147
156
  return sentinel;
148
157
  }
149
158
  #focusSentinel(isPrevious) {
@@ -179,7 +188,7 @@ function containsComposed(container, element) {
179
188
  * Lightweight DOM portal (teleport) utility with fully focus management.
180
189
  * Designed for accessible dialogs, menus, overlays, popovers.
181
190
  *
182
- * @version 1.2.31
191
+ * @version 1.3.0
183
192
  * @author Yusuke Kamiyamane
184
193
  * @license MIT
185
194
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -3,12 +3,15 @@
3
3
  * Lightweight DOM portal (teleport) utility with fully focus management.
4
4
  * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 1.2.31
6
+ * @version 1.3.0
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
10
10
  * @see {@link https://github.com/y14e/portal}
11
11
  */
12
- declare function createPortal(host: Element, container?: HTMLElement): () => void;
12
+ interface PortalOptions {
13
+ noInlineStyle: boolean;
14
+ }
15
+ declare function createPortal(host: Element, container?: HTMLElement, options?: Partial<PortalOptions>): () => void;
13
16
 
14
- export { createPortal };
17
+ export { type PortalOptions, createPortal };
package/dist/index.d.ts CHANGED
@@ -3,12 +3,15 @@
3
3
  * Lightweight DOM portal (teleport) utility with fully focus management.
4
4
  * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 1.2.31
6
+ * @version 1.3.0
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
10
10
  * @see {@link https://github.com/y14e/portal}
11
11
  */
12
- declare function createPortal(host: Element, container?: HTMLElement): () => void;
12
+ interface PortalOptions {
13
+ noInlineStyle: boolean;
14
+ }
15
+ declare function createPortal(host: Element, container?: HTMLElement, options?: Partial<PortalOptions>): () => void;
13
16
 
14
- export { createPortal };
17
+ export { type PortalOptions, createPortal };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { focusElement, getActiveElement, getFocusables, getPreviousFocusable, ge
3
3
 
4
4
  // src/index.ts
5
5
  var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
6
- function createPortal(host, container = document.body) {
6
+ function createPortal(host, container = document.body, options = {}) {
7
7
  if (!(host instanceof Element)) {
8
8
  console.warn("Invalid host element");
9
9
  return () => {
@@ -19,24 +19,31 @@ function createPortal(host, container = document.body) {
19
19
  return () => {
20
20
  };
21
21
  }
22
- const portal = new Portal(host, container);
22
+ const portal = new Portal(host, container, options);
23
23
  return () => portal.destroy();
24
24
  }
25
25
  var Portal = class {
26
26
  #host;
27
27
  #container;
28
+ #settings;
28
29
  #entranceSentinel;
29
30
  #exitSentinel;
30
31
  #focusables = /* @__PURE__ */ new Set();
31
32
  #controller = null;
32
33
  #isDestroyed = false;
33
- constructor(host, container) {
34
+ constructor(host, container, options = {}) {
34
35
  this.#host = host;
35
36
  if (!(container instanceof Element)) {
36
37
  console.warn("Invalid container element. Fallback: <body> element.");
37
38
  container = document.body;
38
39
  }
39
40
  this.#container = container;
41
+ let { noInlineStyle = false } = options;
42
+ if (typeof noInlineStyle !== "boolean") {
43
+ console.warn("Invalid noInlineStyle option. Fallback: false.");
44
+ noInlineStyle = false;
45
+ }
46
+ this.#settings = { noInlineStyle };
40
47
  this.#entranceSentinel = this.#createSentinel();
41
48
  this.#exitSentinel = this.#createSentinel();
42
49
  this.#initialize();
@@ -124,14 +131,14 @@ var Portal = class {
124
131
  ]);
125
132
  for (const focusable of this.#focusables) {
126
133
  if (!current.has(focusable)) {
127
- focusable.isConnected && restoreAttributes([focusable]);
134
+ restoreAttributes(focusable);
128
135
  this.#focusables.delete(focusable);
129
136
  }
130
137
  }
131
138
  for (const focusable of current) {
132
139
  if (!this.#focusables.has(focusable)) {
133
140
  this.#focusables.add(focusable);
134
- saveAttributes([focusable], ["tabindex"]);
141
+ saveAttributes(focusable, "tabindex");
135
142
  focusable.setAttribute("tabindex", "-1");
136
143
  }
137
144
  }
@@ -141,7 +148,9 @@ var Portal = class {
141
148
  sentinel.setAttribute("aria-hidden", "true");
142
149
  sentinel.setAttribute("data-portal-sentinel", "");
143
150
  sentinel.setAttribute("tabindex", "0");
144
- sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
151
+ if (!this.#settings.noInlineStyle) {
152
+ sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
153
+ }
145
154
  return sentinel;
146
155
  }
147
156
  #focusSentinel(isPrevious) {
@@ -177,7 +186,7 @@ function containsComposed(container, element) {
177
186
  * Lightweight DOM portal (teleport) utility with fully focus management.
178
187
  * Designed for accessible dialogs, menus, overlays, popovers.
179
188
  *
180
- * @version 1.2.31
189
+ * @version 1.3.0
181
190
  * @author Yusuke Kamiyamane
182
191
  * @license MIT
183
192
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/portal",
3
- "version": "1.2.31",
3
+ "version": "1.3.0",
4
4
  "description": "Lightweight DOM portal (teleport) utility with fully focus management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -55,7 +55,7 @@
55
55
  "node": ">=18"
56
56
  },
57
57
  "dependencies": {
58
- "@y14e/attributes-utils": "^1.1.3",
59
- "power-focusable": "^4.3.8"
58
+ "@y14e/attributes-utils": "^1.1.4",
59
+ "power-focusable": "^4.3.21"
60
60
  }
61
61
  }