Skip to content

Commit e05701f

Browse files
authored
Merge pull request #516 from jsenv/event_reaction
work
2 parents 96084ea + 20bfab2 commit e05701f

188 files changed

Lines changed: 26076 additions & 18621 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/backend/server/dist/client/route_inspector/route_inspector.html

Lines changed: 8 additions & 8 deletions
Large diffs are not rendered by default.

packages/backend/server/tests/internal_error/_internal_error.test.mjs/0_throw_error/0_throw_error.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal error while handling request
2020
Error: message
2121
at base/internal_error.test.mjs:34:19
2222
at capture (@jsenv/core/packages/tooling/snapshot/src/side_effects/create_capture_side_effects.js:342:29)
23-
at snapshotTests (@jsenv/core/packages/tooling/snapshot/src/side_effects/snapshot_tests.js:199:33)
23+
at snapshotTests (@jsenv/core/packages/tooling/snapshot/src/side_effects/snapshot_tests.js:203:33)
2424
500 Internal Server Error
2525
```
2626

packages/backend/server/tests/internal_error/_internal_error.test.mjs/0_throw_error/log_group.svg

Lines changed: 1 addition & 1 deletion
Loading

packages/frontend/dom/dist/jsenv_dom.js

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ const getEventLabel = (e) => {
420420
return e.type;
421421
}
422422
if (e.type === "keydown") {
423-
const key = e.key === " " ? "space" : e.key.toLowerCase();
423+
const key = e.key === " " ? "space" : e.key?.toLowerCase();
424424
const modifiers = [];
425425
if (e.ctrlKey) {
426426
modifiers.push("ctrl");
@@ -507,6 +507,15 @@ const createIterableWeakSet = () => {
507507
};
508508
};
509509

510+
/**
511+
* Creates a simple publish/subscribe pair.
512+
*
513+
* @param {boolean} [clearOnPublish=false] - When true, all subscribers are removed after each publish call.
514+
* @returns {[publish: (...args: any[]) => any[], subscribe: (callback: Function) => () => void, clear: () => void]}
515+
* - `publish(...args)` — calls all subscribers with the given arguments and returns their return values.
516+
* - `subscribe(callback)` — registers a subscriber and returns an unsubscribe function.
517+
* - `clear()` — removes all subscribers without calling them.
518+
*/
510519
const createPubSub = (clearOnPublish = false) => {
511520
const callbackSet = new Set();
512521

@@ -4593,7 +4602,13 @@ const DEFAULT_BEHAVIORS = [
45934602
{
45944603
test: (el) => el.matches("input[type='radio'], input[type='checkbox']"),
45954604
keys: {
4596-
space: "activate",
4605+
space: (e) => {
4606+
if (e.target.type === "radio" && e.target.checked) {
4607+
// space on checked radio does nothing
4608+
return "";
4609+
}
4610+
return "activate";
4611+
},
45974612
enter: (e) => (e.target.form ? "form_submit" : ""),
45984613
arrowleft: "focus_nav",
45994614
arrowright: "focus_nav",
@@ -4609,17 +4624,20 @@ const DEFAULT_BEHAVIORS = [
46094624
keys: {
46104625
escape: (e) => {
46114626
if (e.target.type === "search") {
4627+
if (e.target.readOnly) {
4628+
return "";
4629+
}
46124630
return e.target.value ? "clear" : "";
46134631
}
46144632
return "";
46154633
},
46164634
enter: (e) => (e.target.form ? "form_submit" : ""),
4617-
arrowleft: "cursor_move",
4618-
arrowright: "cursor_move",
4619-
arrowup: "cursor_move",
4620-
arrowdown: "cursor_move",
4621-
home: "cursor_move",
4622-
end: "cursor_move",
4635+
arrowleft: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4636+
arrowright: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4637+
arrowup: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4638+
arrowdown: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4639+
home: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4640+
end: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
46234641
},
46244642
fallback: (e) => (isTypingIntent(e) ? "type" : undefined),
46254643
},
@@ -4658,27 +4676,27 @@ const DEFAULT_BEHAVIORS = [
46584676
),
46594677
keys: {
46604678
space: "activate",
4661-
enter: (e) => (e.target.form ? "form_submit" : ""),
4679+
enter: "activate",
46624680
arrowleft: "value_change",
46634681
arrowright: "value_change",
46644682
arrowup: "value_change",
46654683
arrowdown: "value_change",
46664684
},
46674685
},
46684686
{
4669-
// Color input: Space opens the color picker, Enter submits the form
4687+
// Color input: Space opens the color picker, Enter too
46704688
test: (el) => el.matches("input[type='color']"),
46714689
keys: {
46724690
space: "activate",
4673-
enter: (e) => (e.target.form ? "form_submit" : ""),
4691+
enter: "activate",
46744692
},
46754693
},
46764694
{
4677-
// File input: Space opens the picker, Enter submits the form
4695+
// File input: Space opens the picker, Enter too
46784696
test: (el) => el.matches("input[type='file']"),
46794697
keys: {
46804698
space: "activate",
4681-
enter: (e) => (e.target.form ? "form_submit" : ""),
4699+
enter: "activate",
46824700
},
46834701
},
46844702
{
@@ -4725,7 +4743,10 @@ const DEFAULT_BEHAVIORS = [
47254743
{
47264744
// SELECT: don't intercept anything while the dropdown may be open
47274745
test: (el) => el.tagName === "SELECT",
4728-
keys: {},
4746+
keys: {
4747+
space: "activate",
4748+
enter: "activate",
4749+
},
47294750
},
47304751
{
47314752
// Non-interactive elements: browser scrolls on Space and arrow keys
@@ -11556,9 +11577,19 @@ const visibleRectEffect = (
1155611577
* @param {HTMLElement} anchor - The anchor element to position against
1155711578
* @param {object} [options]
1155811579
* @param {string} [options.positionX="center"] - Preferred X placement, with viewport fallback.
11580+
* "to-the-left" — element.right = anchor.left (sits entirely to the left of anchor)
11581+
* "left-aligned" — element.left = anchor.left (left edges aligned)
11582+
* "center" — element centered horizontally over anchor (default)
11583+
* "right-aligned" — element.right = anchor.right (right edges aligned)
11584+
* "to-the-right" — element.left = anchor.right (sits entirely to the right of anchor)
1155911585
* @param {string} [options.positionY="below"] - Preferred Y placement, with viewport fallback.
11560-
* @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check.
11561-
* @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check.
11586+
* "above" — element.bottom = anchor.top (sits above, no overlap)
11587+
* "above-overlap" — element.bottom = anchor.bottom (sits above, overlapping anchor)
11588+
* "center" — element centered vertically over anchor
11589+
* "below-overlap" — element.top = anchor.top (sits below, overlapping anchor)
11590+
* "below" — element.top = anchor.bottom (sits below, no overlap) (default)
11591+
* @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check. Same values as positionX.
11592+
* @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check. Same values as positionY.
1156211593
* @param {number} [options.alignToViewportEdgeWhenAnchorNearEdge=0] - Snap to viewport left
1156311594
* edge when anchor is within this many px of the left edge and element is wider than anchor.
1156411595
* @param {number} [options.minLeft=0] - Minimum left coordinate (document-relative).

packages/frontend/dom/src/dom_events.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ const getEventLabel = (e) => {
240240
return e.type;
241241
}
242242
if (e.type === "keydown") {
243-
const key = e.key === " " ? "space" : e.key.toLowerCase();
243+
const key = e.key === " " ? "space" : e.key?.toLowerCase();
244244
const modifiers = [];
245245
if (e.ctrlKey) {
246246
modifiers.push("ctrl");

packages/frontend/dom/src/interaction/keyboard.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,13 @@ const DEFAULT_BEHAVIORS = [
102102
{
103103
test: (el) => el.matches("input[type='radio'], input[type='checkbox']"),
104104
keys: {
105-
space: "activate",
105+
space: (e) => {
106+
if (e.target.type === "radio" && e.target.checked) {
107+
// space on checked radio does nothing
108+
return "";
109+
}
110+
return "activate";
111+
},
106112
enter: (e) => (e.target.form ? "form_submit" : ""),
107113
arrowleft: "focus_nav",
108114
arrowright: "focus_nav",
@@ -118,17 +124,20 @@ const DEFAULT_BEHAVIORS = [
118124
keys: {
119125
escape: (e) => {
120126
if (e.target.type === "search") {
127+
if (e.target.readOnly) {
128+
return "";
129+
}
121130
return e.target.value ? "clear" : "";
122131
}
123132
return "";
124133
},
125134
enter: (e) => (e.target.form ? "form_submit" : ""),
126-
arrowleft: "cursor_move",
127-
arrowright: "cursor_move",
128-
arrowup: "cursor_move",
129-
arrowdown: "cursor_move",
130-
home: "cursor_move",
131-
end: "cursor_move",
135+
arrowleft: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
136+
arrowright: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
137+
arrowup: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
138+
arrowdown: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
139+
home: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
140+
end: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
132141
},
133142
fallback: (e) => (isTypingIntent(e) ? "type" : undefined),
134143
},
@@ -167,27 +176,27 @@ const DEFAULT_BEHAVIORS = [
167176
),
168177
keys: {
169178
space: "activate",
170-
enter: (e) => (e.target.form ? "form_submit" : ""),
179+
enter: "activate",
171180
arrowleft: "value_change",
172181
arrowright: "value_change",
173182
arrowup: "value_change",
174183
arrowdown: "value_change",
175184
},
176185
},
177186
{
178-
// Color input: Space opens the color picker, Enter submits the form
187+
// Color input: Space opens the color picker, Enter too
179188
test: (el) => el.matches("input[type='color']"),
180189
keys: {
181190
space: "activate",
182-
enter: (e) => (e.target.form ? "form_submit" : ""),
191+
enter: "activate",
183192
},
184193
},
185194
{
186-
// File input: Space opens the picker, Enter submits the form
195+
// File input: Space opens the picker, Enter too
187196
test: (el) => el.matches("input[type='file']"),
188197
keys: {
189198
space: "activate",
190-
enter: (e) => (e.target.form ? "form_submit" : ""),
199+
enter: "activate",
191200
},
192201
},
193202
{
@@ -234,7 +243,10 @@ const DEFAULT_BEHAVIORS = [
234243
{
235244
// SELECT: don't intercept anything while the dropdown may be open
236245
test: (el) => el.tagName === "SELECT",
237-
keys: {},
246+
keys: {
247+
space: "activate",
248+
enter: "activate",
249+
},
238250
},
239251
{
240252
// Non-interactive elements: browser scrolls on Space and arrow keys

packages/frontend/dom/src/position/visible_rect.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,9 +491,19 @@ export const visibleRectEffect = (
491491
* @param {HTMLElement} anchor - The anchor element to position against
492492
* @param {object} [options]
493493
* @param {string} [options.positionX="center"] - Preferred X placement, with viewport fallback.
494+
* "to-the-left" — element.right = anchor.left (sits entirely to the left of anchor)
495+
* "left-aligned" — element.left = anchor.left (left edges aligned)
496+
* "center" — element centered horizontally over anchor (default)
497+
* "right-aligned" — element.right = anchor.right (right edges aligned)
498+
* "to-the-right" — element.left = anchor.right (sits entirely to the right of anchor)
494499
* @param {string} [options.positionY="below"] - Preferred Y placement, with viewport fallback.
495-
* @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check.
496-
* @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check.
500+
* "above" — element.bottom = anchor.top (sits above, no overlap)
501+
* "above-overlap" — element.bottom = anchor.bottom (sits above, overlapping anchor)
502+
* "center" — element centered vertically over anchor
503+
* "below-overlap" — element.top = anchor.top (sits below, overlapping anchor)
504+
* "below" — element.top = anchor.bottom (sits below, no overlap) (default)
505+
* @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check. Same values as positionX.
506+
* @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check. Same values as positionY.
497507
* @param {number} [options.alignToViewportEdgeWhenAnchorNearEdge=0] - Snap to viewport left
498508
* edge when anchor is within this many px of the left edge and element is wider than anchor.
499509
* @param {number} [options.minLeft=0] - Minimum left coordinate (document-relative).

packages/frontend/dom/src/pub_sub.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
/**
2+
* Creates a simple publish/subscribe pair.
3+
*
4+
* @param {boolean} [clearOnPublish=false] - When true, all subscribers are removed after each publish call.
5+
* @returns {[publish: (...args: any[]) => any[], subscribe: (callback: Function) => () => void, clear: () => void]}
6+
* - `publish(...args)` — calls all subscribers with the given arguments and returns their return values.
7+
* - `subscribe(callback)` — registers a subscriber and returns an unsubscribe function.
8+
* - `clear()` — removes all subscribers without calling them.
9+
*/
110
export const createPubSub = (clearOnPublish = false) => {
211
const callbackSet = new Set();
312

0 commit comments

Comments
 (0)