0

Make KeyboardFocusableScrollers keyboard-focus but not mouse-focus

This (re-)introduces the concept of IsMouseFocusable, which
represents whether an element should be focused by the mouse. This
is now distinct from IsKeyboardFocusable: elements can be one of
these without being the other. Both require IsFocusable() to be
true.

This behavior is guarded behind the flag KeyboardFocusableScrollers.
When the flag disabled, IsMouseFocusable and IsKeyboardFocusable will
always return the same value. With the flag enabled, scrollers
(without an explicit tabindex) will be keyboard focusable, but will
not be mouse focusable.

Along the way, I ended up (thanks to a comment from dbaron@) doing
a refactor of `SupportsFocus()` and `IsFocusable()` (now called
`IsFocusableState()`) to return a tri-state enum, which explicitly
keeps track of whether the element supports or is focusable due to
keyboard focusable scrollers. That ends up making the logic for
`IsMouseFocusable()` and `IsKeyboardFocusable()` a lot easier to
grok since there aren't hidden dependencies on the logic in
`SupportsFocus()`. I also removed the default parameter values for
both methods, since they were passed explicitly almost everywhere.

Bug: 361072782
Co-authored-by: Di Zhang <dizhangg@chromium.org>
Change-Id: I4f6e2a4f4e6b27ec637315ea17aafe6da39ef235
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5812197
Reviewed-by: David Baron <dbaron@chromium.org>
Reviewed-by: Aaron Leventhal <aleventhal@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1349407}
This commit is contained in:
Mason Freed
2024-08-30 20:54:14 +00:00
committed by Chromium LUCI CQ
parent 0f73fc442e
commit 199d1a7750
55 changed files with 485 additions and 173 deletions

@ -1704,7 +1704,7 @@ Element* ContainerNode::GetAutofocusDelegate() const {
// focusable_area is not click-focusable and the call was initiated by the
// user clicking. I don't believe this is currently possible, so DCHECK
// instead.
DCHECK(focusable_area->IsFocusable());
DCHECK(focusable_area->IsMouseFocusable());
return focusable_area;
}

@ -2844,7 +2844,7 @@ void Document::AttachCompositorTimeline(cc::AnimationTimeline* timeline) const {
void Document::ClearFocusedElementIfNeeded() {
if (clear_focused_element_timer_.IsActive() || !focused_element_ ||
focused_element_->IsFocusable(
Element::UpdateBehavior::kNoneForClearingFocus)) {
Element::UpdateBehavior::kNoneForFocusManagement)) {
return;
}
clear_focused_element_timer_.StartOneShot(base::TimeDelta(), FROM_HERE);

@ -6630,6 +6630,9 @@ bool Element::CanBeKeyboardFocusableScroller(
// However, some lifecycle stages don't allow update here so we use
// UpdateBehavior to guard this behavior.
switch (update_behavior) {
case UpdateBehavior::kAssertNoLayoutUpdates:
CHECK(!GetDocument().NeedsLayoutTreeUpdate());
[[fallthrough]];
case UpdateBehavior::kStyleAndLayout:
GetDocument().UpdateStyleAndLayoutForNode(this,
DocumentUpdateReason::kFocus);
@ -6639,8 +6642,7 @@ bool Element::CanBeKeyboardFocusableScroller(
return false;
}
break;
case UpdateBehavior::kNoneForIsFocused:
case UpdateBehavior::kNoneForClearingFocus:
case UpdateBehavior::kNoneForFocusManagement:
DCHECK(!DisplayLockUtilities::IsDisplayLockedPreventingPaint(this));
break;
}
@ -6656,9 +6658,8 @@ bool Element::CanBeKeyboardFocusableScroller(
// were recomputed.
bool Element::IsKeyboardFocusableScroller(
UpdateBehavior update_behavior) const {
if (!CanBeKeyboardFocusableScroller(update_behavior)) {
return false;
}
DCHECK(
CanBeKeyboardFocusableScroller(UpdateBehavior::kAssertNoLayoutUpdates));
// This condition is to avoid clearing the focus in the middle of a
// keyboard focused scrolling event. If the scroller is currently focused,
// then let it continue to be focused even if focusable children are added.
@ -6678,34 +6679,76 @@ bool Element::IsKeyboardFocusableScroller(
}
bool Element::IsKeyboardFocusable(UpdateBehavior update_behavior) const {
if (!Element::IsFocusable(update_behavior)) {
FocusableState focusable_state = Element::IsFocusableState(update_behavior);
if (focusable_state == FocusableState::kNotFocusable) {
return false;
}
if (!HasElementFlag(ElementFlags::kTabIndexWasSetExplicitly) &&
CanBeKeyboardFocusableScroller(update_behavior)) {
// If the element has a tabindex, then that determines keyboard
// focusability.
if (HasElementFlag(ElementFlags::kTabIndexWasSetExplicitly)) {
return GetIntegralAttribute(html_names::kTabindexAttr, 0) >= 0;
}
// If the element is only potentially focusable because it *might* be a
// keyboard-focusable scroller, then check whether it actually is.
if (focusable_state == FocusableState::kKeyboardFocusableScroller) {
return IsKeyboardFocusableScroller(update_behavior);
}
return GetIntegralAttribute(html_names::kTabindexAttr, 0) >= 0;
// Otherwise, if the element is focusable, then it should be keyboard-
// focusable.
DCHECK_EQ(focusable_state, FocusableState::kFocusable);
return true;
}
bool Element::IsMouseFocusable(UpdateBehavior update_behavior) const {
FocusableState focusable_state = Element::IsFocusableState(update_behavior);
if (focusable_state == FocusableState::kNotFocusable) {
return false;
}
// Any element with tabindex (regardless of its value) is mouse focusable.
if (HasElementFlag(ElementFlags::kTabIndexWasSetExplicitly)) {
return true;
}
DCHECK_EQ(tabIndex(), DefaultTabIndex());
// If the element's default tabindex is >=0, it should be click focusable.
if (DefaultTabIndex() >= 0) {
return true;
}
// If the element is only potentially focusable because it might be a
// keyboard-focusable scroller, then it should not be mouse focusable.
if (focusable_state == FocusableState::kKeyboardFocusableScroller) {
return false;
}
DCHECK_EQ(focusable_state, FocusableState::kFocusable);
return true;
}
bool Element::IsFocusable(UpdateBehavior update_behavior) const {
return isConnected() && IsFocusableStyle(update_behavior) &&
SupportsFocus(update_behavior);
return IsFocusableState(update_behavior) != FocusableState::kNotFocusable;
}
bool Element::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState Element::IsFocusableState(UpdateBehavior update_behavior) const {
if (!isConnected() || !IsFocusableStyle(update_behavior)) {
return FocusableState::kNotFocusable;
}
return SupportsFocus(update_behavior);
}
FocusableState Element::SupportsFocus(UpdateBehavior update_behavior) const {
// SupportsFocus must return true when the element is editable, or else
// it won't be focusable. Furthermore, supportsFocus cannot just return true
// always or else tabIndex() will change for all HTML elements.
if (IsShadowHostWithDelegatesFocus()) {
return false;
return FocusableState::kNotFocusable;
}
return HasElementFlag(ElementFlags::kTabIndexWasSetExplicitly) ||
IsRootEditableElementWithCounting(*this) ||
IsScrollMarkerPseudoElement() ||
CanBeKeyboardFocusableScroller(update_behavior) ||
SupportsSpatialNavigationFocus();
if (HasElementFlag(ElementFlags::kTabIndexWasSetExplicitly) ||
IsRootEditableElementWithCounting(*this) ||
IsScrollMarkerPseudoElement() || SupportsSpatialNavigationFocus()) {
return FocusableState::kFocusable;
}
if (CanBeKeyboardFocusableScroller(update_behavior)) {
return FocusableState::kKeyboardFocusableScroller;
}
return FocusableState::kNotFocusable;
}
bool Element::IsAutofocusable() const {

@ -180,6 +180,12 @@ enum class SelectionBehaviorOnFocus {
kNone,
};
enum class FocusableState {
kNotFocusable,
kFocusable,
kKeyboardFocusableScroller,
};
// https://html.spec.whatwg.org/C/#dom-document-nameditem-filter
enum class NamedItemType {
kNone,
@ -954,27 +960,48 @@ class CORE_EXPORT Element : public ContainerNode, public Animatable {
virtual void blur();
enum class UpdateBehavior {
// The normal update behavior - update style and layout if needed.
kStyleAndLayout,
// Don't update style and layout. This should only be called by
// accessibility-related code, when needed.
kNoneForAccessibility,
kNoneForIsFocused,
kNoneForClearingFocus,
// Don't update style and layout. This should only be called by
// functions that are updating focused state, such as
// ShouldHaveFocusAppearance() and ClearFocusedElementIfNeeded().
kNoneForFocusManagement,
// Don't update style and layout, and assert that layout is clean already.
kAssertNoLayoutUpdates,
};
// IsFocusable is true if the element SupportsFocus(), and is currently
// focusable (using the mouse). This method can be called when layout is not
// clean, but the method might trigger a lifecycle update in that case. This
// method will not trigger a lifecycle update if layout is already clean.
// If UpdateBehavior::kNoneForAccessibility argument is passed, which should
// only be used by a11y code, layout updates will never be performed.
virtual bool IsFocusable(
UpdateBehavior update_behavior = UpdateBehavior::kStyleAndLayout) const;
// IsKeyboardFocusable is true for the subset of mouse focusable elements (for
// which IsFocusable() is true) that are in the tab cycle. This method
// can be called when layout is not clean, but the method might trigger a
// lifecycle update in that case. This method will not trigger a lifecycle
// update if layout is already clean.
// If UpdateBehavior::kNoneForAccessibility argument is passed, which should
// only be used by a11y code, layout updates will never be performed.
// Focusability logic:
// IsFocusable: true if the element can be focused via element.focus().
// IsMouseFocusable: true if clicking on the element will focus it.
// IsKeyboardFocusable: true if the element appears in the sequential
// focus navigation loop. I.e. if the tab key can focus it.
//
// Helpers:
// SupportsFocus: true if it is *possible* for the element to be focused. An
// element supports focus if it has a tabindex attribute, or it is
// editable, etc. Note that the element might *support* focus while not
// *being focusable*, e.g. when the element is disconnected.
// IsFocusableState: can be not focusable, focusable, or focusable because
// of keyboard focusable scrollers.
//
// IsFocusable can only be true if SupportsFocus is true. And both
// IsMouseFocusable and IsKeyboardFocusable require IsFocusable to be true.
// But it is possible for an element to be keyboard-focusable without being
// mouse-focusable, or vice versa.
//
// All of these methods can be called when layout is not clean, but a
// lifecycle update might be triggered in that case. If layout is already
// clean, these methods will not trigger an additional lifecycle update.
// If UpdateBehavior::kNoneForAccessibility is passed (only to be used by
// accessibility code), then no layout updates will be performed even in the
// case that layout is dirty.
bool IsFocusable(
UpdateBehavior update_behavior = UpdateBehavior::kStyleAndLayout) const;
bool IsMouseFocusable(
UpdateBehavior update_behavior = UpdateBehavior::kStyleAndLayout) const;
virtual bool IsKeyboardFocusable(
UpdateBehavior update_behavior = UpdateBehavior::kStyleAndLayout) const;
@ -1507,20 +1534,11 @@ class CORE_EXPORT Element : public ContainerNode, public Animatable {
return NamedItemType::kNone;
}
// SupportsFocus is true if the element is *capable* of being focused. An
// element supports focus if, e.g. it has a tabindex attribute, or it is
// editable, or other conditions. Note that the element might *support* focus
// while not *being focusable*, for example if the element is disconnected
// from the document. This method can be called when layout is not clean,
// but in some cases it might run a style/layout lifecycle update on the
// document.
// If UpdateBehavior::kNoneForAccessibility argument is passed, which should
// only be used by a11y code, layout updates will never be performed.
// This method should stay protected - it is only for use by the Element class
// hierarchy. Outside callers should use `IsFocusable()` and/or
// `IsKeyboardFocusable()`.
virtual bool SupportsFocus(
UpdateBehavior update_behavior = UpdateBehavior::kStyleAndLayout) const;
// See description of SupportsFocus and IsFocusableState above, near
// IsFocusable(). These two methods should stay protected. Use IsFocusable()
// and friends.
virtual FocusableState SupportsFocus(UpdateBehavior update_behavior) const;
virtual FocusableState IsFocusableState(UpdateBehavior update_behavior) const;
bool SupportsSpatialNavigationFocus() const;

@ -578,8 +578,10 @@ LayoutObject* HTMLFencedFrameElement::CreateLayoutObject(const ComputedStyle&) {
return MakeGarbageCollected<LayoutIFrame>(this);
}
bool HTMLFencedFrameElement::SupportsFocus(UpdateBehavior) const {
return frame_delegate_ && frame_delegate_->SupportsFocus();
FocusableState HTMLFencedFrameElement::SupportsFocus(UpdateBehavior) const {
return (frame_delegate_ && frame_delegate_->SupportsFocus())
? FocusableState::kFocusable
: FocusableState::kNotFocusable;
}
PhysicalSize HTMLFencedFrameElement::CoerceFrameSize(

@ -161,8 +161,7 @@ class CORE_EXPORT HTMLFencedFrameElement : public HTMLFrameOwnerElement {
bool LayoutObjectIsNeeded(const DisplayStyle&) const override;
LayoutObject* CreateLayoutObject(const ComputedStyle&) override;
void AttachLayoutTree(AttachContext& context) override;
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
// Set the size of the fenced frame outer container. Used for container size
// specified by FencedFrameConfig.

@ -49,7 +49,9 @@ class ClearButtonElement final : public HTMLDivElement {
private:
void DetachLayoutTree(bool performing_reattach) override;
bool IsFocusable(UpdateBehavior) const override { return false; }
FocusableState IsFocusableState(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
void DefaultEventHandler(Event&) override;
bool IsClearButtonElement() const override;

@ -222,8 +222,10 @@ void DateTimeFieldElement::SetDisabled() {
style_change_extra_data::g_disabled));
}
bool DateTimeFieldElement::SupportsFocus(UpdateBehavior) const {
return !IsDisabled() && !IsFieldOwnerDisabled();
FocusableState DateTimeFieldElement::SupportsFocus(UpdateBehavior) const {
return (!IsDisabled() && !IsFieldOwnerDisabled())
? FocusableState::kFocusable
: FocusableState::kNotFocusable;
}
void DateTimeFieldElement::UpdateVisibleValue(EventBehavior event_behavior) {

@ -118,8 +118,7 @@ class DateTimeFieldElement : public HTMLSpanElement {
bool IsDateTimeFieldElement() const final;
bool IsFieldOwnerDisabled() const;
bool IsFieldOwnerReadOnly() const;
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const final;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const final;
Member<FieldOwner> field_owner_;
DateTimeField type_;

@ -148,9 +148,12 @@ void HTMLFieldSetElement::ChildrenChanged(const ChildrenChange& change) {
focused_element->blur();
}
bool HTMLFieldSetElement::SupportsFocus(UpdateBehavior update_behavior) const {
return HTMLElement::SupportsFocus(update_behavior) &&
!IsDisabledFormControl();
FocusableState HTMLFieldSetElement::SupportsFocus(
UpdateBehavior update_behavior) const {
if (IsDisabledFormControl()) {
return FocusableState::kNotFocusable;
}
return HTMLElement::SupportsFocus(update_behavior);
}
FormControlType HTMLFieldSetElement::FormControlType() const {

@ -48,8 +48,7 @@ class CORE_EXPORT HTMLFieldSetElement final : public HTMLFormControlElement {
private:
bool IsEnumeratable() const override { return true; }
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
LayoutObject* CreateLayoutObject(const ComputedStyle&) override;
LayoutBox* GetLayoutBoxForScrolling() const override;
void DidRecalcStyle(const StyleRecalcChange change) override;

@ -294,8 +294,9 @@ String HTMLFormControlElement::ResultForDialogSubmit() {
return FastGetAttribute(html_names::kValueAttr);
}
bool HTMLFormControlElement::SupportsFocus(UpdateBehavior) const {
return !IsDisabledFormControl();
FocusableState HTMLFormControlElement::SupportsFocus(UpdateBehavior) const {
return IsDisabledFormControl() ? FocusableState::kNotFocusable
: FocusableState::kFocusable;
}
bool HTMLFormControlElement::IsKeyboardFocusable(

@ -188,8 +188,7 @@ class CORE_EXPORT HTMLFormControlElement : public HTMLElement,
void DidChangeForm() override;
void DidMoveToNewDocument(Document& old_document) override;
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
bool IsKeyboardFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
bool ShouldHaveFocusAppearance() const override;

@ -226,7 +226,7 @@ bool HTMLLabelElement::DefaultEventHandlerInternal(Event& evt) {
}
processing_click_ = true;
if (element->IsFocusable() ||
if (element->IsMouseFocusable() ||
(element->IsShadowHostWithDelegatesFocus() &&
RuntimeEnabledFeatures::LabelAndDelegatesFocusNewHandlingEnabled())) {
// If the label is *not* selected, or if the click happened on

@ -78,10 +78,11 @@ void HTMLOptGroupElement::ParseAttribute(
}
}
bool HTMLOptGroupElement::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState HTMLOptGroupElement::SupportsFocus(
UpdateBehavior update_behavior) const {
HTMLSelectElement* select = OwnerSelectElement();
if (select && select->UsesMenuList())
return false;
return FocusableState::kNotFocusable;
return HTMLElement::SupportsFocus(update_behavior);
}

@ -52,8 +52,7 @@ class CORE_EXPORT HTMLOptGroupElement final : public HTMLElement {
void Trace(Visitor*) const override;
private:
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
void ChildrenChanged(const ChildrenChange& change) override;
bool ChildrenChangedAllChildrenRemovedNeedsList() const override;
void ParseAttribute(const AttributeModificationParams&) override;

@ -131,18 +131,21 @@ void HTMLOptionElement::Trace(Visitor* visitor) const {
HTMLElement::Trace(visitor);
}
bool HTMLOptionElement::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState HTMLOptionElement::SupportsFocus(
UpdateBehavior update_behavior) const {
if (is_descendant_of_select_list_) {
return !IsDisabledFormControl();
return IsDisabledFormControl() ? FocusableState::kNotFocusable
: FocusableState::kFocusable;
}
HTMLSelectElement* select = OwnerSelectElement();
if (select && select->UsesMenuList()) {
if (select->IsAppearanceBaseSelect()) {
// If this option is in an appearance:base-select <select>, then we need
// this element to be focusable.
return !IsDisabledFormControl();
return IsDisabledFormControl() ? FocusableState::kNotFocusable
: FocusableState::kFocusable;
}
return false;
return FocusableState::kNotFocusable;
}
return HTMLElement::SupportsFocus(update_behavior);
}

@ -137,8 +137,7 @@ class CORE_EXPORT HTMLOptionElement final : public HTMLElement {
bool IsRichlyEditableForAccessibility() const override { return false; }
private:
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
bool MatchesDefaultPseudoClass() const override;
bool MatchesEnabledPseudoClass() const override;
void ParseAttribute(const AttributeModificationParams&) override;

@ -63,7 +63,8 @@ bool HTMLOutputElement::MatchesEnabledPseudoClass() const {
return false;
}
bool HTMLOutputElement::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState HTMLOutputElement::SupportsFocus(
UpdateBehavior update_behavior) const {
// Skip over HTMLFormControl element, which always supports focus.
return HTMLElement::SupportsFocus(update_behavior);
}

@ -68,8 +68,7 @@ class CORE_EXPORT HTMLOutputElement final : public HTMLFormControlElement {
bool MatchesEnabledPseudoClass() const override;
bool IsEnumeratable() const override { return true; }
bool IsLabelable() const override { return true; }
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
void ResetImpl() override;
bool is_default_value_mode_;

@ -1695,12 +1695,13 @@ void HTMLSelectElement::SelectedOptionElementRemoved(
selectedoption->CloneContentsFromOptionElement(nullptr);
}
bool HTMLSelectElement::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState HTMLSelectElement::SupportsFocus(
UpdateBehavior update_behavior) const {
if (IsAppearanceBaseSelect()) {
// In appearance:base-select mode, the child button gets focus instead of the
// select via delegatesfocus. We must return false here in order to make the
// delegatesfocus focusing code find the child button.
return false;
return FocusableState::kNotFocusable;
}
return HTMLFormControlElementWithState::SupportsFocus(update_behavior);
}

@ -262,7 +262,7 @@ class CORE_EXPORT HTMLSelectElement final
void setSelectedOptionElement(HTMLSelectedOptionElement*);
void DefaultEventHandler(Event&) override;
bool SupportsFocus(UpdateBehavior update_behavior) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
private:
mojom::blink::FormControlType FormControlType() const override;

@ -168,7 +168,9 @@ class CORE_EXPORT HTMLSelectListElement final
bool MayTriggerVirtualKeyboard() const override;
bool AlwaysCreateUserAgentShadowRoot() const override { return false; }
void AppendToFormData(FormData&) override;
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
FormControlState SaveFormControlState() const override;
void RestoreFormControlState(const FormControlState&) override;

@ -89,7 +89,9 @@ class CORE_EXPORT SpinButtonElement final : public HTMLDivElement,
void StopRepeatingTimer();
void RepeatingTimerFired(TimerBase*);
bool ShouldRespondToMouseEvents() const;
bool IsFocusable(UpdateBehavior) const override { return false; }
FocusableState IsFocusableState(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
void CalculateUpDownStateByMouseLocation(Event&);
Member<SpinButtonOwner> spin_button_owner_;

@ -41,7 +41,9 @@ class EditingViewPortElement final : public HTMLDivElement {
const StyleRecalcContext&) override;
private:
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
};
class TextControlInnerEditorElement final : public HTMLDivElement {
@ -57,7 +59,9 @@ class TextControlInnerEditorElement final : public HTMLDivElement {
LayoutObject* CreateLayoutObject(const ComputedStyle&) override;
const ComputedStyle* CustomStyleForLayoutObject(
const StyleRecalcContext&) override;
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
bool is_visible_ = true;
};
@ -69,7 +73,9 @@ class SearchFieldCancelButtonElement final : public HTMLDivElement {
bool WillRespondToMouseClickEvents() override;
private:
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
};
class PasswordRevealButtonElement final : public HTMLDivElement {
@ -80,7 +86,9 @@ class PasswordRevealButtonElement final : public HTMLDivElement {
bool WillRespondToMouseClickEvents() override;
private:
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
};
class PasswordStrongLabelElement final : public HTMLDivElement {
@ -88,7 +96,9 @@ class PasswordStrongLabelElement final : public HTMLDivElement {
explicit PasswordStrongLabelElement(Document&);
private:
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
};
} // namespace blink

@ -156,9 +156,10 @@ HTMLAnchorElement::HTMLAnchorElement(const QualifiedName& tag_name,
HTMLAnchorElement::~HTMLAnchorElement() = default;
bool HTMLAnchorElement::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState HTMLAnchorElement::SupportsFocus(
UpdateBehavior update_behavior) const {
if (IsLink() && !IsEditable(*this)) {
return true;
return FocusableState::kFocusable;
}
return HTMLElement::SupportsFocus(update_behavior);
}
@ -166,17 +167,19 @@ bool HTMLAnchorElement::SupportsFocus(UpdateBehavior update_behavior) const {
bool HTMLAnchorElement::ShouldHaveFocusAppearance() const {
// TODO(crbug.com/1444450): Can't this be done with focus-visible now?
return (GetDocument().LastFocusType() != mojom::blink::FocusType::kMouse) ||
HTMLElement::SupportsFocus(UpdateBehavior::kNoneForIsFocused);
HTMLElement::SupportsFocus(UpdateBehavior::kNoneForFocusManagement) !=
FocusableState::kNotFocusable;
}
bool HTMLAnchorElement::IsFocusable(UpdateBehavior update_behavior) const {
FocusableState HTMLAnchorElement::IsFocusableState(
UpdateBehavior update_behavior) const {
if (!IsFocusableStyle(update_behavior)) {
return false;
return FocusableState::kNotFocusable;
}
if (IsLink()) {
return SupportsFocus(update_behavior);
}
return HTMLElement::IsFocusable(update_behavior);
return HTMLElement::IsFocusableState(update_behavior);
}
bool HTMLAnchorElement::IsKeyboardFocusable(
@ -185,8 +188,12 @@ bool HTMLAnchorElement::IsKeyboardFocusable(
return false;
}
// Anchor is focusable if the base element supports focus and is focusable.
if (Element::SupportsFocus(update_behavior) && IsFocusable(update_behavior)) {
// Anchor is focusable if the base element is focusable. Note that
// because HTMLAnchorElement overrides IsFocusable, we need to check
// both SupportsFocus and IsFocusable.
if (Element::SupportsFocus(update_behavior) !=
FocusableState::kNotFocusable &&
IsFocusable(update_behavior)) {
return HTMLElement::IsKeyboardFocusable(update_behavior);
}

@ -119,16 +119,15 @@ class CORE_EXPORT HTMLAnchorElement : public HTMLElement, public DOMURLUtils {
protected:
void ParseAttribute(const AttributeModificationParams&) override;
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
void FinishParsingChildren() final;
private:
void AttributeChanged(const AttributeModificationParams&) override;
bool ShouldHaveFocusAppearance() const final;
bool IsFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState IsFocusableState(
UpdateBehavior update_behavior) const override;
bool IsKeyboardFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
void DefaultEventHandler(Event&) final;

@ -21,6 +21,7 @@
#include "third_party/blink/renderer/core/html/html_area_element.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_map_element.h"
@ -190,21 +191,26 @@ bool HTMLAreaElement::IsKeyboardFocusable(
return Element::IsKeyboardFocusable(update_behavior);
}
bool HTMLAreaElement::IsFocusable(UpdateBehavior update_behavior) const {
FocusableState HTMLAreaElement::IsFocusableState(
UpdateBehavior update_behavior) const {
// Explicitly skip over the HTMLAnchorElement's mouse focus behavior.
return HTMLElement::IsFocusable(update_behavior);
return HTMLElement::IsFocusableState(update_behavior);
}
bool HTMLAreaElement::IsFocusableStyle(UpdateBehavior update_behavior) const {
if (HTMLImageElement* image = ImageElement()) {
// TODO(crbug.com/1444450): Why is this not just image->IsFocusableStyle()?
if (LayoutObject* layout_object = image->GetLayoutObject()) {
const ComputedStyle& style = layout_object->StyleRef();
return !style.IsInert() && style.Visibility() == EVisibility::kVisible &&
SupportsFocus(update_behavior) && Element::tabIndex() >= 0;
}
HTMLImageElement* image = ImageElement();
if (!image) {
return false;
}
return false;
LayoutObject* layout_object = image->GetLayoutObject();
if (!layout_object) {
return false;
}
const ComputedStyle& style = layout_object->StyleRef();
// TODO(crbug.com/40911863): Why is this not just image->IsFocusableStyle()?
return !style.IsInert() && style.Visibility() == EVisibility::kVisible &&
Element::tabIndex() >= 0 &&
SupportsFocus(update_behavior) != FocusableState::kNotFocusable;
}
void HTMLAreaElement::SetFocused(bool should_be_focused,

@ -62,8 +62,8 @@ class CORE_EXPORT HTMLAreaElement final : public HTMLAnchorElement {
void ParseAttribute(const AttributeModificationParams&) override;
bool IsKeyboardFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
bool IsFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState IsFocusableState(
UpdateBehavior update_behavior) const override;
bool IsFocusableStyle(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
void UpdateSelectionOnFocus(SelectionBehaviorOnFocus,

@ -299,7 +299,8 @@ bool HTMLDialogElement::IsKeyboardFocusable(
}
// This handles cases such as <dialog tabindex=0>, <dialog contenteditable>,
// etc.
return Element::SupportsFocus(update_behavior) &&
return Element::SupportsFocus(update_behavior) !=
FocusableState::kNotFocusable &&
GetIntegralAttribute(html_names::kTabindexAttr, 0) >= 0;
}

@ -64,7 +64,9 @@ class CORE_EXPORT HTMLDialogElement final : public HTMLElement {
// https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-focusing-steps
// can decide to focus the dialog itself if the dialog does not have a focus
// delegate.
bool SupportsFocus(UpdateBehavior) const override { return true; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kFocusable;
}
bool IsKeyboardFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;

@ -2943,10 +2943,16 @@ bool HTMLElement::MatchesReadWritePseudoClass() const {
}
void HTMLElement::HandleKeypressEvent(KeyboardEvent& event) {
if (!IsSpatialNavigationEnabled(GetDocument().GetFrame()) || !SupportsFocus())
if (!IsSpatialNavigationEnabled(GetDocument().GetFrame()) ||
SupportsFocus(UpdateBehavior::kStyleAndLayout) ==
FocusableState::kNotFocusable) {
return;
}
// The SupportsFocus call above will almost always ensure style and layout is
// clean, but it isn't guaranteed for all overrides. So double-check.
GetDocument().UpdateStyleAndLayoutTree();
// if the element is a text form control (like <input type=text> or
// If the element is a text form control (like <input type=text> or
// <textarea>) or has contentEditable attribute on, we should enter a space or
// newline even in spatial navigation mode instead of handling it as a "click"
// action.
@ -3192,8 +3198,12 @@ bool HTMLElement::IsFormAssociatedCustomElement() const {
GetCustomElementDefinition()->IsFormAssociated();
}
bool HTMLElement::SupportsFocus(UpdateBehavior update_behavior) const {
return Element::SupportsFocus(update_behavior) && !IsDisabledFormControl();
FocusableState HTMLElement::SupportsFocus(
UpdateBehavior update_behavior) const {
if (IsDisabledFormControl()) {
return FocusableState::kNotFocusable;
}
return Element::SupportsFocus(update_behavior);
}
bool HTMLElement::IsDisabledFormControl() const {

@ -327,8 +327,7 @@ class CORE_EXPORT HTMLElement : public Element {
void setWritingSuggestions(const AtomicString& value);
protected:
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
enum AllowPercentage { kDontAllowPercentageValues, kAllowPercentageValues };
enum AllowZero { kDontAllowZeroValues, kAllowZeroValues };

@ -210,10 +210,6 @@ void HTMLFrameElementBase::SetLocation(const String& str) {
OpenURL(false);
}
bool HTMLFrameElementBase::SupportsFocus(UpdateBehavior) const {
return true;
}
int HTMLFrameElementBase::DefaultTabIndex() const {
// The logic in focus_controller.cc requires frames to return
// true for IsFocusable(). However, frames are not actually

@ -69,8 +69,9 @@ class CORE_EXPORT HTMLFrameElementBase : public HTMLFrameOwnerElement {
const override;
private:
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const final;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const final {
return FocusableState::kFocusable;
}
int DefaultTabIndex() const final;
void SetFocused(bool, mojom::blink::FocusType) final;

@ -388,9 +388,10 @@ void HTMLPermissionElement::Focus(const FocusParams& params) {
HTMLElement::Focus(params);
}
bool HTMLPermissionElement::SupportsFocus(UpdateBehavior) const {
FocusableState HTMLPermissionElement::SupportsFocus(UpdateBehavior) const {
// The permission element is only focusable if it has a valid type.
return !permission_descriptors_.empty();
return permission_descriptors_.empty() ? FocusableState::kNotFocusable
: FocusableState::kFocusable;
}
int HTMLPermissionElement::DefaultTabIndex() const {

@ -56,7 +56,7 @@ class CORE_EXPORT HTMLPermissionElement final
void AttachLayoutTree(AttachContext& context) override;
void DetachLayoutTree(bool performing_reattach) override;
void Focus(const FocusParams& params) override;
bool SupportsFocus(UpdateBehavior) const override;
FocusableState SupportsFocus(UpdateBehavior) const override;
int DefaultTabIndex() const override;
CascadeFilter GetCascadeFilter() const override;
bool CanGeneratePseudoElement(PseudoId) const override;

@ -625,7 +625,8 @@ void HTMLPlugInElement::DisconnectContentFrame() {
}
bool HTMLPlugInElement::IsFocusableStyle(UpdateBehavior update_behavior) const {
if (HTMLFrameOwnerElement::SupportsFocus(update_behavior) &&
if (HTMLFrameOwnerElement::SupportsFocus(update_behavior) !=
FocusableState::kNotFocusable &&
HTMLFrameOwnerElement::IsFocusableStyle(update_behavior)) {
return true;
}

@ -157,7 +157,9 @@ class CORE_EXPORT HTMLPlugInElement
// Element overrides:
LayoutObject* CreateLayoutObject(const ComputedStyle&) override;
bool SupportsFocus(UpdateBehavior) const final { return true; }
FocusableState SupportsFocus(UpdateBehavior) const final {
return FocusableState::kFocusable;
}
bool IsFocusableStyle(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const final;
bool IsKeyboardFocusable(UpdateBehavior update_behavior =

@ -66,8 +66,12 @@ bool HTMLSummaryElement::IsMainSummary() const {
return false;
}
bool HTMLSummaryElement::SupportsFocus(UpdateBehavior update_behavior) const {
return IsMainSummary() || HTMLElement::SupportsFocus(update_behavior);
FocusableState HTMLSummaryElement::SupportsFocus(
UpdateBehavior update_behavior) const {
if (IsMainSummary()) {
return FocusableState::kFocusable;
}
return HTMLElement::SupportsFocus(update_behavior);
}
int HTMLSummaryElement::DefaultTabIndex() const {

@ -40,8 +40,7 @@ class HTMLSummaryElement final : public HTMLElement {
bool HasActivationBehavior() const override;
HTMLDetailsElement* DetailsElement() const;
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
int DefaultTabIndex() const override;
};

@ -724,21 +724,27 @@ void HTMLMediaElement::ResetMojoState() {
this, GetExecutionContext());
}
bool HTMLMediaElement::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState HTMLMediaElement::SupportsFocus(
UpdateBehavior update_behavior) const {
// TODO(https://crbug.com/911882): Depending on result of discussion, remove.
if (ownerDocument()->IsMediaDocument())
return false;
if (ownerDocument()->IsMediaDocument()) {
return FocusableState::kNotFocusable;
}
// If no controls specified, we should still be able to focus the element if
// it has tabIndex.
return ShouldShowControls() || HTMLElement::SupportsFocus(update_behavior);
if (ShouldShowControls()) {
return FocusableState::kFocusable;
}
return HTMLElement::SupportsFocus(update_behavior);
}
bool HTMLMediaElement::IsFocusable(UpdateBehavior update_behavior) const {
if (!SupportsFocus(update_behavior)) {
return false;
FocusableState HTMLMediaElement::IsFocusableState(
UpdateBehavior update_behavior) const {
if (!IsFullscreen()) {
return SupportsFocus(update_behavior);
}
return !IsFullscreen() || HTMLElement::IsFocusable(update_behavior);
return HTMLElement::IsFocusableState(update_behavior);
}
int HTMLMediaElement::DefaultTabIndex() const {

@ -501,10 +501,8 @@ class CORE_EXPORT HTMLMediaElement
bool AlwaysCreateUserAgentShadowRoot() const final { return true; }
bool AreAuthorShadowsAllowed() const final { return false; }
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const final;
bool IsFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const final;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const final;
FocusableState IsFocusableState(UpdateBehavior update_behavior) const final;
int DefaultTabIndex() const final;
bool LayoutObjectIsNeeded(const DisplayStyle&) const override;
LayoutObject* CreateLayoutObject(const ComputedStyle&) override;

@ -523,13 +523,15 @@ WebInputEventResult MouseEventManager::HandleMouseFocus(
}
for (; element; element = element->ParentOrShadowHostElement()) {
if (element->IsFocusable() && element->IsFocusedElementInDocument())
if (element->IsMouseFocusable() && element->IsFocusedElementInDocument()) {
return WebInputEventResult::kNotHandled;
if (element->IsFocusable() || element->IsShadowHostWithDelegatesFocus()) {
}
if (element->IsMouseFocusable() ||
element->IsShadowHostWithDelegatesFocus()) {
break;
}
}
DCHECK(!element || element->IsFocusable() ||
DCHECK(!element || element->IsMouseFocusable() ||
element->IsShadowHostWithDelegatesFocus());
// To fix <rdar://problem/4895428> Can't drag selected ToDo, we don't focus
@ -570,7 +572,7 @@ WebInputEventResult MouseEventManager::HandleMouseFocus(
// If focus shift is blocked, we eat the event. Note we should never
// clear swallowEvent if the page already set it (e.g., by canceling
// default behavior).
if (element && !element->IsFocusable() &&
if (element && !element->IsMouseFocusable() &&
SlideFocusOnShadowHostIfNecessary(*element)) {
return WebInputEventResult::kHandledSystem;
}

@ -110,7 +110,7 @@ bool NodeRespondsToTapGesture(Node* node) {
// Tapping on a text field or other focusable item should trigger
// adjustment, except that iframe elements are hard-coded to support focus
// but the effect is often invisible so they should be excluded.
if (element->IsFocusable() && !IsA<HTMLIFrameElement>(element)) {
if (element->IsMouseFocusable() && !IsA<HTMLIFrameElement>(element)) {
return true;
}
// Accept nodes that has a CSS effect when touched.

@ -26,6 +26,7 @@
#include "third_party/blink/renderer/core/dom/attr.h"
#include "third_party/blink/renderer/core/dom/attribute.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/core/events/mouse_event.h"
@ -199,18 +200,24 @@ int SVGAElement::DefaultTabIndex() const {
return 0;
}
bool SVGAElement::SupportsFocus(UpdateBehavior update_behavior) const {
FocusableState SVGAElement::SupportsFocus(
UpdateBehavior update_behavior) const {
if (IsEditable(*this)) {
return SVGGraphicsElement::SupportsFocus(update_behavior);
}
if (IsLink()) {
return FocusableState::kFocusable;
}
// If not a link we should still be able to focus the element if it has
// tabIndex.
return IsLink() || SVGGraphicsElement::SupportsFocus(update_behavior);
return SVGGraphicsElement::SupportsFocus(update_behavior);
}
bool SVGAElement::ShouldHaveFocusAppearance() const {
return (GetDocument().LastFocusType() != mojom::blink::FocusType::kMouse) ||
SVGGraphicsElement::SupportsFocus(UpdateBehavior::kNoneForIsFocused);
SVGGraphicsElement::SupportsFocus(
UpdateBehavior::kNoneForFocusManagement) !=
FocusableState::kNotFocusable;
}
bool SVGAElement::IsURLAttribute(const Attribute& attribute) const {

@ -54,8 +54,7 @@ class CORE_EXPORT SVGAElement final : public SVGGraphicsElement,
bool IsLiveLink() const override { return IsLink(); }
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override;
bool ShouldHaveFocusAppearance() const final;
bool IsKeyboardFocusable(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override;

@ -40,7 +40,9 @@ class SVGClipPathElement final : public SVGTransformableElement {
void Trace(Visitor*) const override;
private:
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
void SvgAttributeChanged(const SvgAttributeChangedParams&) override;
void ChildrenChanged(const ChildrenChange&) override;

@ -32,7 +32,9 @@ class SVGDefsElement final : public SVGGraphicsElement {
explicit SVGDefsElement(Document&);
private:
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
LayoutObject* CreateLayoutObject(const ComputedStyle&) override;
};

@ -314,7 +314,9 @@ class CORE_EXPORT SVGElement : public Element {
bool IsStyledElement() const =
delete; // This will catch anyone doing an unnecessary check.
bool SupportsFocus(UpdateBehavior) const override { return false; }
FocusableState SupportsFocus(UpdateBehavior) const override {
return FocusableState::kNotFocusable;
}
void WillRecalcStyle(const StyleRecalcChange) override;
static SVGElementSet& GetDependencyTraversalVisitedSet();

@ -60,9 +60,11 @@ class CORE_EXPORT SVGGraphicsElement : public SVGTransformableElement,
Document&,
ConstructionType = kCreateSVGElement);
bool SupportsFocus(UpdateBehavior update_behavior =
UpdateBehavior::kStyleAndLayout) const override {
return Element::SupportsFocus(update_behavior) || HasFocusEventListeners();
FocusableState SupportsFocus(UpdateBehavior update_behavior) const override {
if (HasFocusEventListeners()) {
return FocusableState::kFocusable;
}
return Element::SupportsFocus(update_behavior);
}
void SvgAttributeChanged(const SvgAttributeChangedParams&) override;

@ -4343,7 +4343,8 @@ bool AXObject::ComputeCanSetFocusAttribute() {
<< "\n* LayoutObject: " << GetLayoutObject();
// Focusable: element supports focus.
return elem->SupportsFocus(Element::UpdateBehavior::kNoneForAccessibility);
return elem->SupportsFocus(Element::UpdateBehavior::kNoneForAccessibility) !=
FocusableState::kNotFocusable;
}
bool AXObject::IsKeyboardFocusable() const {
@ -5564,7 +5565,8 @@ ax::mojom::blink::Role AXObject::DetermineAriaRole() const {
if (IsFrame(GetNode()))
return ax::mojom::blink::Role::kIframePresentational;
if ((GetElement() && GetElement()->SupportsFocus(
Element::UpdateBehavior::kNoneForAccessibility)) ||
Element::UpdateBehavior::kNoneForAccessibility) !=
FocusableState::kNotFocusable) ||
HasAriaAttribute(true /* does_undo_role_presentation */)) {
// Must be exposed with a role if focusable or has a global ARIA property
// that is allowed in this context. See
@ -5601,7 +5603,8 @@ ax::mojom::blink::Role AXObject::DetermineAriaRole() const {
}
} else if (GetElement() &&
GetElement()->SupportsFocus(
Element::UpdateBehavior::kNoneForAccessibility)) {
Element::UpdateBehavior::kNoneForAccessibility) !=
FocusableState::kNotFocusable) {
role = ax::mojom::blink::Role::kComboBoxMenuButton;
}
}
@ -7261,7 +7264,8 @@ bool AXObject::OnNativeClickAction() {
// Explicitly focus the element if it's focusable but not currently
// the focused element, to be consistent with
// EventHandler::HandleMousePressEvent.
if (element->IsFocusable() && !element->IsFocusedElementInDocument()) {
if (element->IsFocusable(Element::UpdateBehavior::kNoneForAccessibility) &&
!element->IsFocusedElementInDocument()) {
Page* const page = GetDocument()->GetPage();
if (page) {
page->GetFocusController().SetFocusedElement(

@ -1665,6 +1665,27 @@ TEST_F(AccessibilityTest, CanSetFocusInCanvasFallbackContent) {
GetAXObjectByElementId("a-hidden-inert")->CanSetFocusAttribute());
}
TEST_F(AccessibilityTest, ScrollerFocusability) {
SetBodyInnerHTML(R"HTML(
<div id=scroller style="overflow:scroll;height:50px;">
<div id=content style="height:1000px"></div>
</div>
)HTML");
auto* scroller = GetAXObjectByElementId("scroller");
auto* scroller_node = scroller->GetNode();
EXPECT_TRUE(scroller_node);
ASSERT_FALSE(scroller_node->IsFocused());
ui::AXActionData action_data;
action_data.action = ax::mojom::blink::Action::kDoDefault;
const ui::AXTreeID div_child_tree_id = ui::AXTreeID::CreateNewAXTreeID();
action_data.target_node_id = scroller->AXObjectID();
action_data.child_tree_id = div_child_tree_id;
scroller->PerformAction(action_data);
ASSERT_TRUE(scroller_node->IsFocused());
}
TEST_F(AccessibilityTest, CanComputeAsNaturalParent) {
SetBodyInnerHTML(R"HTML(M<img usemap="#map"><map name="map"><hr><progress>
<div><input type="range">M)HTML");

@ -0,0 +1,152 @@
<!DOCTYPE html>
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>
<script src='../resources/shadow-dom.js'></script>
<script src='../resources/focus-utils.js'></script>
<script src='../../resources/gesture-util.js'></script>
<!-- Note: Do not move this test to WPT, as "keyboard focusable scrollers"
does not have standard behavior across browsers. -->
<button id=before>Start button</button>
<div id=scroller>
<div id=content>A<br>B<br>C<br>D<br>E<br>F<br>G<br></div>
<button id=button class=invisible>Button</button>
</div>
<button id=after>End button</button>
<style>
#scroller {
overflow:scroll;
width:50px;
height:50px;
}
#content {
width:30px;
height:1000px;
background: lightblue;
}
.invisible {
display:none;
}
</style>
<script>
function clickOn(element) {
const point = pointInElement(element, 1, 1);
return mouseClickOn(point.x, point.y);
}
function resetScrolltop() {
scroller.scrollTo({top: 0, behavior: "instant"});
assert_equals(scroller.scrollTop,0);
}
function runTest(setup, expectedTabStops, scrollerClickFocus, description) {
promise_test(async (t) => {
setup(t);
// Check programmatic focusability.
scroller.focus();
assert_equals(document.activeElement, scroller,
'scrollers are always focusable via element.focus()');
document.activeElement.blur();
assert_equals(document.activeElement, document.body);
// Check keyboard-focusability.
assert_focus_navigation_bidirectional(['before',...expectedTabStops,'after']);
// Check click-focusability.
const expectedFocusElement = scrollerClickFocus ? scroller : document.body;
resetScrolltop(); // Ensure scroller is at the top before clicking.
await clickOn(scroller);
assert_equals(document.activeElement, expectedFocusElement,
`scroller is ${scrollerClickFocus ? "" : "*not* "}` +
`click-focusable (user click)`);
document.activeElement.blur();
resetScrolltop(); // Ensure scroller is at the top before clicking.
await clickOn(content);
assert_equals(document.activeElement, expectedFocusElement,
`scroller is ${scrollerClickFocus ? "" : "*not* "}` +
`click-focusable (user click on content)`);
document.activeElement.blur();
scroller.click();
assert_equals(document.activeElement, document.body,
`scroller is *never* programmatically click()-focusable`);
document.activeElement.blur();
content.click();
assert_equals(document.activeElement, document.body,
`scroller is *never* programmatically click()-focusable (content)`);
document.activeElement.blur();
// Check that the scroller scrolls with arrow keys.
await clickOn(scroller);
const scroll_before = scroller.scrollTop;
// For the contenteditable case, we need to arrow down a few times to
// move the cursor past the end of the scroller.
eventSender.keyDown("ArrowDown");
eventSender.keyDown("ArrowDown");
eventSender.keyDown("ArrowDown");
await waitForEvent(scroller, 'scrollend');
assert_not_equals(scroller.scrollTop, scroll_before, 'arrow keys scroll');
}, description);
}
runTest(() => {},
['scroller'],
/*scrollerClickFocus*/false,
'scroller without focusable content');
runTest((t) => {
t.add_cleanup(() => {scroller.removeAttribute('tabindex')});
scroller.setAttribute('tabindex','0');
},
['scroller'],
/*scrollerClickFocus*/true,
'scroller with tabindex=0');
runTest((t) => {
t.add_cleanup(() => {scroller.removeAttribute('tabindex')});
scroller.setAttribute('tabindex','-1');
},
[],
/*scrollerClickFocus*/true,
'scroller with tabindex=-1');
runTest((t) => {
t.add_cleanup(() => {button.className = 'invisible'});
button.className = '';
},
['button'],
/*scrollerClickFocus*/false,
'scroller with focusable content');
runTest((t) => {
t.add_cleanup(() => {
button.className = 'invisible';
scroller.removeAttribute('tabindex');
});
button.className = '';
scroller.setAttribute('tabindex','0');
},
['scroller','button'],
/*scrollerClickFocus*/true,
'scroller with focusable content and tabindex=0');
runTest((t) => {
t.add_cleanup(() => {scroller.contentEditable = 'false'});
scroller.contentEditable = 'true';
},
['scroller'],
/*scrollerClickFocus*/true,
'contenteditable scroller');
runTest((t) => {
t.add_cleanup(() => {
scroller.contentEditable = 'false';
button.className = 'invisible';
});
scroller.contentEditable = 'true';
button.className = '';
},
['scroller','button'],
/*scrollerClickFocus*/true,
'contenteditable scroller with focusable content');
</script>

@ -1,5 +1,8 @@
'use strict';
// This set of utils also requires the inclusion of
// third_party/blink/web_tests/shadow-dom/resources/shadow-dom.js.
function innermostActiveElement(element) {
element = element || document.activeElement;
if (isIFrameElement(element)) {