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