react-x11 2.52.0 → 2.54.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 +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1096 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/imagedecode-worker.js +32 -0
- package/src/imagedecode.js +140 -2
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/ntk.d.ts +12 -0
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
package/docs/desktop.md
CHANGED
|
@@ -1,75 +1,43 @@
|
|
|
1
1
|
# Desktop integration
|
|
2
2
|
|
|
3
|
-
What an app
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
talks over D-Bus.
|
|
9
|
-
|
|
10
|
-
The menu bar is the other half of this and has a page of its own —
|
|
11
|
-
[globalmenu.md](globalmenu.md) — because on a desktop that shows application
|
|
12
|
-
menus in its panel, `MenuBar` hands the menu over and stops drawing. Same
|
|
13
|
-
default shape as startup notification: on with no configuration, and one
|
|
14
|
-
switch to turn it off.
|
|
15
|
-
|
|
16
|
-
Everything on this page is the app talking _outwards_. The one thing that
|
|
17
|
-
comes the other way — the desktop handing the app a `myapp://…` link to open,
|
|
18
|
-
and the single-instance question that goes with it — is
|
|
19
|
-
[uri-schemes.md](uri-schemes.md). It shares the launch timestamp described
|
|
20
|
-
below: `ctx.timestamp` there is the same `_TIME` suffix `launchTimestamp()`
|
|
21
|
-
parses here, and for the same reason.
|
|
3
|
+
What an app tells the desktop about itself, beyond drawing: startup
|
|
4
|
+
notification (on by default, nothing to call); the launcher's view of the app
|
|
5
|
+
— a badge, a progress bar, a bounce for attention, the Dock's menu; the tray;
|
|
6
|
+
what a password field needs to be reachable by password managers; and the one
|
|
7
|
+
switch that turns off everything here that talks over D-Bus.
|
|
22
8
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
never says so leaves the sequence running until the desktop gives up on it:
|
|
28
|
-
mutter's `STARTUP_TIMEOUT_MS` is 15 seconds, long after the window is on
|
|
29
|
-
screen and being clicked. The mutter source is blunt about what that looks
|
|
30
|
-
like, in a comment on the constant itself: people assume the launch failed
|
|
31
|
-
and start it again.
|
|
32
|
-
|
|
33
|
-
Two other things ride on the same handshake:
|
|
9
|
+
The menu bar has its own page, [globalmenu.md](globalmenu.md). The other
|
|
10
|
+
direction — the desktop handing the app a `myapp://…` link, and single
|
|
11
|
+
instance — is [uri-schemes.md](uri-schemes.md); its `ctx.timestamp` is the
|
|
12
|
+
same `_TIME` suffix [`launchTimestamp()`](#launchtimestamp) parses here.
|
|
34
13
|
|
|
35
|
-
|
|
36
|
-
weighs when deciding whether a new window may come to the front. With
|
|
37
|
-
none, a strict desktop opens the window behind whatever the user was
|
|
38
|
-
doing — correctly, from its point of view, since nothing said a user asked
|
|
39
|
-
for this.
|
|
40
|
-
- **Placement.** The sequence records which workspace the launch happened
|
|
41
|
-
on. Without one the window goes wherever new windows go.
|
|
14
|
+
## Startup notification
|
|
42
15
|
|
|
43
|
-
|
|
16
|
+
A launcher that spawns an app opens a **startup sequence** — the busy cursor —
|
|
17
|
+
and closes it when the app says it is up. An app that never says so leaves the
|
|
18
|
+
cursor spinning until the desktop gives up (15 seconds on mutter), long after
|
|
19
|
+
its window is on screen, and users take that for a failed launch. The same
|
|
20
|
+
handshake carries **focus** — `_NET_WM_USER_TIME`, without which a strict
|
|
21
|
+
desktop opens the new window behind whatever the user was doing — and
|
|
22
|
+
**placement**, the workspace the launch happened on. react-x11 does all of it
|
|
23
|
+
with no configuration:
|
|
44
24
|
|
|
45
25
|
```jsx
|
|
46
26
|
const root = await createRoot(); // that is the whole of it
|
|
47
27
|
```
|
|
48
28
|
|
|
49
|
-
`DESKTOP_STARTUP_ID` is read from the environment and **removed from it
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
that gets this wrong produces that same bug.
|
|
54
|
-
|
|
55
|
-
With no id in the environment — a terminal, CI, XQuartz — nothing is set and
|
|
56
|
-
nothing is sent.
|
|
29
|
+
`DESKTOP_STARTUP_ID` is read from the environment and **removed from it**: the
|
|
30
|
+
variable names one launch, and a child process that inherited it would claim
|
|
31
|
+
the parent's sequence and end it early. With no id in the environment — a
|
|
32
|
+
terminal, CI, XQuartz — nothing is set and nothing is sent.
|
|
57
33
|
|
|
58
34
|
### When is an app "started"?
|
|
59
35
|
|
|
60
|
-
The default is **the first frame that actually painted**.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
a mapped window is an empty rectangle, and stopping the busy cursor there is
|
|
66
|
-
compliant and dishonest — it says "ready" over a blank window.
|
|
67
|
-
|
|
68
|
-
A suspense fallback counts as painted, and should. If the first frame is a
|
|
69
|
-
spinner because the tree is waiting on data, that is exactly the right
|
|
70
|
-
moment to stop the _system's_ spinner: the app is up and is telling the user
|
|
71
|
-
what it is doing. There is no signal for "finished loading", and guessing at
|
|
72
|
-
one is how this ends back at fifteen seconds.
|
|
36
|
+
The default is **the first frame that actually painted**. A window maps a
|
|
37
|
+
frame before it draws, so ending the sequence on map (what GTK does) would say
|
|
38
|
+
"ready" over a blank window. A suspense fallback counts as painted: the app is
|
|
39
|
+
up and telling the user what it is doing, and there is no signal for "finished
|
|
40
|
+
loading".
|
|
73
41
|
|
|
74
42
|
`completeOn` is there for apps the default does not suit:
|
|
75
43
|
|
|
@@ -96,10 +64,8 @@ notifyStartupComplete(); // idempotent, and a no-op if there is no sequence
|
|
|
96
64
|
```
|
|
97
65
|
|
|
98
66
|
**A backstop ends the sequence regardless**, ten seconds after the window
|
|
99
|
-
maps, whichever mode is in force
|
|
100
|
-
|
|
101
|
-
reproducing the bug it exists to fix. Ten is chosen to beat mutter's fifteen
|
|
102
|
-
by a margin while being far longer than any honest first frame.
|
|
67
|
+
maps, whichever mode is in force, so an app that never paints or forgets to
|
|
68
|
+
call cannot leave the cursor spinning.
|
|
103
69
|
|
|
104
70
|
### Turning it off, and supplying an id
|
|
105
71
|
|
|
@@ -109,9 +75,8 @@ await createRoot({ startupNotification: 'launcher/app/1-0_TIME9876' });
|
|
|
109
75
|
```
|
|
110
76
|
|
|
111
77
|
`false` is for an app that runs its own sequence, or an embedder that owns
|
|
112
|
-
the toplevel.
|
|
113
|
-
|
|
114
|
-
read and yours to clear.
|
|
78
|
+
the toplevel. Opting out leaves `DESKTOP_STARTUP_ID` in the environment — if
|
|
79
|
+
you manage the sequence yourself, the id is yours to read and to clear.
|
|
115
80
|
|
|
116
81
|
A string supplies the id for a launch where it did not arrive in the
|
|
117
82
|
environment. A D-Bus-activated app gets it in `platform_data` instead.
|
|
@@ -125,55 +90,50 @@ const when = launchTimestamp(); // number | null
|
|
|
125
90
|
```
|
|
126
91
|
|
|
127
92
|
The X server timestamp of the user action that launched the app, parsed from
|
|
128
|
-
the id's `_TIME` suffix
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Do not substitute `0` for a missing one — EWMH gives zero its own meaning,
|
|
133
|
-
"do not focus this window when it maps".
|
|
93
|
+
the id's `_TIME` suffix — the "when" any later request to come forward is
|
|
94
|
+
weighed against. **`null` is a real answer**, not a failure: an app started
|
|
95
|
+
from a shell has no launch timestamp. Do not substitute `0` for a missing one;
|
|
96
|
+
EWMH reads zero as "do not focus this window when it maps".
|
|
134
97
|
|
|
135
98
|
### On macOS
|
|
136
99
|
|
|
137
|
-
quartz-wm implements none of this
|
|
138
|
-
|
|
139
|
-
|
|
100
|
+
XQuartz's quartz-wm implements none of this: the messages go to a root window
|
|
101
|
+
nobody listens at and `_NET_WM_USER_TIME` is ignored. Harmless, and not a sign
|
|
102
|
+
the feature is broken.
|
|
140
103
|
|
|
141
104
|
### What is deliberately not here
|
|
142
105
|
|
|
143
|
-
- **The launcher half
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this. Keeping it current on every keypress is a separate design with a
|
|
148
|
-
real cost — EWMH is explicit that storing a frequently-changing property
|
|
149
|
-
on the toplevel wakes every client watching that window, which is what
|
|
150
|
-
`_NET_WM_USER_TIME_WINDOW` exists to avoid.
|
|
106
|
+
- **The launcher half** — an app that spawns _another_ app manages that
|
|
107
|
+
child's sequence itself.
|
|
108
|
+
- **Ongoing `_NET_WM_USER_TIME` maintenance** — it is set once, at launch, not
|
|
109
|
+
kept current on every keypress.
|
|
151
110
|
|
|
152
111
|
### Security
|
|
153
112
|
|
|
154
113
|
The id is broadcast to the root window, so every client on the display sees
|
|
155
|
-
it
|
|
156
|
-
|
|
157
|
-
messages carry the id the launcher gave us and nothing invented, and the
|
|
158
|
-
reason the variable's value is never logged.
|
|
114
|
+
it — X11's usual lack of isolation ([security.md](security.md)). The messages
|
|
115
|
+
carry only the id the launcher gave, and its value is never logged.
|
|
159
116
|
|
|
160
117
|
## The launcher: a badge, attention, and the Dock menu
|
|
161
118
|
|
|
162
|
-
The
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
|
168
|
-
|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
| **
|
|
172
|
-
| **
|
|
173
|
-
|
|
|
174
|
-
| **
|
|
175
|
-
|
|
176
|
-
|
|
119
|
+
The marks the user reads on the app's icon without opening it. Each exists on
|
|
120
|
+
a backend only where that backend has a mechanism for it:
|
|
121
|
+
|
|
122
|
+
| | Linux (`launcherentry`) | macOS (`cocoa`) | Windows (`taskbar`) |
|
|
123
|
+
| --------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
|
|
124
|
+
| a **badge** | `com.canonical.Unity.LauncherEntry` | `NSDockTile.badgeLabel` | an overlay icon with the label drawn into it |
|
|
125
|
+
| badge **text** | — the protocol carries a count and nothing else | any label | up to three glyphs; longer clamps to `99+`, ~16px on screen |
|
|
126
|
+
| **progress** | `useProgress(0…1)` — a bar across the tile | — `NSDockTile` has none | `ITaskbarList3::SetProgressValue`, across the taskbar button |
|
|
127
|
+
| **attention** | `states={['demands_attention']}`, or `setUrgent()` for the launcher entry itself | the same prop: the Dock icon bounces until activated | `FlashWindowEx` |
|
|
128
|
+
| the **launcher menu** | `useLauncherMenu(items)` — the launcher protocol's quicklist | `useLauncherMenu(items)` | — the jump list instead (below) |
|
|
129
|
+
| **tasks** | — `Actions=` in the `.desktop` file, not driven from here | — | `useJumpList(tasks)` — the jump list's Tasks category |
|
|
130
|
+
| a **hover toolbar** | — | — | `useThumbnailToolbar(buttons)` — up to seven, under the preview |
|
|
131
|
+
| **recent documents** | — | — the platform has one; this backend does not use it | `useRecentDocument(path)` — the shell's Recent lists |
|
|
132
|
+
|
|
133
|
+
On Linux, KDE, elementary and Cairo-Dock show the badge; GNOME needs an
|
|
134
|
+
extension. On Windows the taskbar button's menu is the jump list, whose entries
|
|
135
|
+
start a _new_ process, so it is `features.tasks` and `useJumpList` rather than
|
|
136
|
+
`useLauncherMenu` ([windows-integrations.md](windows-integrations.md)).
|
|
177
137
|
|
|
178
138
|
### The badge
|
|
179
139
|
|
|
@@ -184,22 +144,18 @@ useBadge(unread); // a count: 3 shows "3", 0 clears
|
|
|
184
144
|
await setBadge(null); // the imperative twin, for code with no component
|
|
185
145
|
```
|
|
186
146
|
|
|
187
|
-
A badge is a **count**,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
`
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
count to and `setBadge` resolves `false`. The entry stays on the bus while
|
|
200
|
-
a badge is shown and is released when it is cleared. A launcher that starts
|
|
201
|
-
after the badge was set will not see it until the next `setBadge`; the
|
|
202
|
-
protocol has no query.
|
|
147
|
+
A badge is a **count**, the one shape both desktops show. A string (`'•'`,
|
|
148
|
+
`'!'`) shows on macOS, where the tile takes any label, and as a count of
|
|
149
|
+
nothing on Linux, whose protocol has no text — pass one only where the Mac is
|
|
150
|
+
the audience. `0`, `null` and `''` all clear it. `useBadge` clears on unmount;
|
|
151
|
+
`setBadge` resolves to whether a launcher was told and never rejects for
|
|
152
|
+
anything about the machine.
|
|
153
|
+
|
|
154
|
+
On Linux the launcher pins the count to the app's `.desktop` file, so it needs
|
|
155
|
+
the identity [`registerApplication({ appId })`](uri-schemes.md) establishes
|
|
156
|
+
and a `.desktop` file of that name; without them `setBadge` resolves `false`.
|
|
157
|
+
A launcher that starts after the badge was set does not see it until the next
|
|
158
|
+
`setBadge`.
|
|
203
159
|
|
|
204
160
|
### Progress
|
|
205
161
|
|
|
@@ -211,25 +167,22 @@ await setProgress(null); // the imperative twin
|
|
|
211
167
|
```
|
|
212
168
|
|
|
213
169
|
A bar drawn across the app's icon, for a download, an export, a long build.
|
|
214
|
-
Out-of-range values are clamped rather than refused
|
|
215
|
-
|
|
170
|
+
Out-of-range values are clamped rather than refused, so a total that briefly
|
|
171
|
+
went to zero does not throw.
|
|
216
172
|
|
|
217
|
-
**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
honest answer to branch on.
|
|
173
|
+
**Not on macOS:** `NSDockTile` has no progress bar; an app that wants one
|
|
174
|
+
there draws it into a custom tile view itself. Branch on
|
|
175
|
+
`useDesktopCapability('launcher').features.progress`.
|
|
221
176
|
|
|
222
177
|
### Attention
|
|
223
178
|
|
|
224
|
-
Nothing new to call: `<window states={['demands_attention']}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
and miniaturize there (@windowkit/appkit 0.30.0), and the other `states`
|
|
232
|
-
names are inert on that backend.
|
|
179
|
+
Nothing new to call: `<window states={['demands_attention']}>`. On the cocoa
|
|
180
|
+
backend it is `NSApp.requestUserAttention` — the Dock icon bounces until the
|
|
181
|
+
user activates the app, and stops when the prop drops the state or the window
|
|
182
|
+
goes away. AppKit ignores the request while the app is already active, as a
|
|
183
|
+
window manager ignores a focused window's urgency hint. There, `fullscreen`,
|
|
184
|
+
`maximized` and `hidden` are AppKit's full-screen Space, zoom and miniaturize,
|
|
185
|
+
and the other `states` names are inert.
|
|
233
186
|
|
|
234
187
|
### The Dock menu
|
|
235
188
|
|
|
@@ -244,25 +197,18 @@ useLauncherMenu([
|
|
|
244
197
|
]);
|
|
245
198
|
```
|
|
246
199
|
|
|
247
|
-
The same `items` vocabulary `MenuBar` and `ContextMenu`
|
|
248
|
-
names](globalmenu.md)), so
|
|
249
|
-
|
|
250
|
-
replaced when `items` changes, taken down on unmount.
|
|
251
|
-
|
|
252
|
-
`NSDockTile`'s menu on the cocoa backend; on Linux it is the launcher
|
|
253
|
-
protocol's **quicklist** — an object path to a `com.canonical.dbusmenu` tree,
|
|
254
|
-
which is the same menu protocol the tray and the global menu already speak.
|
|
255
|
-
ubuntu-dock/Dash-to-Dock, Plank and the Unity-heritage launchers render it.
|
|
256
|
-
|
|
257
|
-
> An earlier version of this page said the freedesktop counterpart was
|
|
258
|
-
> `Actions=` in the `.desktop` file, an install step rather than runtime code.
|
|
259
|
-
> That is a different feature wearing a similar hat: desktop actions are the
|
|
260
|
-
> right place for entries that must work **while the app is not running**, and
|
|
261
|
-
> the quicklist is the right place for a menu that follows the app's state.
|
|
200
|
+
The same `items` vocabulary as `MenuBar` and `ContextMenu` ([menuitem
|
|
201
|
+
names](globalmenu.md)), so the window's, the panel's, the tray's and the
|
|
202
|
+
Dock's menus are one authoring model. Installed while the component is
|
|
203
|
+
mounted, replaced when `items` changes, taken down on unmount.
|
|
262
204
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
205
|
+
On the cocoa backend it is `NSDockTile`'s menu; on Linux, the launcher
|
|
206
|
+
protocol's **quicklist**, which ubuntu-dock/Dash-to-Dock, Plank and the
|
|
207
|
+
Unity-heritage launchers render. Entries that must work **while the app is not
|
|
208
|
+
running** belong in `Actions=` in the `.desktop` file instead; the quicklist
|
|
209
|
+
is for a menu that follows the app's state. Like the badge, it needs
|
|
210
|
+
[`registerApplication({ appId })`](uri-schemes.md) and a `.desktop` file of
|
|
211
|
+
that name.
|
|
266
212
|
|
|
267
213
|
### The app's presence
|
|
268
214
|
|
|
@@ -278,11 +224,10 @@ await createRoot({
|
|
|
278
224
|
});
|
|
279
225
|
```
|
|
280
226
|
|
|
281
|
-
`activationPolicy` is
|
|
282
|
-
|
|
283
|
-
already have flashed its icon
|
|
284
|
-
|
|
285
|
-
`Info.plist` wins, as it should.
|
|
227
|
+
`activationPolicy` is a root option rather than a hook because it is fixed
|
|
228
|
+
before the app finishes launching — an agent app that switched afterwards
|
|
229
|
+
would already have flashed its Dock icon. `appName` renames LaunchServices'
|
|
230
|
+
record of an unbundled process; a bundle's `Info.plist` wins.
|
|
286
231
|
|
|
287
232
|
## The tray
|
|
288
233
|
|
|
@@ -302,15 +247,13 @@ const { available, backend, features, error } = useTray({
|
|
|
302
247
|
|
|
303
248
|
An icon in the system tray for as long as the component is mounted. With
|
|
304
249
|
`menu` a click opens it — the same `items` vocabulary as `MenuBar` and the
|
|
305
|
-
Dock menu
|
|
306
|
-
|
|
307
|
-
the
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Several trays coexist: each is its own item.
|
|
250
|
+
Dock menu. Without one, `onClick` gets the button, where the click was and,
|
|
251
|
+
where the backend knows it, the item's screen rect — all in logical screen
|
|
252
|
+
pixels, the unit a `<popup>`'s `x`/`y` are in, and what a popover under the
|
|
253
|
+
item anchors to ([elements.md](elements.md#a-popover-under-a-tray-item)).
|
|
254
|
+
`title` shows text beside the icon or alone; `visible`, `tooltip`, `attention`
|
|
255
|
+
and the rest follow their values while mounted, and the item is removed on
|
|
256
|
+
unmount. `null` means no item. Several trays coexist: each is its own item.
|
|
314
257
|
|
|
315
258
|
| | Linux (`statusnotifier`) | macOS (`cocoa`) | Windows (`shellnotifyicon`) |
|
|
316
259
|
| --------------- | --------------------------------------- | ---------------------------- | --------------------------- |
|
|
@@ -324,46 +267,41 @@ Several trays coexist: each is its own item.
|
|
|
324
267
|
| click modifiers | — | shift/control/option/command | — |
|
|
325
268
|
| scroll | `Scroll(delta, orientation)` | — | — |
|
|
326
269
|
|
|
327
|
-
**On Linux**
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
**On the cocoa backend** this is `NSStatusItem`, the menu-bar extra. A
|
|
347
|
-
menu-bar app that wants no Dock tile pairs it with
|
|
270
|
+
**On Linux** it is D-Bus only, so it works identically under X11, XWayland and
|
|
271
|
+
Wayland. It needs something _hosting_ a tray: KDE Plasma and most panels do
|
|
272
|
+
out of the box; GNOME needs an AppIndicator extension. A stock GNOME session
|
|
273
|
+
with no extension has no tray, `available` is `false`, and nothing is logged —
|
|
274
|
+
that is a configuration, not a fault.
|
|
275
|
+
|
|
276
|
+
**A click's position on Linux** is in a unit the protocol never names: Plasma
|
|
277
|
+
and GNOME's physical layout send device pixels; GNOME's logical layout (the
|
|
278
|
+
default from GNOME 50), xfce4-panel, LXQt and Cinnamon send logical ones. The
|
|
279
|
+
point is read both ways: a reading on no monitor is dropped, then the one the
|
|
280
|
+
pointer is on wins, else the numbers are taken as sent. One case is misread —
|
|
281
|
+
a device-pixel host under XWayland whose tray, read as logical pixels, still
|
|
282
|
+
lands on a monitor (the top-left quarter of the screen, or with another
|
|
283
|
+
monitor beyond it): there the point is `scale` times as far from the screen's
|
|
284
|
+
origin as the click was.
|
|
285
|
+
|
|
286
|
+
**On the cocoa backend** it is `NSStatusItem`, the menu-bar extra. A menu-bar
|
|
287
|
+
app that wants no Dock tile pairs it with
|
|
348
288
|
`createRoot({ cocoa: { activationPolicy: 'accessory' } })` above. A PNG icon
|
|
349
|
-
is drawn as a **template** — its shape in the bar's ink,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
written inline.
|
|
289
|
+
is drawn as a **template** — its shape in the bar's ink, following light and
|
|
290
|
+
dark like a symbol; `template: false` keeps its colours. The menu is rebuilt
|
|
291
|
+
only when its items change, so a title that ticks every second leaves it
|
|
292
|
+
alone, whether `menu` is memoized or written inline.
|
|
354
293
|
|
|
355
294
|
### What the hook hands back
|
|
356
295
|
|
|
357
|
-
`available` **settles
|
|
296
|
+
`available` **settles**: finding a freedesktop tray takes a bus round trip, so
|
|
358
297
|
it is `false` on the first frame and `true` a tick later if a host is there —
|
|
359
|
-
render the fallback first and upgrade
|
|
360
|
-
|
|
298
|
+
render the fallback first and upgrade. It follows the host for the life of the
|
|
299
|
+
item, so a panel that exits flips it back.
|
|
361
300
|
|
|
362
|
-
**`settled` says whether `available` is an answer yet.** For
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
answer instead:
|
|
301
|
+
**`settled` says whether `available` is an answer yet.** For an app whose
|
|
302
|
+
whole UI is its tray item, the fallback _is_ a window, and rendering it first
|
|
303
|
+
flashes a window up and away on every start on a desktop that has a tray. So
|
|
304
|
+
wait for the answer instead:
|
|
367
305
|
|
|
368
306
|
```jsx
|
|
369
307
|
const tray = useTray({ icon: 'waveform', onClick: open });
|
|
@@ -386,22 +324,18 @@ uses, for the rung that answered. Read it instead of testing the platform:
|
|
|
386
324
|
}
|
|
387
325
|
```
|
|
388
326
|
|
|
389
|
-
`error`
|
|
390
|
-
a tray and it refused me"
|
|
391
|
-
fix.
|
|
392
|
-
puts the reason there.
|
|
327
|
+
`error` separates **"this desktop has no tray"** (`error` null) from **"there
|
|
328
|
+
is a tray and it refused me"** (the reason is there) — only the second has a
|
|
329
|
+
fix.
|
|
393
330
|
|
|
394
331
|
## Feature discovery
|
|
395
332
|
|
|
396
333
|
Everything on this page is a _ladder_, and which rung a machine lands on
|
|
397
|
-
changes what the app may do. `available` alone is not enough
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
buttons and no callback, silently.
|
|
403
|
-
|
|
404
|
-
So the unit of discovery is a **feature set**, not a flag:
|
|
334
|
+
changes what the app may do. `available` alone is not enough: a notification
|
|
335
|
+
daemon with `actions` gives a banner with buttons that call back, and
|
|
336
|
+
`notify-send` gives one-way text — an app that posts reply buttons on the
|
|
337
|
+
second gets a banner with no buttons, silently. So the unit of discovery is a
|
|
338
|
+
**feature set**, not a flag:
|
|
405
339
|
|
|
406
340
|
```jsx
|
|
407
341
|
import { useDesktopCapability } from 'react-x11';
|
|
@@ -413,17 +347,11 @@ if (notifications.available) return <BannerThenOpenApp />;
|
|
|
413
347
|
return <InAppToastOnly />;
|
|
414
348
|
```
|
|
415
349
|
|
|
416
|
-
`{ available, backend, features, settled }`. `backend` names the
|
|
417
|
-
`'dbus'`, `'cocoa'`, `'statusnotifier'`, `'launcherentry'`,
|
|
418
|
-
`'osascript'`, `'win32'`, `'shellnotifyicon'`, `'taskbar'` —
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
A backend **declares** its mechanism rather than having one inferred from it,
|
|
423
|
-
because backends install the same method names for the same rung on purpose —
|
|
424
|
-
that sharing is what makes `useTray` one hook — so there is nothing in the
|
|
425
|
-
methods to tell two of them apart. Branch on `features`; `backend` is for logs
|
|
426
|
-
and bug reports.
|
|
350
|
+
`{ available, backend, features, settled }`. `backend` names the
|
|
351
|
+
**mechanism** — `'dbus'`, `'cocoa'`, `'statusnotifier'`, `'launcherentry'`,
|
|
352
|
+
`'notify-send'`, `'osascript'`, `'win32'`, `'shellnotifyicon'`, `'taskbar'` —
|
|
353
|
+
never the platform. Branch on `features`; `backend` is for logs and bug
|
|
354
|
+
reports.
|
|
427
355
|
|
|
428
356
|
`settled` is `false` until the first probe answers and `true` from then on, so
|
|
429
357
|
an app can hold its fallback back rather than flash it; it is `true` on the
|
|
@@ -448,37 +376,32 @@ the same probe, so they cannot disagree.
|
|
|
448
376
|
|
|
449
377
|
### Degrading well
|
|
450
378
|
|
|
451
|
-
Three rules, in the order they matter:
|
|
452
|
-
|
|
453
379
|
1. **Render the fallback first.** Every desktop answer takes a round trip, so
|
|
454
380
|
the first frame has none. Upgrading a fallback is invisible; downgrading a
|
|
455
381
|
feature the user already reached for is not.
|
|
456
382
|
2. **Never disable a feature permanently on a `false`.** A panel restarts, an
|
|
457
|
-
extension is enabled, a daemon is installed. The hooks re-probe
|
|
458
|
-
|
|
459
|
-
answer would outlive every one of those fixes.
|
|
383
|
+
extension is enabled, a daemon is installed. The hooks re-probe when a
|
|
384
|
+
service appears on the bus and nothing is cached — an app that latched the
|
|
385
|
+
first answer would outlive every one of those fixes.
|
|
460
386
|
3. **A missing desktop service is not an error.** No bus, no tray host, no
|
|
461
387
|
notification daemon and no launcher are all ordinary states of a healthy
|
|
462
|
-
machine. Nothing is logged and nothing throws
|
|
463
|
-
|
|
388
|
+
machine. Nothing is logged and nothing throws. `error` is for a service
|
|
389
|
+
that answered and then refused.
|
|
464
390
|
|
|
465
391
|
### All of it, in one app
|
|
466
392
|
|
|
467
393
|
`examples/desktop.jsx` (`npm run examples:desktop`) is a backup agent that
|
|
468
394
|
uses every hook on this page at once — the tray icon with its attention state
|
|
469
|
-
and
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
`badge.jsx` (the launcher), `tray.jsx` (the tray) and `notify.jsx` (the
|
|
476
|
-
notification).
|
|
395
|
+
and overlay, the badge, the progress bar, the quicklist, the urgency flag, and
|
|
396
|
+
a banner whose buttons are offered only where the daemon has `actions` — and
|
|
397
|
+
whose second pane renders `useDesktopCapability()` for each of `CAPABILITIES`
|
|
398
|
+
beside the live answers. Read it to see what an app will get on a machine you
|
|
399
|
+
do not have. The feature-sized examples are `badge.jsx` (the launcher),
|
|
400
|
+
`tray.jsx` (the tray) and `notify.jsx` (the notification).
|
|
477
401
|
|
|
478
402
|
## Turning the desktop off
|
|
479
403
|
|
|
480
|
-
Three things react-x11 turns on for you reach the **session bus
|
|
481
|
-
of them was asked for:
|
|
404
|
+
Three things react-x11 turns on for you reach the **session bus**:
|
|
482
405
|
|
|
483
406
|
| | | |
|
|
484
407
|
| ------------ | ----------------------------------------------------------------------- | ------------------------------------ |
|
|
@@ -486,17 +409,8 @@ of them was asked for:
|
|
|
486
409
|
| `a11y` | the AT-SPI bridge — whether a screen reader can see the app | [accessibility.md](accessibility.md) |
|
|
487
410
|
| `globalMenu` | a `MenuBar` handing its menu to the panel instead of drawing it | [globalmenu.md](globalmenu.md) |
|
|
488
411
|
|
|
489
|
-
Each is the right default
|
|
490
|
-
|
|
491
|
-
menu in the panel is what the panel is for. But sooner or later an embedder
|
|
492
|
-
owns the thing we assumed was ours, and until
|
|
493
|
-
[#417](https://github.com/sidorares/react-x11/issues/417) the only way out was
|
|
494
|
-
an **environment variable** — `NO_AT_BRIDGE=1`,
|
|
495
|
-
`REACT_X11_NO_GLOBAL_MENU=1`, or unsetting `DBUS_SESSION_BUS_ADDRESS`. That is
|
|
496
|
-
a seam an app cannot reach for itself: the environment is inherited, so
|
|
497
|
-
setting one before `createRoot()` sets it for every child process too, and the
|
|
498
|
-
D-Bus one takes the portals and the app's own exported services down with the
|
|
499
|
-
follower.
|
|
412
|
+
Each is the right default, but an embedder, a kiosk or a test may own the
|
|
413
|
+
thing they assume is theirs:
|
|
500
414
|
|
|
501
415
|
```jsx
|
|
502
416
|
await createRoot({ desktop: false }); // none of the three
|
|
@@ -504,134 +418,95 @@ await createRoot({ desktop: { appearance: false } }); // just that one
|
|
|
504
418
|
```
|
|
505
419
|
|
|
506
420
|
With all three off, **nothing dials the session bus at startup** — no
|
|
507
|
-
connection, no subprocess, no portal probe.
|
|
508
|
-
wants, and a kiosk, and a test that needs the same answer on every machine.
|
|
509
|
-
|
|
510
|
-
Turning `appearance` off means the app draws in its own palette rather than
|
|
511
|
-
the desktop's: `useSystemAppearance()` reports `'no-preference'` with
|
|
512
|
-
`source: null`, and the remembered answer on disk is not read either — an
|
|
513
|
-
opted-out app that started in whatever colours the machine was in last time
|
|
514
|
-
would be the opposite of what the switch is for.
|
|
421
|
+
connection, no subprocess, no portal probe.
|
|
515
422
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
feature already started stays started — pass this on the first root.
|
|
423
|
+
Prefer this to the environment variables that do the same — `NO_AT_BRIDGE=1`,
|
|
424
|
+
`REACT_X11_NO_GLOBAL_MENU=1`, or unsetting `DBUS_SESSION_BUS_ADDRESS`: the
|
|
425
|
+
environment is inherited, so they reach every child process too, and the
|
|
426
|
+
D-Bus one also takes down the portals and the app's own exported services.
|
|
521
427
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
when it did not.
|
|
428
|
+
Turning `appearance` off means the app draws in its own palette:
|
|
429
|
+
`useSystemAppearance()` reports `'no-preference'` with `source: null`, and the
|
|
430
|
+
answer remembered on disk is not read either.
|
|
526
431
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
in front of the X handshake, the layout engine's WebAssembly and the first
|
|
532
|
-
paint, all so the accessibility bridge could find out there was nothing to
|
|
533
|
-
connect to.
|
|
432
|
+
**Off is process-wide, and it latches.** There is one D-Bus identity and one
|
|
433
|
+
AT-SPI bridge per process, so the policy is not per-root: nothing turns an
|
|
434
|
+
integration back on, and a feature already started stays started — pass this
|
|
435
|
+
on the first root.
|
|
534
436
|
|
|
535
|
-
|
|
536
|
-
and hands `dbus-native` a resolved `unix:path=…`. The same wall clock is
|
|
537
|
-
spent, none of it on the loop: the handshake and the layout engine run
|
|
538
|
-
straight through it. There is nothing to configure — it is the same address,
|
|
539
|
-
found the same way, off the critical path.
|
|
437
|
+
## Not blocking the first frame on the bus
|
|
540
438
|
|
|
541
|
-
|
|
542
|
-
|
|
439
|
+
On macOS the session bus address comes from launchd rather than the
|
|
440
|
+
environment; react-x11 looks it up once, asynchronously, so it costs the event
|
|
441
|
+
loop and the first frame nothing. There is nothing to configure. On Linux
|
|
442
|
+
`$DBUS_SESSION_BUS_ADDRESS` or `$XDG_RUNTIME_DIR/bus` answers directly.
|
|
443
|
+
Design notes: [design/desktop.md](../design/desktop.md#not-blocking-the-first-frame-on-the-bus).
|
|
543
444
|
|
|
544
445
|
## Password fields and password managers
|
|
545
446
|
|
|
546
|
-
There is **no password-field protocol on the Linux desktop
|
|
547
|
-
publishes "this is a password field, fill it", and no manager asks.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
they turn out to be.
|
|
447
|
+
There is **no password-field protocol on the Linux desktop**: no toolkit
|
|
448
|
+
publishes "this is a password field, fill it", and no manager asks. There are
|
|
449
|
+
four seams instead, which an application either supports or does not.
|
|
450
|
+
`PasswordInput` supports the two that reach a field.
|
|
451
|
+
How the managers do it: [design/desktop.md](../design/desktop.md#password-fields-and-password-managers).
|
|
552
452
|
|
|
553
453
|
### 1. Typing — XTEST auto-type, and the keymap race
|
|
554
454
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
the keystrokes. Its X11 backend is the whole of the story — `SendKeyEvent()`
|
|
558
|
-
sends `XTestFakeKeyEvent()`, and for a character the current layout cannot
|
|
559
|
-
type, `RemapKeycode()` writes the keysym into a spare keycode with
|
|
560
|
-
`XkbSetMap()` first, `XSync`ing before it types.
|
|
455
|
+
Most desktop managers **auto-type**: KeePassXC matches the focused window's
|
|
456
|
+
**title** against its entries and synthesises the keystrokes over XTEST.
|
|
561
457
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
- **A faked key is an ordinary key.** XTEST events are delivered by the
|
|
565
|
-
server through the normal event path, so `PasswordInput` — and every other
|
|
458
|
+
- **A faked key is an ordinary key.** `PasswordInput` — and every other
|
|
566
459
|
focusable node — cannot tell auto-type from a person, and nothing has to be
|
|
567
|
-
done to support it.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
but the refetch is a round trip, and a manager that remaps, syncs and types
|
|
578
|
-
immediately can land its key before the reply does. The window is small and
|
|
579
|
-
only affects characters outside the user's layout, but a password of ASCII
|
|
580
|
-
is not the case that fails. Nothing here can close it; the fix belongs where
|
|
581
|
-
the map lives.
|
|
460
|
+
done to support it. A sequence of `{USERNAME}{TAB}{PASSWORD}{ENTER}` walks a
|
|
461
|
+
react-x11 form because Tab moves focus and Enter reaches `onSubmit`.
|
|
462
|
+
- **The window needs a title worth matching.** `<window title="…">` is the
|
|
463
|
+
integration surface: a window titled after the document with nothing
|
|
464
|
+
identifying the app is one a user cannot write an auto-type rule for. Set
|
|
465
|
+
`appId` too, for managers that match on it.
|
|
466
|
+
- **Characters outside the user's layout can race.** A manager types them by
|
|
467
|
+
remapping a spare keycode first; one that types immediately after remapping
|
|
468
|
+
can land the key before react-x11 has refetched the keymap. ASCII passwords
|
|
469
|
+
on an ASCII layout are not affected, and nothing on this side can close it.
|
|
582
470
|
|
|
583
471
|
### 2. Pasting — the clipboard, and the hint that keeps it out of history
|
|
584
472
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
Shift+Insert, and strips control characters so a manager's trailing newline
|
|
589
|
-
does not end up inside the secret.
|
|
473
|
+
Copy-to-clipboard is ordinary `CLIPBOARD` interop ([clipboard.md](clipboard.md)):
|
|
474
|
+
`PasswordInput` takes Ctrl+V and Shift+Insert, and strips control characters
|
|
475
|
+
so a manager's trailing newline does not end up inside the secret.
|
|
590
476
|
|
|
591
477
|
If your app ever puts a secret **on** the clipboard — this widget never does
|
|
592
478
|
— offer `x-kde-passwordManagerHint` with the value `secret` beside the text.
|
|
593
479
|
Klipper drops such an offer from its history, KDE Connect refuses to forward
|
|
594
|
-
it, and `wl-clipboard` marks
|
|
595
|
-
|
|
596
|
-
|
|
480
|
+
it, and `wl-clipboard` marks it sensitive. It is a convention rather than a
|
|
481
|
+
specification, and the only thing between a copied password and a clipboard
|
|
482
|
+
manager's on-disk history:
|
|
597
483
|
|
|
598
484
|
```js
|
|
599
485
|
clipboard.write({ UTF8_STRING: secret, 'x-kde-passwordManagerHint': 'secret' });
|
|
600
486
|
```
|
|
601
487
|
|
|
602
|
-
`PasswordInput`
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
can be spent by accident.
|
|
488
|
+
`PasswordInput` never takes the **PRIMARY** selection, which a `<textinput>`
|
|
489
|
+
does on every selection: PRIMARY is pasted by a middle click in any window on
|
|
490
|
+
the display.
|
|
606
491
|
|
|
607
492
|
### 3. Fetching it yourself — the Secret Service
|
|
608
493
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
Inside a sandbox that name is not there. Flatpak's answer is
|
|
619
|
-
`org.freedesktop.portal.Secret`, whose `RetrieveSecret` hands the app a
|
|
620
|
-
per-application master secret down a pipe; libsecret switches to it
|
|
621
|
-
automatically and encrypts a local store with it.
|
|
494
|
+
An app with an account of its own to unlock needs no field at all: the
|
|
495
|
+
**Secret Service** D-Bus API, `org.freedesktop.secrets` (gnome-keyring,
|
|
496
|
+
KWallet, KeePassXC), is reachable through `useSessionBus()`
|
|
497
|
+
([dbus.md](dbus.md)) — search by attributes, unlock the collection if locked,
|
|
498
|
+
read the secret back. react-x11 has no wrapper for it. Inside a Flatpak
|
|
499
|
+
sandbox that name is absent; `org.freedesktop.portal.Secret`'s
|
|
500
|
+
`RetrieveSecret` hands the app a per-application master secret instead, which
|
|
501
|
+
libsecret uses automatically to encrypt a local store.
|
|
622
502
|
|
|
623
503
|
### 4. Reading the field — AT-SPI, which does not mark it yet
|
|
624
504
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
([accessibility.md](accessibility.md)), but `PasswordInput` is a plain
|
|
631
|
-
`role="textbox"` in it and nothing maps a node to `password text`
|
|
632
|
-
(`src/a11y.js` declares the role and no node takes it), so this seam is still
|
|
633
|
-
closed — worth opening as a role in the shared model rather than as a
|
|
634
|
-
password-shaped hole beside it.
|
|
505
|
+
AT-SPI2 gives a password entry the role `password text`, which is how a screen
|
|
506
|
+
reader knows to announce "bullet" and how some automation finds the field.
|
|
507
|
+
react-x11 publishes an accessibility tree ([accessibility.md](accessibility.md)),
|
|
508
|
+
but `PasswordInput` is a plain `role="textbox"` in it, so this seam is still
|
|
509
|
+
closed.
|
|
635
510
|
|
|
636
511
|
### What a field can still do wrong
|
|
637
512
|
|