mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Improve inter-document focus handling (#36649)
*Describe the changes that this pull request makes here. This will be the commit message.* rewritten the PR #28571 Implement [Window#focus](https://html.spec.whatwg.org/multipage/#dom-window-focus), [Window#blur](https://html.spec.whatwg.org/multipage/#dom-window-blur) Testing: WPT Fixes: #8981 #9421 --------- Signed-off-by: kongbai1996 <1782765876@qq.com> Co-authored-by: yvt <i@yvt.jp>
This commit is contained in:
parent
27570987fd
commit
0c0ee04b8e
33 changed files with 1123 additions and 242 deletions
|
@ -127,10 +127,10 @@ use devtools_traits::{
|
||||||
use embedder_traits::resources::{self, Resource};
|
use embedder_traits::resources::{self, Resource};
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy, ImeEvent,
|
AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy,
|
||||||
InputEvent, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, MouseButton,
|
FocusSequenceNumber, ImeEvent, InputEvent, MediaSessionActionType, MediaSessionEvent,
|
||||||
MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg,
|
MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, Theme,
|
||||||
WebDriverLoadStatus,
|
ViewportDetails, WebDriverCommandMsg, WebDriverLoadStatus,
|
||||||
};
|
};
|
||||||
use euclid::Size2D;
|
use euclid::Size2D;
|
||||||
use euclid::default::Size2D as UntypedSize2D;
|
use euclid::default::Size2D as UntypedSize2D;
|
||||||
|
@ -1043,6 +1043,44 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enumerate the specified browsing context's ancestor pipelines up to
|
||||||
|
/// the top-level pipeline.
|
||||||
|
fn ancestor_pipelines_of_browsing_context_iter(
|
||||||
|
&self,
|
||||||
|
browsing_context_id: BrowsingContextId,
|
||||||
|
) -> impl Iterator<Item = &Pipeline> + '_ {
|
||||||
|
let mut state: Option<PipelineId> = self
|
||||||
|
.browsing_contexts
|
||||||
|
.get(&browsing_context_id)
|
||||||
|
.and_then(|browsing_context| browsing_context.parent_pipeline_id);
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
if let Some(pipeline_id) = state {
|
||||||
|
let pipeline = self.pipelines.get(&pipeline_id)?;
|
||||||
|
let browsing_context = self.browsing_contexts.get(&pipeline.browsing_context_id)?;
|
||||||
|
state = browsing_context.parent_pipeline_id;
|
||||||
|
Some(pipeline)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumerate the specified browsing context's ancestor-or-self pipelines up
|
||||||
|
/// to the top-level pipeline.
|
||||||
|
fn ancestor_or_self_pipelines_of_browsing_context_iter(
|
||||||
|
&self,
|
||||||
|
browsing_context_id: BrowsingContextId,
|
||||||
|
) -> impl Iterator<Item = &Pipeline> + '_ {
|
||||||
|
let this_pipeline = self
|
||||||
|
.browsing_contexts
|
||||||
|
.get(&browsing_context_id)
|
||||||
|
.map(|browsing_context| browsing_context.pipeline_id)
|
||||||
|
.and_then(|pipeline_id| self.pipelines.get(&pipeline_id));
|
||||||
|
this_pipeline
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.ancestor_pipelines_of_browsing_context_iter(browsing_context_id))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new browsing context and update the internal bookkeeping.
|
/// Create a new browsing context and update the internal bookkeeping.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn new_browsing_context(
|
fn new_browsing_context(
|
||||||
|
@ -1621,8 +1659,15 @@ where
|
||||||
data,
|
data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
ScriptToConstellationMessage::Focus => {
|
ScriptToConstellationMessage::Focus(focused_child_browsing_context_id, sequence) => {
|
||||||
self.handle_focus_msg(source_pipeline_id);
|
self.handle_focus_msg(
|
||||||
|
source_pipeline_id,
|
||||||
|
focused_child_browsing_context_id,
|
||||||
|
sequence,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ScriptToConstellationMessage::FocusRemoteDocument(focused_browsing_context_id) => {
|
||||||
|
self.handle_focus_remote_document_msg(focused_browsing_context_id);
|
||||||
},
|
},
|
||||||
ScriptToConstellationMessage::SetThrottledComplete(throttled) => {
|
ScriptToConstellationMessage::SetThrottledComplete(throttled) => {
|
||||||
self.handle_set_throttled_complete(source_pipeline_id, throttled);
|
self.handle_set_throttled_complete(source_pipeline_id, throttled);
|
||||||
|
@ -4070,6 +4115,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
new_pipeline.set_throttled(false);
|
new_pipeline.set_throttled(false);
|
||||||
|
self.notify_focus_state(new_pipeline_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_activity(old_pipeline_id);
|
self.update_activity(old_pipeline_id);
|
||||||
|
@ -4275,22 +4321,96 @@ where
|
||||||
feature = "tracing",
|
feature = "tracing",
|
||||||
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
||||||
)]
|
)]
|
||||||
fn handle_focus_msg(&mut self, pipeline_id: PipelineId) {
|
fn handle_focus_msg(
|
||||||
let (browsing_context_id, webview_id) = match self.pipelines.get(&pipeline_id) {
|
&mut self,
|
||||||
Some(pipeline) => (pipeline.browsing_context_id, pipeline.webview_id),
|
pipeline_id: PipelineId,
|
||||||
|
focused_child_browsing_context_id: Option<BrowsingContextId>,
|
||||||
|
sequence: FocusSequenceNumber,
|
||||||
|
) {
|
||||||
|
let (browsing_context_id, webview_id) = match self.pipelines.get_mut(&pipeline_id) {
|
||||||
|
Some(pipeline) => {
|
||||||
|
pipeline.focus_sequence = sequence;
|
||||||
|
(pipeline.browsing_context_id, pipeline.webview_id)
|
||||||
|
},
|
||||||
None => return warn!("{}: Focus parent after closure", pipeline_id),
|
None => return warn!("{}: Focus parent after closure", pipeline_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ignore if the pipeline isn't fully active.
|
||||||
|
if self.get_activity(pipeline_id) != DocumentActivity::FullyActive {
|
||||||
|
debug!(
|
||||||
|
"Ignoring the focus request because pipeline {} is not \
|
||||||
|
fully active",
|
||||||
|
pipeline_id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Focus the top-level browsing context.
|
// Focus the top-level browsing context.
|
||||||
self.webviews.focus(webview_id);
|
self.webviews.focus(webview_id);
|
||||||
self.embedder_proxy
|
self.embedder_proxy
|
||||||
.send(EmbedderMsg::WebViewFocused(webview_id));
|
.send(EmbedderMsg::WebViewFocused(webview_id));
|
||||||
|
|
||||||
|
// If a container with a non-null nested browsing context is focused,
|
||||||
|
// the nested browsing context's active document becomes the focused
|
||||||
|
// area of the top-level browsing context instead.
|
||||||
|
let focused_browsing_context_id =
|
||||||
|
focused_child_browsing_context_id.unwrap_or(browsing_context_id);
|
||||||
|
|
||||||
|
// Send focus messages to the affected pipelines, except
|
||||||
|
// `pipeline_id`, which has already its local focus state
|
||||||
|
// updated.
|
||||||
|
self.focus_browsing_context(Some(pipeline_id), focused_browsing_context_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_focus_remote_document_msg(&mut self, focused_browsing_context_id: BrowsingContextId) {
|
||||||
|
let pipeline_id = match self.browsing_contexts.get(&focused_browsing_context_id) {
|
||||||
|
Some(browsing_context) => browsing_context.pipeline_id,
|
||||||
|
None => return warn!("Browsing context {} not found", focused_browsing_context_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore if its active document isn't fully active.
|
||||||
|
if self.get_activity(pipeline_id) != DocumentActivity::FullyActive {
|
||||||
|
debug!(
|
||||||
|
"Ignoring the remote focus request because pipeline {} of \
|
||||||
|
browsing context {} is not fully active",
|
||||||
|
pipeline_id, focused_browsing_context_id,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.focus_browsing_context(None, focused_browsing_context_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform [the focusing steps][1] for the active document of
|
||||||
|
/// `focused_browsing_context_id`.
|
||||||
|
///
|
||||||
|
/// If `initiator_pipeline_id` is specified, this method avoids sending
|
||||||
|
/// a message to `initiator_pipeline_id`, assuming its local focus state has
|
||||||
|
/// already been updated. This is necessary for performing the focusing
|
||||||
|
/// steps for an object that is not the document itself but something that
|
||||||
|
/// belongs to the document.
|
||||||
|
///
|
||||||
|
/// [1]: https://html.spec.whatwg.org/multipage/#focusing-steps
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
||||||
|
)]
|
||||||
|
fn focus_browsing_context(
|
||||||
|
&mut self,
|
||||||
|
initiator_pipeline_id: Option<PipelineId>,
|
||||||
|
focused_browsing_context_id: BrowsingContextId,
|
||||||
|
) {
|
||||||
|
let webview_id = match self.browsing_contexts.get(&focused_browsing_context_id) {
|
||||||
|
Some(browsing_context) => browsing_context.top_level_id,
|
||||||
|
None => return warn!("Browsing context {} not found", focused_browsing_context_id),
|
||||||
|
};
|
||||||
|
|
||||||
// Update the webview’s focused browsing context.
|
// Update the webview’s focused browsing context.
|
||||||
match self.webviews.get_mut(webview_id) {
|
let old_focused_browsing_context_id = match self.webviews.get_mut(webview_id) {
|
||||||
Some(webview) => {
|
Some(browser) => replace(
|
||||||
webview.focused_browsing_context_id = browsing_context_id;
|
&mut browser.focused_browsing_context_id,
|
||||||
},
|
focused_browsing_context_id,
|
||||||
|
),
|
||||||
None => {
|
None => {
|
||||||
return warn!(
|
return warn!(
|
||||||
"{}: Browsing context for focus msg does not exist",
|
"{}: Browsing context for focus msg does not exist",
|
||||||
|
@ -4299,42 +4419,133 @@ where
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Focus parent iframes recursively
|
// The following part is similar to [the focus update steps][1] except
|
||||||
self.focus_parent_pipeline(browsing_context_id);
|
// that only `Document`s in the given focus chains are considered. It's
|
||||||
}
|
// ultimately up to the script threads to fire focus events at the
|
||||||
|
// affected objects.
|
||||||
|
//
|
||||||
|
// [1]: https://html.spec.whatwg.org/multipage/#focus-update-steps
|
||||||
|
let mut old_focus_chain_pipelines: Vec<&Pipeline> = self
|
||||||
|
.ancestor_or_self_pipelines_of_browsing_context_iter(old_focused_browsing_context_id)
|
||||||
|
.collect();
|
||||||
|
let mut new_focus_chain_pipelines: Vec<&Pipeline> = self
|
||||||
|
.ancestor_or_self_pipelines_of_browsing_context_iter(focused_browsing_context_id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
#[cfg_attr(
|
debug!(
|
||||||
feature = "tracing",
|
"old_focus_chain_pipelines = {:?}",
|
||||||
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
old_focus_chain_pipelines
|
||||||
)]
|
.iter()
|
||||||
fn focus_parent_pipeline(&mut self, browsing_context_id: BrowsingContextId) {
|
.map(|p| p.id.to_string())
|
||||||
let parent_pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
|
.collect::<Vec<_>>()
|
||||||
Some(ctx) => ctx.parent_pipeline_id,
|
);
|
||||||
None => {
|
debug!(
|
||||||
return warn!("{}: Focus parent after closure", browsing_context_id);
|
"new_focus_chain_pipelines = {:?}",
|
||||||
},
|
new_focus_chain_pipelines
|
||||||
};
|
.iter()
|
||||||
let parent_pipeline_id = match parent_pipeline_id {
|
.map(|p| p.id.to_string())
|
||||||
Some(parent_id) => parent_id,
|
.collect::<Vec<_>>()
|
||||||
None => {
|
);
|
||||||
return debug!("{}: Focus has no parent", browsing_context_id);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send a message to the parent of the provided browsing context (if it
|
// At least the last entries should match. Otherwise something is wrong,
|
||||||
// exists) telling it to mark the iframe element as focused.
|
// and we don't want to proceed and crash the top-level pipeline by
|
||||||
let msg = ScriptThreadMessage::FocusIFrame(parent_pipeline_id, browsing_context_id);
|
// sending an impossible `Unfocus` message to it.
|
||||||
let (result, parent_browsing_context_id) = match self.pipelines.get(&parent_pipeline_id) {
|
match (
|
||||||
Some(pipeline) => {
|
&old_focus_chain_pipelines[..],
|
||||||
let result = pipeline.event_loop.send(msg);
|
&new_focus_chain_pipelines[..],
|
||||||
(result, pipeline.browsing_context_id)
|
) {
|
||||||
|
([.., p1], [.., p2]) if p1.id == p2.id => {},
|
||||||
|
_ => {
|
||||||
|
warn!("Aborting the focus operation - focus chain sanity check failed");
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
None => return warn!("{}: Focus after closure", parent_pipeline_id),
|
|
||||||
};
|
|
||||||
if let Err(e) = result {
|
|
||||||
self.handle_send_error(parent_pipeline_id, e);
|
|
||||||
}
|
}
|
||||||
self.focus_parent_pipeline(parent_browsing_context_id);
|
|
||||||
|
// > If the last entry in `old chain` and the last entry in `new chain`
|
||||||
|
// > are the same, pop the last entry from `old chain` and the last
|
||||||
|
// > entry from `new chain` and redo this step.
|
||||||
|
let mut first_common_pipeline_in_chain = None;
|
||||||
|
while let ([.., p1], [.., p2]) = (
|
||||||
|
&old_focus_chain_pipelines[..],
|
||||||
|
&new_focus_chain_pipelines[..],
|
||||||
|
) {
|
||||||
|
if p1.id != p2.id {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
old_focus_chain_pipelines.pop();
|
||||||
|
first_common_pipeline_in_chain = new_focus_chain_pipelines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut send_errors = Vec::new();
|
||||||
|
|
||||||
|
// > For each entry `entry` in `old chain`, in order, run these
|
||||||
|
// > substeps: [...]
|
||||||
|
for &pipeline in old_focus_chain_pipelines.iter() {
|
||||||
|
if Some(pipeline.id) != initiator_pipeline_id {
|
||||||
|
let msg = ScriptThreadMessage::Unfocus(pipeline.id, pipeline.focus_sequence);
|
||||||
|
trace!("Sending {:?} to {}", msg, pipeline.id);
|
||||||
|
if let Err(e) = pipeline.event_loop.send(msg) {
|
||||||
|
send_errors.push((pipeline.id, e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace!(
|
||||||
|
"Not notifying {} - it's the initiator of this focus operation",
|
||||||
|
pipeline.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// > For each entry entry in `new chain`, in reverse order, run these
|
||||||
|
// > substeps: [...]
|
||||||
|
let mut child_browsing_context_id = None;
|
||||||
|
for &pipeline in new_focus_chain_pipelines.iter().rev() {
|
||||||
|
// Don't send a message to the browsing context that initiated this
|
||||||
|
// focus operation. It already knows that it has gotten focus.
|
||||||
|
if Some(pipeline.id) != initiator_pipeline_id {
|
||||||
|
let msg = if let Some(child_browsing_context_id) = child_browsing_context_id {
|
||||||
|
// Focus the container element of `child_browsing_context_id`.
|
||||||
|
ScriptThreadMessage::FocusIFrame(
|
||||||
|
pipeline.id,
|
||||||
|
child_browsing_context_id,
|
||||||
|
pipeline.focus_sequence,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Focus the document.
|
||||||
|
ScriptThreadMessage::FocusDocument(pipeline.id, pipeline.focus_sequence)
|
||||||
|
};
|
||||||
|
trace!("Sending {:?} to {}", msg, pipeline.id);
|
||||||
|
if let Err(e) = pipeline.event_loop.send(msg) {
|
||||||
|
send_errors.push((pipeline.id, e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace!(
|
||||||
|
"Not notifying {} - it's the initiator of this focus operation",
|
||||||
|
pipeline.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
child_browsing_context_id = Some(pipeline.browsing_context_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(pipeline), Some(child_browsing_context_id)) =
|
||||||
|
(first_common_pipeline_in_chain, child_browsing_context_id)
|
||||||
|
{
|
||||||
|
if Some(pipeline.id) != initiator_pipeline_id {
|
||||||
|
// Focus the container element of `child_browsing_context_id`.
|
||||||
|
let msg = ScriptThreadMessage::FocusIFrame(
|
||||||
|
pipeline.id,
|
||||||
|
child_browsing_context_id,
|
||||||
|
pipeline.focus_sequence,
|
||||||
|
);
|
||||||
|
trace!("Sending {:?} to {}", msg, pipeline.id);
|
||||||
|
if let Err(e) = pipeline.event_loop.send(msg) {
|
||||||
|
send_errors.push((pipeline.id, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pipeline_id, e) in send_errors {
|
||||||
|
self.handle_send_error(pipeline_id, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
|
@ -4929,10 +5140,42 @@ where
|
||||||
self.trim_history(top_level_id);
|
self.trim_history(top_level_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.notify_focus_state(change.new_pipeline_id);
|
||||||
|
|
||||||
self.notify_history_changed(change.webview_id);
|
self.notify_history_changed(change.webview_id);
|
||||||
self.update_webview_in_compositor(change.webview_id);
|
self.update_webview_in_compositor(change.webview_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the focus state of the specified pipeline that recently became
|
||||||
|
/// active (thus doesn't have a focused container element) and may have
|
||||||
|
/// out-dated information.
|
||||||
|
fn notify_focus_state(&mut self, pipeline_id: PipelineId) {
|
||||||
|
let pipeline = match self.pipelines.get(&pipeline_id) {
|
||||||
|
Some(pipeline) => pipeline,
|
||||||
|
None => return warn!("Pipeline {} is closed", pipeline_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_focused = match self.webviews.get(pipeline.webview_id) {
|
||||||
|
Some(webview) => webview.focused_browsing_context_id == pipeline.browsing_context_id,
|
||||||
|
None => {
|
||||||
|
return warn!(
|
||||||
|
"Pipeline {}'s top-level browsing context {} is closed",
|
||||||
|
pipeline_id, pipeline.webview_id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the browsing context is focused, focus the document
|
||||||
|
let msg = if is_focused {
|
||||||
|
ScriptThreadMessage::FocusDocument(pipeline_id, pipeline.focus_sequence)
|
||||||
|
} else {
|
||||||
|
ScriptThreadMessage::Unfocus(pipeline_id, pipeline.focus_sequence)
|
||||||
|
};
|
||||||
|
if let Err(e) = pipeline.event_loop.send(msg) {
|
||||||
|
self.handle_send_error(pipeline_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "tracing",
|
feature = "tracing",
|
||||||
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
||||||
|
@ -5382,7 +5625,29 @@ where
|
||||||
None => {
|
None => {
|
||||||
warn!("{parent_pipeline_id}: Child closed after parent");
|
warn!("{parent_pipeline_id}: Child closed after parent");
|
||||||
},
|
},
|
||||||
Some(parent_pipeline) => parent_pipeline.remove_child(browsing_context_id),
|
Some(parent_pipeline) => {
|
||||||
|
parent_pipeline.remove_child(browsing_context_id);
|
||||||
|
|
||||||
|
// If `browsing_context_id` has focus, focus the parent
|
||||||
|
// browsing context
|
||||||
|
if let Some(webview) = self.webviews.get_mut(browsing_context.top_level_id) {
|
||||||
|
if webview.focused_browsing_context_id == browsing_context_id {
|
||||||
|
trace!(
|
||||||
|
"About-to-be-closed browsing context {} is currently focused, so \
|
||||||
|
focusing its parent {}",
|
||||||
|
browsing_context_id, parent_pipeline.browsing_context_id
|
||||||
|
);
|
||||||
|
webview.focused_browsing_context_id =
|
||||||
|
parent_pipeline.browsing_context_id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Browsing context {} contains a reference to \
|
||||||
|
a non-existent top-level browsing context {}",
|
||||||
|
browsing_context_id, browsing_context.top_level_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
debug!("{}: Closed", browsing_context_id);
|
debug!("{}: Closed", browsing_context_id);
|
||||||
|
|
|
@ -25,7 +25,7 @@ use constellation_traits::{LoadData, SWManagerMsg, ScriptToConstellationChan};
|
||||||
use crossbeam_channel::{Sender, unbounded};
|
use crossbeam_channel::{Sender, unbounded};
|
||||||
use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg};
|
use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg};
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
use embedder_traits::{AnimationState, ViewportDetails};
|
use embedder_traits::{AnimationState, FocusSequenceNumber, ViewportDetails};
|
||||||
use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender};
|
use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender};
|
||||||
use ipc_channel::Error;
|
use ipc_channel::Error;
|
||||||
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
|
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
|
||||||
|
@ -102,6 +102,8 @@ pub struct Pipeline {
|
||||||
/// The last compositor [`Epoch`] that was laid out in this pipeline if "exit after load" is
|
/// The last compositor [`Epoch`] that was laid out in this pipeline if "exit after load" is
|
||||||
/// enabled.
|
/// enabled.
|
||||||
pub layout_epoch: Epoch,
|
pub layout_epoch: Epoch,
|
||||||
|
|
||||||
|
pub focus_sequence: FocusSequenceNumber,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initial setup data needed to construct a pipeline.
|
/// Initial setup data needed to construct a pipeline.
|
||||||
|
@ -370,6 +372,7 @@ impl Pipeline {
|
||||||
completely_loaded: false,
|
completely_loaded: false,
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
layout_epoch: Epoch(0),
|
layout_epoch: Epoch(0),
|
||||||
|
focus_sequence: FocusSequenceNumber::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
pipeline.set_throttled(throttled);
|
pipeline.set_throttled(throttled);
|
||||||
|
|
|
@ -138,7 +138,8 @@ mod from_script {
|
||||||
Self::BroadcastStorageEvent(..) => target!("BroadcastStorageEvent"),
|
Self::BroadcastStorageEvent(..) => target!("BroadcastStorageEvent"),
|
||||||
Self::ChangeRunningAnimationsState(..) => target!("ChangeRunningAnimationsState"),
|
Self::ChangeRunningAnimationsState(..) => target!("ChangeRunningAnimationsState"),
|
||||||
Self::CreateCanvasPaintThread(..) => target!("CreateCanvasPaintThread"),
|
Self::CreateCanvasPaintThread(..) => target!("CreateCanvasPaintThread"),
|
||||||
Self::Focus => target!("Focus"),
|
Self::Focus(..) => target!("Focus"),
|
||||||
|
Self::FocusRemoteDocument(..) => target!("FocusRemoteDocument"),
|
||||||
Self::GetTopForBrowsingContext(..) => target!("GetTopForBrowsingContext"),
|
Self::GetTopForBrowsingContext(..) => target!("GetTopForBrowsingContext"),
|
||||||
Self::GetBrowsingContextInfo(..) => target!("GetBrowsingContextInfo"),
|
Self::GetBrowsingContextInfo(..) => target!("GetBrowsingContextInfo"),
|
||||||
Self::GetChildBrowsingContextId(..) => target!("GetChildBrowsingContextId"),
|
Self::GetChildBrowsingContextId(..) => target!("GetChildBrowsingContextId"),
|
||||||
|
|
|
@ -181,12 +181,13 @@ impl DissimilarOriginWindowMethods<crate::DomTypeHolder> for DissimilarOriginWin
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-window-blur
|
// https://html.spec.whatwg.org/multipage/#dom-window-blur
|
||||||
fn Blur(&self) {
|
fn Blur(&self) {
|
||||||
// TODO: Implement x-origin blur
|
// > User agents are encouraged to ignore calls to this `blur()` method
|
||||||
|
// > entirely.
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-focus
|
// https://html.spec.whatwg.org/multipage/#dom-window-focus
|
||||||
fn Focus(&self) {
|
fn Focus(&self) {
|
||||||
// TODO: Implement x-origin focus
|
self.window_proxy().focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-location
|
// https://html.spec.whatwg.org/multipage/#dom-location
|
||||||
|
|
|
@ -30,8 +30,8 @@ use devtools_traits::ScriptToDevtoolsControlMsg;
|
||||||
use dom_struct::dom_struct;
|
use dom_struct::dom_struct;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
AllowOrDeny, AnimationState, CompositorHitTestResult, ContextMenuResult, EditingActionEvent,
|
AllowOrDeny, AnimationState, CompositorHitTestResult, ContextMenuResult, EditingActionEvent,
|
||||||
EmbedderMsg, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction,
|
EmbedderMsg, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton,
|
||||||
MouseButtonEvent, TouchEvent, TouchEventType, TouchId, WheelEvent,
|
MouseButtonAction, MouseButtonEvent, TouchEvent, TouchEventType, TouchId, WheelEvent,
|
||||||
};
|
};
|
||||||
use encoding_rs::{Encoding, UTF_8};
|
use encoding_rs::{Encoding, UTF_8};
|
||||||
use euclid::default::{Point2D, Rect, Size2D};
|
use euclid::default::{Point2D, Rect, Size2D};
|
||||||
|
@ -270,12 +270,11 @@ pub(crate) enum IsHTMLDocument {
|
||||||
|
|
||||||
#[derive(JSTraceable, MallocSizeOf)]
|
#[derive(JSTraceable, MallocSizeOf)]
|
||||||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||||||
enum FocusTransaction {
|
struct FocusTransaction {
|
||||||
/// No focus operation is in effect.
|
/// The focused element of this document.
|
||||||
NotInTransaction,
|
element: Option<Dom<Element>>,
|
||||||
/// A focus operation is in effect.
|
/// See [`Document::has_focus`].
|
||||||
/// Contains the element that has most recently requested focus for itself.
|
has_focus: bool,
|
||||||
InTransaction(Option<Dom<Element>>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about a declarative refresh
|
/// Information about a declarative refresh
|
||||||
|
@ -341,9 +340,16 @@ pub(crate) struct Document {
|
||||||
/// Whether the DOMContentLoaded event has already been dispatched.
|
/// Whether the DOMContentLoaded event has already been dispatched.
|
||||||
domcontentloaded_dispatched: Cell<bool>,
|
domcontentloaded_dispatched: Cell<bool>,
|
||||||
/// The state of this document's focus transaction.
|
/// The state of this document's focus transaction.
|
||||||
focus_transaction: DomRefCell<FocusTransaction>,
|
focus_transaction: DomRefCell<Option<FocusTransaction>>,
|
||||||
/// The element that currently has the document focus context.
|
/// The element that currently has the document focus context.
|
||||||
focused: MutNullableDom<Element>,
|
focused: MutNullableDom<Element>,
|
||||||
|
/// The last sequence number sent to the constellation.
|
||||||
|
#[no_trace]
|
||||||
|
focus_sequence: Cell<FocusSequenceNumber>,
|
||||||
|
/// Indicates whether the container is included in the top-level browsing
|
||||||
|
/// context's focus chain (not considering system focus). Permanently `true`
|
||||||
|
/// for a top-level document.
|
||||||
|
has_focus: Cell<bool>,
|
||||||
/// The script element that is currently executing.
|
/// The script element that is currently executing.
|
||||||
current_script: MutNullableDom<HTMLScriptElement>,
|
current_script: MutNullableDom<HTMLScriptElement>,
|
||||||
/// <https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script>
|
/// <https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script>
|
||||||
|
@ -1120,124 +1126,318 @@ impl Document {
|
||||||
self.focused.get()
|
self.focused.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the last sequence number sent to the constellation.
|
||||||
|
///
|
||||||
|
/// Received focus-related messages with sequence numbers less than the one
|
||||||
|
/// returned by this method must be discarded.
|
||||||
|
pub fn get_focus_sequence(&self) -> FocusSequenceNumber {
|
||||||
|
self.focus_sequence.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the next sequence number for focus-related messages.
|
||||||
|
fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber {
|
||||||
|
self.focus_sequence.set(FocusSequenceNumber(
|
||||||
|
self.focus_sequence
|
||||||
|
.get()
|
||||||
|
.0
|
||||||
|
.checked_add(1)
|
||||||
|
.expect("too many focus messages have been sent"),
|
||||||
|
));
|
||||||
|
self.focus_sequence.get()
|
||||||
|
}
|
||||||
|
|
||||||
/// Initiate a new round of checking for elements requesting focus. The last element to call
|
/// Initiate a new round of checking for elements requesting focus. The last element to call
|
||||||
/// `request_focus` before `commit_focus_transaction` is called will receive focus.
|
/// `request_focus` before `commit_focus_transaction` is called will receive focus.
|
||||||
fn begin_focus_transaction(&self) {
|
fn begin_focus_transaction(&self) {
|
||||||
*self.focus_transaction.borrow_mut() = FocusTransaction::InTransaction(Default::default());
|
// Initialize it with the current state
|
||||||
|
*self.focus_transaction.borrow_mut() = Some(FocusTransaction {
|
||||||
|
element: self.focused.get().as_deref().map(Dom::from_ref),
|
||||||
|
has_focus: self.has_focus.get(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
|
/// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
|
||||||
pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element, can_gc: CanGc) {
|
pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element, can_gc: CanGc) {
|
||||||
|
// Return if `not_focusable` is not the designated focused area of the
|
||||||
|
// `Document`.
|
||||||
if Some(not_focusable) != self.focused.get().as_deref() {
|
if Some(not_focusable) != self.focused.get().as_deref() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.request_focus(
|
|
||||||
self.GetBody().as_ref().map(|e| e.upcast()),
|
|
||||||
FocusType::Element,
|
|
||||||
can_gc,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request that the given element receive focus once the current transaction is complete.
|
let implicit_transaction = self.focus_transaction.borrow().is_none();
|
||||||
/// If None is passed, then whatever element is currently focused will no longer be focused
|
|
||||||
/// once the transaction is complete.
|
|
||||||
pub(crate) fn request_focus(
|
|
||||||
&self,
|
|
||||||
elem: Option<&Element>,
|
|
||||||
focus_type: FocusType,
|
|
||||||
can_gc: CanGc,
|
|
||||||
) {
|
|
||||||
let implicit_transaction = matches!(
|
|
||||||
*self.focus_transaction.borrow(),
|
|
||||||
FocusTransaction::NotInTransaction
|
|
||||||
);
|
|
||||||
if implicit_transaction {
|
if implicit_transaction {
|
||||||
self.begin_focus_transaction();
|
self.begin_focus_transaction();
|
||||||
}
|
}
|
||||||
if elem.is_none_or(|e| e.is_focusable_area()) {
|
|
||||||
*self.focus_transaction.borrow_mut() =
|
// Designate the viewport as the new focused area of the `Document`, but
|
||||||
FocusTransaction::InTransaction(elem.map(Dom::from_ref));
|
// do not run the focusing steps.
|
||||||
|
{
|
||||||
|
let mut focus_transaction = self.focus_transaction.borrow_mut();
|
||||||
|
focus_transaction.as_mut().unwrap().element = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if implicit_transaction {
|
if implicit_transaction {
|
||||||
self.commit_focus_transaction(focus_type, can_gc);
|
self.commit_focus_transaction(FocusInitiator::Local, can_gc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reassign the focus context to the element that last requested focus during this
|
/// Request that the given element receive focus once the current
|
||||||
/// transaction, or none if no elements requested it.
|
/// transaction is complete. `None` specifies to focus the document.
|
||||||
fn commit_focus_transaction(&self, focus_type: FocusType, can_gc: CanGc) {
|
///
|
||||||
let possibly_focused = match *self.focus_transaction.borrow() {
|
/// If there's no ongoing transaction, this method automatically starts and
|
||||||
FocusTransaction::NotInTransaction => unreachable!(),
|
/// commits an implicit transaction.
|
||||||
FocusTransaction::InTransaction(ref elem) => {
|
pub(crate) fn request_focus(
|
||||||
elem.as_ref().map(|e| DomRoot::from_ref(&**e))
|
&self,
|
||||||
},
|
elem: Option<&Element>,
|
||||||
};
|
focus_initiator: FocusInitiator,
|
||||||
*self.focus_transaction.borrow_mut() = FocusTransaction::NotInTransaction;
|
can_gc: CanGc,
|
||||||
if self.focused == possibly_focused.as_deref() {
|
) {
|
||||||
|
// If an element is specified, and it's non-focusable, ignore the
|
||||||
|
// request.
|
||||||
|
if elem.is_some_and(|e| !e.is_focusable_area()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(ref elem) = self.focused.get() {
|
|
||||||
let node = elem.upcast::<Node>();
|
|
||||||
elem.set_focus_state(false);
|
|
||||||
// FIXME: pass appropriate relatedTarget
|
|
||||||
if node.is_connected() {
|
|
||||||
self.fire_focus_event(FocusEventType::Blur, node, None, can_gc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify the embedder to hide the input method.
|
let implicit_transaction = self.focus_transaction.borrow().is_none();
|
||||||
if elem.input_method_type().is_some() {
|
|
||||||
self.send_to_embedder(EmbedderMsg::HideIME(self.webview_id()));
|
if implicit_transaction {
|
||||||
|
self.begin_focus_transaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut focus_transaction = self.focus_transaction.borrow_mut();
|
||||||
|
let focus_transaction = focus_transaction.as_mut().unwrap();
|
||||||
|
focus_transaction.element = elem.map(Dom::from_ref);
|
||||||
|
focus_transaction.has_focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if implicit_transaction {
|
||||||
|
self.commit_focus_transaction(focus_initiator, can_gc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the local focus state accordingly after being notified that the
|
||||||
|
/// document's container is removed from the top-level browsing context's
|
||||||
|
/// focus chain (not considering system focus).
|
||||||
|
pub(crate) fn handle_container_unfocus(&self, can_gc: CanGc) {
|
||||||
|
assert!(
|
||||||
|
self.window().parent_info().is_some(),
|
||||||
|
"top-level document cannot be unfocused",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since this method is called from an event loop, there mustn't be
|
||||||
|
// an in-progress focus transaction
|
||||||
|
assert!(
|
||||||
|
self.focus_transaction.borrow().is_none(),
|
||||||
|
"there mustn't be an in-progress focus transaction at this point"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start an implicit focus transaction
|
||||||
|
self.begin_focus_transaction();
|
||||||
|
|
||||||
|
// Update the transaction
|
||||||
|
{
|
||||||
|
let mut focus_transaction = self.focus_transaction.borrow_mut();
|
||||||
|
focus_transaction.as_mut().unwrap().has_focus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the implicit focus transaction
|
||||||
|
self.commit_focus_transaction(FocusInitiator::Remote, can_gc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reassign the focus context to the element that last requested focus during this
|
||||||
|
/// transaction, or the document if no elements requested it.
|
||||||
|
fn commit_focus_transaction(&self, focus_initiator: FocusInitiator, can_gc: CanGc) {
|
||||||
|
let (mut new_focused, new_focus_state) = {
|
||||||
|
let focus_transaction = self.focus_transaction.borrow();
|
||||||
|
let focus_transaction = focus_transaction
|
||||||
|
.as_ref()
|
||||||
|
.expect("no focus transaction in progress");
|
||||||
|
(
|
||||||
|
focus_transaction
|
||||||
|
.element
|
||||||
|
.as_ref()
|
||||||
|
.map(|e| DomRoot::from_ref(&**e)),
|
||||||
|
focus_transaction.has_focus,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
*self.focus_transaction.borrow_mut() = None;
|
||||||
|
|
||||||
|
if !new_focus_state {
|
||||||
|
// In many browsers, a document forgets its focused area when the
|
||||||
|
// document is removed from the top-level BC's focus chain
|
||||||
|
if new_focused.take().is_some() {
|
||||||
|
trace!(
|
||||||
|
"Forgetting the document's focused area because the \
|
||||||
|
document's container was removed from the top-level BC's \
|
||||||
|
focus chain"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.focused.set(possibly_focused.as_deref());
|
let old_focused = self.focused.get();
|
||||||
|
let old_focus_state = self.has_focus.get();
|
||||||
|
|
||||||
if let Some(ref elem) = self.focused.get() {
|
debug!(
|
||||||
elem.set_focus_state(true);
|
"Committing focus transaction: {:?} → {:?}",
|
||||||
let node = elem.upcast::<Node>();
|
(&old_focused, old_focus_state),
|
||||||
// FIXME: pass appropriate relatedTarget
|
(&new_focused, new_focus_state),
|
||||||
self.fire_focus_event(FocusEventType::Focus, node, None, can_gc);
|
);
|
||||||
// Update the focus state for all elements in the focus chain.
|
|
||||||
// https://html.spec.whatwg.org/multipage/#focus-chain
|
// `*_focused_filtered` indicates the local element (if any) included in
|
||||||
if focus_type == FocusType::Element {
|
// the top-level BC's focus chain.
|
||||||
self.window()
|
let old_focused_filtered = old_focused.as_ref().filter(|_| old_focus_state);
|
||||||
.send_to_constellation(ScriptToConstellationMessage::Focus);
|
let new_focused_filtered = new_focused.as_ref().filter(|_| new_focus_state);
|
||||||
|
|
||||||
|
let trace_focus_chain = |name, element, doc| {
|
||||||
|
trace!(
|
||||||
|
"{} local focus chain: {}",
|
||||||
|
name,
|
||||||
|
match (element, doc) {
|
||||||
|
(Some(e), _) => format!("[{:?}, document]", e),
|
||||||
|
(None, true) => "[document]".to_owned(),
|
||||||
|
(None, false) => "[]".to_owned(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
trace_focus_chain("Old", old_focused_filtered, old_focus_state);
|
||||||
|
trace_focus_chain("New", new_focused_filtered, new_focus_state);
|
||||||
|
|
||||||
|
if old_focused_filtered != new_focused_filtered {
|
||||||
|
if let Some(elem) = &old_focused_filtered {
|
||||||
|
let node = elem.upcast::<Node>();
|
||||||
|
elem.set_focus_state(false);
|
||||||
|
// FIXME: pass appropriate relatedTarget
|
||||||
|
if node.is_connected() {
|
||||||
|
self.fire_focus_event(FocusEventType::Blur, node.upcast(), None, can_gc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the embedder to hide the input method.
|
||||||
|
if elem.input_method_type().is_some() {
|
||||||
|
self.send_to_embedder(EmbedderMsg::HideIME(self.webview_id()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notify the embedder to display an input method.
|
if old_focus_state != new_focus_state && !new_focus_state {
|
||||||
if let Some(kind) = elem.input_method_type() {
|
self.fire_focus_event(FocusEventType::Blur, self.global().upcast(), None, can_gc);
|
||||||
let rect = elem.upcast::<Node>().bounding_content_box_or_zero(can_gc);
|
}
|
||||||
let rect = Rect::new(
|
|
||||||
Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
|
self.focused.set(new_focused.as_deref());
|
||||||
Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
|
self.has_focus.set(new_focus_state);
|
||||||
|
|
||||||
|
if old_focus_state != new_focus_state && new_focus_state {
|
||||||
|
self.fire_focus_event(FocusEventType::Focus, self.global().upcast(), None, can_gc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if old_focused_filtered != new_focused_filtered {
|
||||||
|
if let Some(elem) = &new_focused_filtered {
|
||||||
|
elem.set_focus_state(true);
|
||||||
|
let node = elem.upcast::<Node>();
|
||||||
|
// FIXME: pass appropriate relatedTarget
|
||||||
|
self.fire_focus_event(FocusEventType::Focus, node.upcast(), None, can_gc);
|
||||||
|
|
||||||
|
// Notify the embedder to display an input method.
|
||||||
|
if let Some(kind) = elem.input_method_type() {
|
||||||
|
let rect = elem.upcast::<Node>().bounding_content_box_or_zero(can_gc);
|
||||||
|
let rect = Rect::new(
|
||||||
|
Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
|
||||||
|
Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
|
||||||
|
);
|
||||||
|
let (text, multiline) = if let Some(input) = elem.downcast::<HTMLInputElement>()
|
||||||
|
{
|
||||||
|
(
|
||||||
|
Some((
|
||||||
|
(input.Value()).to_string(),
|
||||||
|
input.GetSelectionEnd().unwrap_or(0) as i32,
|
||||||
|
)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
} else if let Some(textarea) = elem.downcast::<HTMLTextAreaElement>() {
|
||||||
|
(
|
||||||
|
Some((
|
||||||
|
(textarea.Value()).to_string(),
|
||||||
|
textarea.GetSelectionEnd().unwrap_or(0) as i32,
|
||||||
|
)),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
};
|
||||||
|
self.send_to_embedder(EmbedderMsg::ShowIME(
|
||||||
|
self.webview_id(),
|
||||||
|
kind,
|
||||||
|
text,
|
||||||
|
multiline,
|
||||||
|
DeviceIntRect::from_untyped(&rect.to_box2d()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if focus_initiator != FocusInitiator::Local {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are the initiator of the focus operation, so we must broadcast
|
||||||
|
// the change we intend to make.
|
||||||
|
match (old_focus_state, new_focus_state) {
|
||||||
|
(_, true) => {
|
||||||
|
// Advertise the change in the focus chain.
|
||||||
|
// <https://html.spec.whatwg.org/multipage/#focus-chain>
|
||||||
|
// <https://html.spec.whatwg.org/multipage/#focusing-steps>
|
||||||
|
//
|
||||||
|
// If the top-level BC doesn't have system focus, this won't
|
||||||
|
// have an immediate effect, but it will when we gain system
|
||||||
|
// focus again. Therefore we still have to send `ScriptMsg::
|
||||||
|
// Focus`.
|
||||||
|
//
|
||||||
|
// When a container with a non-null nested browsing context is
|
||||||
|
// focused, its active document becomes the focused area of the
|
||||||
|
// top-level browsing context instead. Therefore we need to let
|
||||||
|
// the constellation know if such a container is focused.
|
||||||
|
//
|
||||||
|
// > The focusing steps for an object `new focus target` [...]
|
||||||
|
// >
|
||||||
|
// > 3. If `new focus target` is a browsing context container
|
||||||
|
// > with non-null nested browsing context, then set
|
||||||
|
// > `new focus target` to the nested browsing context's
|
||||||
|
// > active document.
|
||||||
|
let child_browsing_context_id = new_focused
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|elem| elem.downcast::<HTMLIFrameElement>())
|
||||||
|
.and_then(|iframe| iframe.browsing_context_id());
|
||||||
|
|
||||||
|
let sequence = self.increment_fetch_focus_sequence();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Advertising the focus request to the constellation \
|
||||||
|
with sequence number {} and child BC ID {}",
|
||||||
|
sequence,
|
||||||
|
child_browsing_context_id
|
||||||
|
.as_ref()
|
||||||
|
.map(|id| id as &dyn std::fmt::Display)
|
||||||
|
.unwrap_or(&"(none)"),
|
||||||
);
|
);
|
||||||
let (text, multiline) = if let Some(input) = elem.downcast::<HTMLInputElement>() {
|
|
||||||
(
|
self.window()
|
||||||
Some((
|
.send_to_constellation(ScriptToConstellationMessage::Focus(
|
||||||
input.Value().to_string(),
|
child_browsing_context_id,
|
||||||
input.GetSelectionEnd().unwrap_or(0) as i32,
|
sequence,
|
||||||
)),
|
));
|
||||||
false,
|
},
|
||||||
)
|
(false, false) => {
|
||||||
} else if let Some(textarea) = elem.downcast::<HTMLTextAreaElement>() {
|
// Our `Document` doesn't have focus, and we intend to keep it
|
||||||
(
|
// this way.
|
||||||
Some((
|
},
|
||||||
textarea.Value().to_string(),
|
(true, false) => {
|
||||||
textarea.GetSelectionEnd().unwrap_or(0) as i32,
|
unreachable!(
|
||||||
)),
|
"Can't lose the document's focus without specifying \
|
||||||
true,
|
another one to focus"
|
||||||
)
|
);
|
||||||
} else {
|
},
|
||||||
(None, false)
|
|
||||||
};
|
|
||||||
self.send_to_embedder(EmbedderMsg::ShowIME(
|
|
||||||
self.webview_id(),
|
|
||||||
kind,
|
|
||||||
text,
|
|
||||||
multiline,
|
|
||||||
DeviceIntRect::from_untyped(&rect.to_box2d()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1352,7 +1552,10 @@ impl Document {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.begin_focus_transaction();
|
self.begin_focus_transaction();
|
||||||
self.request_focus(Some(&*el), FocusType::Element, can_gc);
|
// Try to focus `el`. If it's not focusable, focus the document
|
||||||
|
// instead.
|
||||||
|
self.request_focus(None, FocusInitiator::Local, can_gc);
|
||||||
|
self.request_focus(Some(&*el), FocusInitiator::Local, can_gc);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(
|
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(
|
||||||
|
@ -1390,7 +1593,9 @@ impl Document {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let MouseButtonAction::Click = event.action {
|
if let MouseButtonAction::Click = event.action {
|
||||||
self.commit_focus_transaction(FocusType::Element, can_gc);
|
if self.focus_transaction.borrow().is_some() {
|
||||||
|
self.commit_focus_transaction(FocusInitiator::Local, can_gc);
|
||||||
|
}
|
||||||
self.maybe_fire_dblclick(
|
self.maybe_fire_dblclick(
|
||||||
hit_test_result.point_in_viewport,
|
hit_test_result.point_in_viewport,
|
||||||
node,
|
node,
|
||||||
|
@ -2217,7 +2422,7 @@ impl Document {
|
||||||
ImeEvent::Dismissed => {
|
ImeEvent::Dismissed => {
|
||||||
self.request_focus(
|
self.request_focus(
|
||||||
self.GetBody().as_ref().map(|e| e.upcast()),
|
self.GetBody().as_ref().map(|e| e.upcast()),
|
||||||
FocusType::Element,
|
FocusInitiator::Local,
|
||||||
can_gc,
|
can_gc,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -3196,7 +3401,7 @@ impl Document {
|
||||||
fn fire_focus_event(
|
fn fire_focus_event(
|
||||||
&self,
|
&self,
|
||||||
focus_event_type: FocusEventType,
|
focus_event_type: FocusEventType,
|
||||||
node: &Node,
|
event_target: &EventTarget,
|
||||||
related_target: Option<&EventTarget>,
|
related_target: Option<&EventTarget>,
|
||||||
can_gc: CanGc,
|
can_gc: CanGc,
|
||||||
) {
|
) {
|
||||||
|
@ -3216,8 +3421,7 @@ impl Document {
|
||||||
);
|
);
|
||||||
let event = event.upcast::<Event>();
|
let event = event.upcast::<Event>();
|
||||||
event.set_trusted(true);
|
event.set_trusted(true);
|
||||||
let target = node.upcast();
|
event.fire(event_target, can_gc);
|
||||||
event.fire(target, can_gc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://html.spec.whatwg.org/multipage/#cookie-averse-document-object>
|
/// <https://html.spec.whatwg.org/multipage/#cookie-averse-document-object>
|
||||||
|
@ -3797,6 +4001,8 @@ impl Document {
|
||||||
.and_then(|charset| Encoding::for_label(charset.as_bytes()))
|
.and_then(|charset| Encoding::for_label(charset.as_bytes()))
|
||||||
.unwrap_or(UTF_8);
|
.unwrap_or(UTF_8);
|
||||||
|
|
||||||
|
let has_focus = window.parent_info().is_none();
|
||||||
|
|
||||||
let has_browsing_context = has_browsing_context == HasBrowsingContext::Yes;
|
let has_browsing_context = has_browsing_context == HasBrowsingContext::Yes;
|
||||||
|
|
||||||
Document {
|
Document {
|
||||||
|
@ -3844,8 +4050,10 @@ impl Document {
|
||||||
stylesheet_list: MutNullableDom::new(None),
|
stylesheet_list: MutNullableDom::new(None),
|
||||||
ready_state: Cell::new(ready_state),
|
ready_state: Cell::new(ready_state),
|
||||||
domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched),
|
domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched),
|
||||||
focus_transaction: DomRefCell::new(FocusTransaction::NotInTransaction),
|
focus_transaction: DomRefCell::new(None),
|
||||||
focused: Default::default(),
|
focused: Default::default(),
|
||||||
|
focus_sequence: Cell::new(FocusSequenceNumber::default()),
|
||||||
|
has_focus: Cell::new(has_focus),
|
||||||
current_script: Default::default(),
|
current_script: Default::default(),
|
||||||
pending_parsing_blocking_script: Default::default(),
|
pending_parsing_blocking_script: Default::default(),
|
||||||
script_blocking_stylesheets_count: Cell::new(0u32),
|
script_blocking_stylesheets_count: Cell::new(0u32),
|
||||||
|
@ -4991,12 +5199,34 @@ impl DocumentMethods<crate::DomTypeHolder> for Document {
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-document-hasfocus
|
// https://html.spec.whatwg.org/multipage/#dom-document-hasfocus
|
||||||
fn HasFocus(&self) -> bool {
|
fn HasFocus(&self) -> bool {
|
||||||
// Step 1-2.
|
// <https://html.spec.whatwg.org/multipage/#has-focus-steps>
|
||||||
if self.window().parent_info().is_none() && self.is_fully_active() {
|
//
|
||||||
return true;
|
// > The has focus steps, given a `Document` object `target`, are as
|
||||||
|
// > follows:
|
||||||
|
// >
|
||||||
|
// > 1. If `target`'s browsing context's top-level browsing context does
|
||||||
|
// > not have system focus, then return false.
|
||||||
|
|
||||||
|
// > 2. Let `candidate` be `target`'s browsing context's top-level
|
||||||
|
// > browsing context's active document.
|
||||||
|
// >
|
||||||
|
// > 3. While true:
|
||||||
|
// >
|
||||||
|
// > 3.1. If `candidate` is target, then return true.
|
||||||
|
// >
|
||||||
|
// > 3.2. If the focused area of `candidate` is a browsing context
|
||||||
|
// > container with a non-null nested browsing context, then set
|
||||||
|
// > `candidate` to the active document of that browsing context
|
||||||
|
// > container's nested browsing context.
|
||||||
|
// >
|
||||||
|
// > 3.3. Otherwise, return false.
|
||||||
|
if self.window().parent_info().is_none() {
|
||||||
|
// 2 → 3 → (3.1 || ⋯ → 3.3)
|
||||||
|
self.is_fully_active()
|
||||||
|
} else {
|
||||||
|
// 2 → 3 → 3.2 → (⋯ → 3.1 || ⋯ → 3.3)
|
||||||
|
self.is_fully_active() && self.has_focus.get()
|
||||||
}
|
}
|
||||||
// TODO Step 3.
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-document-domain
|
// https://html.spec.whatwg.org/multipage/#dom-document-domain
|
||||||
|
@ -6399,6 +6629,17 @@ pub(crate) enum FocusType {
|
||||||
Parent, // Focusing a parent element (an iframe)
|
Parent, // Focusing a parent element (an iframe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specifies the initiator of a focus operation.
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum FocusInitiator {
|
||||||
|
/// The operation is initiated by this document and to be broadcasted
|
||||||
|
/// through the constellation.
|
||||||
|
Local,
|
||||||
|
/// The operation is initiated somewhere else, and we are updating our
|
||||||
|
/// internal state accordingly.
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
/// Focus events
|
/// Focus events
|
||||||
pub(crate) enum FocusEventType {
|
pub(crate) enum FocusEventType {
|
||||||
Focus, // Element gained focus. Doesn't bubble.
|
Focus, // Element gained focus. Doesn't bubble.
|
||||||
|
|
|
@ -32,7 +32,7 @@ use crate::dom::bindings::str::DOMString;
|
||||||
use crate::dom::characterdata::CharacterData;
|
use crate::dom::characterdata::CharacterData;
|
||||||
use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner};
|
use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner};
|
||||||
use crate::dom::customelementregistry::CallbackReaction;
|
use crate::dom::customelementregistry::CallbackReaction;
|
||||||
use crate::dom::document::{Document, FocusType};
|
use crate::dom::document::{Document, FocusInitiator};
|
||||||
use crate::dom::documentfragment::DocumentFragment;
|
use crate::dom::documentfragment::DocumentFragment;
|
||||||
use crate::dom::domstringmap::DOMStringMap;
|
use crate::dom::domstringmap::DOMStringMap;
|
||||||
use crate::dom::element::{AttributeMutation, Element};
|
use crate::dom::element::{AttributeMutation, Element};
|
||||||
|
@ -415,18 +415,19 @@ impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement {
|
||||||
// TODO: Mark the element as locked for focus and run the focusing steps.
|
// TODO: Mark the element as locked for focus and run the focusing steps.
|
||||||
// https://html.spec.whatwg.org/multipage/#focusing-steps
|
// https://html.spec.whatwg.org/multipage/#focusing-steps
|
||||||
let document = self.owner_document();
|
let document = self.owner_document();
|
||||||
document.request_focus(Some(self.upcast()), FocusType::Element, can_gc);
|
document.request_focus(Some(self.upcast()), FocusInitiator::Local, can_gc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-blur
|
// https://html.spec.whatwg.org/multipage/#dom-blur
|
||||||
fn Blur(&self, can_gc: CanGc) {
|
fn Blur(&self, can_gc: CanGc) {
|
||||||
// TODO: Run the unfocusing steps.
|
// TODO: Run the unfocusing steps. Focus the top-level document, not
|
||||||
|
// the current document.
|
||||||
if !self.as_element().focus_state() {
|
if !self.as_element().focus_state() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// https://html.spec.whatwg.org/multipage/#unfocusing-steps
|
// https://html.spec.whatwg.org/multipage/#unfocusing-steps
|
||||||
let document = self.owner_document();
|
let document = self.owner_document();
|
||||||
document.request_focus(None, FocusType::Element, can_gc);
|
document.request_focus(None, FocusInitiator::Local, can_gc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent
|
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent
|
||||||
|
|
|
@ -787,6 +787,32 @@ impl WindowMethods<crate::DomTypeHolder> for Window {
|
||||||
doc.abort(can_gc);
|
doc.abort(can_gc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <https://html.spec.whatwg.org/multipage/#dom-window-focus>
|
||||||
|
fn Focus(&self) {
|
||||||
|
// > 1. Let `current` be this `Window` object's browsing context.
|
||||||
|
// >
|
||||||
|
// > 2. If `current` is null, then return.
|
||||||
|
let current = match self.undiscarded_window_proxy() {
|
||||||
|
Some(proxy) => proxy,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// > 3. Run the focusing steps with `current`.
|
||||||
|
current.focus();
|
||||||
|
|
||||||
|
// > 4. If current is a top-level browsing context, user agents are
|
||||||
|
// > encouraged to trigger some sort of notification to indicate to
|
||||||
|
// > the user that the page is attempting to gain focus.
|
||||||
|
//
|
||||||
|
// TODO: Step 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/#dom-window-blur
|
||||||
|
fn Blur(&self) {
|
||||||
|
// > User agents are encouraged to ignore calls to this `blur()` method
|
||||||
|
// > entirely.
|
||||||
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-open
|
// https://html.spec.whatwg.org/multipage/#dom-open
|
||||||
fn Open(
|
fn Open(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -620,6 +620,23 @@ impl WindowProxy {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run [the focusing steps] with this browsing context.
|
||||||
|
///
|
||||||
|
/// [the focusing steps]: https://html.spec.whatwg.org/multipage/#focusing-steps
|
||||||
|
pub fn focus(&self) {
|
||||||
|
debug!(
|
||||||
|
"Requesting the constellation to initiate a focus operation for \
|
||||||
|
browsing context {}",
|
||||||
|
self.browsing_context_id()
|
||||||
|
);
|
||||||
|
self.global()
|
||||||
|
.script_to_constellation_chan()
|
||||||
|
.send(ScriptToConstellationMessage::FocusRemoteDocument(
|
||||||
|
self.browsing_context_id(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
/// Change the Window that this WindowProxy resolves to.
|
/// Change the Window that this WindowProxy resolves to.
|
||||||
// TODO: support setting the window proxy to a dummy value,
|
// TODO: support setting the window proxy to a dummy value,
|
||||||
|
|
|
@ -72,6 +72,8 @@ impl MixedMessage {
|
||||||
ScriptThreadMessage::UpdateHistoryState(id, ..) => Some(*id),
|
ScriptThreadMessage::UpdateHistoryState(id, ..) => Some(*id),
|
||||||
ScriptThreadMessage::RemoveHistoryStates(id, ..) => Some(*id),
|
ScriptThreadMessage::RemoveHistoryStates(id, ..) => Some(*id),
|
||||||
ScriptThreadMessage::FocusIFrame(id, ..) => Some(*id),
|
ScriptThreadMessage::FocusIFrame(id, ..) => Some(*id),
|
||||||
|
ScriptThreadMessage::FocusDocument(id, ..) => Some(*id),
|
||||||
|
ScriptThreadMessage::Unfocus(id, ..) => Some(*id),
|
||||||
ScriptThreadMessage::WebDriverScriptCommand(id, ..) => Some(*id),
|
ScriptThreadMessage::WebDriverScriptCommand(id, ..) => Some(*id),
|
||||||
ScriptThreadMessage::TickAllAnimations(..) => None,
|
ScriptThreadMessage::TickAllAnimations(..) => None,
|
||||||
ScriptThreadMessage::WebFontLoaded(id, ..) => Some(*id),
|
ScriptThreadMessage::WebFontLoaded(id, ..) => Some(*id),
|
||||||
|
|
|
@ -50,8 +50,9 @@ use devtools_traits::{
|
||||||
};
|
};
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
CompositorHitTestResult, EmbedderMsg, InputEvent, MediaSessionActionType, MouseButton,
|
CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent, MediaSessionActionType,
|
||||||
MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverScriptCommand,
|
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails,
|
||||||
|
WebDriverScriptCommand,
|
||||||
};
|
};
|
||||||
use euclid::Point2D;
|
use euclid::Point2D;
|
||||||
use euclid::default::Rect;
|
use euclid::default::Rect;
|
||||||
|
@ -124,7 +125,7 @@ use crate::dom::customelementregistry::{
|
||||||
CallbackReaction, CustomElementDefinition, CustomElementReactionStack,
|
CallbackReaction, CustomElementDefinition, CustomElementReactionStack,
|
||||||
};
|
};
|
||||||
use crate::dom::document::{
|
use crate::dom::document::{
|
||||||
Document, DocumentSource, FocusType, HasBrowsingContext, IsHTMLDocument, TouchEventResult,
|
Document, DocumentSource, FocusInitiator, HasBrowsingContext, IsHTMLDocument, TouchEventResult,
|
||||||
};
|
};
|
||||||
use crate::dom::element::Element;
|
use crate::dom::element::Element;
|
||||||
use crate::dom::globalscope::GlobalScope;
|
use crate::dom::globalscope::GlobalScope;
|
||||||
|
@ -1803,8 +1804,14 @@ impl ScriptThread {
|
||||||
ScriptThreadMessage::RemoveHistoryStates(pipeline_id, history_states) => {
|
ScriptThreadMessage::RemoveHistoryStates(pipeline_id, history_states) => {
|
||||||
self.handle_remove_history_states(pipeline_id, history_states)
|
self.handle_remove_history_states(pipeline_id, history_states)
|
||||||
},
|
},
|
||||||
ScriptThreadMessage::FocusIFrame(parent_pipeline_id, frame_id) => {
|
ScriptThreadMessage::FocusIFrame(parent_pipeline_id, frame_id, sequence) => {
|
||||||
self.handle_focus_iframe_msg(parent_pipeline_id, frame_id, can_gc)
|
self.handle_focus_iframe_msg(parent_pipeline_id, frame_id, sequence, can_gc)
|
||||||
|
},
|
||||||
|
ScriptThreadMessage::FocusDocument(pipeline_id, sequence) => {
|
||||||
|
self.handle_focus_document_msg(pipeline_id, sequence, can_gc)
|
||||||
|
},
|
||||||
|
ScriptThreadMessage::Unfocus(pipeline_id, sequence) => {
|
||||||
|
self.handle_unfocus_msg(pipeline_id, sequence, can_gc)
|
||||||
},
|
},
|
||||||
ScriptThreadMessage::WebDriverScriptCommand(pipeline_id, msg) => {
|
ScriptThreadMessage::WebDriverScriptCommand(pipeline_id, msg) => {
|
||||||
self.handle_webdriver_msg(pipeline_id, msg, can_gc)
|
self.handle_webdriver_msg(pipeline_id, msg, can_gc)
|
||||||
|
@ -2513,6 +2520,7 @@ impl ScriptThread {
|
||||||
&self,
|
&self,
|
||||||
parent_pipeline_id: PipelineId,
|
parent_pipeline_id: PipelineId,
|
||||||
browsing_context_id: BrowsingContextId,
|
browsing_context_id: BrowsingContextId,
|
||||||
|
sequence: FocusSequenceNumber,
|
||||||
can_gc: CanGc,
|
can_gc: CanGc,
|
||||||
) {
|
) {
|
||||||
let document = self
|
let document = self
|
||||||
|
@ -2532,7 +2540,65 @@ impl ScriptThread {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
document.request_focus(Some(&iframe_element_root), FocusType::Parent, can_gc);
|
if document.get_focus_sequence() > sequence {
|
||||||
|
debug!(
|
||||||
|
"Disregarding the FocusIFrame message because the contained sequence number is \
|
||||||
|
too old ({:?} < {:?})",
|
||||||
|
sequence,
|
||||||
|
document.get_focus_sequence()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.request_focus(Some(&iframe_element_root), FocusInitiator::Remote, can_gc);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_focus_document_msg(
|
||||||
|
&self,
|
||||||
|
pipeline_id: PipelineId,
|
||||||
|
sequence: FocusSequenceNumber,
|
||||||
|
can_gc: CanGc,
|
||||||
|
) {
|
||||||
|
if let Some(doc) = self.documents.borrow().find_document(pipeline_id) {
|
||||||
|
if doc.get_focus_sequence() > sequence {
|
||||||
|
debug!(
|
||||||
|
"Disregarding the FocusDocument message because the contained sequence number is \
|
||||||
|
too old ({:?} < {:?})",
|
||||||
|
sequence,
|
||||||
|
doc.get_focus_sequence()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doc.request_focus(None, FocusInitiator::Remote, can_gc);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Couldn't find document by pipleline_id:{pipeline_id:?} when handle_focus_document_msg."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_unfocus_msg(
|
||||||
|
&self,
|
||||||
|
pipeline_id: PipelineId,
|
||||||
|
sequence: FocusSequenceNumber,
|
||||||
|
can_gc: CanGc,
|
||||||
|
) {
|
||||||
|
if let Some(doc) = self.documents.borrow().find_document(pipeline_id) {
|
||||||
|
if doc.get_focus_sequence() > sequence {
|
||||||
|
debug!(
|
||||||
|
"Disregarding the Unfocus message because the contained sequence number is \
|
||||||
|
too old ({:?} < {:?})",
|
||||||
|
sequence,
|
||||||
|
doc.get_focus_sequence()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doc.handle_container_unfocus(can_gc);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Couldn't find document by pipleline_id:{pipeline_id:?} when handle_unfocus_msg."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_post_message_msg(
|
fn handle_post_message_msg(
|
||||||
|
|
|
@ -27,8 +27,8 @@
|
||||||
[CrossOriginCallable] undefined close();
|
[CrossOriginCallable] undefined close();
|
||||||
[CrossOriginReadable] readonly attribute boolean closed;
|
[CrossOriginReadable] readonly attribute boolean closed;
|
||||||
undefined stop();
|
undefined stop();
|
||||||
//[CrossOriginCallable] void focus();
|
[CrossOriginCallable] undefined focus();
|
||||||
//[CrossOriginCallable] void blur();
|
[CrossOriginCallable] undefined blur();
|
||||||
|
|
||||||
// other browsing contexts
|
// other browsing contexts
|
||||||
[Replaceable, CrossOriginReadable] readonly attribute WindowProxy frames;
|
[Replaceable, CrossOriginReadable] readonly attribute WindowProxy frames;
|
||||||
|
|
|
@ -15,7 +15,8 @@ use base::id::{
|
||||||
use canvas_traits::canvas::{CanvasId, CanvasMsg};
|
use canvas_traits::canvas::{CanvasId, CanvasMsg};
|
||||||
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
|
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
AnimationState, EmbedderMsg, MediaSessionEvent, TouchEventResult, ViewportDetails,
|
AnimationState, EmbedderMsg, FocusSequenceNumber, MediaSessionEvent, TouchEventResult,
|
||||||
|
ViewportDetails,
|
||||||
};
|
};
|
||||||
use euclid::default::Size2D as UntypedSize2D;
|
use euclid::default::Size2D as UntypedSize2D;
|
||||||
use http::{HeaderMap, Method};
|
use http::{HeaderMap, Method};
|
||||||
|
@ -519,8 +520,21 @@ pub enum ScriptToConstellationMessage {
|
||||||
UntypedSize2D<u64>,
|
UntypedSize2D<u64>,
|
||||||
IpcSender<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>,
|
IpcSender<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>,
|
||||||
),
|
),
|
||||||
/// Notifies the constellation that this frame has received focus.
|
/// Notifies the constellation that this pipeline is requesting focus.
|
||||||
Focus,
|
///
|
||||||
|
/// When this message is sent, the sender pipeline has already its local
|
||||||
|
/// focus state updated. The constellation, after receiving this message,
|
||||||
|
/// will broadcast messages to other pipelines that are affected by this
|
||||||
|
/// focus operation.
|
||||||
|
///
|
||||||
|
/// The first field contains the browsing context ID of the container
|
||||||
|
/// element if one was focused.
|
||||||
|
///
|
||||||
|
/// The second field is a sequence number that the constellation should use
|
||||||
|
/// when sending a focus-related message to the sender pipeline next time.
|
||||||
|
Focus(Option<BrowsingContextId>, FocusSequenceNumber),
|
||||||
|
/// Requests the constellation to focus the specified browsing context.
|
||||||
|
FocusRemoteDocument(BrowsingContextId),
|
||||||
/// Get the top-level browsing context info for a given browsing context.
|
/// Get the top-level browsing context info for a given browsing context.
|
||||||
GetTopForBrowsingContext(BrowsingContextId, IpcSender<Option<WebViewId>>),
|
GetTopForBrowsingContext(BrowsingContextId, IpcSender<Option<WebViewId>>),
|
||||||
/// Get the browsing context id of the browsing context in which pipeline is
|
/// Get the browsing context id of the browsing context in which pipeline is
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub mod user_content_manager;
|
||||||
mod webdriver;
|
mod webdriver;
|
||||||
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::fmt::{Debug, Error, Formatter};
|
use std::fmt::{Debug, Display, Error, Formatter};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -784,3 +784,76 @@ pub enum AnimationState {
|
||||||
/// No animations are active but callbacks are queued
|
/// No animations are active but callbacks are queued
|
||||||
NoAnimationCallbacksPresent,
|
NoAnimationCallbacksPresent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A sequence number generated by a script thread for its pipelines. The
|
||||||
|
/// constellation attaches the target pipeline's last seen `FocusSequenceNumber`
|
||||||
|
/// to every focus-related message it sends.
|
||||||
|
///
|
||||||
|
/// This is used to resolve the inconsistency that occurs due to bidirectional
|
||||||
|
/// focus state synchronization and provide eventual consistency. Example:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// script constellation
|
||||||
|
/// -----------------------------------------------------------------------
|
||||||
|
/// send ActivateDocument ----------> receive ActivateDocument
|
||||||
|
/// ,---- send FocusDocument
|
||||||
|
/// |
|
||||||
|
/// focus an iframe |
|
||||||
|
/// send Focus -----------------|---> receive Focus
|
||||||
|
/// | focus the iframe's content document
|
||||||
|
/// receive FocusDocument <-----' send FocusDocument to the content pipeline --> ...
|
||||||
|
/// unfocus the iframe
|
||||||
|
/// focus the document
|
||||||
|
///
|
||||||
|
/// Final state: Final state:
|
||||||
|
/// the iframe is not focused the iframe is focused
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// When the above sequence completes, from the script thread's point of view,
|
||||||
|
/// the iframe is unfocused, but from the constellation's point of view, the
|
||||||
|
/// iframe is still focused.
|
||||||
|
///
|
||||||
|
/// This inconsistency can be resolved by associating a sequence number to each
|
||||||
|
/// message. Whenever a script thread initiates a focus operation, it generates
|
||||||
|
/// and sends a brand new sequence number. The constellation attaches the
|
||||||
|
/// last-received sequence number to each message it sends. This way, the script
|
||||||
|
/// thread can discard out-dated incoming focus messages, and eventually, all
|
||||||
|
/// actors converge to the consistent state which is determined based on the
|
||||||
|
/// last focus message received by the constellation.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// script constellation
|
||||||
|
/// -----------------------------------------------------------------------
|
||||||
|
/// send ActivateDocument ----------> receive ActivateDocument
|
||||||
|
/// ,---- send FocusDocument (0)
|
||||||
|
/// |
|
||||||
|
/// seq_number += 1 |
|
||||||
|
/// focus an iframe |
|
||||||
|
/// send Focus (1) -------------|---> receive Focus (1)
|
||||||
|
/// | focus the iframe's content document
|
||||||
|
/// receive FocusDocument (0) <-' send FocusDocument to the content pipeline --> ...
|
||||||
|
/// ignore it because 0 < 1
|
||||||
|
///
|
||||||
|
/// Final state: Final state:
|
||||||
|
/// the iframe is focused the iframe is focused
|
||||||
|
/// ```
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
MallocSizeOf,
|
||||||
|
PartialEq,
|
||||||
|
Serialize,
|
||||||
|
PartialOrd,
|
||||||
|
)]
|
||||||
|
pub struct FocusSequenceNumber(pub u64);
|
||||||
|
|
||||||
|
impl Display for FocusSequenceNumber {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
|
||||||
|
Display::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ use crossbeam_channel::{RecvTimeoutError, Sender};
|
||||||
use devtools_traits::ScriptToDevtoolsControlMsg;
|
use devtools_traits::ScriptToDevtoolsControlMsg;
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
CompositorHitTestResult, InputEvent, MediaSessionActionType, Theme, ViewportDetails,
|
CompositorHitTestResult, FocusSequenceNumber, InputEvent, MediaSessionActionType, Theme,
|
||||||
WebDriverScriptCommand,
|
ViewportDetails, WebDriverScriptCommand,
|
||||||
};
|
};
|
||||||
use euclid::{Rect, Scale, Size2D, UnknownUnit};
|
use euclid::{Rect, Scale, Size2D, UnknownUnit};
|
||||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||||
|
@ -191,7 +191,15 @@ pub enum ScriptThreadMessage {
|
||||||
RemoveHistoryStates(PipelineId, Vec<HistoryStateId>),
|
RemoveHistoryStates(PipelineId, Vec<HistoryStateId>),
|
||||||
/// Set an iframe to be focused. Used when an element in an iframe gains focus.
|
/// Set an iframe to be focused. Used when an element in an iframe gains focus.
|
||||||
/// PipelineId is for the parent, BrowsingContextId is for the nested browsing context
|
/// PipelineId is for the parent, BrowsingContextId is for the nested browsing context
|
||||||
FocusIFrame(PipelineId, BrowsingContextId),
|
FocusIFrame(PipelineId, BrowsingContextId, FocusSequenceNumber),
|
||||||
|
/// Focus the document. Used when the container gains focus.
|
||||||
|
FocusDocument(PipelineId, FocusSequenceNumber),
|
||||||
|
/// Notifies that the document's container (e.g., an iframe) is not included
|
||||||
|
/// in the top-level browsing context's focus chain (not considering system
|
||||||
|
/// focus) anymore.
|
||||||
|
///
|
||||||
|
/// Obviously, this message is invalid for a top-level document.
|
||||||
|
Unfocus(PipelineId, FocusSequenceNumber),
|
||||||
/// Passes a webdriver command to the script thread for execution
|
/// Passes a webdriver command to the script thread for execution
|
||||||
WebDriverScriptCommand(PipelineId, WebDriverScriptCommand),
|
WebDriverScriptCommand(PipelineId, WebDriverScriptCommand),
|
||||||
/// Notifies script thread that all animations are done
|
/// Notifies script thread that all animations are done
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html]
|
|
||||||
[Check focus event and active element after focusing different site iframe then immediately focusing back]
|
|
||||||
expected: FAIL
|
|
|
@ -1,3 +0,0 @@
|
||||||
[activeelement-after-focusing-different-site-iframe.html]
|
|
||||||
[Check trailing events]
|
|
||||||
expected: FAIL
|
|
|
@ -1,2 +1,3 @@
|
||||||
[activeelement-after-focusing-same-site-iframe-contentwindow.html]
|
[activeelement-after-focusing-same-site-iframe-contentwindow.html]
|
||||||
expected: TIMEOUT
|
[Check result]
|
||||||
|
expected: FAIL
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[activeelement-after-focusing-same-site-iframe.html]
|
|
||||||
[Check trailing events]
|
|
||||||
expected: FAIL
|
|
|
@ -1,2 +1,3 @@
|
||||||
[activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html]
|
[activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html]
|
||||||
expected: TIMEOUT
|
[Check result]
|
||||||
|
expected: FAIL
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
[activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html]
|
[activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html]
|
||||||
expected: TIMEOUT
|
[Check result]
|
||||||
|
expected: FAIL
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
[focus-restoration-in-different-site-iframes-window.html]
|
[focus-restoration-in-different-site-iframes-window.html]
|
||||||
expected: TIMEOUT
|
[Check result]
|
||||||
|
expected: FAIL
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
[focus-restoration-in-same-site-iframes-window.html]
|
[focus-restoration-in-same-site-iframes-window.html]
|
||||||
expected: TIMEOUT
|
[Check result]
|
||||||
|
expected: FAIL
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[iframe-focuses-parent-same-site.html]
|
|
||||||
expected: TIMEOUT
|
|
|
@ -1,7 +1,4 @@
|
||||||
[cross-origin-objects-function-caching.html]
|
[cross-origin-objects-function-caching.html]
|
||||||
[Cross-origin Window methods are cached]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Cross-origin Location `replace` method is cached]
|
[Cross-origin Location `replace` method is cached]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[focus.window.html]
|
|
||||||
[focus]
|
|
||||||
expected: FAIL
|
|
|
@ -328,9 +328,3 @@
|
||||||
|
|
||||||
[A SecurityError exception must be thrown when window.stop is accessed from a different origin.]
|
[A SecurityError exception must be thrown when window.stop is accessed from a different origin.]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[A SecurityError exception should not be thrown when window.blur is accessed from a different origin.]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[A SecurityError exception should not be thrown when window.focus is accessed from a different origin.]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
[window-properties.https.html]
|
[window-properties.https.html]
|
||||||
[Window method: focus]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Window method: blur]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Window method: print]
|
[Window method: print]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
|
@ -1738,9 +1738,6 @@
|
||||||
[Document interface: attribute all]
|
[Document interface: attribute all]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Window interface: operation focus()]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Window interface: attribute scrollbars]
|
[Window interface: attribute scrollbars]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
@ -1870,9 +1867,6 @@
|
||||||
[Document interface: new Document() must inherit property "dir" with the proper type]
|
[Document interface: new Document() must inherit property "dir" with the proper type]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Window interface: window must inherit property "blur()" with the proper type]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Document interface: operation execCommand(DOMString, optional boolean, optional DOMString)]
|
[Document interface: operation execCommand(DOMString, optional boolean, optional DOMString)]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
@ -1897,9 +1891,6 @@
|
||||||
[Document interface: iframe.contentDocument must inherit property "queryCommandEnabled(DOMString)" with the proper type]
|
[Document interface: iframe.contentDocument must inherit property "queryCommandEnabled(DOMString)" with the proper type]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Window interface: operation blur()]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Document interface: iframe.contentDocument must inherit property "onslotchange" with the proper type]
|
[Document interface: iframe.contentDocument must inherit property "onslotchange" with the proper type]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
@ -1924,9 +1915,6 @@
|
||||||
[Document interface: documentWithHandlers must inherit property "onauxclick" with the proper type]
|
[Document interface: documentWithHandlers must inherit property "onauxclick" with the proper type]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Window interface: window must inherit property "focus()" with the proper type]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Document interface: documentWithHandlers must inherit property "onwebkitanimationend" with the proper type]
|
[Document interface: documentWithHandlers must inherit property "onwebkitanimationend" with the proper type]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
[event-listeners.window.html]
|
[event-listeners.window.html]
|
||||||
[Standard event listeners are to be removed from Window]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Standard event listeners are to be removed from Window for an active but not fully active document]
|
[Standard event listeners are to be removed from Window for an active but not fully active document]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Standard event listeners are to be removed from Window for a non-active document that is the associated Document of a Window (frame is removed)]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Custom event listeners are to be removed from Window for an active but not fully active document]
|
[Custom event listeners are to be removed from Window for an active but not fully active document]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
[global-object-implicit-this-value-cross-realm.html]
|
[global-object-implicit-this-value-cross-realm.html]
|
||||||
[Cross-realm global object's operation throws when called on incompatible object]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Cross-realm global object's operation called on null / undefined]
|
[Cross-realm global object's operation called on null / undefined]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
9
tests/wpt/mozilla/meta/MANIFEST.json
vendored
9
tests/wpt/mozilla/meta/MANIFEST.json
vendored
|
@ -12744,7 +12744,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"FocusEvent.html": [
|
"FocusEvent.html": [
|
||||||
"9e002c1088de060b5e7f94c4152bf9fb779c04cc",
|
"7fb7aebf2afbac7f68a16308b9cc5d4588b7022f",
|
||||||
[
|
[
|
||||||
null,
|
null,
|
||||||
{}
|
{}
|
||||||
|
@ -13278,6 +13278,13 @@
|
||||||
{}
|
{}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
"focus_inter_documents.html": [
|
||||||
|
"5c759772367e844066d1df0081917c9e129d09ec",
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
],
|
||||||
"follow-hyperlink.html": [
|
"follow-hyperlink.html": [
|
||||||
"6ac9eaeb5814a663988ed8c664c113072e329dc5",
|
"6ac9eaeb5814a663988ed8c664c113072e329dc5",
|
||||||
[
|
[
|
||||||
|
|
|
@ -48,13 +48,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
element: document.body,
|
|
||||||
expected_events: [
|
|
||||||
{element: input3, event_name: "blur"},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
|
|
207
tests/wpt/mozilla/tests/mozilla/focus_inter_documents.html
vendored
Normal file
207
tests/wpt/mozilla/tests/mozilla/focus_inter_documents.html
vendored
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<iframe id="f1"></iframe>
|
||||||
|
<iframe id="f2"></iframe>
|
||||||
|
<input id="d0">
|
||||||
|
<script>
|
||||||
|
|
||||||
|
/** Wait for an `event` event to be fired on `element`. Resolves to a boolean
|
||||||
|
* value indicating whether the event was fired within a predetermined period. */
|
||||||
|
async function waitForEvent(element, event) {
|
||||||
|
let listener;
|
||||||
|
try {
|
||||||
|
return await new Promise(resolve => {
|
||||||
|
setTimeout(() => resolve(false), 1000);
|
||||||
|
listener = () => resolve(true);
|
||||||
|
element.addEventListener(event, listener);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (listener) {
|
||||||
|
element.removeEventListener(event, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
await new Promise(r => window.onload = r);
|
||||||
|
|
||||||
|
const d0 = document.getElementById("d0");
|
||||||
|
|
||||||
|
// This test requires the document to have focus as a starting condition.
|
||||||
|
if (!document.hasFocus() || document.activeElement !== d0) {
|
||||||
|
const p = new Promise(r => d0.onfocus = r);
|
||||||
|
d0.focus();
|
||||||
|
await p;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_true(document.hasFocus(), "Document has focus as starting condition.");
|
||||||
|
assert_equals(document.activeElement, d0, "`d0` has focus as starting condition.");
|
||||||
|
}, "Starting condition");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const d0 = document.getElementById("d0");
|
||||||
|
const f1 = document.getElementById("f1");
|
||||||
|
f1.contentDocument.body.innerHTML = '<input id=d1>';
|
||||||
|
const d1 = f1.contentDocument.getElementById("d1");
|
||||||
|
|
||||||
|
const p0 = waitForEvent(d1, 'focus');
|
||||||
|
const p1 = waitForEvent(f1, 'focus');
|
||||||
|
const p2 = waitForEvent(f1.contentWindow, 'focus');
|
||||||
|
const p3 = waitForEvent(d0, 'blur');
|
||||||
|
|
||||||
|
d1.focus();
|
||||||
|
|
||||||
|
assert_true(await p0, "`d1.focus` fires in time");
|
||||||
|
await p1; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
|
||||||
|
assert_true(await p2, "`f1.contentWindow.focus` fires in time");
|
||||||
|
assert_true(await p3, "`d0.blur` fires in time");
|
||||||
|
|
||||||
|
assert_equals(document.activeElement, f1, "The top-level document's activeElement is `f1`");
|
||||||
|
assert_true(f1.contentDocument.hasFocus(), "f1's contentDocument has focus");
|
||||||
|
assert_equals(f1.contentDocument.activeElement, d1, "f1's contentDocument's activeElement is `d1`");
|
||||||
|
}, "Focusing an element in a nested browsing context also focuses the container");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const f1 = document.getElementById("f1");
|
||||||
|
const d1 = f1.contentDocument.getElementById("d1");
|
||||||
|
|
||||||
|
const f2 = document.getElementById("f2");
|
||||||
|
f2.contentDocument.body.innerHTML = '<input id=d2>';
|
||||||
|
const d2 = f2.contentDocument.getElementById("d2");
|
||||||
|
|
||||||
|
const p0 = waitForEvent(d1, 'blur');
|
||||||
|
const p1 = waitForEvent(f1, 'blur');
|
||||||
|
const p2 = waitForEvent(f1.contentWindow, 'blur');
|
||||||
|
|
||||||
|
d2.focus();
|
||||||
|
|
||||||
|
assert_true(await p0, "`d1.blur` fires in time");
|
||||||
|
await p1; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
|
||||||
|
assert_true(await p2, "`f1.contentWindow.blur` fires in time");
|
||||||
|
|
||||||
|
// Wait for any ongoing execution of the focus update steps to complete
|
||||||
|
await new Promise(r => window.setTimeout(r, 0));
|
||||||
|
|
||||||
|
assert_equals(document.activeElement, f2, "The top-level document's activeElement is `f2`");
|
||||||
|
assert_true(f2.contentDocument.hasFocus(), "f2's contentDocument has focus");
|
||||||
|
assert_equals(f2.contentDocument.activeElement, d2, "f2's contentDocument's activeElement is `d2`");
|
||||||
|
assert_false(f1.contentDocument.hasFocus(), "f1's contentDocument does not have focus");
|
||||||
|
assert_equals(f1.contentDocument.activeElement, f1.contentDocument.body, "f1's contentDocument's activeElement is its body");
|
||||||
|
}, "Focusing an element in a different container also unfocuses the previously focused element and its container");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const d0 = document.getElementById("d0");
|
||||||
|
|
||||||
|
const f2 = document.getElementById("f2");
|
||||||
|
const d2 = f2.contentDocument.getElementById("d2");
|
||||||
|
|
||||||
|
const p0 = waitForEvent(d2, 'blur');
|
||||||
|
const p1 = waitForEvent(f2, 'blur');
|
||||||
|
const p2 = waitForEvent(f2.contentWindow, 'blur');
|
||||||
|
const p3 = waitForEvent(d0, 'focus');
|
||||||
|
|
||||||
|
d0.focus();
|
||||||
|
|
||||||
|
assert_true(await p0, "`d2.blur` fires in time");
|
||||||
|
await p1; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
|
||||||
|
assert_true(await p2, "`f2.contentWindow.blur` fires in time");
|
||||||
|
assert_true(await p3, "`d0.focus` fires in time");
|
||||||
|
|
||||||
|
// Wait for any ongoing execution of the focus update steps to complete
|
||||||
|
await new Promise(r => window.setTimeout(r, 0));
|
||||||
|
|
||||||
|
assert_equals(document.activeElement, d0, "The top-level document's activeElement is `d0`");
|
||||||
|
assert_false(f2.contentDocument.hasFocus(), "f2's contentDocument does not have focus");
|
||||||
|
assert_equals(f2.contentDocument.activeElement, f2.contentDocument.body, "f2's contentDocument's activeElement is its body");
|
||||||
|
}, "Unfocusing a container also unfocuses any focused elements within");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const f1 = document.getElementById("f1");
|
||||||
|
|
||||||
|
const p0 = waitForEvent(f1, 'focus');
|
||||||
|
const p1 = waitForEvent(f1.contentWindow, 'focus');
|
||||||
|
|
||||||
|
f1.focus();
|
||||||
|
|
||||||
|
await p0; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
|
||||||
|
assert_true(await p1, "`f1.contentWindow.focus` fires in time");
|
||||||
|
|
||||||
|
assert_equals(document.activeElement, f1, "The top-level document's activeElement is `f1`");
|
||||||
|
assert_true(f1.contentDocument.hasFocus(), "f1's contentDocument has focus");
|
||||||
|
}, "Focusing a container changes the contained document's 'has focus steps' result");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const f1 = document.getElementById("f1");
|
||||||
|
|
||||||
|
// `f1` should be focused because of the previous step
|
||||||
|
assert_equals(document.activeElement, f1, "The top-level document's activeElement is `f1`");
|
||||||
|
|
||||||
|
// Navigate the focused container
|
||||||
|
const pLoad = new Promise(resolve => window.subframeIsReady = resolve);
|
||||||
|
f1.srcdoc = "<script>window.parent.subframeIsReady();</" + "script>";
|
||||||
|
await pLoad;
|
||||||
|
|
||||||
|
// Allow some delay before the document finally receives focus
|
||||||
|
if (!f1.contentDocument.hasFocus()) {
|
||||||
|
await waitForEvent(f1.contentWindow, 'focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_true(f1.contentDocument.hasFocus(), "f1's contentDocument has focus");
|
||||||
|
}, "When a focused container navigates, the new document should receive focus");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const f2 = document.getElementById("f2");
|
||||||
|
|
||||||
|
const p0 = waitForEvent(f2, 'focus');
|
||||||
|
const p1 = waitForEvent(f2.contentWindow, 'focus');
|
||||||
|
|
||||||
|
f2.contentWindow.focus();
|
||||||
|
|
||||||
|
await p0; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
|
||||||
|
assert_true(await p1, "`f2.contentWindow.focus` fires in time");
|
||||||
|
|
||||||
|
assert_equals(document.activeElement, f2, "The top-level document's activeElement is `f2`");
|
||||||
|
assert_true(f2.contentDocument.hasFocus(), "f2's contentDocument has focus");
|
||||||
|
}, "Focusing the window of a nested browsing context also focuses the container");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const f2 = document.getElementById("f2");
|
||||||
|
const d2 = f2.contentDocument.getElementById("d2");
|
||||||
|
|
||||||
|
{
|
||||||
|
const p = waitForEvent(d2, 'focus');
|
||||||
|
f2.focus();
|
||||||
|
d2.focus();
|
||||||
|
await p;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p0 = waitForEvent(d2, 'blur');
|
||||||
|
d2.blur();
|
||||||
|
assert_true(await p0, "`d2.blur` fires in time");
|
||||||
|
|
||||||
|
// FIXME: This passes on Firefox, Blink, and WebKit but is not spec-
|
||||||
|
// compliant. Per spec, the top-level document's viewport should be
|
||||||
|
// focused instead.
|
||||||
|
//
|
||||||
|
// <https://html.spec.whatwg.org/multipage/#get-the-focusable-area>
|
||||||
|
//
|
||||||
|
// > The unfocusing steps for an object `old focus target`` that is either a
|
||||||
|
// > focusable area or an element that is not a focusable area are as
|
||||||
|
// > follows: [...]
|
||||||
|
// >
|
||||||
|
// > 7. If `topDocument`'s browsing context has system focus, then run the
|
||||||
|
// > focusing steps for topDocument's viewport.
|
||||||
|
|
||||||
|
assert_equals(document.activeElement, f2, "The top-level document's activeElement is `f2`");
|
||||||
|
assert_equals(f2.contentDocument.activeElement, f2.contentDocument.body, "f2's contentDocument's activeElement is its body");
|
||||||
|
assert_true(f2.contentDocument.hasFocus(), "f2's contentDocument has focus");
|
||||||
|
}, "Blurring an element in a nested browsing context focuses its node document");
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue