2 * This file is part of the select element renderer in WebCore.
4 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
5 * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2015 Apple Inc. All rights reserved.
6 * 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Library General Public License for more details.
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
26 #include "RenderMenuList.h"
28 #include "AXObjectCache.h"
29 #include "AccessibilityMenuList.h"
30 #include "CSSFontSelector.h"
33 #include "FrameView.h"
34 #include "HTMLNames.h"
35 #include "HTMLOptionElement.h"
36 #include "HTMLOptGroupElement.h"
37 #include "HTMLSelectElement.h"
38 #include "NodeRenderStyle.h"
40 #include "PopupMenu.h"
41 #include "RenderScrollbar.h"
42 #include "RenderText.h"
43 #include "RenderTheme.h"
44 #include "RenderView.h"
45 #include "StyleResolver.h"
50 #include "LocalizedStrings.h"
55 using namespace HTMLNames;
58 static size_t selectedOptionCount(const RenderMenuList& renderMenuList)
60 const Vector<HTMLElement*>& listItems = renderMenuList.selectElement().listItems();
61 size_t numberOfItems = listItems.size();
64 for (size_t i = 0; i < numberOfItems; ++i) {
65 if (is<HTMLOptionElement>(*listItems[i]) && downcast<HTMLOptionElement>(*listItems[i]).selected())
72 RenderMenuList::RenderMenuList(HTMLSelectElement& element, RenderStyle&& style)
73 : RenderFlexibleBox(element, WTFMove(style))
74 , m_buttonText(nullptr)
75 , m_innerBlock(nullptr)
76 , m_needsOptionsWidthUpdate(true)
79 , m_popupIsVisible(false)
84 RenderMenuList::~RenderMenuList()
86 // Do not add any code here. Add it to willBeDestroyed() instead.
89 void RenderMenuList::willBeDestroyed()
93 m_popup->disconnectClient();
97 RenderFlexibleBox::willBeDestroyed();
100 void RenderMenuList::createInnerBlock()
103 ASSERT(firstChild() == m_innerBlock);
104 ASSERT(!m_innerBlock->nextSibling());
108 // Create an anonymous block.
109 ASSERT(!firstChild());
110 auto newInnerBlock = createAnonymousBlock();
111 m_innerBlock = newInnerBlock.get();
113 RenderFlexibleBox::addChild(WTFMove(newInnerBlock));
116 void RenderMenuList::adjustInnerStyle()
118 auto& innerStyle = m_innerBlock->mutableStyle();
119 innerStyle.setFlexGrow(1);
120 innerStyle.setFlexShrink(1);
121 // min-width: 0; is needed for correct shrinking.
122 innerStyle.setMinWidth(Length(0, Fixed));
123 // Use margin:auto instead of align-items:center to get safe centering, i.e.
124 // when the content overflows, treat it the same as align-items: flex-start.
125 // But we only do that for the cases where html.css would otherwise use center.
126 if (style().alignItems().position() == ItemPositionCenter) {
127 innerStyle.setMarginTop(Length());
128 innerStyle.setMarginBottom(Length());
129 innerStyle.setAlignSelfPosition(ItemPositionFlexStart);
132 innerStyle.setPaddingBox(theme().popupInternalPaddingBox(style()));
134 if (document().page()->chrome().selectItemWritingDirectionIsNatural()) {
135 // Items in the popup will not respect the CSS text-align and direction properties,
136 // so we must adjust our own style to match.
137 innerStyle.setTextAlign(LEFT);
138 TextDirection direction = (m_buttonText && m_buttonText->text()->defaultWritingDirection() == U_RIGHT_TO_LEFT) ? RTL : LTR;
139 innerStyle.setDirection(direction);
141 } else if (document().page()->chrome().selectItemAlignmentFollowsMenuWritingDirection()) {
142 innerStyle.setTextAlign(style().direction() == LTR ? LEFT : RIGHT);
143 TextDirection direction;
144 EUnicodeBidi unicodeBidi;
145 if (multiple() && selectedOptionCount(*this) != 1) {
146 direction = (m_buttonText && m_buttonText->text()->defaultWritingDirection() == U_RIGHT_TO_LEFT) ? RTL : LTR;
147 unicodeBidi = UBNormal;
148 } else if (m_optionStyle) {
149 direction = m_optionStyle->direction();
150 unicodeBidi = m_optionStyle->unicodeBidi();
152 direction = style().direction();
153 unicodeBidi = style().unicodeBidi();
156 innerStyle.setDirection(direction);
157 innerStyle.setUnicodeBidi(unicodeBidi);
160 } else if (m_optionStyle && document().page()->chrome().selectItemAlignmentFollowsMenuWritingDirection()) {
161 if ((m_optionStyle->direction() != innerStyle.direction() || m_optionStyle->unicodeBidi() != innerStyle.unicodeBidi()))
162 m_innerBlock->setNeedsLayoutAndPrefWidthsRecalc();
163 innerStyle.setTextAlign(style().isLeftToRightDirection() ? LEFT : RIGHT);
164 innerStyle.setDirection(m_optionStyle->direction());
165 innerStyle.setUnicodeBidi(m_optionStyle->unicodeBidi());
167 #endif // !PLATFORM(IOS)
170 HTMLSelectElement& RenderMenuList::selectElement() const
172 return downcast<HTMLSelectElement>(nodeForNonAnonymous());
175 void RenderMenuList::addChild(RenderPtr<RenderObject> newChild, RenderObject* beforeChild)
178 auto& child = *newChild;
179 m_innerBlock->addChild(WTFMove(newChild), beforeChild);
180 ASSERT(m_innerBlock == firstChild());
182 if (AXObjectCache* cache = document().existingAXObjectCache())
183 cache->childrenChanged(this, &child);
186 RenderPtr<RenderObject> RenderMenuList::takeChild(RenderObject& oldChild)
188 if (&oldChild == m_innerBlock || !m_innerBlock) {
190 return RenderFlexibleBox::takeChild(oldChild);
192 return m_innerBlock->takeChild(oldChild);
195 void RenderMenuList::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
197 RenderBlock::styleDidChange(diff, oldStyle);
199 if (m_innerBlock) // RenderBlock handled updating the anonymous block's style.
202 bool fontChanged = !oldStyle || oldStyle->fontCascade() != style().fontCascade();
204 updateOptionsWidth();
205 m_needsOptionsWidthUpdate = false;
209 void RenderMenuList::updateOptionsWidth()
211 float maxOptionWidth = 0;
212 const Vector<HTMLElement*>& listItems = selectElement().listItems();
213 int size = listItems.size();
215 for (int i = 0; i < size; ++i) {
216 HTMLElement* element = listItems[i];
217 if (!is<HTMLOptionElement>(*element))
220 String text = downcast<HTMLOptionElement>(*element).textIndentedToRespectGroupLabel();
221 applyTextTransform(style(), text, ' ');
222 if (theme().popupOptionSupportsTextIndent()) {
223 // Add in the option's text indent. We can't calculate percentage values for now.
224 float optionWidth = 0;
225 if (auto* optionStyle = element->computedStyle())
226 optionWidth += minimumValueForLength(optionStyle->textIndent(), 0);
227 if (!text.isEmpty()) {
228 const FontCascade& font = style().fontCascade();
229 TextRun run = RenderBlock::constructTextRun(text, style());
230 optionWidth += font.width(run);
232 maxOptionWidth = std::max(maxOptionWidth, optionWidth);
233 } else if (!text.isEmpty()) {
234 const FontCascade& font = style().fontCascade();
235 TextRun run = RenderBlock::constructTextRun(text, style());
236 maxOptionWidth = std::max(maxOptionWidth, font.width(run));
240 int width = static_cast<int>(ceilf(maxOptionWidth));
241 if (m_optionsWidth == width)
244 m_optionsWidth = width;
246 setNeedsLayoutAndPrefWidthsRecalc();
249 void RenderMenuList::updateFromElement()
251 if (m_needsOptionsWidthUpdate) {
252 updateOptionsWidth();
253 m_needsOptionsWidthUpdate = false;
257 if (m_popupIsVisible)
258 m_popup->updateFromElement();
261 setTextFromOption(selectElement().selectedIndex());
264 void RenderMenuList::setTextFromOption(int optionIndex)
266 const Vector<HTMLElement*>& listItems = selectElement().listItems();
267 int size = listItems.size();
269 int i = selectElement().optionToListIndex(optionIndex);
270 String text = emptyString();
271 if (i >= 0 && i < size) {
272 Element* element = listItems[i];
273 if (is<HTMLOptionElement>(*element)) {
274 text = downcast<HTMLOptionElement>(*element).textIndentedToRespectGroupLabel();
275 auto* style = element->computedStyle();
276 m_optionStyle = style ? RenderStyle::clonePtr(*style) : nullptr;
282 size_t count = selectedOptionCount(*this);
284 text = htmlSelectMultipleItems(count);
288 setText(text.stripWhiteSpace());
289 didUpdateActiveOption(optionIndex);
292 void RenderMenuList::setText(const String& s)
294 String textToUse = s.isEmpty() ? String(ASCIILiteral("\n")) : s;
297 m_buttonText->setText(textToUse.impl(), true);
299 auto newButtonText = createRenderer<RenderText>(document(), textToUse);
300 m_buttonText = newButtonText.get();
301 addChild(WTFMove(newButtonText));
307 String RenderMenuList::text() const
309 return m_buttonText ? m_buttonText->text() : String();
312 LayoutRect RenderMenuList::controlClipRect(const LayoutPoint& additionalOffset) const
314 // Clip to the intersection of the content box and the content box for the inner box
315 // This will leave room for the arrows which sit in the inner box padding,
316 // and if the inner box ever spills out of the outer box, that will get clipped too.
317 LayoutRect outerBox(additionalOffset.x() + borderLeft() + paddingLeft(),
318 additionalOffset.y() + borderTop() + paddingTop(),
322 LayoutRect innerBox(additionalOffset.x() + m_innerBlock->x() + m_innerBlock->paddingLeft(),
323 additionalOffset.y() + m_innerBlock->y() + m_innerBlock->paddingTop(),
324 m_innerBlock->contentWidth(),
325 m_innerBlock->contentHeight());
327 return intersection(outerBox, innerBox);
330 void RenderMenuList::computeIntrinsicLogicalWidths(LayoutUnit& minLogicalWidth, LayoutUnit& maxLogicalWidth) const
332 maxLogicalWidth = std::max(m_optionsWidth, theme().minimumMenuListSize(style())) + m_innerBlock->paddingLeft() + m_innerBlock->paddingRight();
333 if (!style().width().isPercentOrCalculated())
334 minLogicalWidth = maxLogicalWidth;
337 void RenderMenuList::computePreferredLogicalWidths()
339 m_minPreferredLogicalWidth = 0;
340 m_maxPreferredLogicalWidth = 0;
342 if (style().width().isFixed() && style().width().value() > 0)
343 m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(style().width().value());
345 computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth);
347 if (style().minWidth().isFixed() && style().minWidth().value() > 0) {
348 m_maxPreferredLogicalWidth = std::max(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
349 m_minPreferredLogicalWidth = std::max(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
352 if (style().maxWidth().isFixed()) {
353 m_maxPreferredLogicalWidth = std::min(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
354 m_minPreferredLogicalWidth = std::min(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
357 LayoutUnit toAdd = horizontalBorderAndPaddingExtent();
358 m_minPreferredLogicalWidth += toAdd;
359 m_maxPreferredLogicalWidth += toAdd;
361 setPreferredLogicalWidthsDirty(false);
365 NO_RETURN_DUE_TO_ASSERT
366 void RenderMenuList::showPopup()
368 ASSERT_NOT_REACHED();
371 void RenderMenuList::showPopup()
373 if (m_popupIsVisible)
376 // Create m_innerBlock here so it ends up as the first child.
377 // This is important because otherwise we might try to create m_innerBlock
378 // inside the showPopup call and it would fail.
381 m_popup = document().page()->chrome().createPopupMenu(*this);
382 m_popupIsVisible = true;
384 // Compute the top left taking transforms into account, but use
385 // the actual width of the element to size the popup.
386 FloatPoint absTopLeft = localToAbsolute(FloatPoint(), UseTransforms);
387 IntRect absBounds = absoluteBoundingBoxRectIgnoringTransforms();
388 absBounds.setLocation(roundedIntPoint(absTopLeft));
389 m_popup->show(absBounds, &view().frameView(), selectElement().optionToListIndex(selectElement().selectedIndex()));
393 void RenderMenuList::hidePopup()
401 void RenderMenuList::valueChanged(unsigned listIndex, bool fireOnChange)
403 // Check to ensure a page navigation has not occurred while
405 if (&document() != document().frame()->document())
408 selectElement().optionSelectedByUser(selectElement().listToOptionIndex(listIndex), fireOnChange);
411 void RenderMenuList::listBoxSelectItem(int listIndex, bool allowMultiplySelections, bool shift, bool fireOnChangeNow)
413 selectElement().listBoxSelectItem(listIndex, allowMultiplySelections, shift, fireOnChangeNow);
416 bool RenderMenuList::multiple() const
418 return selectElement().multiple();
421 void RenderMenuList::didSetSelectedIndex(int listIndex)
423 didUpdateActiveOption(selectElement().listToOptionIndex(listIndex));
426 void RenderMenuList::didUpdateActiveOption(int optionIndex)
428 if (!AXObjectCache::accessibilityEnabled())
431 auto* axCache = document().existingAXObjectCache();
435 if (m_lastActiveIndex == optionIndex)
437 m_lastActiveIndex = optionIndex;
439 int listIndex = selectElement().optionToListIndex(optionIndex);
440 if (listIndex < 0 || listIndex >= static_cast<int>(selectElement().listItems().size()))
443 if (auto* menuList = downcast<AccessibilityMenuList>(axCache->get(this)))
444 menuList->didUpdateActiveOption(optionIndex);
447 String RenderMenuList::itemText(unsigned listIndex) const
449 const Vector<HTMLElement*>& listItems = selectElement().listItems();
450 if (listIndex >= listItems.size())
454 Element* element = listItems[listIndex];
455 if (is<HTMLOptGroupElement>(*element))
456 itemString = downcast<HTMLOptGroupElement>(*element).groupLabelText();
457 else if (is<HTMLOptionElement>(*element))
458 itemString = downcast<HTMLOptionElement>(*element).textIndentedToRespectGroupLabel();
460 applyTextTransform(style(), itemString, ' ');
464 String RenderMenuList::itemLabel(unsigned) const
469 String RenderMenuList::itemIcon(unsigned) const
474 String RenderMenuList::itemAccessibilityText(unsigned listIndex) const
476 // Allow the accessible name be changed if necessary.
477 const Vector<HTMLElement*>& listItems = selectElement().listItems();
478 if (listIndex >= listItems.size())
480 return listItems[listIndex]->attributeWithoutSynchronization(aria_labelAttr);
483 String RenderMenuList::itemToolTip(unsigned listIndex) const
485 const Vector<HTMLElement*>& listItems = selectElement().listItems();
486 if (listIndex >= listItems.size())
488 return listItems[listIndex]->title();
491 bool RenderMenuList::itemIsEnabled(unsigned listIndex) const
493 const Vector<HTMLElement*>& listItems = selectElement().listItems();
494 if (listIndex >= listItems.size())
496 HTMLElement* element = listItems[listIndex];
497 if (!is<HTMLOptionElement>(*element))
500 bool groupEnabled = true;
501 if (Element* parentElement = element->parentElement()) {
502 if (is<HTMLOptGroupElement>(*parentElement))
503 groupEnabled = !parentElement->isDisabledFormControl();
508 return !element->isDisabledFormControl();
511 PopupMenuStyle RenderMenuList::itemStyle(unsigned listIndex) const
513 const Vector<HTMLElement*>& listItems = selectElement().listItems();
514 if (listIndex >= listItems.size()) {
515 // If we are making an out of bounds access, then we want to use the style
516 // of a different option element (index 0). However, if there isn't an option element
517 // before at index 0, we fall back to the menu's style.
521 // Try to retrieve the style of an option element we know exists (index 0).
524 HTMLElement* element = listItems[listIndex];
526 Color itemBackgroundColor;
527 bool itemHasCustomBackgroundColor;
528 getItemBackgroundColor(listIndex, itemBackgroundColor, itemHasCustomBackgroundColor);
530 auto& style = *element->computedStyle();
531 return PopupMenuStyle(style.visitedDependentColor(CSSPropertyColor), itemBackgroundColor, style.fontCascade(), style.visibility() == VISIBLE,
532 style.display() == NONE, true, style.textIndent(), style.direction(), isOverride(style.unicodeBidi()),
533 itemHasCustomBackgroundColor ? PopupMenuStyle::CustomBackgroundColor : PopupMenuStyle::DefaultBackgroundColor);
536 void RenderMenuList::getItemBackgroundColor(unsigned listIndex, Color& itemBackgroundColor, bool& itemHasCustomBackgroundColor) const
538 const Vector<HTMLElement*>& listItems = selectElement().listItems();
539 if (listIndex >= listItems.size()) {
540 itemBackgroundColor = style().visitedDependentColor(CSSPropertyBackgroundColor);
541 itemHasCustomBackgroundColor = false;
544 HTMLElement* element = listItems[listIndex];
546 Color backgroundColor = element->computedStyle()->visitedDependentColor(CSSPropertyBackgroundColor);
547 itemHasCustomBackgroundColor = backgroundColor.isValid() && backgroundColor.isVisible();
548 // If the item has an opaque background color, return that.
549 if (backgroundColor.isOpaque()) {
550 itemBackgroundColor = backgroundColor;
554 // Otherwise, the item's background is overlayed on top of the menu background.
555 backgroundColor = style().visitedDependentColor(CSSPropertyBackgroundColor).blend(backgroundColor);
556 if (backgroundColor.isOpaque()) {
557 itemBackgroundColor = backgroundColor;
561 // If the menu background is not opaque, then add an opaque white background behind.
562 itemBackgroundColor = Color(Color::white).blend(backgroundColor);
565 PopupMenuStyle RenderMenuList::menuStyle() const
567 const RenderStyle& styleToUse = m_innerBlock ? m_innerBlock->style() : style();
568 IntRect absBounds = absoluteBoundingBoxRectIgnoringTransforms();
569 return PopupMenuStyle(styleToUse.visitedDependentColor(CSSPropertyColor), styleToUse.visitedDependentColor(CSSPropertyBackgroundColor),
570 styleToUse.fontCascade(), styleToUse.visibility() == VISIBLE, styleToUse.display() == NONE,
571 style().hasAppearance() && style().appearance() == MenulistPart, styleToUse.textIndent(),
572 style().direction(), isOverride(style().unicodeBidi()), PopupMenuStyle::DefaultBackgroundColor,
573 PopupMenuStyle::SelectPopup, theme().popupMenuSize(styleToUse, absBounds));
576 HostWindow* RenderMenuList::hostWindow() const
578 return view().frameView().hostWindow();
581 Ref<Scrollbar> RenderMenuList::createScrollbar(ScrollableArea& scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize)
583 bool hasCustomScrollbarStyle = style().hasPseudoStyle(SCROLLBAR);
584 if (hasCustomScrollbarStyle)
585 return RenderScrollbar::createCustomScrollbar(scrollableArea, orientation, &selectElement());
586 return Scrollbar::createNativeScrollbar(scrollableArea, orientation, controlSize);
589 int RenderMenuList::clientInsetLeft() const
594 int RenderMenuList::clientInsetRight() const
599 const int endOfLinePadding = 2;
601 LayoutUnit RenderMenuList::clientPaddingLeft() const
603 if ((style().appearance() == MenulistPart || style().appearance() == MenulistButtonPart) && style().direction() == RTL) {
604 // For these appearance values, the theme applies padding to leave room for the
605 // drop-down button. But leaving room for the button inside the popup menu itself
606 // looks strange, so we return a small default padding to avoid having a large empty
607 // space appear on the side of the popup menu.
608 return endOfLinePadding;
610 // If the appearance isn't MenulistPart, then the select is styled (non-native), so
611 // we want to return the user specified padding.
612 return paddingLeft() + m_innerBlock->paddingLeft();
615 LayoutUnit RenderMenuList::clientPaddingRight() const
617 if ((style().appearance() == MenulistPart || style().appearance() == MenulistButtonPart) && style().direction() == LTR)
618 return endOfLinePadding;
620 return paddingRight() + m_innerBlock->paddingRight();
623 int RenderMenuList::listSize() const
625 return selectElement().listItems().size();
628 int RenderMenuList::selectedIndex() const
630 return selectElement().optionToListIndex(selectElement().selectedIndex());
633 void RenderMenuList::popupDidHide()
636 m_popupIsVisible = false;
640 bool RenderMenuList::itemIsSeparator(unsigned listIndex) const
642 const Vector<HTMLElement*>& listItems = selectElement().listItems();
643 return listIndex < listItems.size() && listItems[listIndex]->hasTagName(hrTag);
646 bool RenderMenuList::itemIsLabel(unsigned listIndex) const
648 const Vector<HTMLElement*>& listItems = selectElement().listItems();
649 return listIndex < listItems.size() && is<HTMLOptGroupElement>(*listItems[listIndex]);
652 bool RenderMenuList::itemIsSelected(unsigned listIndex) const
654 const Vector<HTMLElement*>& listItems = selectElement().listItems();
655 if (listIndex >= listItems.size())
657 HTMLElement* element = listItems[listIndex];
658 return is<HTMLOptionElement>(*element) && downcast<HTMLOptionElement>(*element).selected();
661 void RenderMenuList::setTextFromItem(unsigned listIndex)
663 setTextFromOption(selectElement().listToOptionIndex(listIndex));
666 FontSelector* RenderMenuList::fontSelector() const
668 return &document().fontSelector();