blob: 95aace33bc7ab3e5929a71ff419b1fb01b2cdb1c [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_popup_menu_element.h"
#include "third_party/blink/renderer/core/css/css_property_value_set.h"
#include "third_party/blink/renderer/core/css/css_style_declaration.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/events/event_listener.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/geometry/dom_rect.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_overflow_menu_button_element.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
#include "third_party/blink/renderer/platform/keyboard_codes.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
namespace {
// Focus the given item in the list if it is displayed. Returns whether it was
// focused.
bool FocusListItemIfDisplayed(Node* node) {
Element* element = ToElement(node);
if (!element->InlineStyle() ||
!element->InlineStyle()->HasProperty(CSSPropertyDisplay)) {
element->focus();
return true;
}
return false;
}
} // anonymous namespace
class MediaControlPopupMenuElement::EventListener final
: public blink::EventListener {
public:
explicit EventListener(MediaControlPopupMenuElement* popup_menu)
: blink::EventListener(kCPPEventListenerType), popup_menu_(popup_menu) {}
~EventListener() final = default;
void StartListening() {
popup_menu_->addEventListener(event_type_names::kKeydown, this, false);
LocalDOMWindow* window = popup_menu_->GetDocument().domWindow();
window->addEventListener(event_type_names::kScroll, this, true);
if (DOMWindow* outer_window = window->top()) {
if (outer_window != window)
outer_window->addEventListener(event_type_names::kScroll, this, true);
outer_window->addEventListener(event_type_names::kResize, this, true);
}
}
void StopListening() {
popup_menu_->removeEventListener(event_type_names::kKeydown, this, false);
LocalDOMWindow* window = popup_menu_->GetDocument().domWindow();
window->removeEventListener(event_type_names::kScroll, this, true);
if (DOMWindow* outer_window = window->top()) {
if (outer_window != window) {
outer_window->removeEventListener(event_type_names::kScroll, this,
true);
}
outer_window->removeEventListener(event_type_names::kResize, this, true);
}
}
bool operator==(const blink::EventListener& other) const final {
return &other == this;
}
void Trace(blink::Visitor* visitor) final {
blink::EventListener::Trace(visitor);
visitor->Trace(popup_menu_);
}
private:
void Invoke(ExecutionContext*, Event* event) final {
if (event->type() == event_type_names::kKeydown &&
event->IsKeyboardEvent()) {
KeyboardEvent* keyboard_event = ToKeyboardEvent(event);
bool handled = true;
switch (keyboard_event->keyCode()) {
case VKEY_TAB:
keyboard_event->shiftKey() ? popup_menu_->SelectNextItem()
: popup_menu_->SelectPreviousitem();
break;
case VKEY_UP:
popup_menu_->SelectNextItem();
break;
case VKEY_DOWN:
popup_menu_->SelectPreviousitem();
break;
case VKEY_ESCAPE:
popup_menu_->CloseFromKeyboard();
break;
case VKEY_RETURN:
case VKEY_SPACE:
ToElement(event->target()->ToNode())->DispatchSimulatedClick(event);
break;
default:
handled = false;
}
if (handled) {
event->stopPropagation();
event->SetDefaultHandled();
}
} else if (event->type() == event_type_names::kResize ||
event->type() == event_type_names::kScroll) {
popup_menu_->SetIsWanted(false);
}
}
Member<MediaControlPopupMenuElement> popup_menu_;
};
MediaControlPopupMenuElement::~MediaControlPopupMenuElement() = default;
void MediaControlPopupMenuElement::SetIsWanted(bool wanted) {
MediaControlDivElement::SetIsWanted(wanted);
if (wanted) {
SetPosition();
SelectFirstItem();
if (!event_listener_)
event_listener_ = MakeGarbageCollected<EventListener>(this);
event_listener_->StartListening();
} else {
if (event_listener_)
event_listener_->StopListening();
}
}
void MediaControlPopupMenuElement::OnItemSelected() {
SetIsWanted(false);
}
void MediaControlPopupMenuElement::DefaultEventHandler(Event& event) {
if (event.type() == event_type_names::kPointermove) {
ToElement(event.target()->ToNode())->focus();
} else if (event.type() == event_type_names::kFocusout) {
GetDocument()
.GetTaskRunner(TaskType::kMediaElementEvent)
->PostTask(FROM_HERE,
WTF::Bind(&MediaControlPopupMenuElement::HideIfNotFocused,
WrapWeakPersistent(this)));
} else if (event.type() == event_type_names::kClick) {
OnItemSelected();
event.stopPropagation();
event.SetDefaultHandled();
}
MediaControlDivElement::DefaultEventHandler(event);
}
bool MediaControlPopupMenuElement::KeepEventInNode(const Event& event) const {
return MediaControlsImpl::IsModern() &&
MediaControlElementsHelper::IsUserInteractionEvent(event);
}
void MediaControlPopupMenuElement::RemovedFrom(ContainerNode& container) {
if (IsWanted())
SetIsWanted(false);
event_listener_ = nullptr;
MediaControlDivElement::RemovedFrom(container);
}
void MediaControlPopupMenuElement::Trace(blink::Visitor* visitor) {
MediaControlDivElement::Trace(visitor);
visitor->Trace(event_listener_);
}
MediaControlPopupMenuElement::MediaControlPopupMenuElement(
MediaControlsImpl& media_controls,
MediaControlElementType type)
: MediaControlDivElement(media_controls, type) {
SetIsWanted(false);
}
void MediaControlPopupMenuElement::SetPosition() {
// The popup is positioned slightly on the inside of the bottom right corner.
static constexpr int kPopupMenuMarginPx = 4;
static const char kImportant[] = "important";
static const char kPx[] = "px";
DOMRect* bounding_client_rect =
EffectivePopupAnchor()->getBoundingClientRect();
LocalDOMWindow* dom_window = GetDocument().domWindow();
DCHECK(bounding_client_rect);
DCHECK(dom_window);
WTF::String bottom_str_value =
WTF::String::Number(dom_window->innerHeight() -
bounding_client_rect->bottom() + kPopupMenuMarginPx);
WTF::String right_str_value =
WTF::String::Number(dom_window->innerWidth() -
bounding_client_rect->right() + kPopupMenuMarginPx);
bottom_str_value.append(kPx);
right_str_value.append(kPx);
style()->setProperty(&GetDocument(), "bottom", bottom_str_value, kImportant,
ASSERT_NO_EXCEPTION);
style()->setProperty(&GetDocument(), "right", right_str_value, kImportant,
ASSERT_NO_EXCEPTION);
}
Element* MediaControlPopupMenuElement::EffectivePopupAnchor() const {
return MediaControlsImpl::IsModern() ? &GetMediaControls().OverflowButton()
: PopupAnchor();
}
void MediaControlPopupMenuElement::HideIfNotFocused() {
if (!IsWanted())
return;
if (!GetDocument().FocusedElement() ||
GetDocument().FocusedElement()->parentElement() != this) {
SetIsWanted(false);
}
}
void MediaControlPopupMenuElement::SelectFirstItem() {
for (Node* target = lastChild(); target; target = target->previousSibling()) {
if (FocusListItemIfDisplayed(target))
break;
}
}
void MediaControlPopupMenuElement::SelectNextItem() {
Element* focused_element = GetDocument().FocusedElement();
if (!focused_element || focused_element->parentElement() != this)
return;
for (Node* target = focused_element->previousSibling(); target;
target = target->previousSibling()) {
if (FocusListItemIfDisplayed(target))
break;
}
}
void MediaControlPopupMenuElement::SelectPreviousitem() {
Element* focused_element = GetDocument().FocusedElement();
if (!focused_element || focused_element->parentElement() != this)
return;
for (Node* target = focused_element->nextSibling(); target;
target = target->nextSibling()) {
if (FocusListItemIfDisplayed(target))
break;
}
}
void MediaControlPopupMenuElement::CloseFromKeyboard() {
SetIsWanted(false);
EffectivePopupAnchor()->focus();
}
} // namespace blink