mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-09 04:42:30 -04:00
webapi: add support for the inert property
Improves the /inert/ WPT category
This commit is contained in:
7 files changed
+191
-9
No files matched your search
@@ -146,6 +146,8 @@ _queued_events_1: std.ArrayList(QueuedEvent) = .empty,
|
||||
_queued_events_2: std.ArrayList(QueuedEvent) = .empty,
|
||||
_queued_events: *std.ArrayList(QueuedEvent) = undefined,
|
||||
|
||||
_focus_fixup_pending: bool = false,
|
||||
|
||||
_style_manager: StyleManager,
|
||||
_script_manager: ScriptManager,
|
||||
|
||||
@@ -2356,6 +2358,25 @@ pub fn queueElementEvent(self: *Frame, element: *Element.Html, kind: QueuedEvent
|
||||
}
|
||||
}
|
||||
|
||||
// An element that becomes inert can't stay focused. Fire its blur on the next tick
|
||||
fn scheduleFocusFixup(self: *Frame) !void {
|
||||
if (self._focus_fixup_pending or self.document._active_element == null) {
|
||||
return;
|
||||
}
|
||||
try self.js.scheduler.add(self, struct {
|
||||
fn run(ctx: *anyopaque) !?u32 {
|
||||
const f: *Frame = @ptrCast(@alignCast(ctx));
|
||||
f._focus_fixup_pending = false;
|
||||
const active = f.document._active_element orelse return null;
|
||||
if (active.asNode().isInert(f)) {
|
||||
try active.blur(f);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.run, 5, .{ .name = "frame.focusFixup" });
|
||||
self._focus_fixup_pending = true;
|
||||
}
|
||||
|
||||
const HashChangeCallback = struct {
|
||||
frame: *Frame,
|
||||
old_url: []const u8,
|
||||
@@ -3135,6 +3156,10 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str
|
||||
} else if (name.eql(comptime .wrap("style"))) {
|
||||
element._flags.has_inline_style = true;
|
||||
self.styleAttributeChanged(element, value.str());
|
||||
} else if (name.eql(comptime .wrap("inert"))) {
|
||||
self.scheduleFocusFixup() catch |err| {
|
||||
log.err(.frame, "scheduleFocusFixup", .{ .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<body>
|
||||
<button id="outside">outside</button>
|
||||
<div id="container" inert>
|
||||
<input id="inner" type="text">
|
||||
<span id="text">hidden</span>
|
||||
</div>
|
||||
<div id="selectable">
|
||||
<p>shown</p>
|
||||
<span inert>hidden</span>
|
||||
<math inert><mi>math</mi></math>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script id="inert_property">
|
||||
{
|
||||
testing.expectEqual(false, $('#outside').inert);
|
||||
testing.expectEqual(true, $('#container').inert);
|
||||
// inherited inertness isn't reflected
|
||||
testing.expectEqual(false, $('#inner').inert);
|
||||
|
||||
const outside = $('#outside');
|
||||
outside.inert = true;
|
||||
testing.expectEqual('', outside.getAttribute('inert'));
|
||||
outside.inert = false;
|
||||
testing.expectEqual(false, outside.hasAttribute('inert'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="inert_unfocusable">
|
||||
{
|
||||
const outside = $('#outside');
|
||||
outside.focus();
|
||||
testing.expectEqual(outside, document.activeElement);
|
||||
|
||||
$('#inner').focus();
|
||||
testing.expectEqual(outside, document.activeElement);
|
||||
|
||||
$('#container').inert = false;
|
||||
$('#inner').focus();
|
||||
testing.expectEqual($('#inner'), document.activeElement);
|
||||
$('#container').inert = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="inert_selection">
|
||||
{
|
||||
const selection = getSelection();
|
||||
selection.selectAllChildren($('#selectable'));
|
||||
// inert only applies to HTML elements, so the MathML text stays
|
||||
testing.expectEqual('shown math', selection.toString().trim().replace(/\s+/g, ' '));
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="inert_focus_fixup" type=module>
|
||||
const state = await testing.async();
|
||||
const nextRendering = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
|
||||
const outside = $('#outside');
|
||||
outside.focus();
|
||||
const events = [];
|
||||
outside.addEventListener('blur', () => events.push('blur'));
|
||||
outside.addEventListener('focusout', () => events.push('focusout'));
|
||||
outside.inert = true;
|
||||
// like browsers, the fixup waits for the next rendering update
|
||||
const syncActive = document.activeElement;
|
||||
await nextRendering();
|
||||
const fixedActive = document.activeElement;
|
||||
outside.inert = false;
|
||||
|
||||
const host = document.createElement('div');
|
||||
host.innerHTML = '<button id="slotted" slot="s">slotted</button>';
|
||||
document.body.appendChild(host);
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const slot = document.createElement('slot');
|
||||
slot.name = 's';
|
||||
slot.inert = true;
|
||||
shadow.appendChild(slot);
|
||||
const shadowButton = document.createElement('button');
|
||||
shadow.appendChild(shadowButton);
|
||||
|
||||
$('#slotted').focus();
|
||||
const slottedActive = document.activeElement;
|
||||
shadowButton.focus();
|
||||
const shadowActive = document.activeElement;
|
||||
host.inert = true;
|
||||
await nextRendering();
|
||||
const hostFixedActive = document.activeElement;
|
||||
host.remove();
|
||||
|
||||
state.resolve();
|
||||
await state.done(() => {
|
||||
testing.expectEqual(outside, syncActive);
|
||||
testing.expectEqual(document.body, fixedActive);
|
||||
testing.expectEqual(['blur', 'focusout'], events);
|
||||
testing.expectEqual(document.body, slottedActive);
|
||||
testing.expectEqual(host, shadowActive);
|
||||
testing.expectEqual(document.body, hostFixedActive);
|
||||
});
|
||||
</script>
|
||||
@@ -1233,12 +1233,17 @@ pub fn focusTabIndex(self: *Element) ?i32 {
|
||||
return null;
|
||||
}
|
||||
|
||||
// A focusable area that can take focus right now: connected and being rendered.
|
||||
// A focusable area that can take focus right now: connected, not inert and
|
||||
// being rendered.
|
||||
pub fn isFocusable(self: *Element, frame: *Frame) bool {
|
||||
if (self.focusTabIndex() == null) {
|
||||
return false;
|
||||
}
|
||||
if (self.asNode().isConnected() == false) {
|
||||
const node = self.asNode();
|
||||
if (node.isConnected() == false) {
|
||||
return false;
|
||||
}
|
||||
if (node.isInert(frame)) {
|
||||
return false;
|
||||
}
|
||||
return self.isVisible(frame);
|
||||
|
||||
@@ -1699,6 +1699,27 @@ pub fn assignedSlot(self: *Node, frame: *const Frame) ?*Element.Html.Slot {
|
||||
return frame.page._assigned_slots.get(self);
|
||||
}
|
||||
|
||||
// An inert element applies to all its chidren, so walk up to see if we have
|
||||
// an inert parent
|
||||
pub fn isInert(self: *Node, frame: *const Frame) bool {
|
||||
var current: ?*Node = self;
|
||||
while (current) |node| {
|
||||
if (node.is(Element)) |el| {
|
||||
if (el._namespace == .html and el.hasAttributeSafe(comptime .wrap("inert"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (node.assignedSlot(frame)) |slot| {
|
||||
current = slot.asNode();
|
||||
} else if (node.is(ShadowRoot)) |shadow| {
|
||||
current = shadow._host.asNode();
|
||||
} else {
|
||||
current = node._parent;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Node);
|
||||
|
||||
|
||||
@@ -719,11 +719,19 @@ fn createContextualFragment(self: *const Range, html: []const u8, frame: *Frame)
|
||||
pub fn toString(self: *const Range, frame: *Frame) ![]const u8 {
|
||||
// Simplified implementation: just extract text content
|
||||
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
|
||||
try self.writeTextContent(&buf.writer);
|
||||
try self.writeTextContent(&buf.writer, null);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
// Selection.toString is almost like Range.toString, except it does not include
|
||||
// the text that an inert element hides.
|
||||
pub fn toSelectionString(self: *const Range, frame: *Frame) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
|
||||
try self.writeTextContent(&buf.writer, frame);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
fn writeTextContent(self: *const Range, writer: *std.Io.Writer, skip_inert: ?*const Frame) !void {
|
||||
if (self._proto.getCollapsed()) return;
|
||||
|
||||
const start_node = self._proto._start_container;
|
||||
@@ -734,7 +742,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
// Same text node — just substring
|
||||
if (start_node == end_node) {
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
if (includeText(cdata, skip_inert)) {
|
||||
const data = cdata.getData().str();
|
||||
const s = byteOffset(data, start_offset);
|
||||
const e = byteOffset(data, end_offset);
|
||||
@@ -748,7 +756,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
|
||||
// Partial start: if start container is a text node, write from offset to end
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
if (includeText(cdata, skip_inert)) {
|
||||
const data = cdata.getData().str();
|
||||
const s = byteOffset(data, start_offset);
|
||||
try writer.writeAll(data[s..]);
|
||||
@@ -775,7 +783,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
if (n == we) break;
|
||||
}
|
||||
if (n.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
if (includeText(cdata, skip_inert)) {
|
||||
try writer.writeAll(cdata.getData().str());
|
||||
}
|
||||
}
|
||||
@@ -786,7 +794,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
// Partial end: if end container is a different text node, write from start to offset
|
||||
if (start_node != end_node) {
|
||||
if (end_node.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
if (includeText(cdata, skip_inert)) {
|
||||
const data = cdata.getData().str();
|
||||
const e = byteOffset(data, end_offset);
|
||||
try writer.writeAll(data[0..e]);
|
||||
@@ -795,6 +803,14 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn includeText(cdata: *Node.CData, skip_inert: ?*const Frame) bool {
|
||||
if (isCommentOrPI(cdata)) {
|
||||
return false;
|
||||
}
|
||||
const frame = skip_inert orelse return true;
|
||||
return cdata.asNode().isInert(frame) == false;
|
||||
}
|
||||
|
||||
fn isCommentOrPI(cdata: *Node.CData) bool {
|
||||
return cdata.is(Node.CData.Comment) != null or cdata.is(Node.CData.ProcessingInstruction) != null;
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, frame: *Frame) !
|
||||
|
||||
pub fn toString(self: *const Selection, frame: *Frame) ![]const u8 {
|
||||
const range = self._range orelse return "";
|
||||
return try range.toString(frame);
|
||||
return try range.toSelectionString(frame);
|
||||
}
|
||||
|
||||
fn setRange(self: *Selection, new_range: ?*Range, frame: *Frame) void {
|
||||
|
||||
@@ -452,6 +452,18 @@ pub fn setHidden(self: *HtmlElement, hidden: bool, frame: *Frame) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getInert(self: *HtmlElement) bool {
|
||||
return self.asElement().hasAttributeSafe(comptime .wrap("inert"));
|
||||
}
|
||||
|
||||
pub fn setInert(self: *HtmlElement, inert: bool, frame: *Frame) !void {
|
||||
if (inert) {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("inert"), .wrap(""), frame);
|
||||
} else {
|
||||
try self.asElement().removeAttribute(comptime .wrap("inert"), frame);
|
||||
}
|
||||
}
|
||||
|
||||
// The translate IDL attribute reflects the element's translation mode:
|
||||
// translate="yes"/"" enables it, "no" disables it, anything else (or no
|
||||
// attribute) inherits from the parent, defaulting to enabled.
|
||||
@@ -1862,6 +1874,7 @@ pub const JsApi = struct {
|
||||
pub const dir = reflect.enumerated("dir", &.{ "ltr", "rtl", "auto" }, .{});
|
||||
pub const draggable = bridge.accessor(HtmlElement.getDraggable, HtmlElement.setDraggable, .{ .ce_reactions = true });
|
||||
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{ .ce_reactions = true });
|
||||
pub const inert = bridge.accessor(HtmlElement.getInert, HtmlElement.setInert, .{ .ce_reactions = true });
|
||||
pub const translate = bridge.accessor(HtmlElement.getTranslate, HtmlElement.setTranslate, .{ .ce_reactions = true });
|
||||
pub const accessKeyLabel = bridge.accessor(HtmlElement.getAccessKeyLabel, null, .{});
|
||||
pub const popover = bridge.accessor(HtmlElement.getPopover, HtmlElement.setPopover, .{ .ce_reactions = true });
|
||||
|
||||
Reference in new issue
Block a user