1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "third_party/blink/renderer/modules/media_controls/elements/media_control_input_element.h"
6 
7 #include "base/metrics/histogram_functions.h"
8 #include "base/strings/strcat.h"
9 #include "third_party/blink/public/platform/web_size.h"
10 #include "third_party/blink/public/strings/grit/blink_strings.h"
11 #include "third_party/blink/renderer/core/css/css_property_names.h"
12 #include "third_party/blink/renderer/core/css_value_keywords.h"
13 #include "third_party/blink/renderer/core/dom/dom_token_list.h"
14 #include "third_party/blink/renderer/core/dom/events/event.h"
15 #include "third_party/blink/renderer/core/events/gesture_event.h"
16 #include "third_party/blink/renderer/core/html/forms/html_label_element.h"
17 #include "third_party/blink/renderer/core/html/html_div_element.h"
18 #include "third_party/blink/renderer/core/html/html_span_element.h"
19 #include "third_party/blink/renderer/core/html/media/html_media_element.h"
20 #include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
21 #include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
22 #include "third_party/blink/renderer/platform/heap/heap.h"
23 #include "third_party/blink/renderer/platform/runtime_enabled_features.h"
24 #include "third_party/blink/renderer/platform/text/platform_locale.h"
25 
26 namespace {
27 
28 // The default size of an overflow button in pixels.
29 constexpr int kDefaultButtonSize = 48;
30 
31 const char kOverflowContainerWithSubtitleCSSClass[] = "with-subtitle";
32 const char kOverflowSubtitleCSSClass[] = "subtitle";
33 
34 }  // namespace
35 
36 namespace blink {
37 
38 // static
ShouldRecordDisplayStates(const HTMLMediaElement & media_element)39 bool MediaControlInputElement::ShouldRecordDisplayStates(
40     const HTMLMediaElement& media_element) {
41   // Only record when the metadat are available so that the display state of the
42   // buttons are fairly stable. For example, before metadata are available, the
43   // size of the element might differ, it's unknown if the file has an audio
44   // track, etc.
45   if (media_element.getReadyState() >= HTMLMediaElement::kHaveMetadata)
46     return true;
47 
48   // When metadata are not available, only record the display state if the
49   // element will require a user gesture in order to load.
50   if (media_element.EffectivePreloadType() ==
51       WebMediaPlayer::Preload::kPreloadNone) {
52     return true;
53   }
54 
55   return false;
56 }
57 
CreateOverflowElement(MediaControlInputElement * button)58 HTMLElement* MediaControlInputElement::CreateOverflowElement(
59     MediaControlInputElement* button) {
60   if (!button)
61     return nullptr;
62 
63   // We don't want the button visible within the overflow menu.
64   button->SetInlineStyleProperty(CSSPropertyID::kDisplay, CSSValueID::kNone);
65 
66   overflow_menu_text_ = MakeGarbageCollected<HTMLSpanElement>(GetDocument());
67   overflow_menu_text_->setInnerText(button->GetOverflowMenuString(),
68                                     ASSERT_NO_EXCEPTION);
69 
70   overflow_label_element_ =
71       MakeGarbageCollected<HTMLLabelElement>(GetDocument());
72   overflow_label_element_->SetShadowPseudoId(
73       AtomicString("-internal-media-controls-overflow-menu-list-item"));
74   overflow_label_element_->setAttribute(html_names::kRoleAttr, "menuitem");
75   // Appending a button to a label element ensures that clicks on the label
76   // are passed down to the button, performing the action we'd expect.
77   overflow_label_element_->ParserAppendChild(button);
78 
79   // Allows to focus the list entry instead of the button.
80   overflow_label_element_->setTabIndex(0);
81   button->setTabIndex(-1);
82 
83   overflow_menu_container_ =
84       MakeGarbageCollected<HTMLDivElement>(GetDocument());
85   overflow_menu_container_->ParserAppendChild(overflow_menu_text_);
86   overflow_menu_container_->setAttribute(html_names::kAriaHiddenAttr, "true");
87   aria_label_ = button->FastGetAttribute(html_names::kAriaLabelAttr) + " " +
88                 button->GetOverflowMenuString();
89   UpdateOverflowSubtitleElement(button->GetOverflowMenuSubtitleString());
90   overflow_label_element_->ParserAppendChild(overflow_menu_container_);
91 
92   // Initialize the internal states of the main element and the overflow one.
93   button->is_overflow_element_ = true;
94   overflow_element_ = button;
95 
96   // Keeping the element hidden by default. This is setting the style in
97   // addition of calling ShouldShowButtonInOverflowMenu() to guarantee that the
98   // internal state matches the CSS state.
99   overflow_label_element_->SetInlineStyleProperty(CSSPropertyID::kDisplay,
100                                                   CSSValueID::kNone);
101   SetOverflowElementIsWanted(false);
102 
103   return overflow_label_element_;
104 }
105 
UpdateOverflowSubtitleElement(String text)106 void MediaControlInputElement::UpdateOverflowSubtitleElement(String text) {
107   DCHECK(overflow_menu_container_);
108 
109   if (!text) {
110     // If setting the text to null, we want to remove the element.
111     RemoveOverflowSubtitleElement();
112     UpdateOverflowLabelAriaLabel("");
113     return;
114   }
115 
116   if (overflow_menu_subtitle_) {
117     // If element exists, just update the text.
118     overflow_menu_subtitle_->setInnerText(text, ASSERT_NO_EXCEPTION);
119   } else {
120     // Otherwise, create a new element.
121     overflow_menu_subtitle_ =
122         MakeGarbageCollected<HTMLSpanElement>(GetDocument());
123     overflow_menu_subtitle_->setInnerText(text, ASSERT_NO_EXCEPTION);
124     overflow_menu_subtitle_->setAttribute("class", kOverflowSubtitleCSSClass);
125 
126     overflow_menu_container_->ParserAppendChild(overflow_menu_subtitle_);
127     overflow_menu_container_->setAttribute(
128         "class", kOverflowContainerWithSubtitleCSSClass);
129   }
130   UpdateOverflowLabelAriaLabel(text);
131 }
132 
RemoveOverflowSubtitleElement()133 void MediaControlInputElement::RemoveOverflowSubtitleElement() {
134   if (!overflow_menu_subtitle_)
135     return;
136 
137   overflow_menu_container_->RemoveChild(overflow_menu_subtitle_);
138   overflow_menu_container_->removeAttribute("class");
139   overflow_menu_subtitle_ = nullptr;
140 }
141 
OverflowElementIsWanted()142 bool MediaControlInputElement::OverflowElementIsWanted() {
143   return overflow_element_ && overflow_element_->IsWanted();
144 }
145 
SetOverflowElementIsWanted(bool wanted)146 void MediaControlInputElement::SetOverflowElementIsWanted(bool wanted) {
147   if (!overflow_element_)
148     return;
149   overflow_element_->SetIsWanted(wanted);
150 }
151 
UpdateOverflowLabelAriaLabel(String subtitle)152 void MediaControlInputElement::UpdateOverflowLabelAriaLabel(String subtitle) {
153   String full_aria_label = aria_label_ + " " + subtitle;
154   overflow_label_element_->setAttribute(html_names::kAriaLabelAttr,
155                                         WTF::AtomicString(full_aria_label));
156 }
157 
MaybeRecordDisplayed()158 void MediaControlInputElement::MaybeRecordDisplayed() {
159   // Display is defined as wanted and fitting. Overflow elements will only be
160   // displayed if their inline counterpart isn't displayed.
161   if (!IsWanted() || !DoesFit()) {
162     if (IsWanted() && overflow_element_)
163       overflow_element_->MaybeRecordDisplayed();
164     return;
165   }
166 
167   // Keep this check after the block above because `display_recorded_` might be
168   // true for the inline element but not for the overflow one.
169   if (display_recorded_)
170     return;
171 
172   RecordCTREvent(CTREvent::kDisplayed);
173   display_recorded_ = true;
174 }
175 
UpdateOverflowString()176 void MediaControlInputElement::UpdateOverflowString() {
177   if (!overflow_menu_text_)
178     return;
179 
180   DCHECK(overflow_element_);
181   overflow_menu_text_->setInnerText(GetOverflowMenuString(),
182                                     ASSERT_NO_EXCEPTION);
183 
184   UpdateOverflowSubtitleElement(GetOverflowMenuSubtitleString());
185 }
186 
MediaControlInputElement(MediaControlsImpl & media_controls)187 MediaControlInputElement::MediaControlInputElement(
188     MediaControlsImpl& media_controls)
189     : HTMLInputElement(media_controls.GetDocument(), CreateElementFlags()),
190       MediaControlElementBase(media_controls, this) {}
191 
GetOverflowStringId() const192 int MediaControlInputElement::GetOverflowStringId() const {
193   NOTREACHED();
194   return IDS_AX_AM_PM_FIELD_TEXT;
195 }
196 
UpdateShownState()197 void MediaControlInputElement::UpdateShownState() {
198   if (is_overflow_element_) {
199     Element* parent = parentElement();
200     DCHECK(parent);
201     DCHECK(IsA<HTMLLabelElement>(parent));
202 
203     if (IsWanted() && DoesFit()) {
204       parent->RemoveInlineStyleProperty(CSSPropertyID::kDisplay);
205     } else {
206       parent->SetInlineStyleProperty(CSSPropertyID::kDisplay,
207                                      CSSValueID::kNone);
208     }
209   }
210 
211   MediaControlElementBase::UpdateShownState();
212 }
213 
DefaultEventHandler(Event & event)214 void MediaControlInputElement::DefaultEventHandler(Event& event) {
215   if (!IsDisabled() && (event.type() == event_type_names::kClick ||
216                         event.type() == event_type_names::kGesturetap)) {
217     MaybeRecordInteracted();
218   }
219 
220   // Unhover the element if the hover is triggered by a tap on
221   // a touch screen device to avoid showing hover circle indefinitely.
222   if (IsA<GestureEvent>(event) && IsHovered())
223     SetHovered(false);
224 
225   HTMLInputElement::DefaultEventHandler(event);
226 }
227 
MaybeRecordInteracted()228 void MediaControlInputElement::MaybeRecordInteracted() {
229   if (interaction_recorded_)
230     return;
231 
232   if (!display_recorded_) {
233     // The only valid reason to not have the display recorded at this point is
234     // if it wasn't allowed. Regardless, the display will now be recorded.
235     DCHECK(!ShouldRecordDisplayStates(MediaElement()));
236     RecordCTREvent(CTREvent::kDisplayed);
237   }
238 
239   RecordCTREvent(CTREvent::kInteracted);
240   interaction_recorded_ = true;
241 }
242 
IsOverflowElement() const243 bool MediaControlInputElement::IsOverflowElement() const {
244   return is_overflow_element_;
245 }
246 
IsMouseFocusable() const247 bool MediaControlInputElement::IsMouseFocusable() const {
248   return false;
249 }
250 
IsMediaControlElement() const251 bool MediaControlInputElement::IsMediaControlElement() const {
252   return true;
253 }
254 
GetOverflowMenuString() const255 String MediaControlInputElement::GetOverflowMenuString() const {
256   return MediaElement().GetLocale().QueryString(GetOverflowStringId());
257 }
258 
GetOverflowMenuSubtitleString() const259 String MediaControlInputElement::GetOverflowMenuSubtitleString() const {
260   return String();
261 }
262 
RecordCTREvent(CTREvent event)263 void MediaControlInputElement::RecordCTREvent(CTREvent event) {
264   base::UmaHistogramEnumeration(
265       base::StrCat({"Media.Controls.CTR.", GetNameForHistograms()}), event);
266 }
267 
SetClass(const AtomicString & class_name,bool should_have_class)268 void MediaControlInputElement::SetClass(const AtomicString& class_name,
269                                         bool should_have_class) {
270   if (should_have_class)
271     classList().Add(class_name);
272   else
273     classList().Remove(class_name);
274 }
275 
UpdateDisplayType()276 void MediaControlInputElement::UpdateDisplayType() {
277   if (overflow_element_)
278     overflow_element_->UpdateDisplayType();
279 }
280 
GetSizeOrDefault() const281 WebSize MediaControlInputElement::GetSizeOrDefault() const {
282   if (IsControlPanelButton()) {
283     return MediaControlElementsHelper::GetSizeOrDefault(
284         *this, WebSize(kDefaultButtonSize, kDefaultButtonSize));
285   }
286   return MediaControlElementsHelper::GetSizeOrDefault(*this, WebSize(0, 0));
287 }
288 
IsDisabled() const289 bool MediaControlInputElement::IsDisabled() const {
290   return FastHasAttribute(html_names::kDisabledAttr);
291 }
292 
Trace(Visitor * visitor) const293 void MediaControlInputElement::Trace(Visitor* visitor) const {
294   HTMLInputElement::Trace(visitor);
295   MediaControlElementBase::Trace(visitor);
296   visitor->Trace(overflow_element_);
297   visitor->Trace(overflow_menu_container_);
298   visitor->Trace(overflow_menu_text_);
299   visitor->Trace(overflow_menu_subtitle_);
300   visitor->Trace(overflow_label_element_);
301 }
302 
303 }  // namespace blink
304