@fivem-nui/svelte 0.0.0-stage → 1.0.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 +284 -2
- package/dist/actions/clickOutside/clickOutside.d.ts +24 -0
- package/dist/actions/clickOutside/clickOutside.d.ts.map +1 -0
- package/dist/actions/clickOutside/clickOutside.js +62 -0
- package/dist/actions/clickOutside/clickOutside.js.map +1 -0
- package/dist/actions/clickOutside/handler.d.ts +11 -0
- package/dist/actions/clickOutside/handler.d.ts.map +1 -0
- package/dist/actions/clickOutside/handler.js +45 -0
- package/dist/actions/clickOutside/handler.js.map +1 -0
- package/dist/actions/clickOutside/index.d.ts +4 -0
- package/dist/actions/clickOutside/index.d.ts.map +1 -0
- package/dist/actions/clickOutside/index.js +4 -0
- package/dist/actions/clickOutside/index.js.map +1 -0
- package/dist/actions/clickOutside/types.d.ts +36 -0
- package/dist/actions/clickOutside/types.d.ts.map +1 -0
- package/dist/actions/clickOutside/types.js +6 -0
- package/dist/actions/clickOutside/types.js.map +1 -0
- package/dist/actions/clickOutside.d.ts +63 -0
- package/dist/actions/clickOutside.d.ts.map +1 -0
- package/dist/actions/clickOutside.js +106 -0
- package/dist/actions/clickOutside.js.map +1 -0
- package/dist/actions/escape/escape.d.ts +26 -0
- package/dist/actions/escape/escape.d.ts.map +1 -0
- package/dist/actions/escape/escape.js +74 -0
- package/dist/actions/escape/escape.js.map +1 -0
- package/dist/actions/escape/handler.d.ts +11 -0
- package/dist/actions/escape/handler.d.ts.map +1 -0
- package/dist/actions/escape/handler.js +50 -0
- package/dist/actions/escape/handler.js.map +1 -0
- package/dist/actions/escape/index.d.ts +4 -0
- package/dist/actions/escape/index.d.ts.map +1 -0
- package/dist/actions/escape/index.js +4 -0
- package/dist/actions/escape/index.js.map +1 -0
- package/dist/actions/escape/types.d.ts +70 -0
- package/dist/actions/escape/types.d.ts.map +1 -0
- package/dist/actions/escape/types.js +12 -0
- package/dist/actions/escape/types.js.map +1 -0
- package/dist/actions/escape.d.ts +99 -0
- package/dist/actions/escape.d.ts.map +1 -0
- package/dist/actions/escape.js +128 -0
- package/dist/actions/escape.js.map +1 -0
- package/dist/actions/focusTrap/focusTrap.d.ts +19 -0
- package/dist/actions/focusTrap/focusTrap.d.ts.map +1 -0
- package/dist/actions/focusTrap/focusTrap.js +198 -0
- package/dist/actions/focusTrap/focusTrap.js.map +1 -0
- package/dist/actions/focusTrap/focusables.d.ts +39 -0
- package/dist/actions/focusTrap/focusables.d.ts.map +1 -0
- package/dist/actions/focusTrap/focusables.js +142 -0
- package/dist/actions/focusTrap/focusables.js.map +1 -0
- package/dist/actions/focusTrap/index.d.ts +5 -0
- package/dist/actions/focusTrap/index.d.ts.map +1 -0
- package/dist/actions/focusTrap/index.js +5 -0
- package/dist/actions/focusTrap/index.js.map +1 -0
- package/dist/actions/focusTrap/trapStack.d.ts +33 -0
- package/dist/actions/focusTrap/trapStack.d.ts.map +1 -0
- package/dist/actions/focusTrap/trapStack.js +84 -0
- package/dist/actions/focusTrap/trapStack.js.map +1 -0
- package/dist/actions/focusTrap/types.d.ts +50 -0
- package/dist/actions/focusTrap/types.d.ts.map +1 -0
- package/dist/actions/focusTrap/types.js +10 -0
- package/dist/actions/focusTrap/types.js.map +1 -0
- package/dist/actions/focusTrap.d.ts +24 -0
- package/dist/actions/focusTrap.d.ts.map +1 -0
- package/dist/actions/focusTrap.js +201 -0
- package/dist/actions/focusTrap.js.map +1 -0
- package/dist/actions/focusTrap.types.d.ts +50 -0
- package/dist/actions/focusTrap.types.d.ts.map +1 -0
- package/dist/actions/focusTrap.types.js +10 -0
- package/dist/actions/focusTrap.types.js.map +1 -0
- package/dist/actions/focusables.d.ts +39 -0
- package/dist/actions/focusables.d.ts.map +1 -0
- package/dist/actions/focusables.js +142 -0
- package/dist/actions/focusables.js.map +1 -0
- package/dist/actions/index.d.ts +5 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +5 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/layer/index.d.ts +4 -0
- package/dist/actions/layer/index.d.ts.map +1 -0
- package/dist/actions/layer/index.js +4 -0
- package/dist/actions/layer/index.js.map +1 -0
- package/dist/actions/layer/layer.d.ts +28 -0
- package/dist/actions/layer/layer.d.ts.map +1 -0
- package/dist/actions/layer/layer.js +118 -0
- package/dist/actions/layer/layer.js.map +1 -0
- package/dist/actions/layer/layerStack.d.ts +10 -0
- package/dist/actions/layer/layerStack.d.ts.map +1 -0
- package/dist/actions/layer/layerStack.js +153 -0
- package/dist/actions/layer/layerStack.js.map +1 -0
- package/dist/actions/layer/types.d.ts +105 -0
- package/dist/actions/layer/types.d.ts.map +1 -0
- package/dist/actions/layer/types.js +14 -0
- package/dist/actions/layer/types.js.map +1 -0
- package/dist/actions/layer.d.ts +133 -0
- package/dist/actions/layer.d.ts.map +1 -0
- package/dist/actions/layer.js +269 -0
- package/dist/actions/layer.js.map +1 -0
- package/dist/actions/trapStack.d.ts +33 -0
- package/dist/actions/trapStack.d.ts.map +1 -0
- package/dist/actions/trapStack.js +84 -0
- package/dist/actions/trapStack.js.map +1 -0
- package/dist/client.d.ts +179 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +176 -0
- package/dist/client.js.map +1 -0
- package/dist/context/index.d.ts +68 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +112 -0
- package/dist/context/index.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/mock/index.d.ts +27 -0
- package/dist/mock/index.d.ts.map +1 -0
- package/dist/mock/index.js +137 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/mock/interceptor.d.ts +19 -0
- package/dist/mock/interceptor.d.ts.map +1 -0
- package/dist/mock/interceptor.js +128 -0
- package/dist/mock/interceptor.js.map +1 -0
- package/dist/mock/types.d.ts +100 -0
- package/dist/mock/types.d.ts.map +1 -0
- package/dist/mock/types.js +27 -0
- package/dist/mock/types.js.map +1 -0
- package/dist/mock/url.d.ts +14 -0
- package/dist/mock/url.d.ts.map +1 -0
- package/dist/mock/url.js +38 -0
- package/dist/mock/url.js.map +1 -0
- package/dist/runes/clickOutside.svelte.d.ts +29 -0
- package/dist/runes/clickOutside.svelte.d.ts.map +1 -0
- package/dist/runes/clickOutside.svelte.js +42 -0
- package/dist/runes/clickOutside.svelte.js.map +1 -0
- package/dist/runes/escape.svelte.d.ts +34 -0
- package/dist/runes/escape.svelte.d.ts.map +1 -0
- package/dist/runes/escape.svelte.js +44 -0
- package/dist/runes/escape.svelte.js.map +1 -0
- package/dist/runes/focusTrap.svelte.d.ts +21 -0
- package/dist/runes/focusTrap.svelte.d.ts.map +1 -0
- package/dist/runes/focusTrap.svelte.js +37 -0
- package/dist/runes/focusTrap.svelte.js.map +1 -0
- package/dist/runes/index.svelte.d.ts +8 -0
- package/dist/runes/index.svelte.d.ts.map +1 -0
- package/dist/runes/index.svelte.js +8 -0
- package/dist/runes/index.svelte.js.map +1 -0
- package/dist/runes/layer.svelte.d.ts +36 -0
- package/dist/runes/layer.svelte.d.ts.map +1 -0
- package/dist/runes/layer.svelte.js +48 -0
- package/dist/runes/layer.svelte.js.map +1 -0
- package/dist/runes/query.svelte.d.ts +104 -0
- package/dist/runes/query.svelte.d.ts.map +1 -0
- package/dist/runes/query.svelte.js +179 -0
- package/dist/runes/query.svelte.js.map +1 -0
- package/dist/runes/state.svelte.d.ts +72 -0
- package/dist/runes/state.svelte.d.ts.map +1 -0
- package/dist/runes/state.svelte.js +98 -0
- package/dist/runes/state.svelte.js.map +1 -0
- package/dist/runes/window.svelte.d.ts +120 -0
- package/dist/runes/window.svelte.d.ts.map +1 -0
- package/dist/runes/window.svelte.js +196 -0
- package/dist/runes/window.svelte.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/nui.d.ts +34 -0
- package/dist/types/nui.d.ts.map +1 -0
- package/dist/types/nui.js +17 -0
- package/dist/types/nui.js.map +1 -0
- package/dist/utils/env.d.ts +21 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +36 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +30 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/keyboard.d.ts +127 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +152 -0
- package/dist/utils/keyboard.js.map +1 -0
- package/package.json +76 -4
- package/src/actions/clickOutside/clickOutside.ts +78 -0
- package/src/actions/clickOutside/handler.ts +49 -0
- package/src/actions/clickOutside/index.ts +11 -0
- package/src/actions/clickOutside/types.ts +54 -0
- package/src/actions/escape/escape.ts +90 -0
- package/src/actions/escape/handler.ts +52 -0
- package/src/actions/escape/index.ts +11 -0
- package/src/actions/escape/types.ts +88 -0
- package/src/actions/focusTrap/focusTrap.ts +228 -0
- package/src/actions/focusTrap/focusables.ts +155 -0
- package/src/actions/focusTrap/index.ts +22 -0
- package/src/actions/focusTrap/trapStack.ts +109 -0
- package/src/actions/focusTrap/types.ts +64 -0
- package/src/actions/index.ts +35 -0
- package/src/actions/layer/index.ts +13 -0
- package/src/actions/layer/layer.ts +140 -0
- package/src/actions/layer/layerStack.ts +169 -0
- package/src/actions/layer/types.ts +132 -0
- package/src/client.ts +323 -0
- package/src/context/index.ts +127 -0
- package/src/index.ts +53 -0
- package/src/mock/index.ts +176 -0
- package/src/mock/interceptor.ts +177 -0
- package/src/mock/types.ts +125 -0
- package/src/mock/url.ts +37 -0
- package/src/runes/clickOutside.svelte.ts +53 -0
- package/src/runes/escape.svelte.ts +71 -0
- package/src/runes/focusTrap.svelte.ts +56 -0
- package/src/runes/index.svelte.ts +38 -0
- package/src/runes/layer.svelte.ts +74 -0
- package/src/runes/query.svelte.ts +313 -0
- package/src/runes/state.svelte.ts +175 -0
- package/src/runes/window.svelte.ts +308 -0
- package/src/utils/env.ts +36 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/keyboard.ts +244 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Action } from 'svelte/action';
|
|
2
|
+
import { nuiFocusTrap, type NuiFocusTrapOptions } from '../focusTrap';
|
|
3
|
+
import {
|
|
4
|
+
type NuiLayerOptions,
|
|
5
|
+
type NuiLayerInput,
|
|
6
|
+
type LayerInstance,
|
|
7
|
+
toLayerOptions
|
|
8
|
+
} from './types';
|
|
9
|
+
import {
|
|
10
|
+
pushLayer,
|
|
11
|
+
removeLayer,
|
|
12
|
+
getTopLayer,
|
|
13
|
+
nextLayerId
|
|
14
|
+
} from './layerStack';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Managing an overlay layer in FiveM NUI.
|
|
18
|
+
*
|
|
19
|
+
* Coordinates:
|
|
20
|
+
* - **Escape key**: Traverses the layer stack top-to-bottom. The topmost active layer that handles Escape
|
|
21
|
+
* receives the event and dismisses. If the topmost layer disables Escape (`onEscape: false`),
|
|
22
|
+
* lower layers can handle it.
|
|
23
|
+
* - **Outside click**: Evaluates strictly the topmost active layer. If the topmost layer disables outside
|
|
24
|
+
* clicks (`onClickOutside: false`), clicks outside are blocked and do NOT fall through to underlying layers,
|
|
25
|
+
* preserving modal barrier isolation.
|
|
26
|
+
* - **Focus trap**: Integrates `nuiFocusTrap` synchronized with layer priority so focus containment and Tab
|
|
27
|
+
* navigation are enforced by the topmost layer even when layers have varying priorities.
|
|
28
|
+
* - **Portal integration**: `allowOutside` allows portaled children (selects, datepickers, tooltips)
|
|
29
|
+
* without triggering outside clicks or breaking focus containment.
|
|
30
|
+
*
|
|
31
|
+
* @param node - The DOM element attaching the layer action.
|
|
32
|
+
* @param parameter - Dismissal callback function or layer configuration options object.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```svelte
|
|
36
|
+
* <div use:nuiLayer={() => (isOpen = false)}>...</div>
|
|
37
|
+
* <div use:nuiLayer={{ onDismiss: close, allowOutside: '.portal-menu' }}>...</div>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const nuiLayer: Action<HTMLElement, NuiLayerInput> = (node, parameter) => {
|
|
41
|
+
let currentParam = parameter;
|
|
42
|
+
let active = false;
|
|
43
|
+
let trapAction: ReturnType<typeof nuiFocusTrap> | null = null;
|
|
44
|
+
const id = toLayerOptions(parameter).id ?? nextLayerId();
|
|
45
|
+
|
|
46
|
+
function getOptions(): NuiLayerOptions {
|
|
47
|
+
return toLayerOptions(currentParam);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isEnabled(): boolean {
|
|
51
|
+
const opts = getOptions();
|
|
52
|
+
const { enabled } = opts;
|
|
53
|
+
return typeof enabled === 'function' ? enabled() : (enabled ?? true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const instance: LayerInstance = {
|
|
57
|
+
id,
|
|
58
|
+
node,
|
|
59
|
+
getOptions
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function syncFocusTrap() {
|
|
63
|
+
const opts = getOptions();
|
|
64
|
+
const enabled = isEnabled();
|
|
65
|
+
|
|
66
|
+
if (!enabled || opts.trapFocus === false) {
|
|
67
|
+
if (trapAction) {
|
|
68
|
+
trapAction.destroy?.();
|
|
69
|
+
trapAction = null;
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const trapConfig: NuiFocusTrapOptions =
|
|
75
|
+
typeof opts.trapFocus === 'object' ? { ...opts.trapFocus } : {};
|
|
76
|
+
|
|
77
|
+
if (trapConfig.allowOutside === undefined && opts.allowOutside !== undefined) {
|
|
78
|
+
trapConfig.allowOutside = opts.allowOutside;
|
|
79
|
+
}
|
|
80
|
+
if (trapConfig.initialFocus === undefined && opts.initialFocus !== undefined) {
|
|
81
|
+
trapConfig.initialFocus = opts.initialFocus;
|
|
82
|
+
}
|
|
83
|
+
if (trapConfig.returnFocus === undefined && opts.returnFocus !== undefined) {
|
|
84
|
+
trapConfig.returnFocus = opts.returnFocus;
|
|
85
|
+
}
|
|
86
|
+
if (trapConfig.autofocus === undefined && opts.autofocus !== undefined) {
|
|
87
|
+
trapConfig.autofocus = opts.autofocus;
|
|
88
|
+
}
|
|
89
|
+
trapConfig.enabled = enabled;
|
|
90
|
+
trapConfig.isTopmost = () => getTopLayer() === instance;
|
|
91
|
+
|
|
92
|
+
if (!trapAction) {
|
|
93
|
+
trapAction = nuiFocusTrap(node, trapConfig);
|
|
94
|
+
} else {
|
|
95
|
+
trapAction.update?.(trapConfig);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setup() {
|
|
100
|
+
if (active) return;
|
|
101
|
+
active = true;
|
|
102
|
+
pushLayer(instance);
|
|
103
|
+
syncFocusTrap();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function deactivate() {
|
|
107
|
+
if (!active) return;
|
|
108
|
+
active = false;
|
|
109
|
+
|
|
110
|
+
if (trapAction) {
|
|
111
|
+
trapAction.destroy?.();
|
|
112
|
+
trapAction = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
removeLayer(instance);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isEnabled()) {
|
|
119
|
+
setup();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
update(newParam) {
|
|
124
|
+
const wasEnabled = active;
|
|
125
|
+
currentParam = newParam;
|
|
126
|
+
const nowEnabled = isEnabled();
|
|
127
|
+
|
|
128
|
+
if (!wasEnabled && nowEnabled) {
|
|
129
|
+
setup();
|
|
130
|
+
} else if (wasEnabled && !nowEnabled) {
|
|
131
|
+
deactivate();
|
|
132
|
+
} else if (nowEnabled) {
|
|
133
|
+
syncFocusTrap();
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
destroy() {
|
|
137
|
+
deactivate();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { NuiLayerDismissReason, type LayerInstance } from './types';
|
|
2
|
+
import { isNodeAllowedOutside } from '../focusTrap';
|
|
3
|
+
import { resolveElement } from '../clickOutside';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Global stack of active overlay layers.
|
|
7
|
+
* Manages hierarchical Escape key and outside click handling across modals, dialogs,
|
|
8
|
+
* drawers, and portaled overlays in FiveM NUI.
|
|
9
|
+
*/
|
|
10
|
+
const layerStack: LayerInstance[] = [];
|
|
11
|
+
|
|
12
|
+
let globalController: AbortController | null = null;
|
|
13
|
+
let layerIdCounter = 0;
|
|
14
|
+
|
|
15
|
+
export function nextLayerId(): string {
|
|
16
|
+
return `nui-layer-${++layerIdCounter}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function pushLayer(instance: LayerInstance): void {
|
|
20
|
+
layerStack.push(instance);
|
|
21
|
+
ensureGlobalListeners();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function removeLayer(instance: LayerInstance): void {
|
|
25
|
+
const idx = layerStack.findIndex((l) => l === instance);
|
|
26
|
+
if (idx !== -1) {
|
|
27
|
+
layerStack.splice(idx, 1);
|
|
28
|
+
}
|
|
29
|
+
cleanupGlobalListeners();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getLayerStack(): readonly LayerInstance[] {
|
|
33
|
+
return [...layerStack];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resetLayerStack(): void {
|
|
37
|
+
layerStack.length = 0;
|
|
38
|
+
cleanupGlobalListeners();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getSortedStack(): LayerInstance[] {
|
|
42
|
+
return [...layerStack].sort((a, b) => {
|
|
43
|
+
return (a.getOptions().priority ?? 0) - (b.getOptions().priority ?? 0);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getTopLayer(): LayerInstance | undefined {
|
|
48
|
+
const sorted = getSortedStack();
|
|
49
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
50
|
+
const layer = sorted[i];
|
|
51
|
+
const opts = layer.getOptions();
|
|
52
|
+
const isEnabled = typeof opts.enabled === 'function' ? opts.enabled() : (opts.enabled ?? true);
|
|
53
|
+
if (isEnabled && layer.node.isConnected) {
|
|
54
|
+
return layer;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onGlobalKeyDown(event: KeyboardEvent) {
|
|
61
|
+
if (event.key !== 'Escape' || event.isComposing) return;
|
|
62
|
+
if (layerStack.length === 0) return;
|
|
63
|
+
|
|
64
|
+
const sorted = getSortedStack();
|
|
65
|
+
|
|
66
|
+
// Find the topmost active layer that handles Escape
|
|
67
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
68
|
+
const layer = sorted[i];
|
|
69
|
+
const opts = layer.getOptions();
|
|
70
|
+
const isEnabled = typeof opts.enabled === 'function' ? opts.enabled() : (opts.enabled ?? true);
|
|
71
|
+
if (!isEnabled || !layer.node.isConnected) continue;
|
|
72
|
+
|
|
73
|
+
const handlesEscape =
|
|
74
|
+
opts.onEscape !== false &&
|
|
75
|
+
(typeof opts.onEscape === 'function' || typeof opts.onDismiss === 'function');
|
|
76
|
+
|
|
77
|
+
if (!handlesEscape) continue;
|
|
78
|
+
|
|
79
|
+
if ((opts.ignoreRepeat ?? true) && event.repeat) {
|
|
80
|
+
if (opts.preventEscapeDefault ?? true) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (opts.preventEscapeDefault ?? true) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
}
|
|
89
|
+
if (opts.stopEscapePropagation) {
|
|
90
|
+
event.stopPropagation();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (typeof opts.onEscape === 'function') {
|
|
95
|
+
opts.onEscape(event);
|
|
96
|
+
} else if (typeof opts.onDismiss === 'function') {
|
|
97
|
+
opts.onDismiss(NuiLayerDismissReason.Escape);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error('[NuiLayer] Error in onEscape/onDismiss callback:', err);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Only the topmost handling layer responds to Escape
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onGlobalPointerDown(event: PointerEvent) {
|
|
109
|
+
if (event.button !== 0 || layerStack.length === 0) return;
|
|
110
|
+
|
|
111
|
+
const topLayer = getTopLayer();
|
|
112
|
+
if (!topLayer) return;
|
|
113
|
+
|
|
114
|
+
const opts = topLayer.getOptions();
|
|
115
|
+
const handlesOutside =
|
|
116
|
+
opts.onClickOutside !== false &&
|
|
117
|
+
(typeof opts.onClickOutside === 'function' || typeof opts.onDismiss === 'function');
|
|
118
|
+
|
|
119
|
+
if (!handlesOutside) return;
|
|
120
|
+
|
|
121
|
+
const path = event.composedPath();
|
|
122
|
+
|
|
123
|
+
// 1. Click inside top layer node
|
|
124
|
+
if (path.includes(topLayer.node)) return;
|
|
125
|
+
|
|
126
|
+
// 2. Click inside any allowed outside target (e.g. portaled dropdown)
|
|
127
|
+
if (opts.allowOutside && isNodeAllowedOutside(event.target as Node | null, opts.allowOutside)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Click inside any ignoreOutside target (e.g. trigger button)
|
|
132
|
+
if (opts.ignoreOutside) {
|
|
133
|
+
for (const item of opts.ignoreOutside) {
|
|
134
|
+
const el = resolveElement(item);
|
|
135
|
+
if (el && path.includes(el)) return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Trigger outside click for the topmost layer only
|
|
140
|
+
try {
|
|
141
|
+
if (typeof opts.onClickOutside === 'function') {
|
|
142
|
+
opts.onClickOutside(event);
|
|
143
|
+
} else if (typeof opts.onDismiss === 'function') {
|
|
144
|
+
opts.onDismiss(NuiLayerDismissReason.Outside);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('[NuiLayer] Error in onClickOutside/onDismiss callback:', err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function ensureGlobalListeners(): void {
|
|
152
|
+
if (globalController || layerStack.length === 0) return;
|
|
153
|
+
globalController = new AbortController();
|
|
154
|
+
document.addEventListener('keydown', onGlobalKeyDown, {
|
|
155
|
+
capture: true,
|
|
156
|
+
signal: globalController.signal
|
|
157
|
+
});
|
|
158
|
+
document.addEventListener('pointerdown', onGlobalPointerDown, {
|
|
159
|
+
capture: true,
|
|
160
|
+
signal: globalController.signal
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function cleanupGlobalListeners(): void {
|
|
165
|
+
if (layerStack.length === 0 && globalController) {
|
|
166
|
+
globalController.abort();
|
|
167
|
+
globalController = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { NuiFocusTrapOptions, FocusTrapAllowOutside } from '../focusTrap';
|
|
2
|
+
import type { ClickOutsideTarget } from '../clickOutside';
|
|
3
|
+
|
|
4
|
+
export enum NuiLayerDismissReason {
|
|
5
|
+
Escape = 'escape',
|
|
6
|
+
Outside = 'outside'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface NuiLayerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Unique identifier for the layer.
|
|
12
|
+
* Auto-generated if omitted.
|
|
13
|
+
*/
|
|
14
|
+
id?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether the layer is active in the layer stack.
|
|
18
|
+
* Accepts a boolean or a reactive getter `() => boolean`.
|
|
19
|
+
* @defaultValue true
|
|
20
|
+
*/
|
|
21
|
+
enabled?: boolean | (() => boolean);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Unified callback invoked when the layer should be dismissed
|
|
25
|
+
* (either via Escape key or outside click while topmost).
|
|
26
|
+
*/
|
|
27
|
+
onDismiss?: (reason: NuiLayerDismissReason) => void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Specific callback invoked when the Escape key is pressed while this layer is topmost.
|
|
31
|
+
* Set to `false` to disable Escape handling for this layer.
|
|
32
|
+
*/
|
|
33
|
+
onEscape?: ((event: KeyboardEvent) => void) | boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Specific callback invoked when a click occurs outside this layer while it is topmost.
|
|
37
|
+
* Set to `false` to disable outside click handling for this layer.
|
|
38
|
+
*/
|
|
39
|
+
onClickOutside?: ((event: PointerEvent) => void) | boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Focus trap configuration for this layer:
|
|
43
|
+
* - `true`: enables default focus trap (default)
|
|
44
|
+
* - `false`: disables focus trap
|
|
45
|
+
* - `NuiFocusTrapOptions`: custom focus trap options (initialFocus, returnFocus, autofocus, etc.)
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* `nuiLayer` automatically controls and overrides `trapFocus.isTopmost` so focus containment
|
|
49
|
+
* is synchronized strictly with layer priority (`getTopLayer() === instance`).
|
|
50
|
+
* @defaultValue true
|
|
51
|
+
*/
|
|
52
|
+
trapFocus?: boolean | NuiFocusTrapOptions;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Elements, selectors, or getters outside the container that are permitted.
|
|
56
|
+
* Shared between focus trap (won't redirect Tab from them) and click-outside (won't dismiss on them).
|
|
57
|
+
* Essential for portaled child elements (e.g. dropdowns, date pickers, tooltips).
|
|
58
|
+
*/
|
|
59
|
+
allowOutside?: FocusTrapAllowOutside;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Additional elements, selectors, or getters to ignore specifically for click-outside detection
|
|
63
|
+
* (e.g. the toggle or trigger button that opened this layer).
|
|
64
|
+
*/
|
|
65
|
+
ignoreOutside?: ClickOutsideTarget[];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Custom element, selector or getter to receive initial focus upon activation.
|
|
69
|
+
* Shorthand for `trapFocus.initialFocus`.
|
|
70
|
+
*/
|
|
71
|
+
initialFocus?: HTMLElement | string | (() => HTMLElement | null | undefined);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether to restore focus to previously focused element upon layer deactivation.
|
|
75
|
+
* Shorthand for `trapFocus.returnFocus`.
|
|
76
|
+
* @defaultValue true
|
|
77
|
+
*/
|
|
78
|
+
returnFocus?: boolean | HTMLElement;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether to automatically move focus inside the layer on activation.
|
|
82
|
+
* Shorthand for `trapFocus.autofocus`.
|
|
83
|
+
* @defaultValue true
|
|
84
|
+
*/
|
|
85
|
+
autofocus?: boolean;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Priority/z-index override. Layers with higher priority are considered on top
|
|
89
|
+
* regardless of activation order. Equal priority layers follow activation order.
|
|
90
|
+
* @defaultValue 0
|
|
91
|
+
*/
|
|
92
|
+
priority?: number;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether to ignore repeated keydown events fired while holding the Escape key down.
|
|
96
|
+
* If `true`, holding Escape will not dismiss subsequent layers in rapid succession.
|
|
97
|
+
* @defaultValue true
|
|
98
|
+
*/
|
|
99
|
+
ignoreRepeat?: boolean;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether to call `event.preventDefault()` when Escape is handled.
|
|
103
|
+
* @defaultValue true
|
|
104
|
+
*/
|
|
105
|
+
preventEscapeDefault?: boolean;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether to call `event.stopPropagation()` when Escape is handled.
|
|
109
|
+
* Defaults to `false` to avoid breaking uncoordinated parent listeners on window/document.
|
|
110
|
+
* @defaultValue false
|
|
111
|
+
*/
|
|
112
|
+
stopEscapePropagation?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type NuiLayerInput =
|
|
116
|
+
| ((reason: NuiLayerDismissReason) => void)
|
|
117
|
+
| NuiLayerOptions
|
|
118
|
+
| undefined;
|
|
119
|
+
|
|
120
|
+
export function toLayerOptions(input: NuiLayerInput): NuiLayerOptions {
|
|
121
|
+
if (!input) return { enabled: true };
|
|
122
|
+
if (typeof input === 'function') {
|
|
123
|
+
return { enabled: true, onDismiss: input };
|
|
124
|
+
}
|
|
125
|
+
return input;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface LayerInstance {
|
|
129
|
+
id: string;
|
|
130
|
+
node: HTMLElement;
|
|
131
|
+
getOptions: () => NuiLayerOptions;
|
|
132
|
+
}
|