react-x11 0.0.1 → 1.2.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.
@@ -0,0 +1,138 @@
1
+ // Widget components built purely on the host primitives — no reconciler
2
+ // support needed. Plain createElement (no JSX) so the library stays
3
+ // build-step-free for consumers.
4
+
5
+ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
6
+ import { labelContent, useTheme } from './theme.js';
7
+ import { centerRect } from './anchor.js';
8
+ import { XK_ESCAPE } from './keys.js';
9
+
10
+ const h = React.createElement;
11
+
12
+ const DEFAULT_WIDTH = 360;
13
+
14
+ const DEFAULT_HEIGHT = 170;
15
+
16
+ /**
17
+ * <Dialog open title onClose actions>…</Dialog> — a modal dialog in a
18
+ * `<popup trapFocus grab>`, centred over the owner window.
19
+ *
20
+ * The focus behaviour is the renderer's, not this component's: `trapFocus`
21
+ * keeps Tab inside the dialog, stops presses elsewhere from moving focus,
22
+ * and hands focus back to whatever had it — usually the button that opened
23
+ * the dialog — when it closes. Put `autoFocus` on a control inside to choose
24
+ * the first stop; otherwise the dialog surface itself takes focus, so
25
+ * Escape and Tab work immediately (`docs/events.md`, "Focus scopes").
26
+ *
27
+ * Escape closes: keys go to the focused node inside the popup and bubble out
28
+ * through the popup's place in the JSX tree. `grab` makes a press anywhere
29
+ * else in the session close it too. Pointer modality is *not* enforced —
30
+ * widgets in the owner window stay clickable behind the dialog, so use it
31
+ * for confirmations rather than as a security boundary.
32
+ *
33
+ * A `<popup>` is a real X window and needs its size up front, hence explicit
34
+ * `width`/`height` rather than sizing to content.
35
+ */
36
+ export function Dialog({
37
+ open,
38
+ title,
39
+ children,
40
+ onClose,
41
+ actions,
42
+ width = DEFAULT_WIDTH,
43
+ height = DEFAULT_HEIGHT,
44
+ ...boxProps
45
+ }) {
46
+ const theme = useTheme();
47
+ const anchor = useRef(null);
48
+ const surface = useRef(null);
49
+ const [rect, setRect] = useState(null);
50
+
51
+ // the popup needs screen coordinates, which are only knowable once the
52
+ // anchor is laid out inside a realized window — hence a layout effect
53
+ // rather than placement at render time
54
+ useLayoutEffect(() => {
55
+ if (!open) {
56
+ setRect(null);
57
+ return;
58
+ }
59
+ const next = centerRect(anchor.current, { width, height });
60
+ if (next) setRect(next);
61
+ }, [open, width, height]);
62
+
63
+ // nothing inside claimed focus (no autoFocus): the dialog takes it itself,
64
+ // the way a browser focuses a <dialog> element, so keys have somewhere to
65
+ // land and the trap has an inside
66
+ useEffect(() => {
67
+ const node = surface.current;
68
+ if (!rect || !node || node.focusWithin) return;
69
+ node.focus();
70
+ }, [rect]);
71
+
72
+ const onKeyDown = (ev) => {
73
+ if (ev.keysym === XK_ESCAPE) {
74
+ ev.preventDefault();
75
+ onClose?.();
76
+ }
77
+ };
78
+
79
+ return h(
80
+ 'box',
81
+ // out of flow: an anchor for the window geometry, never part of layout
82
+ { ref: anchor, position: 'absolute', width: 0, height: 0 },
83
+ rect &&
84
+ h(
85
+ 'popup',
86
+ {
87
+ x: rect.x,
88
+ y: rect.y,
89
+ width: rect.width,
90
+ height: rect.height,
91
+ trapFocus: true,
92
+ grab: true,
93
+ windowType: 'dialog',
94
+ backgroundColor: theme.background,
95
+ onDismiss: () => onClose?.(),
96
+ onKeyDown,
97
+ },
98
+ h(
99
+ 'box',
100
+ {
101
+ ref: surface,
102
+ // focusable but not tabbable: a fallback focus holder, never a
103
+ // stop in the dialog's own tab order
104
+ tabIndex: -1,
105
+ flexGrow: 1,
106
+ padding: 16,
107
+ gap: 12,
108
+ borderWidth: 1,
109
+ borderColor: theme.border,
110
+ backgroundColor: theme.background,
111
+ ...boxProps,
112
+ },
113
+ title &&
114
+ h(
115
+ 'text',
116
+ { fontSize: 15, fontWeight: 'bold', color: theme.text },
117
+ title,
118
+ ),
119
+ h(
120
+ 'box',
121
+ { flexGrow: 1, gap: 8 },
122
+ labelContent(children, { color: theme.text }),
123
+ ),
124
+ actions &&
125
+ h(
126
+ 'box',
127
+ {
128
+ flexDirection: 'row',
129
+ justifyContent: 'flex-end',
130
+ alignItems: 'center',
131
+ gap: 8,
132
+ },
133
+ actions,
134
+ ),
135
+ ),
136
+ ),
137
+ );
138
+ }