foldkit 0.35.2 → 0.36.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/dist/task/dom.js CHANGED
@@ -1,11 +1,14 @@
1
- import { Array, Effect, Equal, Match as M, Number, Option } from 'effect';
1
+ import { Array, Effect, Equal, Function, Match as M, Number, Option, } from 'effect';
2
2
  import { ElementNotFound } from './error';
3
+ const BASE_DIALOG_Z_INDEX = 2147483600;
4
+ let openDialogCount = 0;
5
+ const dialogCleanups = new WeakMap();
3
6
  const FOCUSABLE_SELECTOR = Array.join([
4
- 'a[href]',
5
- 'button:not([disabled])',
6
- 'input:not([disabled])',
7
- 'select:not([disabled])',
8
- 'textarea:not([disabled])',
7
+ 'a[href]:not([tabindex="-1"])',
8
+ 'button:not([disabled]):not([tabindex="-1"])',
9
+ 'input:not([disabled]):not([tabindex="-1"])',
10
+ 'select:not([disabled]):not([tabindex="-1"])',
11
+ 'textarea:not([disabled]):not([tabindex="-1"])',
9
12
  '[tabindex]:not([tabindex="-1"])',
10
13
  ], ', ');
11
14
  /**
@@ -31,7 +34,10 @@ export const focus = (selector) => Effect.async(resume => {
31
34
  });
32
35
  });
33
36
  /**
34
- * Opens a dialog element as a modal using `showModal()`.
37
+ * Opens a dialog element using `show()` with high z-index, focus trapping,
38
+ * and Escape key handling. Uses `show()` instead of `showModal()` so that
39
+ * DevTools (and any other high-z-index overlay) remains interactive — the
40
+ * Dialog component provides its own backdrop, scroll locking, and transitions.
35
41
  * Uses requestAnimationFrame to ensure the DOM is updated before attempting to show.
36
42
  * Fails with `ElementNotFound` if the selector does not match an `HTMLDialogElement`.
37
43
  *
@@ -44,7 +50,27 @@ export const showModal = (selector) => Effect.async(resume => {
44
50
  requestAnimationFrame(() => {
45
51
  const element = document.querySelector(selector);
46
52
  if (element instanceof HTMLDialogElement) {
47
- element.showModal();
53
+ element.style.position = 'fixed';
54
+ element.style.inset = '0';
55
+ openDialogCount++;
56
+ element.style.zIndex = String(BASE_DIALOG_Z_INDEX + openDialogCount);
57
+ element.show();
58
+ const handleKeydown = (event) => {
59
+ if (!element.open) {
60
+ return;
61
+ }
62
+ M.value(event.key).pipe(M.when('Escape', () => {
63
+ if (event.defaultPrevented) {
64
+ return;
65
+ }
66
+ event.preventDefault();
67
+ element.dispatchEvent(new Event('cancel', { cancelable: true }));
68
+ }), M.when('Tab', () => {
69
+ trapFocusWithinDialog(event, element);
70
+ }), M.orElse(Function.constVoid));
71
+ };
72
+ document.addEventListener('keydown', handleKeydown);
73
+ dialogCleanups.set(element, () => document.removeEventListener('keydown', handleKeydown));
48
74
  resume(Effect.void);
49
75
  }
50
76
  else {
@@ -52,8 +78,24 @@ export const showModal = (selector) => Effect.async(resume => {
52
78
  }
53
79
  });
54
80
  });
81
+ const trapFocusWithinDialog = (event, dialog) => {
82
+ const focusable = Array.fromIterable(dialog.querySelectorAll(FOCUSABLE_SELECTOR));
83
+ if (Array.isNonEmptyArray(focusable)) {
84
+ const first = Array.headNonEmpty(focusable);
85
+ const last = Array.lastNonEmpty(focusable);
86
+ if (event.shiftKey && document.activeElement === first) {
87
+ event.preventDefault();
88
+ last.focus();
89
+ }
90
+ else if (!event.shiftKey && document.activeElement === last) {
91
+ event.preventDefault();
92
+ first.focus();
93
+ }
94
+ }
95
+ };
55
96
  /**
56
97
  * Closes a dialog element using `.close()`.
98
+ * Cleans up the keyboard handlers installed by `showModal`.
57
99
  * Uses requestAnimationFrame to ensure the DOM is updated before attempting to close.
58
100
  * Fails with `ElementNotFound` if the selector does not match an `HTMLDialogElement`.
59
101
  *
@@ -67,6 +109,12 @@ export const closeModal = (selector) => Effect.async(resume => {
67
109
  const element = document.querySelector(selector);
68
110
  if (element instanceof HTMLDialogElement) {
69
111
  element.close();
112
+ openDialogCount = Math.max(0, openDialogCount - 1);
113
+ const cleanup = dialogCleanups.get(element);
114
+ if (cleanup) {
115
+ cleanup();
116
+ dialogCleanups.delete(element);
117
+ }
70
118
  resume(Effect.void);
71
119
  }
72
120
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.35.2",
3
+ "version": "0.36.1",
4
4
  "description": "A frontend framework for TypeScript, built on Effect, using The Elm Architecture",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- {"version":3,"file":"errorUI.d.ts","sourceRoot":"","sources":["../../src/runtime/errorUI.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B,OAAO,EAAE,IAAI,EAAQ,MAAM,SAAS,CAAA;AAEpC,eAAO,MAAM,YAAY;8BACG,OAAO;6BACR,OAAO;CACjC,CAAA;AAmBD,eAAO,MAAM,gBAAgB,GAAI,OAAO,KAAK,EAAE,YAAY,OAAO,KAAG,IA4LpE,CAAA"}