mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
dispatch focusin/focusout events with relatedTarget
focus() and blur() now dispatch all four spec-required FocusEvents: blur (no bubble) → focusout (bubbles) → focus (no bubble) → focusin (bubbles) Each event carries the correct relatedTarget: the element gaining focus for blur/focusout, and the element losing focus for focus/focusin. All four events are composed per W3C spec. Relates to #1161
This commit is contained in:
@@ -81,6 +81,172 @@
|
||||
</script>
|
||||
|
||||
|
||||
<script id="focusin_focusout_events">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
const input2 = $('#input2');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let events = [];
|
||||
|
||||
input1.addEventListener('focus', () => events.push('focus1'));
|
||||
input1.addEventListener('focusin', () => events.push('focusin1'));
|
||||
input1.addEventListener('blur', () => events.push('blur1'));
|
||||
input1.addEventListener('focusout', () => events.push('focusout1'));
|
||||
input2.addEventListener('focus', () => events.push('focus2'));
|
||||
input2.addEventListener('focusin', () => events.push('focusin2'));
|
||||
|
||||
// Focus input1 — should fire focus then focusin
|
||||
input1.focus();
|
||||
testing.expectEqual('focus1,focusin1', events.join(','));
|
||||
|
||||
// Focus input2 — should fire blur, focusout on input1, then focus, focusin on input2
|
||||
events = [];
|
||||
input2.focus();
|
||||
testing.expectEqual('blur1,focusout1,focus2,focusin2', events.join(','));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focusin_bubbles">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let bodyFocusin = 0;
|
||||
let bodyFocus = 0;
|
||||
|
||||
document.body.addEventListener('focusin', () => bodyFocusin++);
|
||||
document.body.addEventListener('focus', () => bodyFocus++);
|
||||
|
||||
input1.focus();
|
||||
|
||||
// focusin should bubble to body, focus should not
|
||||
testing.expectEqual(1, bodyFocusin);
|
||||
testing.expectEqual(0, bodyFocus);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focusout_bubbles">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
|
||||
input1.focus();
|
||||
|
||||
let bodyFocusout = 0;
|
||||
let bodyBlur = 0;
|
||||
|
||||
document.body.addEventListener('focusout', () => bodyFocusout++);
|
||||
document.body.addEventListener('blur', () => bodyBlur++);
|
||||
|
||||
input1.blur();
|
||||
|
||||
// focusout should bubble to body, blur should not
|
||||
testing.expectEqual(1, bodyFocusout);
|
||||
testing.expectEqual(0, bodyBlur);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focus_relatedTarget">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
const input2 = $('#input2');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let focusRelated = null;
|
||||
let blurRelated = null;
|
||||
let focusinRelated = null;
|
||||
let focusoutRelated = null;
|
||||
|
||||
input1.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||
input1.addEventListener('focusout', (e) => { focusoutRelated = e.relatedTarget; });
|
||||
input2.addEventListener('focus', (e) => { focusRelated = e.relatedTarget; });
|
||||
input2.addEventListener('focusin', (e) => { focusinRelated = e.relatedTarget; });
|
||||
|
||||
input1.focus();
|
||||
input2.focus();
|
||||
|
||||
// blur/focusout on input1 should have relatedTarget = input2 (gaining focus)
|
||||
testing.expectEqual(input2, blurRelated);
|
||||
testing.expectEqual(input2, focusoutRelated);
|
||||
|
||||
// focus/focusin on input2 should have relatedTarget = input1 (losing focus)
|
||||
testing.expectEqual(input1, focusRelated);
|
||||
testing.expectEqual(input1, focusinRelated);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="blur_relatedTarget_null">
|
||||
{
|
||||
const btn = $('#btn1');
|
||||
|
||||
btn.focus();
|
||||
|
||||
let blurRelated = 'not_set';
|
||||
btn.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||
btn.blur();
|
||||
|
||||
// blur without moving to another element should have relatedTarget = null
|
||||
testing.expectEqual(null, blurRelated);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focus_event_properties">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
const input2 = $('#input2');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let focusEvent = null;
|
||||
let focusinEvent = null;
|
||||
let blurEvent = null;
|
||||
let focusoutEvent = null;
|
||||
|
||||
input1.addEventListener('blur', (e) => { blurEvent = e; });
|
||||
input1.addEventListener('focusout', (e) => { focusoutEvent = e; });
|
||||
input2.addEventListener('focus', (e) => { focusEvent = e; });
|
||||
input2.addEventListener('focusin', (e) => { focusinEvent = e; });
|
||||
|
||||
input1.focus();
|
||||
input2.focus();
|
||||
|
||||
// All four should be FocusEvent instances
|
||||
testing.expectEqual(true, blurEvent instanceof FocusEvent);
|
||||
testing.expectEqual(true, focusoutEvent instanceof FocusEvent);
|
||||
testing.expectEqual(true, focusEvent instanceof FocusEvent);
|
||||
testing.expectEqual(true, focusinEvent instanceof FocusEvent);
|
||||
|
||||
// All four should be composed per spec
|
||||
testing.expectEqual(true, blurEvent.composed);
|
||||
testing.expectEqual(true, focusoutEvent.composed);
|
||||
testing.expectEqual(true, focusEvent.composed);
|
||||
testing.expectEqual(true, focusinEvent.composed);
|
||||
|
||||
// None should be cancelable
|
||||
testing.expectEqual(false, blurEvent.cancelable);
|
||||
testing.expectEqual(false, focusoutEvent.cancelable);
|
||||
testing.expectEqual(false, focusEvent.cancelable);
|
||||
testing.expectEqual(false, focusinEvent.cancelable);
|
||||
|
||||
// blur/focus don't bubble, focusin/focusout do
|
||||
testing.expectEqual(false, blurEvent.bubbles);
|
||||
testing.expectEqual(true, focusoutEvent.bubbles);
|
||||
testing.expectEqual(false, focusEvent.bubbles);
|
||||
testing.expectEqual(true, focusinEvent.bubbles);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focus_disconnected">
|
||||
{
|
||||
const focused = document.activeElement;
|
||||
|
||||
@@ -775,25 +775,43 @@ pub fn remove(self: *Element, page: *Page) void {
|
||||
}
|
||||
|
||||
pub fn focus(self: *Element, page: *Page) !void {
|
||||
const Event = @import("Event.zig");
|
||||
const FocusEvent = @import("event/FocusEvent.zig");
|
||||
|
||||
// Capture relatedTarget before anything changes
|
||||
const old_related: ?*@import("EventTarget.zig") = if (page.document._active_element) |old| old.asEventTarget() else null;
|
||||
const new_target = self.asEventTarget();
|
||||
|
||||
if (page.document._active_element) |old| {
|
||||
if (old == self) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
|
||||
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(old.asEventTarget(), blur_event);
|
||||
const old_target = old.asEventTarget();
|
||||
|
||||
// Dispatch blur on old element (no bubble, composed)
|
||||
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true, .relatedTarget = new_target }, page);
|
||||
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
||||
|
||||
// Dispatch focusout on old element (bubbles, composed)
|
||||
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, page);
|
||||
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
||||
}
|
||||
|
||||
if (self.asNode().isConnected()) {
|
||||
page.document._active_element = self;
|
||||
}
|
||||
|
||||
const focus_event = try Event.initTrusted(comptime .wrap("focus"), null, page);
|
||||
defer if (!focus_event._v8_handoff) focus_event.deinit(false);
|
||||
try page._event_manager.dispatch(self.asEventTarget(), focus_event);
|
||||
// Dispatch focus on new element (no bubble, composed)
|
||||
const focus_event = try FocusEvent.initTrusted(comptime .wrap("focus"), .{ .composed = true, .relatedTarget = old_related }, page);
|
||||
defer if (!focus_event.asEvent()._v8_handoff) focus_event.deinit(false);
|
||||
try page._event_manager.dispatch(new_target, focus_event.asEvent());
|
||||
|
||||
// Dispatch focusin on new element (bubbles, composed)
|
||||
const focusin_event = try FocusEvent.initTrusted(comptime .wrap("focusin"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, page);
|
||||
defer if (!focusin_event.asEvent()._v8_handoff) focusin_event.deinit(false);
|
||||
try page._event_manager.dispatch(new_target, focusin_event.asEvent());
|
||||
}
|
||||
|
||||
pub fn blur(self: *Element, page: *Page) !void {
|
||||
@@ -801,10 +819,18 @@ pub fn blur(self: *Element, page: *Page) !void {
|
||||
|
||||
page.document._active_element = null;
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
|
||||
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(self.asEventTarget(), blur_event);
|
||||
const FocusEvent = @import("event/FocusEvent.zig");
|
||||
const old_target = self.asEventTarget();
|
||||
|
||||
// Dispatch blur (no bubble, composed)
|
||||
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true }, page);
|
||||
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
||||
|
||||
// Dispatch focusout (bubbles, composed)
|
||||
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true }, page);
|
||||
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
||||
}
|
||||
|
||||
pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const String = @import("../../../string.zig").String;
|
||||
const Page = @import("../../Page.zig");
|
||||
const js = @import("../../js/js.zig");
|
||||
@@ -38,23 +40,32 @@ pub const Options = Event.inheritOptions(
|
||||
FocusEventOptions,
|
||||
);
|
||||
|
||||
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*FocusEvent {
|
||||
const arena = try page.getArena(.{ .debug = "FocusEvent.trusted" });
|
||||
errdefer page.releaseArena(arena);
|
||||
return initWithTrusted(arena, typ, _opts, true, page);
|
||||
}
|
||||
|
||||
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*FocusEvent {
|
||||
const arena = try page.getArena(.{ .debug = "FocusEvent" });
|
||||
errdefer page.releaseArena(arena);
|
||||
const type_string = try String.init(arena, typ, .{});
|
||||
return initWithTrusted(arena, type_string, _opts, false, page);
|
||||
}
|
||||
|
||||
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*FocusEvent {
|
||||
const opts = _opts orelse Options{};
|
||||
|
||||
const event = try page._factory.uiEvent(
|
||||
arena,
|
||||
type_string,
|
||||
typ,
|
||||
FocusEvent{
|
||||
._proto = undefined,
|
||||
._related_target = opts.relatedTarget,
|
||||
},
|
||||
);
|
||||
|
||||
Event.populatePrototypes(event, opts, false);
|
||||
Event.populatePrototypes(event, opts, trusted);
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user